Created
December 15, 2019 21:05
-
-
Save keith5000/43a51fc521b993454d503ab3af67c9f1 to your computer and use it in GitHub Desktop.
JSON converter to support derived types/polymorphism (ASP.NET Core 3)
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
/* | |
This gist uses a custom JsonConverter to support derived types in ASP.NET Core 3 requests and responses. By default, .NET Core | |
does not support derived types in JSON serialization; the properties of derived types are not serialized in responses, and in | |
requests there is inherently a lack of support in JSON for identifying inherited types. | |
More info: https://stackoverflow.com/questions/59308763/derived-types-properties-missing-in-json-response-from-asp-net-core-api | |
To implement this code in your project: | |
1. Copy the DerivedTypeJsonConverter class below into your project. | |
2. For each of your base types, create a class that derives from DerivedTypeJsonConverter. Follow the MyResultJsonConverter | |
example below. | |
3. Register the converters in Startup.cs. | |
4. In requests to the API, objects of derived types will need to include a $type property. | |
Example (using the StringResult type from below): { "Value":"Hi!", "$type":"StringResult" } | |
*/ | |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |
// CODE TO COPY | |
using System; | |
using System.Collections.Generic; | |
using System.Dynamic; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
/// <summary> | |
/// <para> | |
/// A base JSON converter that allows objects accepted or returned by the API using a base type or interface to be converted to a derived type. The derived type is | |
/// specified in the object's JSON in a field named $typeName. The actual format of this value is up to the classes that derive from this class. | |
/// </para> | |
/// <para> | |
/// Create a derived converter class for each base type or interface for which derived types will be passed to or received from the API methods. Each derived converter class | |
/// should 1) override <see cref="DerivedTypeJsonConverter{TBase}.NameToType(string)"/>, which receives the value of the $typeName field, and should instantiate objects based | |
/// on the $typeName value, and 2) override <see cref="DerivedTypeJsonConverter{TBase}.TypeToName(Type)"/> which returns the $typeName value to use based on the object's type. | |
/// </para> | |
/// </summary> | |
/// <typeparam name="TBase"></typeparam> | |
/// <remarks> | |
/// <para>This is almost what JSON.Net's "type name handling" does. However, type naming handling emits the object's type along with its namespace and assembly.</para> | |
/// </remarks> | |
public abstract class DerivedTypeJsonConverter<TBase> : JsonConverter<TBase> | |
{ | |
#region Abstract members | |
/// <summary> | |
/// Returns the value to use for the $type property. | |
/// </summary> | |
/// <returns></returns> | |
protected abstract string TypeToName(Type type); | |
/// <summary> | |
/// Returns the type that corresponds to the specified $type value. | |
/// </summary> | |
/// <param name="typeName"></param> | |
/// <returns></returns> | |
protected abstract Type NameToType(string typeName); | |
#endregion | |
#region Properties | |
/// <summary> | |
/// The name of the "type" property in JSON. | |
/// </summary> | |
private const string TypePropertyName = "$type"; | |
#endregion | |
#region JsonConverter implementation | |
public override bool CanConvert(Type objectType) | |
{ | |
return typeof(TBase) == objectType; | |
} | |
public override TBase Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |
{ | |
// get the $type value by parsing the JSON string into a JsonDocument | |
JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader); | |
jsonDocument.RootElement.TryGetProperty(TypePropertyName, out JsonElement typeNameElement); | |
string typeName = (typeNameElement.ValueKind == JsonValueKind.String) ? typeNameElement.GetString() : null; | |
if (string.IsNullOrWhiteSpace(typeName)) throw new InvalidOperationException($"Missing or invalid value for {TypePropertyName} (base type {typeof(TBase).FullName})."); | |
// get the JSON text that was read by the JsonDocument | |
string json; | |
using (var stream = new MemoryStream()) | |
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Encoder = options.Encoder })) { | |
jsonDocument.WriteTo(writer); | |
writer.Flush(); | |
json = Encoding.UTF8.GetString(stream.ToArray()); | |
} | |
// deserialize the JSON to the type specified by $type | |
try { | |
return (TBase)JsonSerializer.Deserialize(json, NameToType(typeName), options); | |
} | |
catch (Exception ex) { | |
throw new InvalidOperationException("Invalid JSON in request.", ex); | |
} | |
} | |
public override void Write(Utf8JsonWriter writer, TBase value, JsonSerializerOptions options) | |
{ | |
// create an ExpandoObject from the value to serialize so we can dynamically add a $type property to it | |
ExpandoObject expando = ToExpandoObject(value); | |
expando.TryAdd(TypePropertyName, TypeToName(value.GetType())); | |
// serialize the expando | |
JsonSerializer.Serialize(writer, expando, options); | |
} | |
#endregion | |
#region Private methods | |
/// <summary> | |
/// Returns an <see cref="ExpandoObject"/> whose values are copied from the specified object's public properties. | |
/// </summary> | |
/// <param name="obj"></param> | |
/// <returns></returns> | |
private static ExpandoObject ToExpandoObject(object obj) | |
{ | |
var expando = new ExpandoObject(); | |
if (obj != null) { | |
// copy all public properties | |
foreach (PropertyInfo property in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanRead)) { | |
expando.TryAdd(property.Name, property.GetValue(obj)); | |
} | |
} | |
return expando; | |
} | |
#endregion | |
} |
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; | |
// Sample base type | |
public interface IMyResult { } | |
// Sample derived type | |
public class StringResult : IMyResult | |
{ | |
public string Value { get; set; } | |
} | |
// Example of how to implement DerivedTypeJsonConverter | |
public class MyResultJsonConverter : DerivedTypeJsonConverter<IMyResult> | |
{ | |
protected override Type NameToType(string typeName) | |
{ | |
return typeName switch | |
{ | |
nameof(StringResult) => typeof(StringResult), | |
// TODO: Create a case for each derived type | |
_ => throw new InvalidRequestException($"Unsupported type string \"{typeName}\".") | |
}; | |
} | |
protected override string TypeToName(Type type) | |
{ | |
if (type == typeof(StringResult)) return nameof(StringResult); | |
// TODO: Create a condition for each derived type | |
throw new ArgumentException($"Type {type.FullName} not supported.", nameof(type)); | |
} | |
} |
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
/* | |
In ConfigureServices, modify the call to AddControllers to add your converters: | |
*/ | |
services.AddControllers() | |
.AddJsonOptions(options => { | |
options.JsonSerializerOptions.Converters.Add(new MyResultJsonConverter()); | |
// TODO: Add each converter | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment