From 8d4006fc5d39f8528a0327a7b2daa794b0e0632d Mon Sep 17 00:00:00 2001 From: halgari Date: Thu, 11 Jan 2024 09:34:50 -0700 Subject: [PATCH] Break out the serializer registry into its own class --- .../TypeAttributeDefinition.cs | 1 + .../EntityAttributeDefinition.cs | 1 + .../EntityStructureRegistry.cs | 54 ++++++++++++ .../MultiEntityAttributeDefinition.cs | 20 ++++- .../ScalarAttribute.cs | 1 + .../Serialization/ISerializationRegistry.cs | 25 ++++++ ...Serializer.cs => BinaryEventSerializer.cs} | 56 ++++--------- .../Serialization/SerializationRegistry.cs | 82 +++++++++++++++++++ src/NexusMods.EventSourcing/Services.cs | 1 + .../Model/Loadout.cs | 13 +-- 10 files changed, 200 insertions(+), 54 deletions(-) create mode 100644 src/NexusMods.EventSourcing.Abstractions/EntityStructureRegistry.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializationRegistry.cs rename src/NexusMods.EventSourcing/Serialization/{EventSerializer.cs => BinaryEventSerializer.cs} (90%) create mode 100644 src/NexusMods.EventSourcing/Serialization/SerializationRegistry.cs diff --git a/src/NexusMods.EventSourcing.Abstractions/AttributeDefinitions/TypeAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/AttributeDefinitions/TypeAttributeDefinition.cs index d0f9df10..319412ff 100644 --- a/src/NexusMods.EventSourcing.Abstractions/AttributeDefinitions/TypeAttributeDefinition.cs +++ b/src/NexusMods.EventSourcing.Abstractions/AttributeDefinitions/TypeAttributeDefinition.cs @@ -30,6 +30,7 @@ public ScalarAccumulator CreateAccumulator() /// public Type Get(TCtx context, EntityId owner) where TCtx : IEntityContext { + EntityStructureRegistry.Register(this); if (context.GetReadOnlyAccumulator>( new EntityId(owner), this, out var accumulator)) return accumulator.Value; diff --git a/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs index 8ea7da53..733973f8 100644 --- a/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs +++ b/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs @@ -31,6 +31,7 @@ public void Link(TContext context, EntityId owner, EntityId, ScalarAccumulator>>(owner, this, out var accumulator)) accumulator.Value = value; + EntityStructureRegistry.Register(this); } /// diff --git a/src/NexusMods.EventSourcing.Abstractions/EntityStructureRegistry.cs b/src/NexusMods.EventSourcing.Abstractions/EntityStructureRegistry.cs new file mode 100644 index 00000000..9052b358 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/EntityStructureRegistry.cs @@ -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; + +/// +/// Contains structure information about entities (what attributes they have, etc). +/// +public static class EntityStructureRegistry +{ + private static readonly ConcurrentDictionary> _entityStructures = new(); + + /// + /// Register an attribute in the global registry. + /// + /// + 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(); + dict.TryAdd(attribute.Name, attribute); + if (!_entityStructures.TryAdd(attribute.Owner, dict)) + { + goto TOP; + } + } + + /// + /// Returns all attributes for the given entity type. + /// + /// + /// + public static bool TryGetAttributes(Type owner, [NotNullWhen(true)] out ConcurrentDictionary? result) + { + if (_entityStructures.TryGetValue(owner, out var found )) + { + result = found; + return true; + } + + result = default!; + return false; + } + +} diff --git a/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs index 4c5b7b84..407c5271 100644 --- a/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs +++ b/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs @@ -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. /// -/// /// /// -public class MultiEntityAttributeDefinition(string name) : IAttribute> +public class MultiEntityAttributeDefinition : IAttribute> where TOwner : AEntity, IEntity where TOther : AEntity { + private readonly string _name; + + /// + /// A collection of entity links to other entities. Think of this as a one to many FK relationship in a + /// database. + /// + /// + /// + /// + public MultiEntityAttributeDefinition(string name) + { + _name = name; + EntityStructureRegistry.Register(this); + } + /// public MultiEntityAccumulator CreateAccumulator() { @@ -85,7 +99,7 @@ public ReadOnlyObservableCollection Get(TOwner entity) public Type Owner => typeof(TOwner); /// - public string Name => name; + public string Name => _name; } /// diff --git a/src/NexusMods.EventSourcing.Abstractions/ScalarAttribute.cs b/src/NexusMods.EventSourcing.Abstractions/ScalarAttribute.cs index a087c83c..74bc85d9 100644 --- a/src/NexusMods.EventSourcing.Abstractions/ScalarAttribute.cs +++ b/src/NexusMods.EventSourcing.Abstractions/ScalarAttribute.cs @@ -32,6 +32,7 @@ public void Set(TContext context, EntityId owner, TType value) { if (context.GetAccumulator, ScalarAccumulator>(owner, this, out var accumulator)) accumulator.Value = value; + EntityStructureRegistry.Register(this); } /// diff --git a/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializationRegistry.cs b/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializationRegistry.cs new file mode 100644 index 00000000..d9be3932 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/Serialization/ISerializationRegistry.cs @@ -0,0 +1,25 @@ +using System; + +namespace NexusMods.EventSourcing.Abstractions.Serialization; + +/// +/// 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. +/// +public interface ISerializationRegistry +{ + /// + /// Gets a serializer that can serialize the given type. + /// + /// + /// + public ISerializer GetSerializer(Type serializedType); + + /// + /// Register a serializer for a given type, used to override the default serializers. + /// + /// + /// + public void RegisterSerializer(Type serializedType, ISerializer serializer); +} diff --git a/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs b/src/NexusMods.EventSourcing/Serialization/BinaryEventSerializer.cs similarity index 90% rename from src/NexusMods.EventSourcing/Serialization/EventSerializer.cs rename to src/NexusMods.EventSourcing/Serialization/BinaryEventSerializer.cs index d0d5e379..d670a943 100644 --- a/src/NexusMods.EventSourcing/Serialization/EventSerializer.cs +++ b/src/NexusMods.EventSourcing/Serialization/BinaryEventSerializer.cs @@ -21,6 +21,7 @@ public sealed class BinaryEventSerializer : IEventSerializer, IVariableSizeSeria private readonly Dictionary _serializerDelegates = new(); private readonly Dictionary _deserializerDelegates = new(); + private readonly ISerializationRegistry _serializerRegistry; /// /// Write an event to the given writer, and return the @@ -29,17 +30,19 @@ public sealed class BinaryEventSerializer : IEventSerializer, IVariableSizeSeria private delegate int EventDeserializerDelegate(ReadOnlySpan data, out IEvent @event); - public BinaryEventSerializer(IEnumerable diInjectedSerializers, IEnumerable eventDefinitions) + public BinaryEventSerializer(ISerializationRegistry registry, IEnumerable 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; } @@ -60,8 +63,14 @@ public IEvent Deserialize(ReadOnlySpan data) return @event; } + /// + /// Gets a serializer that can serialize the given type, for IEvent types, this class is returned. + /// + /// + /// + 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() @@ -69,7 +78,7 @@ public IEvent Deserialize(ReadOnlySpan data) .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(); @@ -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(); - 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().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 fixedParams, int fixedSize) { @@ -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)); diff --git a/src/NexusMods.EventSourcing/Serialization/SerializationRegistry.cs b/src/NexusMods.EventSourcing/Serialization/SerializationRegistry.cs new file mode 100644 index 00000000..ff57678b --- /dev/null +++ b/src/NexusMods.EventSourcing/Serialization/SerializationRegistry.cs @@ -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; + +/// +/// Manages ISerializer instances, specializes them and manages a cache for them for serialized types. +/// +public class SerializationRegistry : ISerializationRegistry +{ + private ConcurrentDictionary _cachedSerializers = new(); + private readonly ISerializer[] _diInjectedSerializers; + private readonly IGenericSerializer[] _genericSerializers; + private readonly GenericArraySerializer _arraySerializer; + + /// + /// DI constructor. + /// + /// + public SerializationRegistry(IEnumerable diInjectedSerializers) + { + _diInjectedSerializers = diInjectedSerializers.ToArray(); + _genericSerializers = _diInjectedSerializers.OfType().ToArray(); + _arraySerializer = _diInjectedSerializers.OfType().First(); + } + + /// + /// Gets a serializer that can serialize the given type. + /// + /// + /// Called when the cache needs to recursively create another type serializer + /// + /// + 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}"); + } + + /// + /// Adds a serializer to the registry. + /// + /// + /// + public void RegisterSerializer(Type serializedType, ISerializer serializer) + { + _cachedSerializers.TryAdd(serializedType, serializer); + } +} diff --git a/src/NexusMods.EventSourcing/Services.cs b/src/NexusMods.EventSourcing/Services.cs index 0ef4a32d..60434904 100644 --- a/src/NexusMods.EventSourcing/Services.cs +++ b/src/NexusMods.EventSourcing/Services.cs @@ -11,6 +11,7 @@ public static class Services public static IServiceCollection AddEventSourcing(this IServiceCollection services) { return services + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs b/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs index f997e416..de199961 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Runtime.CompilerServices; using DynamicData; using NexusMods.EventSourcing.Abstractions; @@ -10,16 +9,8 @@ public class Loadout(IEntityContext context, EntityId id) : AEntity /// The human readable name of the loadout. /// - public string Name - { - get - { - CallSite> site; - return _name.Get(this); - } - } - - internal static readonly dynamic _name = new ScalarAttribute(nameof(Name)); + public string Name => _name.Get(this); + internal static readonly ScalarAttribute _name = new(nameof(Name)); /// /// The mods in the loadout.