Skip to content

Instantly share code, notes, and snippets.

@AlexeyRaga
Last active March 8, 2021 14:33
Show Gist options
  • Save AlexeyRaga/5bbcfdc8b5b5de7d710bd006efd96c17 to your computer and use it in GitHub Desktop.
Save AlexeyRaga/5bbcfdc8b5b5de7d710bd006efd96c17 to your computer and use it in GitHub Desktop.
Serialisation stuff
// 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);
}
// 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; }
}
// 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 };
}
}
// 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;
}
}
// 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!");
}
}
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