Skip to content
This repository has been archived by the owner on Jan 16, 2024. It is now read-only.

Support for persistence to EventStoreDB. Fixes #3 #4

Merged
merged 10 commits into from
Oct 27, 2020
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="EventStore.Client" Version="20.6.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DomainLib.Persistence\DomainLib.Serialization.csproj" />
<ProjectReference Include="..\DomainLib\DomainLib.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using EventStore.ClientAPI;

namespace DomainLib.Persistence.EventStore
{
public static class EventStoreEventDataExtensions
{
public static EventData ToEventData(this IEventSerializer @eventSerializer, object @event, string eventName)
{
var eventPersistenceData = eventSerializer.GetPersistenceData(@event, eventName);
return new EventData(eventPersistenceData.EventId, eventPersistenceData.EventName, eventPersistenceData.IsJsonBytes, eventPersistenceData.EventData, eventPersistenceData.EventMetadata);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dude wrap this 😛

}
}
}
65 changes: 65 additions & 0 deletions src/DomainLib.Persistence.EventStore/EventStoreEventsRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
using DomainLib.Aggregates;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;

namespace DomainLib.Persistence.EventStore
{
public class EventStoreEventsRepository : IEventsRepository
{
private readonly IEventStoreConnection _connection;
private readonly IEventSerializer _serializer;

public EventStoreEventsRepository(IEventStoreConnection connection, IEventSerializer serializer)
{
_connection = connection;
_serializer = serializer;
}

public async Task<long> SaveEventsAsync<TEvent>(string streamName, long expectedStreamVersion, IEnumerable<TEvent> events)
{
var eventDatas = events.Select(e =>
{
var eventName = e switch
{
INamedEvent ne => ne.EventName,
_ => e.GetType().Name
};

return _serializer.ToEventData(e, eventName);
});

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have an issue to address this TODO?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #8

// TODO: Handle failure cases
var writeResult = await _connection.AppendToStreamAsync(streamName, expectedStreamVersion, eventDatas);

return writeResult.NextExpectedVersion;
}

public async Task<IEnumerable<TEvent>> LoadEventsAsync<TEvent>(string streamName)
{
// TODO: Handle large streams in batches
var eventsSlice = await _connection.ReadStreamEventsForwardAsync(streamName, 0, ClientApiConstants.MaxReadSize, false);

switch (eventsSlice.Status)
{
case SliceReadStatus.Success:
break;
case SliceReadStatus.StreamNotFound:
// TOOO: Better exception
throw new InvalidOperationException($"Unable to find stream {streamName}");
case SliceReadStatus.StreamDeleted:
throw new StreamDeletedException(streamName);
default:
throw new ArgumentOutOfRangeException();
}

var events = eventsSlice.Events;

return events.Select(e => _serializer.DeserializeEvent<TEvent>(e.OriginalEvent.Data, e.OriginalEvent.EventType));
}
}
}
15 changes: 15 additions & 0 deletions src/DomainLib.Persistence/DomainLib.Serialization.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Text.Json" Version="4.7.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DomainLib\DomainLib.csproj" />
</ItemGroup>

</Project>
43 changes: 43 additions & 0 deletions src/DomainLib.Persistence/InvalidEventTypeException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Runtime.Serialization;

