I have an abstract class for a DomainEvent
and several derived classes for the actual events. I want to serialise these events into JSON with an envelope format such as:
{
type: "event.foo",
payload: {
...
}
}
Between the domain events and the serialisation I want to define some DTOs for the events to define the shape of the JSON. The last constraint is that the class doing the mapping will be storing the events in the DomainEvent
base type. Somehow I need to get this base type into a mapping function for the correct derived type so that I can put it into the payload of the envelope.
My current approach is to use attributes to tag the mapper functions and use reflection to initialise a mapper singleton with a lookup of type to mapper function. These functions operate using object
types so they can be stored together. There is a lot of weird stuff going on with this implementation and I am wondering if there is a better approach to solving this problem?
EDIT:
Here is my current code. It works fine but I am interested in alternative solutions:
public class EventSerializer
{
private readonly IDictionary<Type, Func<object, object>> _sourceTypeToDtoMappings = new Dictionary<Type, Func<object, object>>();
public EventSerializer()
{
var mapFunctions = Assembly.GetExecutingAssembly()
.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(m => m.GetCustomAttribute(typeof(MapsEventAttribute), false) != null);
foreach (var mapFunction in mapFunctions)
{
var functionParameters = mapFunction.GetParameters();
if (functionParameters.Length != 1)
throw new InvalidOperationException($"Map function {mapFunction.Name} must have exactly one parameter");
var inputType = mapFunction.GetParameters().FirstOrDefault()?.ParameterType
?? throw new InvalidOperationException("Map function must have an input parameter");
if (mapFunction.ReturnType == typeof(void))
throw new InvalidOperationException("Map function must have a return type");
_sourceTypeToDtoMappings.Add(inputType, obj => mapFunction.Invoke(null, new[] { obj })!);
}
}
public string Serialize(GameEvent gameEvent)
{
var payload = DynamicMap(gameEvent, gameEvent.GetType());
var dto = new GameEventDto
{
Type = gameEvent.GetType().Name,
Payload = payload
};
return JsonSerializer.Serialize(dto);
}
private object DynamicMap(object obj, Type sourceType)
{
var success = _sourceTypeToDtoMappings.TryGetValue(sourceType, out var mapper);
if (!success)
throw new InvalidOperationException($"No mapping found for type {sourceType}");
return mapper!(obj);
}
}
I moved the dictionary initialisation into the constructor to make it easier to see the whole picture here.