diff --git a/src/NexusMods.EventSourcing.Abstractions/IndexedMultiEntityAttributeDefinition.cs b/src/NexusMods.EventSourcing.Abstractions/IndexedMultiEntityAttributeDefinition.cs
new file mode 100644
index 00000000..138655a4
--- /dev/null
+++ b/src/NexusMods.EventSourcing.Abstractions/IndexedMultiEntityAttributeDefinition.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using NexusMods.EventSourcing.Abstractions.Serialization;
+
+namespace NexusMods.EventSourcing.Abstractions;
+
+///
+/// Like , but with the data stored in a dictionary
+///
+public class IndexedMultiEntityAttributeDefinition(string name) : IAttribute>
+ where TOwner : AEntity, IEntity
+ where TOther : AEntity
+ where TKey : notnull
+{
+ ///
+ public Type Owner => typeof(TOwner);
+
+ ///
+ public string Name => name;
+
+ public IndexedMultiEntityAccumulator CreateAccumulator()
+ {
+ return new IndexedMultiEntityAccumulator();
+ }
+
+ public void Add(TContext context, EntityId owner, TKey key, EntityId value)
+ where TContext : IEventContext
+ {
+ if (context.GetAccumulator,
+ IndexedMultiEntityAccumulator>(owner, this, out var accumulator))
+ {
+ if (accumulator._keys.TryGetValue(value, out var existingKey))
+ {
+ accumulator._values.Remove(existingKey);
+ accumulator._keys.Remove(value);
+ }
+ accumulator._values.Add(key, value);
+ accumulator._keys.Add(value, key);
+ }
+ }
+
+ IAccumulator IAttribute.CreateAccumulator()
+ {
+ return CreateAccumulator();
+ }
+
+ ///
+ /// Gets the other entities linked to the given entity.
+ ///
+ ///
+ ///
+ public Dictionary> Get(TOwner entity)
+ {
+ if (!entity.Context
+ .GetReadOnlyAccumulator,
+ IndexedMultiEntityAccumulator>(entity.Id, this, out var accumulator, true))
+ throw new InvalidOperationException("No accumulator found for entity");
+ return accumulator._values;
+ }
+}
+
+public class IndexedMultiEntityAccumulator : IAccumulator
+ where TOther : AEntity
+ where TKey : notnull
+{
+ internal Dictionary> _values = new();
+ internal Dictionary, TKey> _keys = new();
+
+ public void WriteTo(IBufferWriter writer, ISerializationRegistry registry)
+ {
+ var getSpan = writer.GetSpan(2);
+ BinaryPrimitives.WriteUInt16BigEndian(getSpan, (ushort) _values.Count);
+ writer.Advance(2);
+
+ foreach (var (key, value) in _values)
+ {
+ registry.Serialize(writer, key);
+ registry.Serialize(writer, value);
+ }
+ }
+
+ public int ReadFrom(ref ReadOnlySpan input, ISerializationRegistry registry)
+ {
+ var originalSize = input.Length;
+ var data = input;
+
+ var count = BinaryPrimitives.ReadUInt16BigEndian(data);
+ data = data.Slice(sizeof(ushort));
+
+ for (var idx = 0; idx < count; idx++)
+ {
+ var written = registry.Deserialize(data, out TKey key);
+ data = data.Slice(written);
+ written = registry.Deserialize(data, out EntityId value);
+ data = data.Slice(written);
+ _values.Add(key, value);
+ _keys.Add(value, key);
+ }
+
+ return originalSize - data.Length;
+ }
+}
diff --git a/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs b/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs
index 52edd223..a5959c99 100644
--- a/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs
+++ b/tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs
@@ -14,6 +14,7 @@ public void Apply(T context) where T : IEventContext
IEntity.TypeAttribute.New(context, Id);
Loadout._name.Set(context, Id, Name);
LoadoutRegistry._loadouts.Add(context, LoadoutRegistry.SingletonId, Id);
+ LoadoutRegistry._loadoutNames.Add(context, LoadoutRegistry.SingletonId, Name, Id);
}
public static EntityId Create(ITransaction tx, string name)
{
diff --git a/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs b/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs
index d98072c7..1cc257fe 100644
--- a/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs
+++ b/tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs
@@ -18,4 +18,8 @@ public class LoadoutRegistry(IEntityContext context) : AEntity(
public ReadOnlyObservableCollection Loadouts => _loadouts.Get(this);
internal static readonly MultiEntityAttributeDefinition _loadouts = new(nameof(Loadouts));
+
+ public Dictionary> LoadoutNames => _loadoutNames.Get(this);
+ internal static readonly IndexedMultiEntityAttributeDefinition _loadoutNames = new(nameof(LoadoutNames));
+
}
diff --git a/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs b/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs
index b5a183c5..84d96787 100644
--- a/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs
+++ b/tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs
@@ -31,6 +31,11 @@ public void CanSetupBasicLoadout()
var loadout = _ctx.Get(loadoutId);
loadout.Should().NotBeNull();
loadout.Name.Should().Be("Test");
+
+ var loadoutRegistry = _ctx.Get();
+ loadoutRegistry.Loadouts.Should().NotBeEmpty();
+ loadoutRegistry.Loadouts.First().Name.Should().Be("Test");
+ loadoutRegistry.LoadoutNames["Test"].Should().Be(loadoutId);
}
[Fact]