Last active
September 12, 2015 18:53
-
-
Save jnm2/00023e368ae0680e8df8 to your computer and use it in GitHub Desktop.
Provides a fluent API to map discriminator property values to derived types and handle both serialization and deserialization.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
using Newtonsoft.Json.Serialization; | |
public sealed class DerivedDiscriminatorConverter<TBase> : JsonConverter where TBase : class | |
{ | |
private readonly string discriminatorProperty; | |
private readonly Dictionary<object, Type> typeByDiscriminator = new Dictionary<object, Type>(); | |
public DerivedDiscriminatorConverter(string discriminatorProperty) | |
{ | |
this.discriminatorProperty = discriminatorProperty; | |
} | |
public DerivedDiscriminatorConverter<TBase> Map<TDerived>(object propertyValue) | |
where TDerived : TBase, new() | |
{ | |
// Avoid null key issue by using DBNull.Value | |
typeByDiscriminator.Add(propertyValue ?? DBNull.Value, typeof(TDerived)); | |
return this; | |
} | |
public override bool CanConvert(Type objectType) => typeof(TBase).IsAssignableFrom(objectType); | |
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |
{ | |
var buffered = JObject.Load(reader); | |
object r; | |
Type typeToActivate; | |
var property = buffered[discriminatorProperty]; | |
var discriminatorValue = property == null ? DBNull.Value : (serializer.Deserialize(buffered[discriminatorProperty].CreateReader()) ?? DBNull.Value); | |
if (typeByDiscriminator.TryGetValue(discriminatorValue, out typeToActivate)) | |
{ | |
r = Activator.CreateInstance(typeToActivate); | |
} | |
else | |
{ | |
try | |
{ | |
r = Activator.CreateInstance(objectType); | |
} | |
catch (MissingMethodException ex) | |
{ | |
throw new InvalidOperationException("No mapping found for " + (discriminatorValue == DBNull.Value ? "a null discriminator" : "the discriminator '" + discriminatorValue + "'") + " and the base type " + typeof(TBase).Name + " cannot be instantiated.", ex); | |
} | |
} | |
serializer.Populate(buffered.CreateReader(), r); | |
return r; | |
} | |
private bool TryGetBestMatchDiscriminator(Type valueType, out object discriminatorValue) | |
{ | |
var bestMatchType = (Type)null; | |
var bestMatchDiscriminatorValue = (object)null; | |
foreach (var kvp in typeByDiscriminator) | |
{ | |
if (kvp.Value == valueType) | |
{ | |
discriminatorValue = kvp.Key; | |
return true; | |
} | |
if (!valueType.IsSubclassOf(kvp.Value)) continue; | |
if (bestMatchType == null || bestMatchType.IsSubclassOf(kvp.Value)) | |
{ | |
bestMatchType = kvp.Value; | |
bestMatchDiscriminatorValue = kvp.Key; | |
} | |
} | |
discriminatorValue = bestMatchDiscriminatorValue == DBNull.Value ? null : bestMatchDiscriminatorValue; | |
return bestMatchDiscriminatorValue != null; | |
} | |
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |
{ | |
var valueType = value.GetType(); | |
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(valueType); | |
var prevProperty = contract.Properties.Contains("$schema") ? contract.Properties["$schema"] : null; | |
var prevPropertyIndex = prevProperty == null ? -1 : contract.Properties.IndexOf(prevProperty); | |
contract.Properties.Remove(prevProperty); | |
object discriminatorValue; | |
if (TryGetBestMatchDiscriminator(valueType, out discriminatorValue)) | |
contract.Properties.Insert(0, new JsonProperty | |
{ | |
PropertyName = discriminatorProperty, | |
Readable = true, | |
ValueProvider = new ConstantValueProvider(discriminatorValue), | |
PropertyType = typeof(object) | |
}); | |
var internalWriterType = typeof(JsonSerializer).Assembly.GetType("Newtonsoft.Json.Serialization.JsonSerializerInternalWriter"); | |
var internalWriter = internalWriterType.GetConstructor(new[] { typeof(JsonSerializer) }).Invoke(new object[] { serializer }); | |
// serializer is a proxy. Its ContractResolver property returns what it should, but JsonSerializerInternalWriter.GetContractSafe uses the _contractResolver field instead of the proxied ContractResolver property. | |
// This results in child types not following the contract resolver you configured, such as the CamelCasePropertyNamesContractResolver. | |
var contractResolverField = typeof(JsonSerializer).GetField("_contractResolver", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); | |
var prevContractResolverFieldValue = contractResolverField.GetValue(serializer); | |
try | |
{ | |
contractResolverField.SetValue(serializer, serializer.ContractResolver); | |
internalWriterType.GetMethod("SerializeObject", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) | |
.Invoke(internalWriter, new object[] { writer, value, contract, null, null, null }); | |
} | |
finally | |
{ | |
contractResolverField.SetValue(serializer, prevContractResolverFieldValue); | |
} | |
contract.Properties.Remove(discriminatorProperty); | |
if (prevProperty != null) contract.Properties.Insert(prevPropertyIndex, prevProperty); | |
} | |
} | |
public sealed class ConstantValueProvider : IValueProvider | |
{ | |
private readonly object value; | |
public ConstantValueProvider(object value) | |
{ | |
this.value = value; | |
} | |
public object GetValue(object target) => value; | |
public void SetValue(object target, object value) | |
{ | |
throw new NotSupportedException(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment