Last active
March 8, 2021 14:33
-
-
Save AlexeyRaga/5bbcfdc8b5b5de7d710bd006efd96c17 to your computer and use it in GitHub Desktop.
Serialisation stuff
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
// A Codec interface. Specific _CODECS_ (and not messages) | |
// should be implementing this interface. | |
public interface IMessageCodec<T> | |
{ | |
string Encode(T value); | |
T Decode(string value); | |
} |
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
// Let's say that we have some data type called SampleEvent | |
// that we want to be able to encode and decode | |
// | |
// I specifically didn't want it to implement things like | |
// Json encoding/decoding interfaces because it doesn't seem | |
// to be a concern of this type. | |
// Why should it care about how (and if) I chose to persist it?! | |
public sealed class SampleEvent | |
{ | |
public string Value { get; set; } | |
} |
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
// Then we can write a codec FOR that specific `SampleEvent` message | |
// Without messing with that event's hierarchy and without | |
// requiring it to implement extra interfaces. | |
// Because how we want to serialise something in different parts of | |
// our system should not be a concern of that "something", | |
// we should be able to do it externally. | |
// | |
// These implementations can serialise messages to/from JSON, | |
// or Protobuf+Base64 or whatever. Their responsibility. | |
public sealed class MessageCodecA : IMessageCodec<SampleEvent> | |
{ | |
public string Encode(SampleEvent value) | |
{ | |
return value.Value; | |
} | |
public SampleEvent Decode(string value) | |
{ | |
return new SampleEvent() { Value = value }; | |
} | |
} |
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
// An intermediate representation for a serialised message. | |
// Because we need to keep track of what the message type was. | |
// Potentially can carry some extra information, too: | |
// a property `Headers` can be added to enrich the message with some | |
// "service" information like timestamps, originator info, etc. | |
public sealed class Envelope | |
{ | |
public string Type { get; private set; } | |
public string Body { get; private set; } | |
public Envelope(string type, string body) | |
{ | |
Type = type; | |
Body = body; | |
} | |
} |
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
// Let's see how it all works first | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
// Let's test! | |
var msg = new SampleEvent() {Value = "Hi there!"}; | |
var env = Codec.EncodeEnvelope(msg); | |
var res = Codec.DecodeEnvelope(env); | |
if (((SampleEvent)res).Value == msg.Value) | |
{ | |
Console.WriteLine("The world is safe!"); | |
} | |
else | |
{ | |
Console.WriteLine("Boo!"); | |
} | |
Console.WriteLine("Hello World!"); | |
} | |
} |
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 static class Codec | |
{ | |
// Find and store all the codecs that can be found in a system. | |
// Key is a message type | |
// Value is an untyped codec wrapper for a given message type. | |
// | |
// Unfortunately C# doesn't have proper classes, so emulate | |
// them this way | |
private static Dictionary<string, UntypedCodec> _allCodecs = FindCodecs(); | |
// Given an envelope, decode the value | |
public static object DecodeEnvelope(Envelope envelope) | |
{ | |
return _allCodecs[envelope.Type].Decode(envelope.Body); | |
} | |
// Given the value, prepare and envelope with the value encoded | |
public static Envelope EncodeEnvelope(object value) | |
{ | |
var type = value.GetType().FullName; | |
var payload = _allCodecs[type].Encode(value); | |
return new Envelope(type, payload); | |
} | |
// A working horse. | |
// We will: | |
// - find all codecs (final classes that implement ICodec<>), | |
// - wrap them into an untyped wrapper (so that we can use them uniformly without worrying about generics) | |
// - ensure global coherence (each message type should only have one codec, globally) | |
// | |
// Warning: A lot of reflection, ugly, look away :) | |
private static Dictionary<string, UntypedCodec> FindCodecs() | |
{ | |
// This is what we are looking for | |
var codecType = typeof(IMessageCodec<>); | |
// All sealed classes that implement IMessageCodec<>, ever | |
var allCodecs = | |
from asm in AppDomain.CurrentDomain.GetAssemblies() | |
from typ in asm.GetTypes() | |
where !typ.IsAbstract && !typ.IsInterface && typ.IsSealed && typ.GetConstructor(Type.EmptyTypes) != null | |
from inf in typ.GetInterfaces() | |
let bt = typ.BaseType | |
where inf.IsGenericType && codecType.IsAssignableFrom(inf.GetGenericTypeDefinition()) | |
select new { MessageType = inf.GetGenericArguments()[0], CodecType = typ, CodecI = inf }; | |
// Group all the codecs by types of messages that they handle | |
var codecsPerType = allCodecs.ToList() | |
.GroupBy(x => x.MessageType) | |
.Select(x => new | |
{ | |
MessageType = x.Key, | |
Codecs = x.Select(y => new {CodecType = y.CodecType, CodecI = y.CodecI}).ToList() | |
}) | |
.ToList(); | |
// ============== COHERENCE CHECK ================================= | |
// Optionally check that each message type has only one codec. | |
// This is a nice property to have... | |
// First see if there are any message types that have more than 1 codec | |
var incoherentCodecs = codecsPerType | |
.Where(x => x.Codecs.Count != 1); | |
// And if there any, do something with it. | |
// I would probably prefer stopping the world here and fail with some incoherence exception | |
foreach (var codec in incoherentCodecs) | |
{ | |
Console.WriteLine($"Type {codec.MessageType.FullName} has more than one codecs defined: {codec.Codecs.Select(x=>x.CodecType.FullName).ToList()}"); | |
} | |
// ============== END COHERENCE CHECK ============================== | |
return codecsPerType | |
.Select(x => | |
{ | |
var key = x.MessageType; | |
var ct = x.Codecs.First(); //we know that there is only one | |
// Instantiate the codec | |
var inst = Activator.CreateInstance(ct.CodecType); | |
var encode = ct.CodecI.GetMethod("Encode"); | |
var decode = ct.CodecI.GetMethod("Decode"); | |
// And wrap it into an untyped wrapper which just delegates to the "real" one | |
var untypedCodec = new UntypedCodec( | |
x => (string) encode.Invoke(inst, new object[] {x}), | |
x => decode.Invoke(inst, new object[] {x}) | |
); | |
return (x.MessageType, untypedCodec); | |
}) | |
.ToDictionary(x => x.Item1.FullName, x => x.Item2); | |
} | |
} | |
// That untyped wrapper, nothing special about it. | |
internal sealed class UntypedCodec | |
{ | |
private Func<string, object> _decode; | |
private Func<object, string> _encode; | |
public UntypedCodec(Func<object, string> encode, Func<string, object> decode) | |
{ | |
_decode = decode; | |
_encode = encode; | |
} | |
public object Decode(string value) => _decode(value); | |
public string Encode(object value) => _encode(value); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment