-
Notifications
You must be signed in to change notification settings - Fork 1
Support for persistence to EventStoreDB. Fixes #3 #4
Changes from 3 commits
27a3bc5
7cce8f8
208f764
1bd70bd
69bf6cb
ae9f572
2dfd3c0
c3a5251
d3dde80
06d0aac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
} | ||
} | ||
} |
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); | ||
}); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we have an issue to address this TODO? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
} | ||
} |
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> |
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) | ||
{ | ||
} | ||
} | ||
} |
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; } | ||
} | ||
} |
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); | ||
} | ||
} | ||
} |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we rename the interface to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dude wrap this 😛