Skip to content

Commit

Permalink
Basic tests and project layout
Browse files Browse the repository at this point in the history
  • Loading branch information
halgari committed Dec 7, 2023
1 parent 9e14301 commit 2db5b71
Show file tree
Hide file tree
Showing 22 changed files with 499 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<LangVersion>12</LangVersion>
<ImplicitUsings>false</ImplicitUsings>
<WarningsAsErrors>Nullable</WarningsAsErrors>
</PropertyGroup>
Expand Down
7 changes: 7 additions & 0 deletions NexusMods.EventSourcing.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Tests", "tests\NexusMods.EventSourcing.Tests\NexusMods.EventSourcing.Tests.csproj", "{30CBEB4A-E0C0-4B11-A0CF-F97BFACEEF89}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.EventSourcing.Abstractions", "src\NexusMods.EventSourcing.Abstractions\NexusMods.EventSourcing.Abstractions.csproj", "{6737673E-5898-42EC-B0B2-60DE2CFFF0AF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -36,6 +38,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{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}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A92DED3D-BC67-4E04-9A06-9A1B302B3070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand All @@ -46,5 +49,9 @@ Global
{30CBEB4A-E0C0-4B11-A0CF-F97BFACEEF89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30CBEB4A-E0C0-4B11-A0CF-F97BFACEEF89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30CBEB4A-E0C0-4B11-A0CF-F97BFACEEF89}.Release|Any CPU.Build.0 = Release|Any CPU
{6737673E-5898-42EC-B0B2-60DE2CFFF0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
EndGlobal
42 changes: 42 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/EntityId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using TransparentValueObjects;

namespace NexusMods.EventSourcing.Abstractions;

[ValueObject<Guid>]
public readonly partial struct EntityId
{
public EntityId<T> Cast<T>() where T : IEntity => new(this);

}


/// <summary>
/// A strongly typed <see cref="EntityId"/> for a specific <see cref="IEntity"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
public readonly struct EntityId<T> where T : IEntity
{
/// <summary>
/// Creates a new instance of <see cref="EntityId{T}"/>.
/// </summary>
/// <returns></returns>
public static EntityId<T> NewId() => new(EntityId.NewId());

/// <summary>
/// Creates a new instance of <see cref="EntityId{T}"/>.
/// </summary>
/// <param name="id"></param>
public EntityId(EntityId id) => Value = id;

/// <summary>
/// The underlying value.
/// </summary>
public readonly EntityId Value;

/// <inheritdoc />
public override string ToString()
{
return typeof(T).Name + "<" + Value.Value + ">";
}
}
7 changes: 7 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/IEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NexusMods.EventSourcing.Abstractions;

public interface IEntity
{
public EntityId Id { get; }
}

47 changes: 47 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/IEntityContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Threading.Tasks;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// A context for working with entities and events. Multiple contexts can be used to work with different sets of entities
/// as of different transaction ids.
/// </summary>
public interface IEntityContext
{
/// <summary>
/// Adds the event to the event store, and advances the "as of" transaction id to the transaction id of the event.
/// </summary>
/// <param name="event"></param>
/// <returns></returns>
public ValueTask Transact(IEvent @event);

/// <summary>
/// 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.
/// </summary>
/// <param name="entityId"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public ValueTask<T> Retrieve<T>(EntityId<T> entityId) where T : IEntity;

/// <summary>
/// The current "as of" transaction id. The entities in this context are up-to-date as of this transaction id.
/// </summary>
public TransactionId AsOf { get; }

/// <summary>
/// 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.
/// </summary>
/// <param name="transactionId"></param>
/// <returns></returns>
public ValueTask Advance(TransactionId transactionId);

/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
public ValueTask Advance();


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

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// A single event that can be applied to an entity.
/// </summary>
[MemoryPackable(GenerateType.NoGenerate)]
public interface IEvent
{
/// <summary>
/// Applies the event to the entities attached to the event.
/// </summary>
ValueTask Apply<T>(T context) where T : IEventContext;

/// <summary>
/// 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.
/// </summary>
/// <param name="handler"></param>
void ModifiedEntities(Action<EntityId> handler);
}
27 changes: 27 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/IEventContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading.Tasks;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// This is the context interface passed to event handlers, it allows the handler to attach new entities to the context
/// </summary>
public interface IEventContext
{

/// <summary>
/// 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.
/// </summary>
/// <param name="entityId"></param>
/// <param name="entity"></param>
/// <typeparam name="TEntity"></typeparam>
public void AttachEntity<TEntity>(EntityId<TEntity> entityId, TEntity entity) where TEntity : IEntity;

/// <summary>
/// Retrieve an entity from the context, this may require the context to load the entity via replaying
/// the events up to the current transaction.
/// </summary>
/// <param name="id"></param>
/// <typeparam name="T"></typeparam>
public ValueTask<T> Retrieve<T>(EntityId<T> id) where T : IEntity;
}
12 changes: 12 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/IEventIngester.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// A mostly internal interface that is used to ingest events from the event store.
/// </summary>
public interface IEventIngester
{
public ValueTask Ingest(IEvent @event);
}
13 changes: 13 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/IEventStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Threading.Tasks;

namespace NexusMods.EventSourcing.Abstractions;


public interface IEventStore
{
public ValueTask Add<T>(T eventEntity) where T : IEvent;

public ValueTask EventsForEntity<TEntity, TIngester>(EntityId<TEntity> entityId, TIngester ingester)
where TEntity : IEntity
where TIngester : IEventIngester;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>NexusMods.EventSourcing.Abstractions</RootNamespace>
</PropertyGroup>

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

<Import Project="$([MSBuild]::GetPathOfFileAbove('NuGet.Build.props', '$(MSBuildThisFileDirectory)../'))" />
</Project>
19 changes: 19 additions & 0 deletions src/NexusMods.EventSourcing.Abstractions/TransactionId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using TransparentValueObjects;

namespace NexusMods.EventSourcing.Abstractions;

/// <summary>
/// A transaction id. In the context of this library, a transaction is a set of events that are applied to the entities
/// that they reference. The transaction id is a monotonic increasing number that is used to order the events over an
/// abstract idea of time. Transaction X is always considered to have happened before transaction X + 1.
/// </summary>
[ValueObject<ulong>]
public readonly partial struct TransactionId
{

/// <summary>
/// Get the next transaction id.
/// </summary>
/// <returns></returns>
public TransactionId Next() => new(Value + 1);
}
5 changes: 4 additions & 1 deletion src/NexusMods.EventSourcing/NexusMods.EventSourcing.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>NexusMods.EventSourcing</RootNamespace>
<RootNamespace>NexusMods.EventSourcing</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TransparentValueObjects" PrivateAssets="all" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<Import Project="$([MSBuild]::GetPathOfFileAbove('NuGet.Build.props', '$(MSBuildThisFileDirectory)../'))" />
</Project>
38 changes: 38 additions & 0 deletions tests/NexusMods.EventSourcing.Tests/BasicFunctionalityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using NexusMods.EventSourcing.Abstractions;
using NexusMods.EventSourcing.Tests.Contexts;
using NexusMods.EventSourcing.Tests.DataObjects;
using NexusMods.EventSourcing.Tests.Events;

namespace NexusMods.EventSourcing.Tests;

public class BasicFunctionalityTests
{
private readonly TestContext _ctx;
public BasicFunctionalityTests(TestContext ctx)
{
_ctx = ctx;
}

[Fact]
public async void CanApplyEvents()
{
var newId = EntityId<CountedEntity>.NewId();
await _ctx.Transact(new CreateCountedEntity
{
Name = "Test",
Id = newId,
InitialCount = 0
});
var entity = await _ctx.Retrieve(newId);
entity.Count.Should().Be(0);
await _ctx.Transact(new IncrementCount {Entity = newId, Increment = 1});
entity.Count.Should().Be(1);
await _ctx.Transact(new IncrementCount {Entity = newId, Increment = 1});
entity.Count.Should().Be(2);

_ctx.ResetCache();
entity = await _ctx.Retrieve(newId);
entity.Count.Should().Be(2);

}
}
51 changes: 51 additions & 0 deletions tests/NexusMods.EventSourcing.Tests/Contexts/InMemoryEventStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using MemoryPack;
using MemoryPack.Formatters;
using NexusMods.EventSourcing.Abstractions;
using NexusMods.EventSourcing.Tests.Events;

namespace NexusMods.EventSourcing.Tests.Contexts;

public class InMemoryEventStore : IEventStore
{
private readonly Dictionary<EntityId,IList<byte[]>> _events = new();

public InMemoryEventStore()
{
var formatter = new DynamicUnionFormatter<IEvent>(new[]
{
( (ushort)1, typeof(CreateCountedEntity)),
( (ushort)2, typeof(IncrementCount))
});
MemoryPackFormatterProvider.Register(formatter);
}

public ValueTask Add<T>(T entity) where T : IEvent
{
lock (_events)
{
var data = MemoryPackSerializer.Serialize(entity);
entity.ModifiedEntities(id =>
{
if (!_events.TryGetValue(id, out var value))
{
value = new List<byte[]>();
_events.Add(id, value);
}
value.Add(data);
});
}
return ValueTask.CompletedTask;
}


public ValueTask EventsForEntity<TEntity, TIngester>(EntityId<TEntity> entityId, TIngester ingester)
where TEntity : IEntity where TIngester : IEventIngester
{
foreach (var data in _events[entityId.Value])
{
var @event = MemoryPackSerializer.Deserialize<IEvent>(data)!;
ingester.Ingest(@event);
}
return ValueTask.CompletedTask;
}
}
Loading

0 comments on commit 2db5b71

Please sign in to comment.