Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serializing/Desierializing Events by their name #25

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Tacta.EventStore.DependencyInjection;
using Tacta.EventStore.Domain;
using Xunit;

namespace Tacta.EventStore.Test.DependencyInjection
{
#region DuplicateEventForTesting
public sealed class BacklogItemCreated : DomainEvent
{
public BacklogItemCreated(string aggregateId) : base(aggregateId) { }
public BacklogItemCreated(Guid id, string aggregateId, DateTime createdAt) : base(id, aggregateId, createdAt) { }
}
#endregion

public class EventStoreConfigurationTest
{
[Fact]
public void GivenTwoEventsWithTheSameName_WhenRegisteringEventStore_ShouldThrowException()
{
// Given
var services = new ServiceCollection();
var assemblies = new Assembly[]
{
typeof(BacklogItemCreated).Assembly,
};

// When + Then
var exception = Assert.Throws<ArgumentException>(() => services.AddEventStoreRepository(assemblies));
Assert.Contains(typeof(Domain.DomainEvents.BacklogItemCreated).FullName!, exception.Message);
Assert.Contains(typeof(Domain.DomainEvents.BacklogItemCreated).Assembly.FullName!, exception.Message);
Assert.Contains(typeof(BacklogItemCreated).FullName!, exception.Message);
Assert.Contains(typeof(BacklogItemCreated).Assembly.FullName!, exception.Message);
}
}
}
12 changes: 8 additions & 4 deletions Tacta.EventStore.Test/Projector/ProjectionProcessorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
using System.Data;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using Tacta.EventStore.DependencyInjection;
using Tacta.EventStore.Domain;
using Tacta.EventStore.Projector;
using Tacta.EventStore.Repository;
using Tacta.EventStore.Repository.Models;
using Tacta.EventStore.Test.Projector.DomainEvents;
using Tacta.EventStore.Test.Projector.Projections;
using Tacta.EventStore.Test.Repository;
using Tacta.EventStore.Test.Repository.DomainEvents;
using Xunit;
Expand All @@ -33,7 +31,13 @@ public class ProjectionProcessorTest : SqlBaseTest
public ProjectionProcessorTest()
{
_projectionMock = new Mock<IProjection>();
_eventStoreRepository = new EventStoreRepository(ConnectionFactory);
var domainEvents = new Dictionary<string, Type>
{
{nameof(FooRegistered), typeof(FooRegistered)},
{nameof(BooCreated), typeof(BooCreated)},
{nameof(BooActivated), typeof(BooActivated)}
};
_eventStoreRepository = new EventStoreRepository(ConnectionFactory, new EventNameToTypeConverter(domainEvents));
}

[Fact]
Expand Down
111 changes: 110 additions & 1 deletion Tacta.EventStore.Test/Repository/EventStoreRepositoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Tacta.EventStore.DependencyInjection;
using Tacta.EventStore.Domain;
using Tacta.EventStore.Repository;
using Tacta.EventStore.Repository.Exceptions;
Expand All @@ -20,7 +22,15 @@ public class EventStoreRepositoryTest : SqlBaseTest

public EventStoreRepositoryTest()
{
_eventStoreRepository = new EventStoreRepository(ConnectionFactory);
var eventNameConverter = new EventNameToTypeConverter(new Dictionary<string, Type>
{
{nameof(BacklogItemCreated), typeof(BacklogItemCreated)},
{nameof(SubTaskAdded), typeof(SubTaskAdded)},
{nameof(FooRegistered), typeof(FooRegistered)},
{nameof(BooCreated), typeof(BooCreated)},
{nameof(BooActivated), typeof(BooActivated)}
});
_eventStoreRepository = new EventStoreRepository(ConnectionFactory, eventNameConverter);
}


Expand Down Expand Up @@ -49,6 +59,105 @@ public async Task InsertAsync_GetAsync_SingleAggregate()
Assert.Equal(booCreated.Id, results.Single(x => x.AggregateId == booId).Id);
}

[Fact]
public async Task InsertAsync_OldSerialization_GetAsync_CorrectDeserialization()
{
// Given
const string booId = "001";
var booCreated = new BooCreated(booId, 100M, false);
var booActivated = new BooActivated(booId);
var jsonSerializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
NullValueHandling = NullValueHandling.Ignore,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead
};
var eventNameConverter = new EventNameToTypeConverter(new Dictionary<string, Type>
{
{nameof(BacklogItemCreated), typeof(BacklogItemCreated)},
{nameof(SubTaskAdded), typeof(SubTaskAdded)},
{nameof(FooRegistered), typeof(FooRegistered)},
{nameof(BooCreated), typeof(BooCreated)},
{nameof(BooActivated), typeof(BooActivated)}
});
var eventStoreRepository = new EventStoreRepository(ConnectionFactory, eventNameConverter,
jsonSerializerSettings);

