Reputation: 1499
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:
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...
Upvotes: 4
Views: 2023
Reputation: 62228
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());
Upvotes: 2
Reputation: 438
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
.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...
Upvotes: 1