From 3687b0cc0e5435b77559a57fb6680f9ffb5c0d30 Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Wed, 13 Dec 2023 11:56:44 -0700 Subject: [PATCH] Can retract attributes --- .../ACollectionAttribute.cs | 22 +++++++- .../AScalarAttribute.cs | 5 ++ .../AttributeDefinition.cs | 2 +- .../EntityAttributeDefinition.cs | 7 ++- .../IAccumulator.cs | 6 +++ .../IEntityContext.cs | 5 +- .../IEventContext.cs | 25 +++++++++ .../IEventStore.cs | 3 +- .../MultiEntityAttributeDefinition.cs | 10 +++- src/NexusMods.EventSourcing/EntityContext.cs | 2 +- .../Events/AddMod.cs | 3 +- .../Events/DeleteMod.cs | 17 ++++++ .../Services.cs | 1 + .../BasicFunctionalityTests.cs | 39 ++++++++++++++ .../Contexts/InMemoryEventStore.cs | 17 ++++-- .../Contexts/TestContext.cs | 52 ++++++++++++++----- 16 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 tests/NexusMods.EventSourcing.TestModel/Events/DeleteMod.cs diff --git a/src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs b/src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs index 738aafee..ad9fe113 100644 --- a/src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs +++ b/src/NexusMods.EventSourcing.Abstractions/ACollectionAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace NexusMods.EventSourcing.Abstractions; @@ -10,7 +11,26 @@ public class ACollectionAttribute(string name) : IAttribute public string Name => name; public IAccumulator CreateAccumulator() { - throw new NotImplementedException(); + return new Accumulator(); + } + + protected class Accumulator : IAccumulator + { + private HashSet _values = new(); + public void Add(object value) + { + _values.Add((TType) value); + } + + public void Retract(object value) + { + _values.Remove((TType) value); + } + + public object Get() + { + return _values; + } } public Type Type => typeof(TType); diff --git a/src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs b/src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs index 9c995466..4fa29511 100644 --- a/src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs +++ b/src/NexusMods.EventSourcing.Abstractions/AScalarAttribute.cs @@ -30,6 +30,11 @@ public void Add(object value) _value = (TVal) value; } + public void Retract(object value) + { + _value = default!; + } + public object Get() { return _value!; diff --git a/src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs index acb6b1f1..84b5690f 100644 --- a/src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs +++ b/src/NexusMods.EventSourcing.Abstractions/AttributeDefinition.cs @@ -17,5 +17,5 @@ public class AttributeDefinition(string attrName) : AScalarAttrib /// /// /// - public TType Get(TOwner owner) => (TType)owner.Context.GetAccumulator(owner.Id, this).Get(); + 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 index f6c58a6c..59cfdd6f 100644 --- a/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs +++ b/src/NexusMods.EventSourcing.Abstractions/EntityAttributeDefinition.cs @@ -6,5 +6,10 @@ public class EntityAttributeDefinition(string attrName) : Attribu where TOwner : AEntity where TType : IEntity { - public TType GetEntity(TOwner owner) => throw new NotImplementedException(); + public TType GetEntity(TOwner owner) + { + var accumulator = owner.Context.GetAccumulator>(owner.Id, this); + var entityId = (EntityId) accumulator.Get(); + return owner.Context.Get(entityId); + } } diff --git a/src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs b/src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs index 6f94dfe9..fb612153 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IAccumulator.cs @@ -11,6 +11,12 @@ public interface IAccumulator /// public void Add(object value); + /// + /// Retracts a value from the accumulator. + /// + /// + public void Retract(object value); + /// /// Gets the accumulated value. /// diff --git a/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs b/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs index 50e11ce4..a71ada08 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs @@ -33,7 +33,10 @@ public interface IEntityContext /// /// /// + /// /// - IAccumulator GetAccumulator(EntityId ownerId, AttributeDefinition attributeDefinition) where TOwner : IEntity; + IAccumulator GetAccumulator(EntityId ownerId, TAttribute attributeDefinition) + where TOwner : IEntity + where TAttribute : IAttribute; } diff --git a/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs b/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs index d2abc281..5a88687a 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEventContext.cs @@ -32,6 +32,31 @@ public void Emit(EntityId entity, MultiEntityAttributeDefi where TOwner : IEntity where TVal : IEntity; + + /// + /// Retracts a value for the given attribute on the given entity + /// + /// + /// + /// + /// + /// + public void Retract(EntityId entity, AttributeDefinition attr, TVal value) + where TOwner : IEntity; + + /// + /// Retracts a member value for the given attribute on the given entity + /// + /// + /// + /// + /// + /// + public void Retract(EntityId entity, MultiEntityAttributeDefinition attr, + EntityId value) + where TOwner : IEntity + where TVal : IEntity; + /// /// Emits the type attribute for the given entity so that polymorphic queries can be performed /// diff --git a/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs b/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs index 1661e675..850428a6 100644 --- a/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs +++ b/src/NexusMods.EventSourcing.Abstractions/IEventStore.cs @@ -7,7 +7,6 @@ public interface IEventStore { public ValueTask Add(T eventEntity) where T : IEvent; - public void EventsForEntity(EntityId entityId, TIngester ingester) - where TEntity : IEntity + public void EventsForEntity(EntityId entityId, TIngester ingester) where TIngester : IEventIngester; } diff --git a/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs index 42639197..da87f370 100644 --- a/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs +++ b/src/NexusMods.EventSourcing.Abstractions/MultiEntityAttributeDefinition.cs @@ -7,5 +7,13 @@ public class MultiEntityAttributeDefinition(string name) : ACollectionAttribute>(name) where TOwner : IEntity where TType : IEntity { - public IEnumerable GetAll(TOwner owner) => throw new NotImplementedException(); + public IEnumerable GetAll(TOwner owner) + { + var tmp = owner.Context.GetAccumulator>(owner.Id, this); + var ids = (HashSet>) tmp.Get(); + foreach (var id in ids) + { + yield return owner.Context.Get(id); + } + } } diff --git a/src/NexusMods.EventSourcing/EntityContext.cs b/src/NexusMods.EventSourcing/EntityContext.cs index 048e815a..a2ee425f 100644 --- a/src/NexusMods.EventSourcing/EntityContext.cs +++ b/src/NexusMods.EventSourcing/EntityContext.cs @@ -16,7 +16,7 @@ public ValueTask Add(TEvent entity) where TEvent : IEvent throw new System.NotImplementedException(); } - public IAccumulator GetAccumulator(EntityId ownerId, AttributeDefinition attributeDefinition) where TOwner : IEntity + public IAccumulator GetAccumulator(EntityId ownerId, TAttribute attributeDefinition) where TOwner : IEntity where TAttribute : IAttribute { throw new System.NotImplementedException(); } diff --git a/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs b/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs index 4b3638a8..a43e6032 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs @@ -21,6 +21,7 @@ public async ValueTask Apply(T context) where T : IEventContext context.Emit(Id, Mod._enabled, Enabled); context.Emit(Id, Mod._loadout, LoadoutId); context.Emit(LoadoutId, Loadout._mods, Id); - } + + public static AddMod Create(string name, EntityId loadoutId) => new() { Name = name, Enabled = true, Id = EntityId.NewId(), LoadoutId = loadoutId }; } diff --git a/tests/NexusMods.EventSourcing.TestModel/Events/DeleteMod.cs b/tests/NexusMods.EventSourcing.TestModel/Events/DeleteMod.cs new file mode 100644 index 00000000..63459da8 --- /dev/null +++ b/tests/NexusMods.EventSourcing.TestModel/Events/DeleteMod.cs @@ -0,0 +1,17 @@ +using MemoryPack; +using NexusMods.EventSourcing.Abstractions; +using NexusMods.EventSourcing.TestModel.Model; + +namespace NexusMods.EventSourcing.TestModel.Events; + +[EventId("5CD171BF-4FFE-40E5-819B-987C48A20DF6")] +[MemoryPackable] +public partial record DeleteMod(EntityId ModId, EntityId LoadoutId) : IEvent +{ + public ValueTask Apply(T context) where T : IEventContext + { + context.Retract(LoadoutId, Loadout._mods, ModId); + context.Retract(ModId, Mod._loadout, LoadoutId); + return ValueTask.CompletedTask; + } +} diff --git a/tests/NexusMods.EventSourcing.TestModel/Services.cs b/tests/NexusMods.EventSourcing.TestModel/Services.cs index 0ba2f28f..326fe000 100644 --- a/tests/NexusMods.EventSourcing.TestModel/Services.cs +++ b/tests/NexusMods.EventSourcing.TestModel/Services.cs @@ -16,6 +16,7 @@ public static IServiceCollection AddEvents(this IServiceCollection coll) coll.AddEvent(); coll.AddEvent(); coll.AddEvent(); + coll.AddEvent(); return coll; } diff --git a/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs b/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs index a506d233..6667b0f4 100644 --- a/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs +++ b/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs @@ -34,4 +34,43 @@ public async void ChangingPropertyChangesTheValue() await _ctx.Add(RenameLoadout.Create(createEvent.Id, "New Name")); loadout.Name.Should().Be("New Name"); } + + [Fact] + public async void CanLinkEntities() + { + var loadoutEvent = CreateLoadout.Create("Test"); + await _ctx.Add(loadoutEvent); + var loadout = _ctx.Get(loadoutEvent.Id); + loadout.Name.Should().Be("Test"); + + var modEvent = AddMod.Create("First Mod", loadoutEvent.Id); + await _ctx.Add(modEvent); + + loadout.Mods.First().Name.Should().Be("First Mod"); + loadout.Mods.First().Loadout.Should().BeSameAs(loadout); + } + + + [Fact] + public async void CanDeleteEntities() + { + var loadoutEvent = CreateLoadout.Create("Test"); + await _ctx.Add(loadoutEvent); + var loadout = _ctx.Get(loadoutEvent.Id); + loadout.Name.Should().Be("Test"); + + var modEvent1 = AddMod.Create("First Mod", loadoutEvent.Id); + await _ctx.Add(modEvent1); + + var modEvent2 = AddMod.Create("Second Mod", loadoutEvent.Id); + await _ctx.Add(modEvent2); + + loadout.Mods.Count().Should().Be(2); + + await _ctx.Add(new DeleteMod(modEvent1.Id, loadoutEvent.Id)); + + loadout.Mods.Count().Should().Be(1); + + loadout.Mods.First().Name.Should().Be("Second Mod"); + } } diff --git a/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs b/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs index f8e001ed..379d6863 100644 --- a/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs +++ b/tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs @@ -44,17 +44,26 @@ public void Emit(EntityId entity, MultiEntityAttributeDefi Entities.Add(entity.Value); } + public void Retract(EntityId entity, AttributeDefinition attr, TVal value) where TOwner : IEntity + { + Entities.Add(entity.Value); + } + + public void Retract(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); } } - - public void EventsForEntity(EntityId entityId, TIngester ingester) - where TEntity : IEntity where TIngester : IEventIngester + public void EventsForEntity(EntityId entityId, TIngester ingester) + where TIngester : IEventIngester { - foreach (var data in _events[entityId.Value]) + foreach (var data in _events[entityId]) { var @event = serializer.Deserialize(data)!; ingester.Ingest(@event); diff --git a/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs b/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs index 0dd9fe86..bec5c647 100644 --- a/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs +++ b/tests/NexusMods.EventSourcing.Tests/Contexts/TestContext.cs @@ -19,36 +19,38 @@ public TEntity Get(EntityId id) where TEntity : IEntity return (TEntity)entity; } - var ingester = new Ingester(id.Value); - - _store.EventsForEntity(id, ingester); - - var type = (Type)ingester.Values[IEntity.TypeAttribute].Get(); + var values = LoadValues(id.Value); + var type = (Type)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; } + private Dictionary LoadValues(EntityId id) + { + var ingester = new Ingester(id); + _store.EventsForEntity(id, ingester); + _values.Add(id, ingester.Values); + return ingester.Values; + } + public async ValueTask Add(TEvent entity) where TEvent : IEvent { - _entities.Clear(); _values.Clear(); await _store.Add(entity); } - public IAccumulator GetAccumulator(EntityId ownerId, AttributeDefinition attributeDefinition) - where TOwner : IEntity + public IAccumulator GetAccumulator(EntityId ownerId, TAttribute attributeDefinition) + where TOwner: IEntity + where TAttribute : IAttribute { if (_values.TryGetValue(ownerId, out var values)) return values[attributeDefinition]; - Get(EntityId.From(ownerId.Value)); - values = _values[ownerId]; - - return values[attributeDefinition]; + var loadedValues = LoadValues(ownerId); + return loadedValues[attributeDefinition]; } private readonly struct Ingester(EntityId id) : IEventIngester, IEventContext @@ -94,7 +96,29 @@ public void Emit(EntityId entity, AttributeDefinition(EntityId entity, MultiEntityAttributeDefinition attr, EntityId value) where TOwner : IEntity where TVal : IEntity { - throw new NotImplementedException(); + if (entity.Value != id.Value) + return; + + var accumulator = GetAccumulator(attr); + accumulator.Add(value); + } + + public void Retract(EntityId entity, AttributeDefinition attr, TVal value) where TOwner : IEntity + { + if (entity.Value != id.Value) + return; + + var accumulator = GetAccumulator(attr); + accumulator.Retract(value!); + } + + public void Retract(EntityId entity, MultiEntityAttributeDefinition attr, EntityId value) where TOwner : IEntity where TVal : IEntity + { + if (entity.Value != id.Value) + return; + + var accumulator = GetAccumulator(attr); + accumulator.Retract(value); } public void New(EntityId newId) where TType : IEntity