Skip to content

Commit

Permalink
Break out the serializer registry into its own class
Browse files Browse the repository at this point in the history
  • Loading branch information
halgari committed Jan 11, 2024
1 parent 66e6b5c commit 8d4006f
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public ScalarAccumulator<Type> CreateAccumulator()
/// <returns></returns>
public Type Get<TCtx>(TCtx context, EntityId owner) where TCtx : IEntityContext
{
EntityStructureRegistry.Register(this);
if (context.GetReadOnlyAccumulator<IEntity, TypeAttributeDefinition, ScalarAccumulator<Type>>(
new EntityId<IEntity>(owner), this, out var accumulator))
return accumulator.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public void Link<TContext>(TContext context, EntityId<TOwner> owner, EntityId<TO
{
if (context.GetAccumulator<TOwner, EntityAttributeDefinition<TOwner, TOther>, ScalarAccumulator<EntityId<TOther>>>(owner, this, out var accumulator))
accumulator.Value = value;
EntityStructureRegistry.Register(this);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography.X509Certificates;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// Contains structure information about entities (what attributes they have, etc).
/// </summary>
public static class EntityStructureRegistry
{
private static readonly ConcurrentDictionary<Type, ConcurrentDictionary<string, IAttribute>> _entityStructures = new();

/// <summary>
/// Register an attribute in the global registry.
/// </summary>
/// <param name="attribute"></param>
public static void Register(IAttribute attribute)
{
TOP:
if (_entityStructures.TryGetValue(attribute.Owner, out var found))
{
found.TryAdd(attribute.Name, attribute);
return;
}

var dict = new ConcurrentDictionary<string, IAttribute>();
dict.TryAdd(attribute.Name, attribute);
if (!_entityStructures.TryAdd(attribute.Owner, dict))
{
goto TOP;
}
}

/// <summary>
/// Returns all attributes for the given entity type.
/// </summary>
/// <param name="owner"></param>
/// <returns></returns>
public static bool TryGetAttributes(Type owner, [NotNullWhen(true)] out ConcurrentDictionary<string, IAttribute>? result)
{
if (_entityStructures.TryGetValue(owner, out var found ))
{
result = found;
return true;
}

result = default!;
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,27 @@ namespace NexusMods.EventSourcing.Abstractions;
/// A collection of entity links to other entities. Think of this as a one to many FK relationship in a
/// database.
/// </summary>
/// <param name="name"></param>
/// <typeparam name="TOwner"></typeparam>
/// <typeparam name="TOther"></typeparam>
public class MultiEntityAttributeDefinition<TOwner, TOther>(string name) : IAttribute<MultiEntityAccumulator<TOther>>
public class MultiEntityAttributeDefinition<TOwner, TOther> : IAttribute<MultiEntityAccumulator<TOther>>
where TOwner : AEntity<TOwner>, IEntity
where TOther : AEntity<TOther>
{
private readonly string _name;

/// <summary>
/// A collection of entity links to other entities. Think of this as a one to many FK relationship in a
/// database.
/// </summary>
/// <param name="name"></param>
/// <typeparam name="TOwner"></typeparam>
/// <typeparam name="TOther"></typeparam>
public MultiEntityAttributeDefinition(string name)
{
_name = name;
EntityStructureRegistry.Register(this);
}

/// <inheritdoc />
public MultiEntityAccumulator<TOther> CreateAccumulator()
{
Expand Down Expand Up @@ -85,7 +99,7 @@ public ReadOnlyObservableCollection<TOther> Get(TOwner entity)
public Type Owner => typeof(TOwner);

/// <inheritdoc />
public string Name => name;
public string Name => _name;
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public void Set<TContext>(TContext context, EntityId<TOwner> owner, TType value)
{
if (context.GetAccumulator<TOwner, ScalarAttribute<TOwner, TType>, ScalarAccumulator<TType>>(owner, this, out var accumulator))
accumulator.Value = value;
EntityStructureRegistry.Register(this);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace NexusMods.EventSourcing.Abstractions.Serialization;

/// <summary>
/// A registry of serializers, this class can be queried at runtime to get a serializer for a given type.
/// The registry is populated by the DI container, and the GetSerializer method is backed by a cache, so
/// calling it in inner loops is not a problem.
/// </summary>
public interface ISerializationRegistry
{
/// <summary>
/// Gets a serializer that can serialize the given type.
/// </summary>
/// <param name="serializedType"></param>
/// <returns></returns>
public ISerializer GetSerializer(Type serializedType);

/// <summary>
/// Register a serializer for a given type, used to override the default serializers.
/// </summary>
/// <param name="serializedType"></param>
/// <param name="serializer"></param>
public void RegisterSerializer(Type serializedType, ISerializer serializer);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public sealed class BinaryEventSerializer : IEventSerializer, IVariableSizeSeria

private readonly Dictionary<Type, EventSerializerDelegate> _serializerDelegates = new();
private readonly Dictionary<UInt128, EventDeserializerDelegate> _deserializerDelegates = new();
private readonly ISerializationRegistry _serializerRegistry;

/// <summary>
/// Write an event to the given writer, and return the
Expand All @@ -29,17 +30,19 @@ public sealed class BinaryEventSerializer : IEventSerializer, IVariableSizeSeria

private delegate int EventDeserializerDelegate(ReadOnlySpan<byte> data, out IEvent @event);

public BinaryEventSerializer(IEnumerable<ISerializer> diInjectedSerializers, IEnumerable<EventDefinition> eventDefinitions)
public BinaryEventSerializer(ISerializationRegistry registry, IEnumerable<EventDefinition> eventDefinitions)
{
_serializerRegistry = registry;
_writer = new PooledMemoryBufferWriter();
PopulateSerializers(diInjectedSerializers.ToArray(), eventDefinitions.ToArray());
_serializerRegistry.RegisterSerializer(typeof(IEvent), this);
PopulateSerializers(eventDefinitions.ToArray());
}

private void PopulateSerializers(ISerializer[] diInjectedSerializers, EventDefinition[] eventDefinitions)
private void PopulateSerializers(EventDefinition[] eventDefinitions)
{
foreach (var eventDefinition in eventDefinitions)
{
var (serializer, deserializer) = MakeSerializer(eventDefinition, diInjectedSerializers);
var (serializer, deserializer) = MakeSerializer(eventDefinition);
_serializerDelegates[eventDefinition.Type] = serializer;
_deserializerDelegates[eventDefinition.Id] = deserializer;
}
Expand All @@ -60,16 +63,22 @@ public IEvent Deserialize(ReadOnlySpan<byte> data)
return @event;
}

/// <summary>
/// Gets a serializer that can serialize the given type, for IEvent types, this class is returned.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private ISerializer GetSerializer(Type type) => type == typeof(IEvent) ? this : _serializerRegistry.GetSerializer(type);

private (EventSerializerDelegate, EventDeserializerDelegate) MakeSerializer(EventDefinition definition, ISerializer[] serializers)
private (EventSerializerDelegate, EventDeserializerDelegate) MakeSerializer(EventDefinition definition)
{
var deconstructParams = definition.Type.GetMethod("Deconstruct")?.GetParameters().ToArray()!;
var ctorParams = definition.Type.GetConstructors()
.First(c => c.GetParameters().Length == deconstructParams.Length)
.GetParameters();

var paramDefinitions = deconstructParams.Zip(ctorParams)
.Select(p => (p.First, p.Second, Expression.Variable(p.Second.ParameterType, p.Second.Name), GetSerializer(serializers, p.Second.ParameterType)))
.Select(p => (p.First, p.Second, Expression.Variable(p.Second.ParameterType, p.Second.Name), GetSerializer(p.Second.ParameterType)))
.ToArray();


Expand Down Expand Up @@ -215,41 +224,7 @@ private EventDeserializerDelegate BuildVariableSizeDeserializer(EventDefinition
return lambda.Compile();
}

private ISerializer GetSerializer(ISerializer[] serializers, Type type)
{
if (type == typeof(IEvent))
{
return this;
}

var result = serializers.FirstOrDefault(s => s.CanSerialize(type));
if (result != null)
{
return result;
}

if (type.IsConstructedGenericType)
{
var genericMakers = serializers.OfType<IGenericSerializer>();
foreach (var maker in genericMakers)
{
if (maker.TrySpecialize(type.GetGenericTypeDefinition(),
type.GetGenericArguments(), t => GetSerializer(serializers, t), out var serializer))
{
return serializer;
}
}
}

if (type.IsArray)
{
var arrayMaker = serializers.OfType<GenericArraySerializer>().First();
arrayMaker.TrySpecialize(type, [type.GetElementType()!], t => GetSerializer(serializers, t), out var serializer);
return serializer!;
}

throw new Exception($"No serializer found for {type}");
}

private EventDeserializerDelegate BuildFixedSizeDeserializer(EventDefinition definitions, MemberDefinition[] allDefinitions, List<MemberDefinition> fixedParams, int fixedSize)
{
Expand Down Expand Up @@ -354,6 +329,7 @@ private Expression MakeReadonlyWindowExpression(Expression span, int offset, int
private MethodInfo _readonlySliceFastStartMethodInfo =
typeof(BinaryEventSerializer).GetMethod(nameof(ReadOnlySliceFastStart), BindingFlags.Static | BindingFlags.NonPublic)!;


private Expression MakeReadonlyWindowExpression(Expression span, int offset)
{
return Expression.Call(null, _readonlySliceFastStartMethodInfo, span, Expression.Constant(offset));
Expand Down
82 changes: 82 additions & 0 deletions src/NexusMods.EventSourcing/Serialization/SerializationRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using NexusMods.EventSourcing.Abstractions.Serialization;

namespace NexusMods.EventSourcing.Serialization;

/// <summary>
/// Manages ISerializer instances, specializes them and manages a cache for them for serialized types.
/// </summary>
public class SerializationRegistry : ISerializationRegistry
{
private ConcurrentDictionary<Type, ISerializer> _cachedSerializers = new();
private readonly ISerializer[] _diInjectedSerializers;
private readonly IGenericSerializer[] _genericSerializers;
private readonly GenericArraySerializer _arraySerializer;

/// <summary>
/// DI constructor.
/// </summary>
/// <param name="diInjectedSerializers"></param>
public SerializationRegistry(IEnumerable<ISerializer> diInjectedSerializers)
{
_diInjectedSerializers = diInjectedSerializers.ToArray();
_genericSerializers = _diInjectedSerializers.OfType<IGenericSerializer>().ToArray();
_arraySerializer = _diInjectedSerializers.OfType<GenericArraySerializer>().First();
}

/// <summary>
/// Gets a serializer that can serialize the given type.
/// </summary>
/// <param name="type"></param>
/// <param name="recursiveGetSerializer">Called when the cache needs to recursively create another type serializer</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public ISerializer GetSerializer(Type type)
{
TOP:
if (_cachedSerializers.TryGetValue(type, out var found))
return found;

var result = _diInjectedSerializers.FirstOrDefault(s => s.CanSerialize(type));
if (result != null)
{
return result;
}

if (type.IsConstructedGenericType)
{
foreach (var maker in _genericSerializers)
{
if (maker.TrySpecialize(type.GetGenericTypeDefinition(),
type.GetGenericArguments(), GetSerializer, out var serializer))
{
return serializer;
}
}
}

if (type.IsArray)
{
_arraySerializer.TrySpecialize(type, [type.GetElementType()!], GetSerializer, out var serializer);

if (!_cachedSerializers.TryAdd(type, serializer!))
goto TOP;
return serializer!;
}

throw new Exception($"No serializer found for {type}");
}

/// <summary>
/// Adds a serializer to the registry.
/// </summary>
/// <param name="serializedType"></param>
/// <param name="serializer"></param>
public void RegisterSerializer(Type serializedType, ISerializer serializer)
{
_cachedSerializers.TryAdd(serializedType, serializer);
}
}
1 change: 1 addition & 0 deletions src/NexusMods.EventSourcing/Services.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static class Services
public static IServiceCollection AddEventSourcing(this IServiceCollection services)
{
return services
.AddSingleton<ISerializationRegistry, SerializationRegistry>()
.AddSingleton<ISerializer, GenericArraySerializer>()
.AddSingleton<ISerializer, GenericEntityIdSerializer>()
.AddSingleton<ISerializer, StringSerializer>()
Expand Down
13 changes: 2 additions & 11 deletions tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using DynamicData;
using NexusMods.EventSourcing.Abstractions;

Expand All @@ -10,16 +9,8 @@ public class Loadout(IEntityContext context, EntityId<Loadout> id) : AEntity<Loa
/// <summary>
/// The human readable name of the loadout.
/// </summary>
public string Name
{
get
{
CallSite<Func<int, float>> site;
return _name.Get(this);
}
}

internal static readonly dynamic _name = new ScalarAttribute<Loadout, string>(nameof(Name));
public string Name => _name.Get(this);
internal static readonly ScalarAttribute<Loadout, string> _name = new(nameof(Name));

/// <summary>
/// The mods in the loadout.
Expand Down

0 comments on commit 8d4006f

Please sign in to comment.