Skip to content

Commit

Permalink
Implement some basic serialization and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
halgari committed Dec 11, 2023
1 parent 2db5b71 commit ada3004
Show file tree
Hide file tree
Showing 26 changed files with 421 additions and 85 deletions.
7 changes: 7 additions & 0 deletions NexusMods.EventSourcing.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Abstractions", "src\NexusMods.EventSourcing.Abstractions\NexusMods.EventSourcing.Abstractions.csproj", "{6737673E-5898-42EC-B0B2-60DE2CFFF0AF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.TestModel", "tests\NexusMods.EventSourcing.TestModel\NexusMods.EventSourcing.TestModel.csproj", "{66DCB10E-1D80-4A83-8380-B2E08BEEE7AE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -39,6 +41,7 @@ Global
{A92DED3D-BC67-4E04-9A06-9A1B302B3070} = {0377EBE6-F147-4233-86AD-32C821B9567E}
{30CBEB4A-E0C0-4B11-A0CF-F97BFACEEF89} = {6ED01F9D-5E12-4EB2-9601-64A2ADC719DE}
{6737673E-5898-42EC-B0B2-60DE2CFFF0AF} = {0377EBE6-F147-4233-86AD-32C821B9567E}
{66DCB10E-1D80-4A83-8380-B2E08BEEE7AE} = {6ED01F9D-5E12-4EB2-9601-64A2ADC719DE}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A92DED3D-BC67-4E04-9A06-9A1B302B3070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand All @@ -53,5 +56,9 @@ Global
{6737673E-5898-42EC-B0B2-60DE2CFFF0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6737673E-5898-42EC-B0B2-60DE2CFFF0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6737673E-5898-42EC-B0B2-60DE2CFFF0AF}.Release|Any CPU.Build.0 = Release|Any CPU
{66DCB10E-1D80-4A83-8380-B2E08BEEE7AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66DCB10E-1D80-4A83-8380-B2E08BEEE7AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66DCB10E-1D80-4A83-8380-B2E08BEEE7AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66DCB10E-1D80-4A83-8380-B2E08BEEE7AE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// DI extensions for the event sourcing library.
/// </summary>
public static class DependencyInjectionExtensions
{
/// <summary>
/// Registers an event with the service collection.
/// </summary>
/// <param name="collection"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static IServiceCollection AddEvent<T>(this IServiceCollection collection) where T : class, IEvent
{
var type = typeof(T);
var attribute = type.GetCustomAttribute<EventIdAttribute>();
if (attribute is null)
{
throw new ArgumentException($"Event type {type.Name} does not have an EventIdAttribute.");
}
collection.AddSingleton(s => new EventDefinition(attribute.Guid, type));
return collection;
}

}
10 changes: 10 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/EventDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// A record that defines an event and the unique GUID that identifies it.
/// </summary>
/// <param name="Guid"></param>
/// <param name="Type"></param>
public record EventDefinition(Guid Guid, Type Type);
25 changes: 25 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/EventIdAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// Marks a an event as having the given GUID id
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class EventIdAttribute : Attribute
{
/// <summary>
/// The GUID of the entity type.
/// </summary>
public readonly Guid Guid;


/// <summary>
/// Creates a new instance of the <see cref="EventIdAttribute"/> class.
/// </summary>
/// <param name="guid"></param>
public EventIdAttribute(string guid)
{
Guid = Guid.Parse(guid);
}
}
9 changes: 9 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/ISingletonEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// Marks this entity as a singleton entity, the singleton id is used to retrieve the entity from the cache.
/// </summary>
public interface ISingletonEntity
{
public static virtual EntityId SingletonId { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

<ItemGroup>
<PackageReference Include="MemoryPack.Core" Version="1.10.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="TransparentValueObjects" PrivateAssets="all" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

Expand Down
46 changes: 46 additions & 0 deletions src/NexusMods.EventSourcing/EventFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MemoryPack;
using MemoryPack.Formatters;
using NexusMods.EventSourcing.Abstractions;

namespace NexusMods.EventSourcing;

public class EventFormatter : MemoryPackFormatter<IEvent>
{
private static Guid _zeroGuid = Guid.Empty;
private readonly Dictionary<Guid,Type> _eventByGuid;
private readonly Dictionary<Type,Guid> _eventsByType;

public EventFormatter(IEnumerable<EventDefinition> events)
{
_eventByGuid = events.ToDictionary(e => e.Guid, e => e.Type);
_eventsByType = events.ToDictionary(e => e.Type, e => e.Guid);
}

public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref IEvent? value)
{
if (value == null)
{
writer.WriteValue(_zeroGuid);
return;
}

var type = value.GetType();
writer.WriteValue(_eventsByType[type]);
writer.WriteValue(type, (object)value);
}

public override void Deserialize(ref MemoryPackReader reader, scoped ref IEvent? value)
{
var readValue = reader.ReadValue<Guid>();
if (readValue == _zeroGuid)
{
value = null;
return;
}
var mappedType = _eventByGuid[readValue];
value = (IEvent)reader.ReadValue(mappedType)!;
}
}
30 changes: 30 additions & 0 deletions src/NexusMods.EventSourcing/EventSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MemoryPack;
using MemoryPack.Formatters;
using NexusMods.EventSourcing.Abstractions;

namespace NexusMods.EventSourcing;

public class EventSerializer
{
public EventSerializer(IEnumerable<EventDefinition> events)
{
var formatter = new EventFormatter(events);
if (!MemoryPackFormatterProvider.IsRegistered<IEvent>())
MemoryPackFormatterProvider.Register(formatter);
}

public byte[] Serialize(IEvent @event)
{
return MemoryPackSerializer.Serialize(@event);
}

public IEvent Deserialize(byte[] data)
{
return MemoryPackSerializer.Deserialize<IEvent>(data)!;
}

}
6 changes: 6 additions & 0 deletions src/NexusMods.EventSourcing/NexusMods.EventSourcing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@
<ItemGroup>
<PackageReference Include="TransparentValueObjects" PrivateAssets="all" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NexusMods.EventSourcing.Abstractions\NexusMods.EventSourcing.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Extensions\" />
</ItemGroup>
<Import Project="$([MSBuild]::GetPathOfFileAbove('NuGet.Build.props', '$(MSBuildThisFileDirectory)../'))" />
</Project>
12 changes: 12 additions & 0 deletions src/NexusMods.EventSourcing/Services.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;

namespace NexusMods.EventSourcing;

public static class Services
{
public static IServiceCollection AddEventSourcing(this IServiceCollection services)
{
return services.AddSingleton<EventSerializer>();
}

}
36 changes: 36 additions & 0 deletions tests/NexusMods.EventSourcing.TestModel/Events/AddMod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using DynamicData;
using MemoryPack;
using NexusMods.EventSourcing.Abstractions;
using NexusMods.EventSourcing.TestModel.Model;

namespace NexusMods.EventSourcing.TestModel.Events;

[EventId("7DC8F80B-50B6-43B7-B805-43450E9F0C2B")]
[MemoryPackable]
public partial class AddMod : IEvent
{
public required string Name { get; init; } = string.Empty;
public required bool Enabled { get; init; } = true;
public required EntityId<Mod> Id { get; init; }
public required EntityId<Loadout> Loadout { get; init; }

public async ValueTask Apply<T>(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);

}

public void ModifiedEntities(Action<EntityId> handler)
{
handler(Id.Value);
handler(Loadout.Value);
}
}
35 changes: 35 additions & 0 deletions tests/NexusMods.EventSourcing.TestModel/Events/CreateLoadout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using DynamicData;
using MemoryPack;
using NexusMods.EventSourcing.Abstractions;
using NexusMods.EventSourcing.TestModel.Model;

namespace NexusMods.EventSourcing.TestModel.Events;

[EventId("63A4CB90-27E2-468A-BE94-CB01A38D8C09")]
[MemoryPackable]
public partial class CreateLoadout : IEvent
{
public required string Name { get; init; }

public required EntityId<Loadout> Id { get; init; }


public async ValueTask Apply<T>(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);
}

public static CreateLoadout Create(string name) => new() { Name = name, Id = EntityId<Loadout>.NewId() };

public void ModifiedEntities(Action<EntityId> handler)
{
handler(Id.Value);
}
}
29 changes: 29 additions & 0 deletions tests/NexusMods.EventSourcing.TestModel/Events/SwapModEnabled.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using MemoryPack;
using NexusMods.EventSourcing.Abstractions;
using NexusMods.EventSourcing.TestModel.Model;

namespace NexusMods.EventSourcing.TestModel.Events;

[EventId("8492075A-DED5-42BF-8D01-B4CDCE2526CF")]
[MemoryPackable]
public partial class SwapModEnabled : IEvent
{
public required EntityId<Mod> Id { get; init; }
public async ValueTask Apply<T>(T context) where T : IEventContext
{
var mod = await context.Retrieve(Id);
mod.Enabled = !mod.Enabled;
}

/// <summary>
/// Helper method to create a new event instance.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public static SwapModEnabled Create(EntityId<Mod> id) => new() { Id = id };

public void ModifiedEntities(Action<EntityId> handler)
{
handler(Id.Value);
}
}
26 changes: 26 additions & 0 deletions tests/NexusMods.EventSourcing.TestModel/Model/Collection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.ObjectModel;
using DynamicData;
using NexusMods.EventSourcing.Abstractions;
using ReactiveUI.Fody.Helpers;

namespace NexusMods.EventSourcing.TestModel.Model;

public class Collection
{
public Loadout Loadout { get; internal set; } = null!;

internal SourceCache<Mod, EntityId> _mods = new(x => x.Id);

private ReadOnlyObservableCollection<Mod> _modsConnected = null!;
public ReadOnlyObservableCollection<Mod> Mods { get; internal set; } = null!;

[Reactive]
public string Name { get; internal set; } = string.Empty;

public Collection()
{
_mods.Connect()
.Bind(out _modsConnected)
.Subscribe();
}
}
13 changes: 13 additions & 0 deletions tests/NexusMods.EventSourcing.TestModel/Model/Loadout.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using DynamicData;
using NexusMods.EventSourcing.Abstractions;

namespace NexusMods.EventSourcing.TestModel.Model;

public class Loadout : IEntity
{
public EntityId Id { get; internal set; }

public string Name { get; internal set; } = string.Empty;

internal SourceCache<Mod, EntityId> _mods = new(x => x.Id);
}
24 changes: 24 additions & 0 deletions tests/NexusMods.EventSourcing.TestModel/Model/LoadoutRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.ObjectModel;
using DynamicData;
using NexusMods.EventSourcing.Abstractions;

namespace NexusMods.EventSourcing.TestModel.Model;

public class LoadoutRegistry : IEntity
{
internal readonly SourceCache<Loadout, EntityId> _loadouts = new(x => x.Id);

private ReadOnlyObservableCollection<Loadout> _loadoutsConnected;
public ReadOnlyObservableCollection<Loadout> Loadouts => _loadoutsConnected;

public LoadoutRegistry()
{
_loadouts.Connect()
.Bind(out _loadoutsConnected)
.Subscribe();
}

public static EntityId<LoadoutRegistry> StaticId = new(EntityId.From(Guid.Parse("7F3E3745-51B9-44CB-BBDA-B1555191330E")));

public EntityId Id { get; } = StaticId.Value;
}
Loading

0 comments on commit ada3004

Please sign in to comment.