var aggregateRecord = new AggregateRecord(booId, "Boo", 0);
var eventRecords = new List<EventRecord<DomainEvent>>
{
new EventRecord<DomainEvent>(booCreated.Id, booCreated.CreatedAt, booCreated),
new EventRecord<DomainEvent>(booActivated.Id, booActivated.CreatedAt, booActivated)
};
// When
await eventStoreRepository.SaveAsync(aggregateRecord, eventRecords).ConfigureAwait(false);

// Then
var results = await eventStoreRepository.GetAsync<DomainEvent>(booId).ConfigureAwait(false);

Assert.Equal(2, results.Count);
Assert.Contains(
results.Select(x => x.Event.GetType()).ToList(),
type => type == booCreated.GetType());
Assert.Contains(
results.Select(x => x.Event.GetType()).ToList(),
type => type == booActivated.GetType());

var resultBooCreated = results.First(x => x.Event.GetType() == booCreated.GetType());
var resultBooActivated = results.First(x => x.Event.GetType() == booActivated.GetType());
Assert.Equal(booCreated.CreatedAt.ToShortTimeString(), resultBooCreated.CreatedAt.ToShortTimeString());
Assert.Equal(booActivated.CreatedAt.ToShortTimeString(), resultBooActivated.CreatedAt.ToShortTimeString());
Assert.Equal(booCreated.Id, resultBooCreated.Id);
Assert.Equal(booActivated.Id, resultBooActivated.Id);
}

[Fact]
public async Task InsertAsync_Aggregate_OldSerialization_GetAsync_CorrectDeserialization()
{
// Given
var jsonSerializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
NullValueHandling = NullValueHandling.Ignore,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead
};
var eventNameConverter = new EventNameToTypeConverter(new Dictionary<string, Type>
{
{nameof(BacklogItemCreated), typeof(BacklogItemCreated)},
{nameof(SubTaskAdded), typeof(SubTaskAdded)},
{nameof(FooRegistered), typeof(FooRegistered)},
{nameof(BooCreated), typeof(BooCreated)},
{nameof(BooActivated), typeof(BooActivated)}
});
var eventStoreRepository = new EventStoreRepository(ConnectionFactory, eventNameConverter,
jsonSerializerSettings);

var aggregateId = new BacklogItemId();
var summary = "summary";
var aggregate = BacklogItem.FromSummary(aggregateId, summary);
var taskTitle = "task-title";
aggregate.AddTask(taskTitle);

// When
await eventStoreRepository.SaveAsync(aggregate).ConfigureAwait(false);

// Then
var results = await eventStoreRepository.GetAsync<DomainEvent>(aggregateId.ToString()).ConfigureAwait(false);

Assert.Equal(2, results.Count);
Assert.Contains(
results.Select(x => x.Event.GetType()).ToList(),
type => type == typeof(BacklogItemCreated));
Assert.Contains(
results.Select(x => x.Event.GetType()).ToList(),
type => type == typeof(SubTaskAdded));

var backlogItemCreated = (BacklogItemCreated) results.First(x => x.Event is BacklogItemCreated).Event;
var subTaskAdded = (SubTaskAdded) results.First(x => x.Event is SubTaskAdded).Event;
Assert.Equal(summary, backlogItemCreated.Summary);
Assert.Equal(taskTitle, subTaskAdded.Title);
}

[Fact]
public async Task InsertAsync_GetAsync_SingleAggregate_WhenSavedAsCollection()
{
Expand Down
3 changes: 2 additions & 1 deletion Tacta.EventStore.Test/Repository/ExceptionHandlingTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Tacta.EventStore.DependencyInjection;
using Tacta.EventStore.Domain;
using Tacta.EventStore.Repository;
using Tacta.EventStore.Repository.Exceptions;
Expand All @@ -18,7 +19,7 @@ public class ExceptionHandlingTest

public ExceptionHandlingTest()
{
_eventStoreRepository = new EventStoreRepository(null);
_eventStoreRepository = new EventStoreRepository(null, (IEventNameToTypeConverter)null);
}

[Fact]
Expand Down
32 changes: 32 additions & 0 deletions Tacta.EventStore/DependencyInjection/EventNameToTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;

namespace Tacta.EventStore.DependencyInjection
{
public class EventNameToTypeConverter : IEventNameToTypeConverter
{
private readonly IDictionary<string, Type> _eventTypeLookup;

public EventNameToTypeConverter(IDictionary<string, Type> eventTypeLookup)
{
_eventTypeLookup = eventTypeLookup;
}

public Type GetType(string eventName)
{
if (string.IsNullOrEmpty(eventName))
{
throw new ArgumentException($"Event Name cannot be empty or NULL!");
}

if (_eventTypeLookup.ContainsKey(eventName) == false)
{
throw new ArgumentException($"No Event registered for name: '{eventName}'");
}

var type = _eventTypeLookup[eventName];

return type;
}
}
}
62 changes: 62 additions & 0 deletions Tacta.EventStore/DependencyInjection/EventStoreConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Tacta.EventStore.Domain;
using Tacta.EventStore.Repository;

namespace Tacta.EventStore.DependencyInjection
{
public static class EventStoreConfiguration
{
public static IServiceCollection AddEventStoreRepository(this IServiceCollection services, Assembly[] assemblies)
{
GuardAgainstEventsWithSameName(assemblies);

var domainEvents = assemblies
.SelectMany(assembly => assembly.GetTypes())
.Where(t => typeof(IDomainEvent).IsAssignableFrom(t))
.ToDictionary(t => t.Name);

AddEventStoreRepository(services, domainEvents);

return services;
}

public static IServiceCollection AddEventStoreRepository(this IServiceCollection services, IDictionary<string, Type> domainEvents)
{
services.AddTransient<IEventStoreRepository, EventStoreRepository>();
services.AddSingleton<IEventNameToTypeConverter>(new EventNameToTypeConverter(domainEvents));

return services;
}

private static void GuardAgainstEventsWithSameName(Assembly[] assemblies)
{
var domainEvents = assemblies
.SelectMany(assembly => assembly.GetTypes())
.Where(t => typeof(IDomainEvent).IsAssignableFrom(t))
.Select(t => (Name: t.Name, Description: t.AssemblyQualifiedName))
.ToList();

var duplicates = domainEvents
.GroupBy(tuple => tuple.Name)
.Where(g => g.Count() > 1)
.ToList();

if (duplicates.Any())
{
var formattedDuplicates = new StringBuilder();
var duplicateDomainEvents = duplicates.SelectMany(d => domainEvents.Where(de => de.Name == d.Key));
foreach (var duplicateDomainEvent in duplicateDomainEvents)
formattedDuplicates.AppendLine(duplicateDomainEvent.Description);

throw new ArgumentException("Every event has to have a unique name! Following events collide:" +
$"{Environment.NewLine}{formattedDuplicates}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace Tacta.EventStore.DependencyInjection
{
public interface IEventNameToTypeConverter
{
Type GetType(string eventName);
}
}
15 changes: 10 additions & 5 deletions Tacta.EventStore/Repository/EventStoreRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Dapper;
using Newtonsoft.Json;
using Tacta.Connection;
using Tacta.EventStore.DependencyInjection;
using Tacta.EventStore.Domain;
using Tacta.EventStore.Repository.Exceptions;
using Tacta.EventStore.Repository.Models;
Expand All @@ -15,23 +16,27 @@ namespace Tacta.EventStore.Repository
public sealed class EventStoreRepository : IEventStoreRepository
{
private readonly IConnectionFactory _sqlConnectionFactory;
private readonly IEventNameToTypeConverter _nameToTypeConverter;

private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
TypeNameHandling = TypeNameHandling.None,
NullValueHandling = NullValueHandling.Ignore,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead
};

public EventStoreRepository(IConnectionFactory connectionFactory)
public EventStoreRepository(IConnectionFactory connectionFactory, IEventNameToTypeConverter nameToTypeConverter)
{
_sqlConnectionFactory = connectionFactory;
_nameToTypeConverter = nameToTypeConverter;
}

public EventStoreRepository(IConnectionFactory connectionFactory,
internal EventStoreRepository(IConnectionFactory connectionFactory,
IEventNameToTypeConverter nameToTypeConverter,
JsonSerializerSettings jsonSerializerSettings)
{
_sqlConnectionFactory = connectionFactory;
_nameToTypeConverter = nameToTypeConverter;
_jsonSerializerSettings = jsonSerializerSettings;
}

Expand Down Expand Up @@ -192,10 +197,10 @@ public async Task<IReadOnlyCollection<EventStoreRecord<T>>> GetAsync<T>(string q
.ToList().AsReadOnly();

if (!storedEvents.Any()) return new List<EventStoreRecord<T>>();

return storedEvents.Select(@event => new EventStoreRecord<T>
{
Event = JsonConvert.DeserializeObject<T>(@event.Payload, _jsonSerializerSettings),
Event = (T)JsonConvert.DeserializeObject(@event.Payload, _nameToTypeConverter.GetType(@event.Name)),
AggregateId = @event.AggregateId,
CreatedAt = @event.CreatedAt,
Id = @event.Id,
Expand Down
Loading
Loading