src/Microsoft.Azure.WebJobs.Host/Protocols/PolymorphicJsonConverter.cs (190 lines of code) (raw):
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
#if PUBLICPROTOCOL
namespace Microsoft.Azure.WebJobs.Protocols
#else
namespace Microsoft.Azure.WebJobs.Host.Protocols
#endif
{
/// <remarks>
/// Unlike $type in JSON.NET, this converter decouples the message data from the .NET class and assembly names.
/// It also allows emitting a type on the root object.
/// </remarks>
#if PUBLICPROTOCOL
public class PolymorphicJsonConverter : JsonConverter
#else
internal class PolymorphicJsonConverter : JsonConverter
#endif
{
private readonly string _typePropertyName;
private readonly IDictionary<string, Type> _nameToTypeMap;
private readonly IDictionary<Type, string> _typeToNameMap;
private static readonly ConcurrentDictionary<Type, NonCircularContractResolver> _nonCircularResolverCache = new ConcurrentDictionary<Type, NonCircularContractResolver>();
/// <summary>Initializes a new instance of the <see cref="PolymorphicJsonConverter"/> class.</summary>
/// <param name="typeMapping">The type names to use when serializing types.</param>
public PolymorphicJsonConverter(IDictionary<string, Type> typeMapping)
: this("$$type", typeMapping)
{
}
/// <summary>Initializes a new instance of the <see cref="PolymorphicJsonConverter"/> class.</summary>
/// <param name="typePropertyName">The name of the property in which to serialize the type name.</param>
/// <param name="typeMapping">The type names to use when serializing types.</param>
public PolymorphicJsonConverter(string typePropertyName, IDictionary<string, Type> typeMapping)
{
if (typePropertyName == null)
{
throw new ArgumentNullException("typePropertyName");
}
if (typeMapping == null)
{
throw new ArgumentNullException("typeMapping");
}
_typePropertyName = typePropertyName;
_nameToTypeMap = typeMapping;
_typeToNameMap = new Dictionary<Type, string>();
foreach (KeyValuePair<string, Type> item in _nameToTypeMap)
{
_typeToNameMap.Add(item.Value, item.Key);
}
}
/// <summary>Gets the name of the property in which to serialize the type name.</summary>
public string TypePropertyName
{
get { return _typePropertyName; }
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return _typeToNameMap.ContainsKey(objectType);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader == null)
{
throw new ArgumentNullException("reader");
}
if (objectType == null)
{
throw new NotSupportedException("Deserialization is not supported without specifying a default object Type.");
}
if (serializer == null)
{
throw new ArgumentNullException("serializer");
}
if (reader.TokenType == JsonToken.Null)
{
return null;
}
JToken json = JToken.ReadFrom(reader);
Type typeToCreate = GetTypeToCreate(json) ?? objectType;
object target = Activator.CreateInstance(typeToCreate);
serializer.Populate(json.CreateReader(), target);
return target;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (writer == null)
{
throw new ArgumentNullException("writer");
}
if (serializer == null)
{
throw new ArgumentNullException("serializer");
}
if (value == null)
{
writer.WriteNull();
return;
}
Type valueType = value.GetType();
// Now that we've handled the type, temporarily remove this converter so we can serialize this element and
// its children without infinite recursion.
IContractResolver originalContractResolver = serializer.ContractResolver;
serializer.ContractResolver = _nonCircularResolverCache.GetOrAdd(valueType, type => new NonCircularContractResolver(type));
JObject json = JObject.FromObject(value, serializer);
string typeName = GetTypeName(valueType);
if (typeName != null)
{
if (json.Property(_typePropertyName) != null)
{
json.Remove(_typePropertyName);
}
json.AddFirst(new JProperty(_typePropertyName, typeName));
}
serializer.Serialize(writer, json);
// Restore this converter so that subsequent siblings can use it.
serializer.ContractResolver = originalContractResolver;
}
/// <summary>Gets all type name mappings in a type hierarchy.</summary>
/// <typeparam name="T">The root type of the type hierarchy.</typeparam>
/// <returns>All type name mappings in the type hierarchy.</returns>
public static IDictionary<string, Type> GetTypeMapping<T>()
{
IDictionary<string, Type> typeMapping = new Dictionary<string, Type>();
foreach (Type type in GetTypesInHierarchy<T>())
{
typeMapping.Add(GetDeclaredTypeName(type), type);
}
return typeMapping;
}
private static IEnumerable<Type> GetTypesInHierarchy<T>()
{
return typeof(T).Assembly.GetTypes().Where(t => typeof(T).IsAssignableFrom(t));
}
private static string GetDeclaredTypeName(Type type)
{
Debug.Assert(type != null, "type must not be null");
JsonTypeNameAttribute[] attributes = (JsonTypeNameAttribute[])type.GetCustomAttributes(
typeof(JsonTypeNameAttribute), inherit: false);
if (attributes != null && attributes.Length > 0)
{
return attributes[0].TypeName;
}
return type.Name;
}
private string GetTypeName(Type type)
{
if (!_typeToNameMap.ContainsKey(type))
{
return null;
}
return _typeToNameMap[type];
}
private Type GetTypeToCreate(JToken token)
{
JObject tokenObject = token as JObject;
if (tokenObject == null)
{
return null;
}
JProperty typeProperty = tokenObject.Property(_typePropertyName);
if (typeProperty == null)
{
return null;
}
JValue typeValue = typeProperty.Value as JValue;
if (typeValue == null)
{
return null;
}
string typeString = typeValue.Value as string;
if (typeString == null)
{
return null;
}
if (!_nameToTypeMap.ContainsKey(typeString))
{
return null;
}
return _nameToTypeMap[typeString];
}
private class NonCircularContractResolver : DefaultContractResolver
{
private readonly Type _contractType;
public NonCircularContractResolver(Type contractType)
{
Debug.Assert(contractType != null, "contract type must not be null");
_contractType = contractType;
}
protected override JsonContract CreateContract(Type objectType)
{
JsonContract contract = base.CreateContract(objectType);
if (_contractType.IsAssignableFrom(objectType))
{
contract.Converter = null;
}
return contract;
}
}
}
}