Last active
February 21, 2021 14:12
-
-
Save shadeglare/6b46baa340346e575b2751475733405c to your computer and use it in GitHub Desktop.
Discriminating Unions
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.Text.Json; | |
var inProgress = StatusFactory.InProgress(); | |
var complete = StatusFactory.Complete("Expected data"); | |
var failure = StatusFactory.Failure(404); | |
var options = new JsonSerializerOptions | |
{ | |
PropertyNamingPolicy = JsonNamingPolicy.CamelCase | |
}; | |
Console.WriteLine(JsonSerializer.Serialize(inProgress, options)); | |
Console.WriteLine(JsonSerializer.Serialize(complete, options)); | |
Console.WriteLine(JsonSerializer.Serialize(failure, options)); | |
public static class StatusFactory | |
{ | |
public static Status InProgress() => new InProgressStatus { Started = DateTime.UtcNow }; | |
public static Status Complete(String result) => new CompleteStatus { Result = result }; | |
public static Status Failure(Int32 errorCode) => new FailureStatus { ErrorCode = errorCode }; | |
} |
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
[UnionTag(nameof(Kind))] | |
[UnionCase(typeof(InProgressStatus), StatusKind.InProgress)] | |
[UnionCase(typeof(CompleteStatus), StatusKind.Complete)] | |
[UnionCase(typeof(FailureStatus), StatusKind.Failure)] | |
public abstract class Status | |
{ | |
public abstract String Kind { get; } | |
} |
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
[AttributeUsage(AttributeTargets.Class)] | |
public sealed class UnionTagAttribute : Attribute | |
{ | |
public String TagPropertyName { get; } | |
public UnionTagAttribute(String tagPropertyName) => this.TagPropertyName = tagPropertyName; | |
} | |
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] | |
public sealed class UnionCaseAttribute : Attribute | |
{ | |
public Type CaseType { get; } | |
public String TagPropertyValue { get; } | |
public UnionCaseAttribute(Type caseType, String tagPropertyValue) => | |
(this.CaseType, this.TagPropertyValue) = (caseType, tagPropertyValue); | |
} |
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.Buffers; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reflection; | |
using System.Text.Json; | |
using System.Text.Json.Serialization; | |
var inProgress = StatusFactory.InProgress(); | |
var complete = StatusFactory.Complete("Expected data"); | |
var failure = StatusFactory.Failure(404); | |
var options = new JsonSerializerOptions | |
{ | |
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | |
Converters = { new UnionConverterFactory() } | |
}; | |
Console.WriteLine(JsonSerializer.Serialize(inProgress, options)); | |
Console.WriteLine(JsonSerializer.Serialize(complete, options)); | |
Console.WriteLine(JsonSerializer.Serialize(failure, options)); | |
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Status>(JsonSerializer.Serialize(inProgress, options), options))); | |
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Status>(JsonSerializer.Serialize(complete, options), options))); | |
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Status>(JsonSerializer.Serialize(failure, options), options))); | |
public static class StatusFactory | |
{ | |
public static Status InProgress() => new InProgressStatus { Started = DateTime.UtcNow }; | |
public static Status Complete(String result) => new CompleteStatus { Result = result }; | |
public static Status Failure(Int32 errorCode) => new FailureStatus { ErrorCode = errorCode }; | |
} | |
public static class StatusKind | |
{ | |
public const String InProgress = nameof(InProgress); | |
public const String Complete = nameof(Complete); | |
public const String Failure = nameof(Failure); | |
} | |
[AttributeUsage(AttributeTargets.Class)] | |
public sealed class UnionTagAttribute : Attribute | |
{ | |
public String TagPropertyName { get; } | |
public UnionTagAttribute(String tagPropertyName) => this.TagPropertyName = tagPropertyName; | |
} | |
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] | |
public sealed class UnionCaseAttribute : Attribute | |
{ | |
public Type CaseType { get; } | |
public String TagPropertyValue { get; } | |
public UnionCaseAttribute(Type caseType, String tagPropertyValue) => | |
(this.CaseType, this.TagPropertyValue) = (caseType, tagPropertyValue); | |
} | |
public sealed class UnionConverter<T> : JsonConverter<T> where T : class | |
{ | |
private String TagPropertyName { get; } | |
private Dictionary<String, Type> UnionTypes { get; } | |
public UnionConverter() | |
{ | |
var type = typeof(T); | |
var unionTag = type.GetCustomAttribute<UnionTagAttribute>(); | |
if (unionTag is null) throw new InvalidOperationException(); | |
var concreteTypeFactory = type.CreateConcreteTypeFactory(); | |
this.TagPropertyName = unionTag.TagPropertyName; | |
this.UnionTypes = type.GetCustomAttributes<UnionCaseAttribute>() | |
.ToDictionary(k => k.TagPropertyValue, e => concreteTypeFactory(e.CaseType)); | |
} | |
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |
{ | |
using var document = JsonDocument.ParseValue(ref reader); | |
var propertyName = options.PropertyNamingPolicy?.ConvertName(this.TagPropertyName) ?? this.TagPropertyName; | |
var property = document.RootElement.GetProperty(propertyName); | |
var type = this.UnionTypes[property.GetString() ?? throw new InvalidOperationException()]; | |
return (T?)document.ToObject(type, options); | |
} | |
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => | |
JsonSerializer.Serialize(writer, value, value.GetType(), options); | |
} | |
public sealed class UnionConverterFactory : JsonConverterFactory | |
{ | |
public override Boolean CanConvert(Type typeToConvert) => | |
typeToConvert.GetCustomAttribute<UnionTagAttribute>(false) is not null; | |
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) | |
{ | |
var converterType = typeof(UnionConverter<>).MakeGenericType(typeToConvert); | |
return (JsonConverter?)Activator.CreateInstance(converterType); | |
} | |
} | |
public static class TypeExtensions | |
{ | |
public static Func<Type, Type> CreateConcreteTypeFactory(this Type type) | |
{ | |
if (type.IsGenericType) | |
{ | |
var genericArgs = type.GetGenericArguments(); | |
return givenType => givenType.MakeGenericType(genericArgs); | |
} | |
else | |
{ | |
return givenType => givenType; | |
} | |
} | |
} | |
public static class JsonExtensions | |
{ | |
public static Object? ToObject(this JsonElement element, Type type, JsonSerializerOptions options) | |
{ | |
var bufferWriter = new ArrayBufferWriter<Byte>(); | |
using (var writer = new Utf8JsonWriter(bufferWriter)) | |
{ | |
element.WriteTo(writer); | |
} | |
return JsonSerializer.Deserialize(bufferWriter.WrittenSpan, type, options); | |
} | |
public static Object? ToObject(this JsonDocument document, Type type, JsonSerializerOptions options) | |
{ | |
if (document is null) throw new ArgumentNullException(nameof(document)); | |
return document.RootElement.ToObject(type, options); | |
} | |
} | |
[UnionTag(nameof(Kind))] | |
[UnionCase(typeof(InProgressStatus), StatusKind.InProgress)] | |
[UnionCase(typeof(CompleteStatus), StatusKind.Complete)] | |
[UnionCase(typeof(FailureStatus), StatusKind.Failure)] | |
public abstract class Status | |
{ | |
public abstract String Kind { get; } | |
} | |
public sealed class InProgressStatus : Status | |
{ | |
public override String Kind => StatusKind.InProgress; | |
public DateTime Started { get; init; } | |
} | |
public sealed class CompleteStatus : Status | |
{ | |
public override String Kind => StatusKind.Complete; | |
public String Result { get; init; } = default!; | |
} | |
public sealed class FailureStatus : Status | |
{ | |
public override String Kind => StatusKind.Failure; | |
public Int32 ErrorCode { get; init; } | |
} |
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; | |
public static class StatusKind | |
{ | |
public const String InProgress = nameof(InProgress); | |
public const String Complete = nameof(Complete); | |
public const String Failure = nameof(Failure); | |
} | |
public abstract class Status | |
{ | |
public abstract String Kind { get; } | |
} | |
public sealed class InProgressStatus : Status | |
{ | |
public override String Kind => StatusKind.InProgress; | |
public DateTime Started { get; init; } | |
} | |
public sealed class CompleteStatus : Status | |
{ | |
public override String Kind => StatusKind.Complete; | |
public String Result { get; init; } = default!; | |
} |
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
interface InProgressStatus { | |
readonly kind: "InProgress"; | |
readonly started: Date; | |
} | |
interface SuccessStatus { | |
readonly kind: "Success"; | |
readonly result: string; | |
} | |
interface FailureStatus { | |
readonly kind: "Failure"; | |
readonly errorCode: number; | |
} | |
type Status = | |
| InProgressStatus | |
| SuccessStatus | |
| FailureStatus; |
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
var inProgress = StatusFactory.InProgress(); | |
var complete = StatusFactory.Complete("Expected data"); | |
var failure = StatusFactory.Failure(404); | |
var options = new JsonSerializerOptions | |
{ | |
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | |
Converters = { new UnionConverterFactory() } | |
}; | |
Console.WriteLine(JsonSerializer.Serialize(inProgress, options)); | |
Console.WriteLine(JsonSerializer.Serialize(complete, options)); | |
Console.WriteLine(JsonSerializer.Serialize(failure, options)); | |
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Status>(JsonSerializer.Serialize(inProgress, options), options))); | |
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Status>(JsonSerializer.Serialize(complete, options), options))); | |
Console.WriteLine(ObjectDumper.Dump(JsonSerializer.Deserialize<Status>(JsonSerializer.Serialize(failure, options), options))); |
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
public sealed class UnionConverter<T> : JsonConverter<T> where T : class | |
{ | |
private String TagPropertyName { get; } | |
private Dictionary<String, Type> UnionTypes { get; } | |
public UnionConverter() | |
{ | |
var type = typeof(T); | |
var unionTag = type.GetCustomAttribute<UnionTagAttribute>(); | |
if (unionTag is null) throw new InvalidOperationException(); | |
var concreteTypeFactory = type.CreateConcreteTypeFactory(); | |
this.TagPropertyName = unionTag.TagPropertyName; | |
this.UnionTypes = type.GetCustomAttributes<UnionCaseAttribute>() | |
.ToDictionary(k => k.TagPropertyValue, e => concreteTypeFactory(e.CaseType)); | |
} | |
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | |
{ | |
using var document = JsonDocument.ParseValue(ref reader); | |
var propertyName = options.PropertyNamingPolicy?.ConvertName(this.TagPropertyName) ?? this.TagPropertyName; | |
var property = document.RootElement.GetProperty(propertyName); | |
var type = this.UnionTypes[property.GetString() ?? throw new InvalidOperationException()]; | |
return (T?)document.ToObject(type, options); | |
} | |
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => | |
JsonSerializer.Serialize(writer, value, value.GetType(), options); | |
} | |
public static class TypeExtensions | |
{ | |
public static Func<Type, Type> CreateConcreteTypeFactory(this Type type) | |
{ | |
if (type.IsGenericType) | |
{ | |
var genericArgs = type.GetGenericArguments(); | |
return givenType => givenType.MakeGenericType(genericArgs); | |
} | |
else | |
{ | |
return givenType => givenType; | |
} | |
} | |
} | |
public static class JsonExtensions | |
{ | |
public static Object? ToObject(this JsonElement element, Type type, JsonSerializerOptions options) | |
{ | |
var bufferWriter = new ArrayBufferWriter<Byte>(); | |
using (var writer = new Utf8JsonWriter(bufferWriter)) | |
{ | |
element.WriteTo(writer); | |
} | |
return JsonSerializer.Deserialize(bufferWriter.WrittenSpan, type, options); | |
} | |
public static Object? ToObject(this JsonDocument document, Type type, JsonSerializerOptions options) | |
{ | |
if (document is null) throw new ArgumentNullException(nameof(document)); | |
return document.RootElement.ToObject(type, options); | |
} | |
} |
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
public sealed class UnionConverterFactory : JsonConverterFactory | |
{ | |
public override Boolean CanConvert(Type typeToConvert) => | |
typeToConvert.GetCustomAttribute<UnionTagAttribute>(false) is not null; | |
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) | |
{ | |
var converterType = typeof(UnionConverter<>).MakeGenericType(typeToConvert); | |
return (JsonConverter?)Activator.CreateInstance(converterType); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment