4

I'm trying to come up with a pattern that can detect when a property has been set to null. Something similar to the Nullable<T> class, but a little more advanced. Let's call it MoreThanNullable<T>. Basically I need to do something different depending on the following 3 scenarios:

  1. The property was never set
  2. The property was set to null
  3. The property is set to an instance of T.

I created my own class to do this with an "instantiated" property and it all works within a test scenario. Some sample code:

public struct MoreThanNullable<T>
{
    private bool hasValue;
    internal T value;
    public bool Instantiated { get; set; }

    public MoreThanNullable(T value)
    {
        this.value = value;
        this.hasValue = true;
        Instantiated = true;
    }
// ... etc etc...

And the test that passes as I expect it to:

[TestFixture]
public class MoreThanNullableTests
{
    public class AccountViewModel
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public MoreThanNullable<string> Test1 { get; set; }
        public MoreThanNullable<string> Test2 { get; set; }
        public MoreThanNullable<string> Test3 { get; set; }
    }

    [Test]
    public void Tests()
    {
        var myClass = new AccountViewModel();
        Assert.AreEqual(false, myClass.Test1.Instantiated);
        myClass.Test1 = null;
        Assert.AreEqual(true, myClass.Test1.Instantiated);
    }
}

Next, using this same view model I wire it up to a POST on my REST service and I pass in the following JSON:

{
    Name:"test",
    Test1:null,
    Test2:"test"
}

... and it all falls to pieces. Neither Test1 nor Test3 get instantiated! Obviously, I'm not expecting Test3 to get instantiated, but Test1 remaining uninstantiated was unexpected. Effectively I can't tell the difference between a property I did and didn't receive via REST. (Note: In our application there is a distinct difference between not set and set to null)

Am I doing someting wrong? Or is this correct behaviour for Web API?

** UPDATE **

What I probably didn't make clear is that I have effectively replicated the Nullable<T> class to achieve this, including overloading the implicit operator like this:

public static implicit operator MoreThanNullable<T>(T value)
{
    return new MoreThanNullable<T>(value);
}

I always seem to leave out important stuff in my questions...

4
  • 1
    could you share your final set of MoreThanNullable classes? Thanks! Commented Dec 6, 2017 at 17:52
  • 1
    It ended up being quite a task as you may have to consider the opposite too. How do want GETs to return these types? It's quite lengthy for a comment, but I wrote a couple of blog posts on it in the past (there are 2 parts!), so here are the links. Hope you find them useful: Link to Part 1 and Link to Part 2. To add... in future I might consider using patches instead of doing this as it ended up being quite cumbersome. Commented Dec 11, 2017 at 10:51
  • 1
    Thanks Conor! One issue I've run into to with using complex types with hasValue, instantiated etc is they show up in webapi autogenerated swagger documentation. It's a difficult problem to solve! Commented Dec 11, 2017 at 12:13
  • 1
    I did solve that issue as I have used swagger, but can’t recall how. If we say the type is called “Excludable” I think you can add a mapping from “Excludable<DateTime>” to display as DateTime instead. I might have a copy of the code someplace, if I find it I’ll post it here or on the blog. Commented Dec 12, 2017 at 12:51

2 Answers 2

2

Why does it not work? In your JSON

{
    Name:"test",
    Test1:null,
    Test2:"test"
}

Test1 end Test2 are primitive types but you created your own type and those are the properties it should map over to so its no longer primitive but a complex object. So really your JSON is expected to also contain objects. It would work if you changed it to

{
    Name:"test",
    Test1:{Instantiated:false, value: null},
    Test2:{Instantiated:false, value: "test"},
}

as now they are objects and deserialized as such. Also your struct has a default empty constructor, if it were a class with a private default constructor it would not work again.

Now, how do you get a primitive type like a string to deserialize over to your custom type? You might be able to do this with a custom web api action filter and parse the incoming json and map it to your object in the OnActionExecuting.

EDIT 2

Assuming you are using JSON.NET from newtonsoft you can plug in a custom JSON converter. This would be a global change across the web api application, no need for custom filters.

public sealed class MoreThanNullableConverter : Newtonsoft.Json.JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof (MoreThanNullable<>);
    }
    public override bool CanWrite { get { return false; } }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // return a new MoreThanNullable instance with value
        return Activator.CreateInstance(objectType, reader.Value);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Change to webapiconfig.cs

var formatters = GlobalConfiguration.Configuration.Formatters;
var jsonFormatter = formatters.JsonFormatter;
var settings = jsonFormatter.SerializerSettings;
jsonFormatter.SerializerSettings.Converters.Add(new MoreThanNullableConverter());
Sign up to request clarification or add additional context in comments.

3 Comments

But one thing you've missed is setting Test2:"test" works exactly as I expect. Maybe I should have included that I have overloaded the implicit / explicit operators to make it behave like a Nullable<T>. I understand that Nullable has more funky stuff going on under the hood as it's baked into the language, but either way I would have expected the Deserializer to utilise the same methods as my test classes do.
@ConorGallagher - good point, I missed that. See my latest edit, this should point you in the correct direction I think. Json.net is a good tool to use for json serialization/deserialization and frankly I am surprised its not the default serializer for json in the web api as its pretty much the defacto standard.
Thanks for the edit. Using this I managed to solve the problem. One addition, and for the sake of a clean answer, this is the code I used in the ReadJson override method: return Activator.CreateInstance(objectType, reader.Value);. More than happy for you to update your answer to include this line as it makes it a more complete answer.
1

i think you have fallen in a common problem with deserialization on services in .net

I've always seen this problem with WCF, but i think it's also present in json deserialization

https://msdn.microsoft.com/en-us/library/system.runtime.serialization.ondeserializedattribute(v=vs.110).aspx

.NET during deserialization doesn't execute constructor, so if you need to execute some code after deserialization you have to add some code as these

[OnDeserialized()]
internal void OnDeserializedMethod(StreamingContext context) 
{
   // ... logic here after deserialization
}

In that logic you should complete private field with missing information. hasValue field must setted depending if value is null or not. Instantiated should have a private field with setter private. It should setted on true on public setter of value. If hasValue is true, instantiated is obviously true, how can be otherwise? So the problem is, how can we have hasvalue false but instatiated true?

This is the only case when you cannot determine the value with other values, so we need somthing else. Making the setter of instantiated public is the simplest way but for ensure object consistence you should check it after deserialization to ensure previous object rules...

3 Comments

Although this is useful to know it doesn't look like it will help solve my particular problem. The issue is although it gives me a hook to the deserialization, the context is relatively useless. If it included the original json or something then I might be in luck, but unfortunately I have still have no way of knowing whether I received a null or the property wasn't set.
I've added some explanation... I hope it's help you
Thanks for the edit. The problem with this solution is I'm still stuck in that last scenario of not knowing what I got passed so I don't have any reference to what I update my private field with. Is it null? Or was it not set? So I have to come up with a manual way to handle it which isn't ideal. In this instance the JsonConverter method provided a cleaner solution for me so I went with that

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.