I'm trying to overcome a problem with command handling, there are easier ways using flags and enums, but if I can make a magical class that stops coupling I want it.
Basically I have a simple class:
public class ServiceProperty<T>
{
public T Value { get; set; }
}
And all I want is to map the data simply. The reason behind this class is that we want the "update" command to be flexible (e.g. the client can update one attribute without being forced to send everything, risking reverting a change to another field that may have occurred during the process due to timing).
The problem is that null is acceptable for some fields, so we can't rely on nullable, we need a class that has the value (which can be null) and if the property is not included at all we then know they don't wish to update it.
the problem is the resulting JSON required (in this example we are updating the string "description"):
{
"description": { "value": "lol" }
}
I wish to change the required JSON to the following:
{
"description": "lol"
}
and use converters to go between. I've built the converter:
public class ServicePropertyConverter<T> : JsonConverter where T : class
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var str = reader.Value.ToString();
return new ServiceProperty<T> { Value = string.IsNullOrEmpty(str) ? null : (T)JsonConvert.DeserializeObject(reader.Value.ToString(), typeof(T)) };
}
... (other methods here)...
}
So that should work fine, however swagger still asks for the first JSON example.
I've tried the following:
services.AddMvc(o =>
{
...(Unimportant code here)...
}).AddJsonOptions(x =>
{
x.SerializerSettings.ContractResolver = new ServicePropertyContractResolver();
x.SerializerSettings.Converters.Add(new ServicePropertyConverter<string>());
});
The string implementation may seem silly, but it's just a first step to get the description example working, then I'll work on everything else, but it didn't work. As you can see I also created a custom 'ContractResolver':
protected override JsonConverter ResolveContractConverter(Type objectType)
{
if (objectType == null || !objectType.IsAssignableFrom(typeof(ServiceProperty<>)))
{
return base.ResolveContractConverter(objectType);
}
return new ServicePropertyConverter<string>();
}
But this didn't make any difference either. At this point I'm a little desperate, I don't want to waste any more time, so if there isn't a simple solution I'll go back to the boilerplate solution and give up on this more elegant approach.
Any help is greatly appreciated - I am new to ASP.NET, so I'm learning as I go.
What you want to do is to create a single converter ServicePropertyConverter
that works for all ServiceProperty<T>
for all T
. This is fairly simple to do with the following modifications to your type:
public abstract class ServicePropertyBase
{
public abstract object GetValue();
}
// Possibly this class should be sealed.
public class ServiceProperty<T> : ServicePropertyBase
{
public ServiceProperty() { }
public ServiceProperty(T value) { this.Value = value; }
public T Value { get; set; }
public override object GetValue()
{
return Value;
}
}
Notice that:
There is an abstract base type that returns the value as an object
. The presence of this base type makes WriteJson()
simpler.
The generic type now has both parameterized and parameterless constructors. The presence of the parameterized constructor makes ReadJson()
simpler.
Then, create the following converter:
public class ServicePropertyConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.GetServicePropertyValueType() != null;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var valueType = objectType.GetServicePropertyValueType();
var value = serializer.Deserialize(reader, valueType);
// Use the parameterized constructor.
return Activator.CreateInstance(objectType, value);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var baseValue = (ServicePropertyBase)value;
serializer.Serialize(writer, baseValue.GetValue());
}
}
internal static class ServicePropertyExtensions
{
public static Type GetServicePropertyValueType(this Type objectType)
{
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(ServiceProperty<>))
{
return objectType.GetGenericArguments()[0];
}
return null;
}
}
And, serialize and deserialize with the following settings:
var settings = new JsonSerializerSettings
{
Converters = { new ServicePropertyConverter() },
//NullValueHandling.Ignore cannot be used because ServicePropertyConverter.ReadJson()
//will not get called during reading to allocate an empty ServiceProperty<T>.
//NullValueHandling = NullValueHandling.Ignore,
//Instead DefaultValueHandling.IgnoreAndPopulate must be used to skip serialization
//of a null ServiceProperty<T> when serializing but force ServicePropertyConverter.ReadJson()
//to be called when deserializing.
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
};
Note the use of DefaultValueHandling.IgnoreAndPopulate
. Using this setting ensures that a property with a null value for a ServiceProperty<T>
is completely skipped when serializing, but populated during deserialization when present. (If NullValueHandling.Ignore
were used instead, ServicePropertyConverter.ReadJson()
would not get not called when a null value is encountered, breaking your design.)
If you cannot set DefaultValueHandling
in settings, you could set it via a custom contract resolver:
public class ServicePropertyContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (property.DefaultValueHandling == null && property.PropertyType.GetServicePropertyValueType() != null)
{
property.DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate;
}
return property;
}
}
And with settings:
var settings = new JsonSerializerSettings
{
Converters = { new ServicePropertyConverter() },
ContractResolver = new ServicePropertyContractResolver(),
};
You may want to cache the contract resolver for best performance.
Sample fiddle.