Skip to content

Instantly share code, notes, and snippets.

@jnm2
Last active September 12, 2015 18:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnm2/00023e368ae0680e8df8 to your computer and use it in GitHub Desktop.
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.
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