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)
{
var eventPersistenceData = eventSerializer.GetPersistenceData(@event);
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 😛

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

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 => _serializer.ToEventData(e));

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>
46 changes: 46 additions & 0 deletions src/DomainLib.Persistence/InvalidEventTypeException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Runtime.Serialization;

namespace DomainLib.Serialization
{
[Serializable]
public class InvalidEventTypeException : Exception
{
public string SerializedEventType { get; }
public string ClrTypeName { get; }

public InvalidEventTypeException(string serializedEventType, string clrTypeName)
{
SerializedEventType = serializedEventType;
ClrTypeName = clrTypeName;
}

public InvalidEventTypeException(string message, string serializedEventType, string clrTypeName)
: base(message)
{
SerializedEventType = serializedEventType;
ClrTypeName = clrTypeName;
}

public InvalidEventTypeException(string message, string serializedEventType, string clrTypeName, Exception inner)
: base(message, inner)
{
SerializedEventType = serializedEventType;
ClrTypeName = clrTypeName;
}

protected InvalidEventTypeException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
if (info == null)
{
throw new ArgumentNullException(nameof(info));
}

info.AddValue(nameof(ClrTypeName), ClrTypeName);
info.AddValue(nameof(SerializedEventType), SerializedEventType);

base.GetObjectData(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 readonly 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 DomainLib.Aggregates;
using DomainLib.Persistence;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DomainLib.Serialization
{
public class JsonEventSerializer : IEventSerializer
{
private readonly JsonSerializerOptions _options;
private readonly IEventNameMap _eventNameMap = new EventNameMap();

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(IEventNameMap eventNameMap)
{
_eventNameMap.Merge(eventNameMap);
}

public IEventPersistenceData GetPersistenceData(object @event)
{
var eventName = _eventNameMap.GetEventNameForClrType(@event.GetType());
return new JsonEventPersistenceData(Guid.NewGuid(), eventName, JsonSerializer.SerializeToUtf8Bytes(@event, _options), null);
}

public TEvent DeserializeEvent<TEvent>(byte[] eventData, string eventName)
{
var clrType = _eventNameMap.GetClrTypeForEventName(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.FullName}", eventName, runtTimeType.FullName);
}
}
}
26 changes: 25 additions & 1 deletion src/DomainLib.Tests/Aggregates/ApplyEventRouterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

namespace DomainLib.Tests.Aggregates
{
public static class EventNames
{
public const string SomethingHappened = "SomethingHappened";
public const string SomethingElseHappened = "SomethingElseHappened";
}

[TestFixture]
public class ApplyEventRouterTests
{
Expand Down Expand Up @@ -77,10 +83,27 @@ public void DefaultEventRouteIsInvokedWhenSpecified()
Assert.That(newState.OtherState, Is.Null);
}

[Test]
public void EventNamesAreMappedWhenEventRoutesAreAdded()
{
var builder = new ApplyEventRouterBuilder<MyAggregateRoot, IEvent>();
builder.Add<SomethingHappened>((agg, e) => agg.ApplyEvent(e));
builder.Add<SomethingElseHappened>((agg, e) => agg.ApplyEvent(e));

var router = builder.Build();

Assert.That(router.EventNameMap.GetEventNameForClrType(typeof(SomethingHappened)),
Is.EqualTo(EventNames.SomethingHappened));

Assert.That(router.EventNameMap.GetEventNameForClrType(typeof(SomethingElseHappened)),
Is.EqualTo(EventNames.SomethingElseHappened));
}

private interface IEvent
{
}

[EventName(EventNames.SomethingHappened)]
private class SomethingHappened : IEvent
{
public SomethingHappened(string state)
Expand All @@ -90,7 +113,8 @@ public SomethingHappened(string state)

public string State { get; }
}


[EventName(EventNames.SomethingElseHappened)]
private class SomethingElseHappened : IEvent
{
public SomethingElseHappened(string otherState)
Expand Down
Loading