From 06bc06ae39e3a69c51b1ee6167eab37098918a66 Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Wed, 13 Dec 2023 10:15:02 -0700 Subject: [PATCH] Big rewrite using datom-like attributes --- .../ACollectionAttribute.cs | 17 +++ .../AEntity.cs | 12 ++ .../AScalarAttribute.cs | 43 +++++++ .../AttributeDefinition.cs | 21 ++++ .../EntityAttributeDefinition.cs | 10 ++ .../EntityCollectionAttributeDefinition.cs | 7 ++ .../EntityId.cs | 18 +++ .../IAccumulator.cs | 19 +++ .../IAttribute.cs | 33 ++++++ .../IEntity.cs | 20 +++- .../IEntityContext.cs | 38 +++--- .../IEvent.cs | 7 -- .../IEventContext.cs | 33 ++++-- .../IEventStore.cs | 2 +- .../ITransaction.cs | 34 ++++++ .../MultiEntityAttributeDefinition.cs | 11 ++ src/NexusMods.EventSourcing/EntityContext.cs | 23 ++++ src/NexusMods.EventSourcing/EventAndIds.cs | 21 ++++ src/NexusMods.EventSourcing/EventFormatter.cs | 7 +- .../Events/TransactionEvent.cs | 29 +++++ .../Events/AddMod.cs | 22 +--- .../Events/CreateLoadout.cs | 10 +- .../Events/SwapModEnabled.cs | 12 +- .../Model/Loadout.cs | 15 ++- .../Model/LoadoutRegistry.cs | 27 ++--- .../Model/Mod.cs | 21 ++-- .../BasicFunctionalityTests.cs | 5 +- .../Contexts/InMemoryEventStore.cs | 51 ++++---- .../Contexts/TestContext.cs | 111 +++++++++--------- .../EventSerializerTests.cs | 4 +- 30 files changed, 504 insertions(+), 179 deletions(-) create mode 100644 src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/AEntity.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/EntityCollectionAttributeDefinition.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/IAttribute.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/ITransaction.cs create mode 100644 src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs create mode 100644 src/NexusMods.EventSourcing/EntityContext.cs create mode 100644 src/NexusMods.EventSourcing/EventAndIds.cs create mode 100644 src/NexusMods.EventSourcing/Events/TransactionEvent.cs diff --git a/src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs b/src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs new file mode 100644 index 00000000..738aafee --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace NexusMods.EventSourcing.Abstractions; + +public class ACollectionAttribute(string name) : IAttribute +where TOwner : IEntity +{ + public bool IsScalar => false; + public Type Owner => typeof(TOwner); + public string Name => name; + public IAccumulator CreateAccumulator() + { + throw new NotImplementedException(); + } + + public Type Type => typeof(TType); +} diff --git a/src/NexusMods.EventSourcing.Abstractions/AEntity.cs b/src/NexusMods.EventSourcing.Abstractions/AEntity.cs new file mode 100644 index 00000000..5a8ec775 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/AEntity.cs @@ -0,0 +1,12 @@ +namespace NexusMods.EventSourcing.Abstractions; + +/// +/// The base class for all entities. +/// +public abstract class AEntity(IEntityContext context, EntityId id) : IEntity +{ + public EntityId Id => id; + + public IEntityContext Context => context; + +} diff --git a/src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs b/src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs new file mode 100644 index 00000000..9c995466 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs @@ -0,0 +1,43 @@ +using System; + +namespace NexusMods.EventSourcing.Abstractions; + +/// +/// A scalar attribute that can be exposed on an entity. +/// +public abstract class AScalarAttribute(string attrName) : IAttribute +{ + /// + public bool IsScalar => false; + + /// + public Type Owner => typeof(TOwner); + + /// + public string Name => attrName; + + /// + public IAccumulator CreateAccumulator() + { + return new Accumulator(); + } + + private class Accumulator : IAccumulator + { + private TVal _value = default! ; + public void Add(object value) + { + _value = (TVal) value; + } + + public object Get() + { + return _value!; + } + } + + /// + /// The data type of the attribute. + /// + public Type AttributeType => typeof(TType); +} diff --git a/src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs new file mode 100644 index 00000000..acb6b1f1 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs @@ -0,0 +1,21 @@ +using System; + +namespace NexusMods.EventSourcing.Abstractions; + +/// +/// An attribute definition for an entity. +/// +/// +/// +/// +public class AttributeDefinition(string attrName) : AScalarAttribute(attrName) +where TOwner : IEntity +{ + /// + /// Gets the value of the attribute for the given entity. + /// + /// + /// + /// + public TType Get(TOwner owner) => (TType)owner.Context.GetAccumulator(owner.Id, this).Get(); +} diff --git a/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs new file mode 100644 index 00000000..f6c58a6c --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs @@ -0,0 +1,10 @@ +using System; + +namespace NexusMods.EventSourcing.Abstractions; + +public class EntityAttributeDefinition(string attrName) : AttributeDefinition>(attrName) + where TOwner : AEntity + where TType : IEntity +{ + public TType GetEntity(TOwner owner) => throw new NotImplementedException(); +} diff --git a/src/NexusMods.EventSourcing.Abstractions/EntityCollectionAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/EntityCollectionAttributeDefinition.cs new file mode 100644 index 00000000..a4ba52fa --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/EntityCollectionAttributeDefinition.cs @@ -0,0 +1,7 @@ +namespace NexusMods.EventSourcing.Abstractions; + +public class EntityCollectionAttributeDefinition(string name) : ACollectionAttribute(name) where TOwner : IEntity + where TEntity : IEntity +{ + +} diff --git a/src/NexusMods.EventSourcing.Abstractions/EntityId.cs b/src/NexusMods.EventSourcing.Abstractions/EntityId.cs index 78b559b3..7791ac97 100644 --- a/src/NexusMods.EventSourcing.Abstractions/EntityId.cs +++ b/src/NexusMods.EventSourcing.Abstractions/EntityId.cs @@ -23,6 +23,24 @@ public readonly partial struct EntityId /// public static EntityId NewId() => new(EntityId.NewId()); + + /// + /// Gets the from the specified . + /// + /// + /// + public static EntityId From(Guid id) => new(EntityId.From(id)); + + + + /// + /// Gets the from the specified . + /// + /// + /// + public static EntityId From(string id) => From(Guid.Parse(id)); + + /// /// Creates a new instance of . /// diff --git a/src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs b/src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs new file mode 100644 index 00000000..6f94dfe9 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs @@ -0,0 +1,19 @@ +namespace NexusMods.EventSourcing.Abstractions; + +/// +/// An accumulator is used to accumulate values from events. +/// +public interface IAccumulator +{ + /// + /// Adds a value to the accumulator. + /// + /// + public void Add(object value); + + /// + /// Gets the accumulated value. + /// + /// + public object Get(); +} diff --git a/src/NexusMods.EventSourcing.Abstractions/IAttribute.cs b/src/NexusMods.EventSourcing.Abstractions/IAttribute.cs new file mode 100644 index 00000000..46ca9972 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/IAttribute.cs @@ -0,0 +1,33 @@ +using System; + +namespace NexusMods.EventSourcing.Abstractions; + +/// +/// Marker interface for attributes that can be exposed on an entity. +/// +public interface IAttribute +{ + /// + /// True if the attribute is a scalar, false if it is a collection. + /// + public bool IsScalar { get; } + + /// + /// The data type of the entity that owns the attribute. + /// + public Type Owner { get; } + + /// + /// The name of the attribute, needs to be unique in a given entity but not unique across entities. + /// + public string Name { get; } + + + /// + /// Creates a new accumulator for the attribute. + /// + /// + public IAccumulator CreateAccumulator(); + +} + diff --git a/src/NexusMods.EventSourcing.Abstractions/IEntity.cs b/src/NexusMods.EventSourcing.Abstractions/IEntity.cs index f90a4e43..4c13dd60 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEntity.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEntity.cs @@ -1,7 +1,25 @@ +using System; + namespace NexusMods.EventSourcing.Abstractions; +/// +/// The base interface for all entities. +/// public interface IEntity { + /// + /// The globally unique identifier of the entity. + /// public EntityId Id { get; } -} + /// + /// The context this entity belongs to. + /// + public IEntityContext Context { get; } + + + /// + /// The type descriptor for all entities. Emitted by the method. + /// + public static readonly AttributeDefinition TypeAttribute = new("$Type"); +} diff --git a/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs b/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs index 0dae6b2f..50e11ce4 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs @@ -9,39 +9,31 @@ namespace NexusMods.EventSourcing.Abstractions; public interface IEntityContext { /// - /// Adds the event to the event store, and advances the "as of" transaction id to the transaction id of the event. + /// Gets the entity with the specified id. /// - /// + /// + /// /// - public ValueTask Transact(IEvent @event); + public TEntity Get(EntityId id) where TEntity : IEntity; - /// - /// Get the entity with the given id from the context, the entity will be up-to-date as of the current "as of" transaction id. - /// - /// - /// - /// - public ValueTask Retrieve(EntityId entityId) where T : IEntity; - - /// - /// The current "as of" transaction id. The entities in this context are up-to-date as of this transaction id. - /// - public TransactionId AsOf { get; } /// - /// Advances the "as of" transaction id to the given transaction id, all objects in this context will be updated - /// to reflect the new transaction id. + /// Transacts a new event into the context. /// - /// + /// + /// /// - public ValueTask Advance(TransactionId transactionId); + public ValueTask Add(TEvent entity) where TEvent : IEvent; + /// - /// Advances the "as of" transaction id to the most recent transaction id, all objects in this context will be updated - /// to reflect the new transaction id. + /// Gets the value of the attribute for the given entity. /// + /// + /// + /// + /// /// - public ValueTask Advance(); - + IAccumulator GetAccumulator(EntityId ownerId, AttributeDefinition attributeDefinition) where TOwner : IEntity; } diff --git a/src/NexusMods.EventSourcing.Abstractions/IEvent.cs b/src/NexusMods.EventSourcing.Abstractions/IEvent.cs index d5b11937..0671f0ff 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEvent.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEvent.cs @@ -14,11 +14,4 @@ public interface IEvent /// Applies the event to the entities attached to the event. /// ValueTask Apply(T context) where T : IEventContext; - - /// - /// When called, the handler should be called for each entity that was modified by this event. Not for - /// those which are referenced, but not modified. - /// - /// - void ModifiedEntities(Action handler); } diff --git a/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs b/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs index 5d784ff8..d2abc281 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; namespace NexusMods.EventSourcing.Abstractions; @@ -7,21 +8,35 @@ namespace NexusMods.EventSourcing.Abstractions; /// public interface IEventContext { + /// + /// Emits a new value for the given attribute on the given entity + /// + /// + /// + /// + /// + /// + public void Emit(EntityId entity, AttributeDefinition attr, TVal value) + where TOwner : IEntity; /// - /// Attach an entity to the context, this entity will be tracked by the context and should only be used in events - /// that intend to create an entity from scratch. + /// Emits a new member value for the given attribute on the given entity /// - /// /// - /// - public void AttachEntity(EntityId entityId, TEntity entity) where TEntity : IEntity; + /// + /// + /// + /// + public void Emit(EntityId entity, MultiEntityAttributeDefinition attr, + EntityId value) + where TOwner : IEntity + where TVal : IEntity; /// - /// Retrieve an entity from the context, this may require the context to load the entity via replaying - /// the events up to the current transaction. + /// Emits the type attribute for the given entity so that polymorphic queries can be performed /// /// - /// - public ValueTask Retrieve(EntityId id) where T : IEntity; + /// + /// + public void New(EntityId id) where TType : IEntity; } diff --git a/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs b/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs index 9615c9e2..1661e675 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs @@ -7,7 +7,7 @@ public interface IEventStore { public ValueTask Add(T eventEntity) where T : IEvent; - public ValueTask EventsForEntity(EntityId entityId, TIngester ingester) + public void EventsForEntity(EntityId entityId, TIngester ingester) where TEntity : IEntity where TIngester : IEventIngester; } diff --git a/src/NexusMods.EventSourcing.Abstractions/ITransaction.cs b/src/NexusMods.EventSourcing.Abstractions/ITransaction.cs new file mode 100644 index 00000000..30d199d2 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/ITransaction.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace NexusMods.EventSourcing.Abstractions; + +/// +/// A interface for a transaction that can be used to add new events to storage. +/// +public interface ITransaction : IDisposable +{ + /// + /// Confirms the transaction and commits the changes to the underlying storage. + /// + /// + public ValueTask CommitAsync(); + + /// + /// Gets the current state of an entity. + /// + /// + /// + /// + public T Retrieve(EntityId entityId) where T : IEntity; + + /// + /// Adds a new event to the transaction, this will also update the current + /// entity states + /// + /// + /// + /// + public ValueTask Add(T eventToAdd) where T : IEvent; + +} diff --git a/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs new file mode 100644 index 00000000..42639197 --- /dev/null +++ b/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace NexusMods.EventSourcing.Abstractions; + +public class MultiEntityAttributeDefinition(string name) : + ACollectionAttribute>(name) where TOwner : IEntity + where TType : IEntity +{ + public IEnumerable GetAll(TOwner owner) => throw new NotImplementedException(); +} diff --git a/src/NexusMods.EventSourcing/EntityContext.cs b/src/NexusMods.EventSourcing/EntityContext.cs new file mode 100644 index 00000000..048e815a --- /dev/null +++ b/src/NexusMods.EventSourcing/EntityContext.cs @@ -0,0 +1,23 @@ +using System.Threading; +using System.Threading.Tasks; +using NexusMods.EventSourcing.Abstractions; + +namespace NexusMods.EventSourcing; + +public class EntityContext : IEntityContext +{ + public TEntity Get(EntityId id) where TEntity : IEntity + { + throw new System.NotImplementedException(); + } + + public ValueTask Add(TEvent entity) where TEvent : IEvent + { + throw new System.NotImplementedException(); + } + + public IAccumulator GetAccumulator(EntityId ownerId, AttributeDefinition attributeDefinition) where TOwner : IEntity + { + throw new System.NotImplementedException(); + } +} diff --git a/src/NexusMods.EventSourcing/EventAndIds.cs b/src/NexusMods.EventSourcing/EventAndIds.cs new file mode 100644 index 00000000..5ef5e2cf --- /dev/null +++ b/src/NexusMods.EventSourcing/EventAndIds.cs @@ -0,0 +1,21 @@ +using MemoryPack; +using NexusMods.EventSourcing.Abstractions; + +namespace NexusMods.EventSourcing; + +/// +/// A pair of an event and the entity ids it applies to. +/// +[MemoryPackable] +public class EventAndIds +{ + /// + /// The event + /// + public required IEvent Event { get; init; } + + /// + /// The entities retrieved by the event + /// + public required EntityId[] EntityIds { get; init; } +} diff --git a/src/NexusMods.EventSourcing/EventFormatter.cs b/src/NexusMods.EventSourcing/EventFormatter.cs index 4a415185..d41ed383 100644 --- a/src/NexusMods.EventSourcing/EventFormatter.cs +++ b/src/NexusMods.EventSourcing/EventFormatter.cs @@ -15,8 +15,9 @@ public class EventFormatter : MemoryPackFormatter public EventFormatter(IEnumerable events) { - _eventByGuid = events.ToDictionary(e => e.Guid, e => e.Type); - _eventsByType = events.ToDictionary(e => e.Type, e => e.Guid); + var eventsArray = events.ToArray(); + _eventByGuid = eventsArray.ToDictionary(e => e.Guid, e => e.Type); + _eventsByType = eventsArray.ToDictionary(e => e.Type, e => e.Guid); } public override void Serialize(ref MemoryPackWriter writer, scoped ref IEvent? value) @@ -29,7 +30,7 @@ public override void Serialize(ref MemoryPackWriter + /// A list of events that are part of the transaction. + /// + public required EventAndIds[] Events { get; init; } + + /// + /// Applies all the events in the transaction to the entities attached to the events. + /// + /// + /// + /// + public ValueTask Apply(T context) where T : IEventContext + { + foreach (var evt in Events) + evt.Event.Apply(context); + + return ValueTask.CompletedTask; + } +} diff --git a/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs b/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs index 44db6709..4b3638a8 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs @@ -12,25 +12,15 @@ public partial class AddMod : IEvent public required string Name { get; init; } = string.Empty; public required bool Enabled { get; init; } = true; public required EntityId Id { get; init; } - public required EntityId Loadout { get; init; } + public required EntityId LoadoutId { get; init; } public async ValueTask Apply(T context) where T : IEventContext { - var loadout = await context.Retrieve(Loadout); - var mod = new Mod - { - Id = Id.Value, - Name = Name, - Enabled = Enabled, - }; - loadout._mods.AddOrUpdate(mod); - context.AttachEntity(Id, mod); + context.New(Id); + context.Emit(Id, Mod._name, Name); + context.Emit(Id, Mod._enabled, Enabled); + context.Emit(Id, Mod._loadout, LoadoutId); + context.Emit(LoadoutId, Loadout._mods, Id); } - - public void ModifiedEntities(Action handler) - { - handler(Id.Value); - handler(Loadout.Value); - } } diff --git a/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs b/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs index 06b16a8e..b28e45c9 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs @@ -16,14 +16,8 @@ public partial class CreateLoadout : IEvent public async ValueTask Apply(T context) where T : IEventContext { - var registry = await context.Retrieve(LoadoutRegistry.StaticId); - var loadout = new Loadout - { - Id = Id.Value, - Name = Name - }; - registry._loadouts.AddOrUpdate(loadout); - context.AttachEntity(Id, loadout); + context.New(Id); + context.Emit(Id, Loadout._name, Name); } public static CreateLoadout Create(string name) => new() { Name = name, Id = EntityId.NewId() }; diff --git a/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs b/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs index 3bed6bc6..5d512bbd 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs @@ -9,21 +9,17 @@ namespace NexusMods.EventSourcing.TestModel.Events; public partial class SwapModEnabled : IEvent { public required EntityId Id { get; init; } + public required bool Enabled { get; init; } public async ValueTask Apply(T context) where T : IEventContext { - var mod = await context.Retrieve(Id); - mod.Enabled = !mod.Enabled; + context.Emit(Id, Mod._enabled, Enabled); } /// /// Helper method to create a new event instance. /// /// + /// /// - public static SwapModEnabled Create(EntityId id) => new() { Id = id }; - - public void ModifiedEntities(Action handler) - { - handler(Id.Value); - } + public static SwapModEnabled Create(EntityId id, bool enabled) => new() { Id = id, Enabled = enabled}; } diff --git a/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs b/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs index dc19928b..a94a098c 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs @@ -3,11 +3,18 @@ namespace NexusMods.EventSourcing.TestModel.Model; -public class Loadout : IEntity +public class Loadout(IEntityContext context, EntityId id) : AEntity(context, id) { - public EntityId Id { get; internal set; } + /// + /// The human readable name of the loadout. + /// + public string Name => _name.Get(this); + internal static readonly AttributeDefinition _name = new(nameof(Name)); - public string Name { get; internal set; } = string.Empty; + /// + /// The mods in the loadout. + /// + public IEnumerable Mods => _mods.GetAll(this); + internal static readonly MultiEntityAttributeDefinition _mods = new(nameof(Mods)); - internal SourceCache _mods = new(x => x.Id); } diff --git a/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs b/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs index cd88df26..428231aa 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs @@ -4,21 +4,20 @@ namespace NexusMods.EventSourcing.TestModel.Model; -public class LoadoutRegistry : IEntity +public class LoadoutRegistry(IEntityContext context, EntityId id) : AEntity(context, id) { - internal readonly SourceCache _loadouts = new(x => x.Id); + /// + /// Gets the instance of the loadout registry from the entity context. + /// + /// + /// + public static LoadoutRegistry GetInstance(IEntityContext context) => + context.Get(EntityId.From("10BAE6BA-D5F9-40F4-AF7F-CCA1417C3BB0")); - private ReadOnlyObservableCollection _loadoutsConnected; - public ReadOnlyObservableCollection Loadouts => _loadoutsConnected; + /// + /// The loadouts in the registry. + /// + public IEnumerable Loadouts => _loadouts.GetAll(this); + internal static readonly MultiEntityAttributeDefinition _loadouts = new(nameof(Loadouts)); - public LoadoutRegistry() - { - _loadouts.Connect() - .Bind(out _loadoutsConnected) - .Subscribe(); - } - - public static EntityId StaticId = new(EntityId.From(Guid.Parse("7F3E3745-51B9-44CB-BBDA-B1555191330E"))); - - public EntityId Id { get; } = StaticId.Value; } diff --git a/tests/NexusMods.EventSourcing.TestModel/Model/Mod.cs b/tests/NexusMods.EventSourcing.TestModel/Model/Mod.cs index e62eb072..3eba425d 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Model/Mod.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Model/Mod.cs @@ -3,16 +3,21 @@ namespace NexusMods.EventSourcing.TestModel.Model; -public class Mod : IEntity +public class Mod(IEntityContext context, EntityId id) : AEntity(context, id) { - public EntityId Id { get; internal set; } - [Reactive] - public string Name { get; internal set; } = string.Empty; + public Loadout Loadout => _loadout.GetEntity(this); + internal static readonly EntityAttributeDefinition _loadout = new(nameof(Loadout)); - [Reactive] - public bool Enabled { get; internal set; } + /// + /// The human readable name of the mod. + /// + public string Name => _name.Get(this); + internal static readonly AttributeDefinition _name = new(nameof(Name)); - [Reactive] - public Collection? Collection { get; internal set; } + /// + /// The enabled state of the mod. + /// + public bool Enabled => _enabled.Get(this); + internal static readonly AttributeDefinition _enabled = new(nameof(Enabled)); } diff --git a/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs b/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs index a6da42b4..cacf7d28 100644 --- a/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs +++ b/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs @@ -17,8 +17,9 @@ public BasicFunctionalityTests(TestContext ctx) public async void CanSetupBasicLoadout() { var createEvent = CreateLoadout.Create("Test"); - await _ctx.Transact(createEvent); - var loadout = await _ctx.Retrieve(createEvent.Id); + await _ctx.Add(createEvent); + var loadout = _ctx.Get(createEvent.Id); + loadout.Should().NotBeNull(); loadout.Name.Should().Be("Test"); } } diff --git a/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs b/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs index d1d93492..f8e001ed 100644 --- a/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs +++ b/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs @@ -5,27 +5,18 @@ namespace NexusMods.EventSourcing.Tests.Contexts; -public class InMemoryEventStore : IEventStore +public class InMemoryEventStore(EventSerializer serializer) : IEventStore { private readonly Dictionary> _events = new(); - public InMemoryEventStore() - { - var formatter = new DynamicUnionFormatter(new[] - { - ( (ushort)3, typeof(CreateLoadout)), - ( (ushort)4, typeof(AddMod)), - ( (ushort)5, typeof(SwapModEnabled)) - }); - MemoryPackFormatterProvider.Register(formatter); - } - - public ValueTask Add(T entity) where T : IEvent + public async ValueTask Add(T entity) where T : IEvent { + var data = serializer.Serialize(entity); + var logger = new ModifiedEntityLogger(); + await entity.Apply(logger); lock (_events) { - var data = MemoryPackSerializer.Serialize(entity); - entity.ModifiedEntities(id => + foreach (var id in logger.Entities) { if (!_events.TryGetValue(id, out var value)) { @@ -33,20 +24,40 @@ public ValueTask Add(T entity) where T : IEvent _events.Add(id, value); } value.Add(data); - }); + } + } + } + + /// + /// Simplistic context that just logs the entities that were modified. + /// + private readonly struct ModifiedEntityLogger() : IEventContext + { + public readonly HashSet Entities = new(); + public void Emit(EntityId entity, AttributeDefinition attr, TVal value) where TOwner : IEntity + { + Entities.Add(entity.Value); + } + + public void Emit(EntityId entity, MultiEntityAttributeDefinition attr, EntityId value) where TOwner : IEntity where TVal : IEntity + { + Entities.Add(entity.Value); + } + + public void New(EntityId id) where TType : IEntity + { + Entities.Add(id.Value); } - return ValueTask.CompletedTask; } - public ValueTask EventsForEntity(EntityId entityId, TIngester ingester) + public void EventsForEntity(EntityId entityId, TIngester ingester) where TEntity : IEntity where TIngester : IEventIngester { foreach (var data in _events[entityId.Value]) { - var @event = MemoryPackSerializer.Deserialize(data)!; + var @event = serializer.Deserialize(data)!; ingester.Ingest(@event); } - return ValueTask.CompletedTask; } } diff --git a/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs b/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs index 11bbd902..4210e32f 100644 --- a/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs +++ b/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs @@ -1,88 +1,93 @@ +using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; using NexusMods.EventSourcing.Abstractions; namespace NexusMods.EventSourcing.Tests.Contexts; -public class TestContext(ILogger logger) : IEventContext, IEntityContext +public class TestContext(ILogger logger, EventSerializer serializer) : IEntityContext { - private readonly InMemoryEventStore _store = new(); - private readonly Dictionary _entities = new(); + private readonly InMemoryEventStore _store = new(serializer); + private readonly Dictionary _entities = new(); + private readonly Dictionary> _values = new(); private TransactionId _currentTransactionId = TransactionId.From(0); - - public void AttachEntity(EntityId entityId, TEntity entity) where TEntity : IEntity + public TEntity Get(EntityId id) where TEntity : IEntity { - if (_entities.ContainsKey(entityId.Value)) + if (_entities.TryGetValue(id.Value, out var entity)) { - throw new InvalidOperationException($"Entity with id {entityId} already exists in the context"); + return (TEntity)entity; } - _entities.Add(entityId.Value, (entity, _currentTransactionId)); + + var ingester = new Ingester(); + + _store.EventsForEntity(id, ingester); + + var type = (Type)ingester.Values[IEntity.TypeAttribute].Get(); + + var createdEntity = (TEntity)Activator.CreateInstance(type, this, id.Value)!; + _entities.Add(id.Value, createdEntity); + _values.Add(id.Value, ingester.Values); + + return createdEntity; } - /// - /// Resets the cache of entities, this is useful for testing purposes - /// - public void ResetCache() + public async ValueTask Add(TEvent entity) where TEvent : IEvent { _entities.Clear(); + _values.Clear(); + await _store.Add(entity); } - public ValueTask Transact(IEvent @event) + public IAccumulator GetAccumulator(EntityId ownerId, AttributeDefinition attributeDefinition) + where TOwner : IEntity { - var newId = _currentTransactionId.Next(); - _currentTransactionId = newId; - _store.Add(@event); - @event.Apply(this); - logger.LogInformation("Applied {Event} to context txId {Tx}", @event, _currentTransactionId); - return ValueTask.CompletedTask; + return _values[ownerId][attributeDefinition]; } - public ValueTask Retrieve(EntityId entityId) where T : IEntity + private struct Ingester() : IEventIngester, IEventContext { - if (_entities.TryGetValue(entityId.Value, out var entity)) + public Dictionary Values { get; set; } = new(); + + public ValueTask Ingest(IEvent @event) { - return new ValueTask((T)entity.Entity); + @event.Apply(this); + return ValueTask.CompletedTask; } - return LoadEntity(entityId); - } - - private async ValueTask LoadEntity(EntityId entityId) where T : IEntity - { - logger.LogInformation("Loading entity {EntityId} replaying events", entityId); - var ingester = new Ingester(this); - await _store.EventsForEntity(entityId, ingester); - var result = (T)_entities[entityId.Value].Entity; - logger.LogInformation("Loaded entity {EntityId} with {EventCount} events", entityId, ingester.EventCount); - return result; - } - - private class Ingester : IEventIngester - { - private readonly TestContext _ctx; - public int EventCount { get; private set; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IAccumulator GetAccumulator(IAttribute attribute) + { + if (!Values.TryGetValue(attribute, out var accumulator)) + { + accumulator = attribute.CreateAccumulator(); + Values.Add(attribute, accumulator); + } + return accumulator; + } - public Ingester(TestContext ctx) + public void Emit(EntityId entity, AttributeDefinition attr, TVal value) + where TOwner : IEntity { - _ctx = ctx; + var accumulator = GetAccumulator(attr); + accumulator.Add(value!); } - public async ValueTask Ingest(IEvent @event) + public void Emit(EntityId entity, AttributeDefinition attr, TVal value) + where TOwner : IEntity { - EventCount++; - await @event.Apply(_ctx); + var accumulator = GetAccumulator(attr); + accumulator.Add(value!); } - } - public TransactionId AsOf { get; } - public ValueTask Advance(TransactionId transactionId) - { - throw new NotImplementedException(); - } + public void Emit(EntityId entity, MultiEntityAttributeDefinition attr, EntityId value) where TOwner : IEntity where TVal : IEntity + { + throw new NotImplementedException(); + } - public ValueTask Advance() - { - throw new NotImplementedException(); + public void New(EntityId id) where TType : IEntity + { + Emit(id.Value, IEntity.TypeAttribute, typeof(TType)); + } } } diff --git a/tests/NexusMods.EventSourcing.Tests/EventSerializerTests.cs b/tests/NexusMods.EventSourcing.Tests/EventSerializerTests.cs index f52f4232..29afa3d9 100644 --- a/tests/NexusMods.EventSourcing.Tests/EventSerializerTests.cs +++ b/tests/NexusMods.EventSourcing.Tests/EventSerializerTests.cs @@ -10,14 +10,14 @@ public class EventSerializerTests(EventSerializer serializer) [Fact] public void CanSerializeEvents() { - serializer.Serialize(SwapModEnabled.Create(EntityId.NewId())); + serializer.Serialize(SwapModEnabled.Create(EntityId.NewId(), true)); } [Fact] public void CanDeserializeEvents() { var id = EntityId.NewId(); - var @event = SwapModEnabled.Create(id); + var @event = SwapModEnabled.Create(id, true); var serialized = serializer.Serialize(@event); var deserialized = serializer.Deserialize(serialized); deserialized.Should().BeEquivalentTo(@event);