Skip to content

Instantly share code, notes, and snippets.

@shadeglare
Last active February 21, 2021 14:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shadeglare/6b46baa340346e575b2751475733405c to your computer and use it in GitHub Desktop.
Save shadeglare/6b46baa340346e575b2751475733405c to your computer and use it in GitHub Desktop.
Discriminating Unions
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 };
}
[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; }
}
[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);
}
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; }
}
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!;
}
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;
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 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);
}
}
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