namespace DomainLib.Serialization
{
[Serializable]
public class InvalidEventTypeException : Exception
{
private readonly string _serializedEventType;
private readonly Type _runtTimeType;

//
// For guidelines regarding the creation of new exception types, see
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
// and
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
//

public InvalidEventTypeException(string serializedEventType, Type runtTimeType)
{
_serializedEventType = serializedEventType;
_runtTimeType = runtTimeType;
}

public InvalidEventTypeException(string message, string serializedEventType, Type runtTimeType) : base(message)
{
_serializedEventType = serializedEventType;
_runtTimeType = runtTimeType;
}

public InvalidEventTypeException(string message, string serializedEventType, Type runtTimeType, Exception inner) : base(message, inner)
{
_serializedEventType = serializedEventType;
_runtTimeType = runtTimeType;
}

protected InvalidEventTypeException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}
23 changes: 23 additions & 0 deletions src/DomainLib.Persistence/JsonEventPersistenceData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using DomainLib.Persistence;

namespace DomainLib.Serialization
{
public struct JsonEventPersistenceData : IEventPersistenceData
{
public JsonEventPersistenceData(Guid eventId, string eventName, byte[] eventData, byte[] eventMetadata) : this()
{
EventId = eventId;
EventName = eventName;
IsJsonBytes = true;
EventData = eventData;
EventMetadata = eventMetadata;
}

public Guid EventId { get; }
public string EventName { get; }
public bool IsJsonBytes { get; }
public byte[] EventData { get; }
public byte[] EventMetadata { get; }
}
}
59 changes: 59 additions & 0 deletions src/DomainLib.Persistence/JsonEventSerializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using DomainLib.Aggregates;
using DomainLib.Persistence;

namespace DomainLib.Serialization
{
public class JsonEventSerializer : IEventSerializer
{
private readonly JsonSerializerOptions _options;
private readonly IEventTypeMapping _eventTypeMappings = new EventTypeMapping();

public JsonEventSerializer()
{
_options = new JsonSerializerOptions
{
WriteIndented = true,
AllowTrailingCommas = false,
};
}

public JsonEventSerializer(JsonSerializerOptions options)
{
_options = options;
}

public void RegisterConverter(JsonConverter customConverter)
{
_options.Converters.Add(customConverter);
}

public void RegisterEventTypeMappings(IEventTypeMapping typeMapping)
{
_eventTypeMappings.Merge(typeMapping);
}

public IEventPersistenceData GetPersistenceData(object @event, string eventName)
{
return new JsonEventPersistenceData(Guid.NewGuid(), eventName, JsonSerializer.SerializeToUtf8Bytes(@event, _options), null);
}

public TEvent DeserializeEvent<TEvent>(byte[] eventData, string eventName)
{
var clrType = _eventTypeMappings.GetClrTypeForEvent(eventName);

var evt = JsonSerializer.Deserialize(eventData, clrType, _options);

if (evt is TEvent @event)
{
return @event;
}

var runtTimeType = typeof(TEvent);
throw new InvalidEventTypeException($"Cannot cast event of type {eventName} to {runtTimeType}", eventName, runtTimeType);
}
}
}
26 changes: 26 additions & 0 deletions src/DomainLib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DomainLib.Tests", "DomainLi
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{083A78FB-1B77-4383-868C-D9D3EB0E682E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DomainLib.Serialization", "DomainLib.Persistence\DomainLib.Serialization.csproj", "{9DB7AC64-6806-4C20-A9C8-99CB4AE20F57}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DomainLib.Persistence.EventStore", "DomainLib.Persistence.EventStore\DomainLib.Persistence.EventStore.csproj", "{10159135-E0DC-4ABC-9956-228D69E3653E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shopping.Infrastructure", "Examples\Shopping.Infrastructure\Shopping.Infrastructure.csproj", "{B3F13EA6-5E1C-41AE-97C5-C94AD16734E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shopping.Infrastructure.Tests", "Examples\Shopping.Infrastructure.Tests\Shopping.Infrastructure.Tests.csproj", "{94C7E8B3-87C9-4CC3-9177-B1D5164B2D7D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -35,13 +43,31 @@ Global
{B62C04E6-2DF3-49BC-8F5E-0A4388DCD9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B62C04E6-2DF3-49BC-8F5E-0A4388DCD9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B62C04E6-2DF3-49BC-8F5E-0A4388DCD9F6}.Release|Any CPU.Build.0 = Release|Any CPU
{9DB7AC64-6806-4C20-A9C8-99CB4AE20F57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9DB7AC64-6806-4C20-A9C8-99CB4AE20F57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9DB7AC64-6806-4C20-A9C8-99CB4AE20F57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9DB7AC64-6806-4C20-A9C8-99CB4AE20F57}.Release|Any CPU.Build.0 = Release|Any CPU
{10159135-E0DC-4ABC-9956-228D69E3653E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10159135-E0DC-4ABC-9956-228D69E3653E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10159135-E0DC-4ABC-9956-228D69E3653E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10159135-E0DC-4ABC-9956-228D69E3653E}.Release|Any CPU.Build.0 = Release|Any CPU
{B3F13EA6-5E1C-41AE-97C5-C94AD16734E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3F13EA6-5E1C-41AE-97C5-C94AD16734E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3F13EA6-5E1C-41AE-97C5-C94AD16734E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3F13EA6-5E1C-41AE-97C5-C94AD16734E7}.Release|Any CPU.Build.0 = Release|Any CPU
{94C7E8B3-87C9-4CC3-9177-B1D5164B2D7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94C7E8B3-87C9-4CC3-9177-B1D5164B2D7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94C7E8B3-87C9-4CC3-9177-B1D5164B2D7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94C7E8B3-87C9-4CC3-9177-B1D5164B2D7D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8E12A7F8-2C98-444C-9242-07164DE58E9C} = {083A78FB-1B77-4383-868C-D9D3EB0E682E}
{643C07DB-082E-4117-8D75-3CF917E33815} = {083A78FB-1B77-4383-868C-D9D3EB0E682E}
{B3F13EA6-5E1C-41AE-97C5-C94AD16734E7} = {083A78FB-1B77-4383-868C-D9D3EB0E682E}
{94C7E8B3-87C9-4CC3-9177-B1D5164B2D7D} = {083A78FB-1B77-4383-868C-D9D3EB0E682E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BA018103-0F59-4D7F-9119-099BF2CC020F}
Expand Down
2 changes: 2 additions & 0 deletions src/DomainLib.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Datas/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
6 changes: 5 additions & 1 deletion src/DomainLib/Aggregates/ApplyEventRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ public class ApplyEventRouter<TAggregateRoot, TDomainEventBase>
{
private readonly IReadOnlyDictionary<Type, ApplyEvent<TAggregateRoot, TDomainEventBase>> _routes;

public ApplyEventRouter(IEnumerable<KeyValuePair<Type, ApplyEvent<TAggregateRoot, TDomainEventBase>>> routes)
public ApplyEventRouter(IEnumerable<KeyValuePair<Type, ApplyEvent<TAggregateRoot, TDomainEventBase>>> routes,
IEnumerable<KeyValuePair<string, Type>> eventTypeMappings)
{
_routes = ImmutableDictionary.CreateRange(routes);
EventTypeMapping = new EventTypeMapping(eventTypeMappings);
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename the interface to IEventNameMap, and the implementation to EventNameMap? Then the property here can also be called EventNameMap. Seems a bit more concise and consistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed

public IEventTypeMapping EventTypeMapping { get; }

public TAggregateRoot Route(TAggregateRoot aggregateRoot, TDomainEventBase @event)
{
var eventType = @event.GetType();
Expand Down
20 changes: 19 additions & 1 deletion src/DomainLib/Aggregates/ApplyEventRouterBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,23 @@ public class ApplyEventRouterBuilder<TAggregateRoot, TDomainEventBase> :
private readonly List<KeyValuePair<Type, ApplyEvent<TAggregateRoot, TDomainEventBase>>> _routes =
new List<KeyValuePair<Type, ApplyEvent<TAggregateRoot, TDomainEventBase>>>();

private readonly List<KeyValuePair<string, Type>> _eventTypeMappings = new List<KeyValuePair<string, Type>>();

public void Add<TDomainEvent>(Func<TAggregateRoot, TDomainEvent, TAggregateRoot> eventApplier)
where TDomainEvent : TDomainEventBase
{
var route = KeyValuePair.Create<Type, ApplyEvent<TAggregateRoot, TDomainEventBase>>(
typeof(TDomainEvent), (agg, e) => eventApplier(agg, (TDomainEvent) e));

_routes.Add(route);

var eventName = GetEventName<TDomainEvent>();
_eventTypeMappings.Add(KeyValuePair.Create(eventName, typeof(TDomainEvent)));
}

public ApplyEventRouter<TAggregateRoot, TDomainEventBase> Build()
{
return new ApplyEventRouter<TAggregateRoot, TDomainEventBase>(this);
return new ApplyEventRouter<TAggregateRoot, TDomainEventBase>(this, _eventTypeMappings);
}

IEnumerator IEnumerable.GetEnumerator()
Expand All @@ -36,5 +41,18 @@ public IEnumerator<KeyValuePair<Type, ApplyEvent<TAggregateRoot, TDomainEventBas
{
return _routes.GetEnumerator();
}

private static string GetEventName<TDomainEvent>()
{
// TODO: This is kind of hacky. Have a think if there are better ways to do this
var eventName = typeof(TDomainEvent).GetField("EventName").GetValue(null) as string;

if (!string.IsNullOrEmpty(eventName))
{
return eventName;
}

return typeof(TDomainEvent).Name;
}
}
}
Loading