diff --git a/EventSauce.MongoDB/MongoDBSauceStore.cs b/EventSauce.MongoDB/MongoDBSauceStore.cs index 1ead646..fb53ead 100644 --- a/EventSauce.MongoDB/MongoDBSauceStore.cs +++ b/EventSauce.MongoDB/MongoDBSauceStore.cs @@ -17,13 +17,17 @@ public MongoDBSauceStore(IMongoCollection collection) _collection = collection; } - public async Task>> ReadEvents(TAggregateId id) where TAggregateId : SaucyAggregateId + public async Task>> ReadEvents(TAggregateId id) { try { const string fieldName = nameof(SaucyEvent.AggregateId); - using var cursor = await _collection.FindAsync(x => x[fieldName] == id.Id); + var serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + + var value = serializer.ToBsonValue(id); + + using var cursor = await _collection.FindAsync(x => x[fieldName] == value); var result = new List>(); @@ -40,7 +44,7 @@ public async Task>> ReadEvents(SaucyEvent sourceEvent, SaucyAggregateId? performedBy) where TAggregateId : SaucyAggregateId + public async Task AppendEvent(SaucyEvent sourceEvent, object? performedBy) { try { @@ -51,9 +55,13 @@ public async Task AppendEvent(SaucyEvent sourceEvent var bsonDocument = sourceEvent.ToBsonDocument(); + bsonDocument.Add("_id", ObjectId.GenerateNewId()); + if (performedBy != null) { - bsonDocument.Add("_performedBy", performedBy.Id); + var serializer = BsonSerializer.SerializerRegistry.GetSerializer(performedBy.GetType()); + + bsonDocument.Add("_performedBy", serializer.ToBsonValue(performedBy)); } await _collection.InsertOneAsync(bsonDocument); diff --git a/EventSauce.MongoDB/MongoDBSauceStoreFactory.cs b/EventSauce.MongoDB/MongoDBSauceStoreFactory.cs index 5243ceb..5ff9982 100644 --- a/EventSauce.MongoDB/MongoDBSauceStoreFactory.cs +++ b/EventSauce.MongoDB/MongoDBSauceStoreFactory.cs @@ -1,35 +1,33 @@ using MongoDB.Bson; -using MongoDB.Bson.Serialization; using MongoDB.Driver; using System; -using System.Reflection; +using MongoDB.Bson.Serialization.Conventions; namespace EventSauce.MongoDB { public class MongoDBSauceStoreFactory { - private readonly Assembly[] _assemblies; private readonly MongoClientSettings _clientSettings; private readonly string _database; private readonly string _collection; public MongoDBSauceStoreFactory( - Assembly[] assemblies, string connectionString, - string database) : this(assemblies, MongoClientSettings.FromConnectionString(connectionString), database, "events") { } + string database) : this(MongoClientSettings.FromConnectionString(connectionString), database, "events") + { + } public MongoDBSauceStoreFactory( - Assembly[] assemblies, MongoClientSettings clientSettings, - string database) : this(assemblies, clientSettings, database, "events") { } + string database) : this(clientSettings, database, "events") + { + } public MongoDBSauceStoreFactory( - Assembly[] assemblies, MongoClientSettings clientSettings, string database, string collection) { - _assemblies = assemblies; _clientSettings = clientSettings; _database = database; _collection = collection; @@ -41,28 +39,14 @@ private void FindEvents() { try { + var pack = new ConventionPack + { + new IgnoreExtraElementsConvention(true) + }; + var genericEventType = typeof(SaucyEvent<>); - foreach (var assembly in _assemblies) - { - foreach (var type in assembly.GetTypes()) - { - if (IsSubclassOfRawGeneric(genericEventType, type)) - { - var map = new BsonClassMap(type); - - map.AutoMap(); - map.SetIgnoreExtraElements(true); - - BsonClassMap.RegisterClassMap(map); - } - - if (type.IsSubclassOf(typeof(SaucyAggregateId))) - { - BsonSerializer.RegisterSerializer(type, new SaucyAggregateIdSerializer(type)); - } - } - } + ConventionRegistry.Register("Sauce Conventions", pack, type => IsSubclassOfRawGeneric(genericEventType, type)); } catch (Exception ex) { @@ -97,7 +81,7 @@ private IMongoCollection CreateCollection() var collection = database.GetCollection(_collection, settings); - CreateIndex(collection, nameof(SaucyEvent.AggregateId), false); + CreateIndex(collection, nameof(SaucyEvent.AggregateId), false); return collection; } @@ -115,42 +99,14 @@ private static void CreateIndex(IMongoCollection collection, strin collection.Indexes.CreateOne(indexModel); } - private class SaucyAggregateIdSerializer : IBsonSerializer + private static bool IsSubclassOfRawGeneric(Type generic, Type? toCheck) { - private readonly Type _type; - - public SaucyAggregateIdSerializer(Type type) - { - _type = type; - } - - public object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + while (toCheck != null && toCheck != typeof(object)) { - var id = BsonSerializer.Deserialize(context.Reader); - - var ctor = _type.GetConstructor(new[] { typeof(Guid) }); - - var instance = ctor!.Invoke(new object[] { id }); - - return instance; - } - - public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) - { - var aggregateId = (SaucyAggregateId)value; - - BsonSerializer.Serialize(context.Writer, aggregateId.Id); - } - - public Type ValueType => typeof(SaucyAggregateId); - } - - private static bool IsSubclassOfRawGeneric(Type generic, Type? toCheck) { - while (toCheck != null && toCheck != typeof(object)) { - var current = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; - if (generic == current) { + if (generic == current) + { return true; } diff --git a/EventSauce.Postgre/PostgreSauceStore.cs b/EventSauce.Postgre/PostgreSauceStore.cs index 5d1c6e2..f50a9cb 100644 --- a/EventSauce.Postgre/PostgreSauceStore.cs +++ b/EventSauce.Postgre/PostgreSauceStore.cs @@ -13,7 +13,6 @@ internal class PostgreSauceStore : ISauceStore private readonly string _tableName; private readonly Dictionary _eventTypes; - private readonly Dictionary _aggregateTypes; private readonly JsonSerializerOptions _options; @@ -21,17 +20,15 @@ public PostgreSauceStore( NpgsqlConnection connection, string tableName, Dictionary eventTypes, - Dictionary aggregateTypes, JsonSerializerOptions options) { _connection = connection; _tableName = tableName; _eventTypes = eventTypes; - _aggregateTypes = aggregateTypes; _options = options; } - public async Task>> ReadEvents(TAggregateId id) where TAggregateId : SaucyAggregateId + public async Task>> ReadEvents(TAggregateId id) { try { @@ -39,48 +36,40 @@ public async Task>> ReadEvents>(); - TAggregateId? aggregate = null; + TAggregateId? aggregate = default; var eventTypeIndex = reader.GetOrdinal("EventType"); var eventDataIndex = reader.GetOrdinal("EventData"); - var eventIdIndex = reader.GetOrdinal("EventId"); var createdIndex = reader.GetOrdinal("Created"); var aggregateVersionIndex = reader.GetOrdinal("AggregateVersion"); - var aggregateIdIndex = reader.GetOrdinal("AggregateId"); - var aggregateIdTypeNameIndex = reader.GetOrdinal("AggregateIdType"); while (reader.Read()) { var eventTypeName = reader.GetSaucyString(eventTypeIndex); var eventData = reader.GetSaucyString(eventDataIndex); - var eventId = reader.GetSaucyGuid(eventIdIndex); var created = reader.GetSaucyDate(createdIndex); var aggregateVersion = reader.GetSaucyLong(aggregateVersionIndex); var eventType = _eventTypes[eventTypeName]; - if (aggregate == null) + if (eventType.IsGenericType) { - var aggregateId = reader.GetSaucyGuid(aggregateIdIndex); - var aggregateIdTypeName = reader.GetSaucyString(aggregateIdTypeNameIndex); - var aggregateIdType = _aggregateTypes[aggregateIdTypeName]; - aggregate = (TAggregateId) Activator.CreateInstance(aggregateIdType, aggregateId)!; + eventType = eventType.MakeGenericType(typeof(TAggregateId)); } - var sourceEvent = (SaucyEvent) JsonSerializer.Deserialize(eventData, eventType, _options)!; + var sourceEvent = (SaucyEvent)JsonSerializer.Deserialize(eventData, eventType, _options)!; result.Add(sourceEvent with { AggregateVersion = aggregateVersion, - AggregateId = aggregate, - Id = eventId, + AggregateId = id, Created = created }); } @@ -93,7 +82,7 @@ public async Task>> ReadEvents(SaucyEvent sourceEvent, SaucyAggregateId? performedBy) where TAggregateId : SaucyAggregateId + public async Task AppendEvent(SaucyEvent sourceEvent, object? performedBy) { try { @@ -105,14 +94,14 @@ public async Task AppendEvent(SaucyEvent sourceEvent await using var command = new NpgsqlCommand(sql, _connection); - command.Parameters.AddWithValue("aggregate_id", sourceEvent.AggregateId?.Id ?? throw new ArgumentNullException(nameof(sourceEvent.AggregateId))); - command.Parameters.AddWithValue("aggregate_id_type", sourceEvent.AggregateId?.IdType ?? throw new ArgumentNullException(nameof(sourceEvent.AggregateId))); + command.Parameters.AddWithValue("aggregate_id", sourceEvent.AggregateId); + command.Parameters.AddWithValue("aggregate_id_type", typeof(TAggregateId).Name); command.Parameters.AddWithValue("aggregate_version", sourceEvent.AggregateVersion); command.Parameters.AddWithValue("created", sourceEvent.Created); - command.Parameters.AddWithValue("event_id", sourceEvent.Id); + command.Parameters.AddWithValue("event_id", Guid.NewGuid()); command.Parameters.AddWithValue("event_type", eventType); command.Parameters.AddWithValue("event_data", NpgsqlDbType.Jsonb, eventData); - command.Parameters.AddWithValue("performed_by", performedBy?.Id ?? Guid.Empty); + command.Parameters.AddWithValue("performed_by", performedBy ?? Guid.Empty); await command.ExecuteNonQueryAsync(); } diff --git a/EventSauce.Postgre/PostgreSauceStoreFactory.cs b/EventSauce.Postgre/PostgreSauceStoreFactory.cs index d394b1c..ef315d8 100644 --- a/EventSauce.Postgre/PostgreSauceStoreFactory.cs +++ b/EventSauce.Postgre/PostgreSauceStoreFactory.cs @@ -14,7 +14,6 @@ public class PostgreSauceStoreFactory private readonly string _tableName; private readonly Dictionary _eventTypes = new(); - private readonly Dictionary _aggregateTypes = new(); public PostgreSauceStoreFactory( Assembly[] assemblies, @@ -70,11 +69,6 @@ private void FindEvents() { _eventTypes.Add(type.Name, type); } - - if (type.IsSubclassOf(typeof(SaucyAggregateId))) - { - _aggregateTypes.Add(type.Name, type); - } } } } @@ -90,7 +84,7 @@ public ISauceStore Create() { var connection = CreateConnection(); - return new PostgreSauceStore(connection, _tableName, _eventTypes, _aggregateTypes, _options); + return new PostgreSauceStore(connection, _tableName, _eventTypes, _options); } catch (Exception ex) { diff --git a/EventSauce.Tests/Integration/InMemoryTests.cs b/EventSauce.Tests/Integration/InMemoryTests.cs index cac3de2..696234c 100644 --- a/EventSauce.Tests/Integration/InMemoryTests.cs +++ b/EventSauce.Tests/Integration/InMemoryTests.cs @@ -49,27 +49,26 @@ public InMemoryTests(InMemoryStorageFixture fixture) [Fact] public void Aggregate_WhenCreated_ShouldHaveOneEvent() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); var events = user.GetUncommittedEvents().ToList(); Assert.Single(events); - var createdEvent = events[0] as UserCreatedEvent; + var createdEvent = events[0] as UserCreatedEvent; Assert.NotNull(createdEvent); - Assert.Equal(userId, createdEvent?.AggregateId); - Assert.Equal(email, createdEvent?.Email); - Assert.Equal(auth, createdEvent?.AuthId); - Assert.Equal(0, createdEvent?.AggregateVersion); + Assert.Equal(userId, createdEvent.AggregateId); + Assert.Equal(email, createdEvent.Email); + Assert.Equal(auth, createdEvent.AuthId); + Assert.Equal(0, createdEvent.AggregateVersion); - Assert.NotEqual(Guid.Empty, createdEvent?.Id); Assert.True(createdEvent?.Created > PastDate()); Assert.Equal(userId, user.Id); @@ -80,12 +79,12 @@ public void Aggregate_WhenCreated_ShouldHaveOneEvent() [Fact] public void Aggregate_WhenCreatedAndEmailChanged_ShouldHaveTwoEvents() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -95,19 +94,18 @@ public void Aggregate_WhenCreatedAndEmailChanged_ShouldHaveTwoEvents() Assert.Equal(2, events.Count); - var createdEvent = events[0] as UserCreatedEvent; + var createdEvent = events[0] as UserCreatedEvent; Assert.NotNull(createdEvent); - Assert.Equal(userId, createdEvent?.AggregateId); - Assert.Equal(email, createdEvent?.Email); - Assert.Equal(auth, createdEvent?.AuthId); - Assert.Equal(0, createdEvent?.AggregateVersion); + Assert.Equal(userId, createdEvent.AggregateId); + Assert.Equal(email, createdEvent.Email); + Assert.Equal(auth, createdEvent.AuthId); + Assert.Equal(0, createdEvent.AggregateVersion); - Assert.NotEqual(Guid.Empty, createdEvent?.Id); - Assert.True(createdEvent?.Created > PastDate()); + Assert.True(createdEvent.Created > PastDate()); - var changeEmailEvent = events[1] as UserChangeEmailEvent; + var changeEmailEvent = events[1] as UserChangeEmailEvent; Assert.NotNull(createdEvent); @@ -115,7 +113,6 @@ public void Aggregate_WhenCreatedAndEmailChanged_ShouldHaveTwoEvents() Assert.Equal(newEmail, changeEmailEvent?.Email); Assert.Equal(1, changeEmailEvent?.AggregateVersion); - Assert.NotEqual(Guid.Empty, changeEmailEvent?.Id); Assert.True(changeEmailEvent?.Created > PastDate()); Assert.Equal(userId, user.Id); @@ -126,12 +123,12 @@ public void Aggregate_WhenCreatedAndEmailChanged_ShouldHaveTwoEvents() [Fact] public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -139,7 +136,7 @@ public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() var sut = _fixture.GetSutObject(); - await sut.Save(user); + await sut.Save, User>(user); var events = user.GetUncommittedEvents().ToList(); @@ -153,39 +150,39 @@ public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() [Fact] public async Task Aggregate_WhenCreatedAndSaved_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, User>(userId); Assert.Null(repoUser); - await sut.Save(user); + await sut.Save, User>(user); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, User>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(email, repoUser?.Email); - Assert.Equal(0, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(email, repoUser.Email); + Assert.Equal(0, repoUser.Version); } [Fact] public async Task Aggregate_WhenCreatedChangedAndSaved_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -193,19 +190,19 @@ public async Task Aggregate_WhenCreatedChangedAndSaved_ShouldRetrieve() var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, User>(userId); Assert.Null(repoUser); - await sut.Save(user); + await sut.Save, User>(user); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, User>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(newEmail, repoUser?.Email); - Assert.Equal(1, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(newEmail, repoUser.Email); + Assert.Equal(1, repoUser.Version); } } } diff --git a/EventSauce.Tests/Integration/MongoDBTests.cs b/EventSauce.Tests/Integration/MongoDBTests.cs index 11ff429..4927ae8 100644 --- a/EventSauce.Tests/Integration/MongoDBTests.cs +++ b/EventSauce.Tests/Integration/MongoDBTests.cs @@ -1,9 +1,11 @@ using EventSauce.Extensions.Microsoft.DependencyInjection; using EventSauce.MongoDB; using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson.Serialization; using System; using System.Linq; using System.Threading.Tasks; +using MongoDB.Bson; using Xunit; #pragma warning disable CA1711 @@ -16,6 +18,9 @@ public sealed class MongoDBStorageFixture : IDisposable public MongoDBStorageFixture() { + var objectIdSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + BsonSerializer.RegisterSerializer(new UserSerializer(objectIdSerializer)); + var services = new ServiceCollection(); const string connectionString = @@ -24,10 +29,6 @@ public MongoDBStorageFixture() services.AddEventSauce(options => { var factory = new MongoDBSauceStoreFactory( - new[] - { - typeof(MongoDBTests).Assembly - }, connectionString, "sauce"); @@ -64,12 +65,12 @@ public MongoDBTests(MongoDBStorageFixture fixture) [Fact] public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -77,7 +78,7 @@ public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() var sut = _fixture.GetSutObject(); - await sut.Save(user); + await sut.Save, User>(user); var events = user.GetUncommittedEvents().ToList(); @@ -91,39 +92,39 @@ public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() [Fact] public async Task Aggregate_WhenCreatedAndSaved_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, User>(userId); Assert.Null(repoUser); - await sut.Save(user); + await sut.Save, User>(user); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, User>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(email, repoUser?.Email); - Assert.Equal(0, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(email, repoUser.Email); + Assert.Equal(0, repoUser.Version); } [Fact] public async Task Aggregate_WhenCreatedChangedAndSaved_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -131,30 +132,30 @@ public async Task Aggregate_WhenCreatedChangedAndSaved_ShouldRetrieve() var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, User>(userId); Assert.Null(repoUser); - await sut.Save(user); + await sut.Save, User>(user); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, User>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(newEmail, repoUser?.Email); - Assert.Equal(1, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(newEmail, repoUser.Email); + Assert.Equal(1, repoUser.Version); } [Fact] public async Task Aggregate_WhenCreatedChangedAndSavedWithPerformedBy_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -162,30 +163,30 @@ public async Task Aggregate_WhenCreatedChangedAndSavedWithPerformedBy_ShouldRetr var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, User>(userId); Assert.Null(repoUser); - await sut.Save(user, userId); + await sut.Save, User>(user, userId); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, User>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(newEmail, repoUser?.Email); - Assert.Equal(1, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(newEmail, repoUser.Email); + Assert.Equal(1, repoUser.Version); } [Fact] public async Task Aggregate_WhenCreatedChangedThenChangedToInvalidAndSavedWithPerformedBy_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = User.New(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; const string invalidEmail = "faker_email"; @@ -195,19 +196,19 @@ public async Task Aggregate_WhenCreatedChangedThenChangedToInvalidAndSavedWithPe var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, User>(userId); Assert.Null(repoUser); - await sut.Save(user, userId); + await sut.Save, User>(user, userId); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, User>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(invalidEmail, repoUser?.Email); - Assert.Equal(2, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(invalidEmail, repoUser.Email); + Assert.Equal(2, repoUser.Version); } } } diff --git a/EventSauce.Tests/Integration/PostgreTests.cs b/EventSauce.Tests/Integration/PostgreTests.cs index 88723c7..8bcd695 100644 --- a/EventSauce.Tests/Integration/PostgreTests.cs +++ b/EventSauce.Tests/Integration/PostgreTests.cs @@ -66,12 +66,12 @@ public PostgreTests(PostgreStorageFixture fixture) [Fact] public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() { - var userId = User.NewUser(); + var userId = Guid.NewGuid(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -79,7 +79,7 @@ public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() var sut = _fixture.GetSutObject(); - await sut.Save(user); + await sut.Save, Guid>(user); var events = user.GetUncommittedEvents().ToList(); @@ -93,39 +93,39 @@ public async Task Aggregate_WhenCreatedAndSaved_ShouldHaveNoUncommittedEvents() [Fact] public async Task Aggregate_WhenCreatedAndSaved_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = Guid.NewGuid(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, Guid>(userId); Assert.Null(repoUser); - await sut.Save(user); + await sut.Save, Guid>(user); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, Guid>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(email, repoUser?.Email); - Assert.Equal(0, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(email, repoUser.Email); + Assert.Equal(0, repoUser.Version); } [Fact] public async Task Aggregate_WhenCreatedChangedAndSaved_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = Guid.NewGuid(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -133,30 +133,30 @@ public async Task Aggregate_WhenCreatedChangedAndSaved_ShouldRetrieve() var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, Guid>(userId); Assert.Null(repoUser); - await sut.Save(user); + await sut.Save, Guid>(user); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, Guid>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(newEmail, repoUser?.Email); - Assert.Equal(1, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(newEmail, repoUser.Email); + Assert.Equal(1, repoUser.Version); } [Fact] public async Task Aggregate_WhenCreatedChangedAndSavedWithPerformedBy_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = Guid.NewGuid(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; @@ -164,30 +164,30 @@ public async Task Aggregate_WhenCreatedChangedAndSavedWithPerformedBy_ShouldRetr var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, Guid>(userId); Assert.Null(repoUser); - await sut.Save(user, userId); + await sut.Save, Guid>(user, userId); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, Guid>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(newEmail, repoUser?.Email); - Assert.Equal(1, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(newEmail, repoUser.Email); + Assert.Equal(1, repoUser.Version); } [Fact] public async Task Aggregate_WhenCreatedChangedThenChangedToInvalidAndSavedWithPerformedBy_ShouldRetrieve() { - var userId = User.NewUser(); + var userId = Guid.NewGuid(); const string? email = "origina@gmail.com"; const string? auth = "auth_id"; - var user = new UserAggregate(userId, email, auth); + var user = new UserAggregate(userId, email, auth); const string newEmail = "changed@gmail.com"; const string invalidEmail = "faker_email"; @@ -197,19 +197,19 @@ public async Task Aggregate_WhenCreatedChangedThenChangedToInvalidAndSavedWithPe var sut = _fixture.GetSutObject(); - var repoUser = await sut.GetById(userId); + var repoUser = await sut.GetById, Guid>(userId); Assert.Null(repoUser); - await sut.Save(user, userId); + await sut.Save, Guid>(user, userId); - repoUser = await sut.GetById(userId); + repoUser = await sut.GetById, Guid>(userId); Assert.NotNull(repoUser); - Assert.Equal(userId, repoUser?.Id); - Assert.Equal(invalidEmail, repoUser?.Email); - Assert.Equal(2, repoUser?.Version); + Assert.Equal(userId, repoUser.Id); + Assert.Equal(invalidEmail, repoUser.Email); + Assert.Equal(2, repoUser.Version); } } } diff --git a/EventSauce.Tests/Integration/TestAggregate.cs b/EventSauce.Tests/Integration/TestAggregate.cs index 90208fe..007f978 100644 --- a/EventSauce.Tests/Integration/TestAggregate.cs +++ b/EventSauce.Tests/Integration/TestAggregate.cs @@ -1,16 +1,20 @@ -using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; namespace EventSauce.Tests.Integration { - public class UserAggregate : SaucyAggregate + public class UserAggregate : SaucyAggregate { private UserAggregate() { } public string Email { get; private set; } = string.Empty; - public UserAggregate(User id, string email, string authId) + public UserAggregate(T id, string email, string authId) { - IssueEvent(new UserCreatedEvent + Id = id; + + IssueEvent(new UserCreatedEvent { Email = email, AuthId = authId, @@ -20,7 +24,7 @@ public UserAggregate(User id, string email, string authId) public void ChangeEmail(string email) { - IssueEvent(new UserChangeEmailEvent + IssueEvent(new UserChangeEmailEvent { Email = email }); @@ -28,7 +32,7 @@ public void ChangeEmail(string email) public void ChangeEmailToInvalid(string email) { - IssueEvent(new UserChangeEmailToInvalidEvent + IssueEvent(new UserChangeEmailToInvalidEvent { Email = email, IsValid = true @@ -41,41 +45,48 @@ public void ChangeEmailToInvalid(string email) /// how data is loaded back to the aggregate when /// reading from the database. /// - public void Apply(UserCreatedEvent domainEvent) + public void Apply(UserCreatedEvent domainEvent) { Email = domainEvent.Email; } - public void Apply(UserChangeEmailEvent domainEvent) + public void Apply(UserChangeEmailEvent domainEvent) { Email = domainEvent.Email; } } - public record UserCreatedEvent : SaucyEvent + public record UserCreatedEvent : SaucyEvent { public string Email { get; init; } = string.Empty; public string AuthId { get; init; } = string.Empty; } - public record UserChangeEmailEvent : SaucyEvent + public record UserChangeEmailEvent : SaucyEvent { public string Email { get; init; } = string.Empty; } - public record UserChangeEmailToInvalidEvent : UserChangeEmailEvent + public record UserChangeEmailToInvalidEvent : UserChangeEmailEvent { public bool IsValid { get; init; } = false; } - public record User : SaucyAggregateId + public record User(ObjectId Value) { - public User(Guid id) : base(id) { } + public static User New() => new(ObjectId.GenerateNewId()); + } - public static User NewUser() - { - return new (Guid.NewGuid()); - } + public class UserSerializer : SerializerBase + { + private readonly IBsonSerializer _serializer; + public UserSerializer(IBsonSerializer serializer) => _serializer = serializer; + + public override User Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + => new(_serializer.Deserialize(context, args)); + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, User value) + => _serializer.Serialize(context, args, value.Value); } } diff --git a/EventSauce.Tests/Unit/EventSourcingTests.cs b/EventSauce.Tests/Unit/EventSourcingTests.cs index 6378adb..64d4c0a 100644 --- a/EventSauce.Tests/Unit/EventSourcingTests.cs +++ b/EventSauce.Tests/Unit/EventSourcingTests.cs @@ -6,56 +6,27 @@ namespace EventSauce.Tests.Unit { public class EventSourcingTests { - internal record UnitUserCreatedEvent : SaucyEvent + internal record UnitUserCreatedEvent : SaucyEvent { public string Email { get; init; } = string.Empty; public string AuthId { get; init; } = string.Empty; } - internal record UnitUser : SaucyAggregateId - { - public UnitUser(Guid id) : base(id) { } - - public static UnitUser NewUser() - { - return new (Guid.NewGuid()); - } - } - - [Fact] - public void AggregateIdBase_ShouldBeCompared_BasedOnProperties() - { - var guid = Guid.NewGuid(); - - var idOne = new UnitUser(guid); - - var idTwo = new UnitUser(guid); - - Assert.Equal(idOne, idTwo); - - idTwo = idTwo with - { - Id = Guid.NewGuid() - }; - - Assert.NotEqual(idOne, idTwo); - } - [Fact] public void DomainEvents_ShouldBeCompared_BasedOnProperties() { var eventOne = new UnitUserCreatedEvent { Email = "joseph@gmail.com", - AggregateId = UnitUser.NewUser(), + AggregateId = Guid.NewGuid(), AggregateVersion = 1 }; var eventTwo = eventOne with { Email = "adolf@gmail.com", - AggregateId = UnitUser.NewUser(), + AggregateId = Guid.NewGuid(), AggregateVersion = 2 }; @@ -68,7 +39,7 @@ public void DomainEvents_ShouldBeSerialized_WithoutBaseProperties() var domainEvent = new UnitUserCreatedEvent { Email = "joseph@gmail.com", - AggregateId = UnitUser.NewUser(), + AggregateId = Guid.NewGuid(), AggregateVersion = 1, AuthId = "some random id" }; diff --git a/EventSauce/EventSourcingRepository.cs b/EventSauce/EventSourcingRepository.cs index a87df37..ea7f554 100644 --- a/EventSauce/EventSourcingRepository.cs +++ b/EventSauce/EventSourcingRepository.cs @@ -8,12 +8,10 @@ namespace EventSauce public interface ISaucyRepository { Task GetById(TAggregateId id, CancellationToken cancellationToken = default) - where TAggregate : SaucyAggregate - where TAggregateId : SaucyAggregateId; + where TAggregate : SaucyAggregate; - Task Save(TAggregate aggregate, SaucyAggregateId? performedBy = null, CancellationToken cancellationToken = default) - where TAggregate : SaucyAggregate - where TAggregateId : SaucyAggregateId; + Task Save(TAggregate aggregate, object? performedBy = null, CancellationToken cancellationToken = default) + where TAggregate : SaucyAggregate; } public class SaucyRepository : ISaucyRepository @@ -31,12 +29,13 @@ public SaucyRepository( public async Task GetById(TAggregateId id, CancellationToken cancellationToken = default) where TAggregate : SaucyAggregate - where TAggregateId : SaucyAggregateId { try { var aggregate = CreateEmptyAggregate(); + aggregate.Id = id; + foreach (var domainEvent in await _sauceStore.ReadEvents(id)) { aggregate.ApplyEvent(domainEvent); @@ -54,9 +53,8 @@ public SaucyRepository( } } - public async Task Save(TAggregate aggregate, SaucyAggregateId? performedBy = null, CancellationToken cancellationToken = default) + public async Task Save(TAggregate aggregate, object? performedBy = null, CancellationToken cancellationToken = default) where TAggregate : SaucyAggregate - where TAggregateId : SaucyAggregateId { try { diff --git a/EventSauce/EventStore.cs b/EventSauce/EventStore.cs index 2e2f3a4..f27caa7 100644 --- a/EventSauce/EventStore.cs +++ b/EventSauce/EventStore.cs @@ -7,19 +7,19 @@ namespace EventSauce { public interface ISauceStore : IDisposable { - Task>> ReadEvents(TAggregateId id) where TAggregateId : SaucyAggregateId; + Task>> ReadEvents(TAggregateId id); - Task AppendEvent(SaucyEvent sourceEvent, SaucyAggregateId? performedBy) where TAggregateId : SaucyAggregateId; + Task AppendEvent(SaucyEvent sourceEvent, object? performedBy); } public interface ISaucyBus { - Task Publish(SaucyEvent saucyEvent) where TAggregateId : SaucyAggregateId; + Task Publish(SaucyEvent saucyEvent); } public class StubbedSauceBus : ISaucyBus { - public Task Publish(SaucyEvent saucyEvent) where TAggregateId : SaucyAggregateId + public Task Publish(SaucyEvent saucyEvent) { return Task.CompletedTask; } @@ -33,7 +33,7 @@ public void Dispose() { } - public Task>> ReadEvents(TAggregateId id) where TAggregateId : SaucyAggregateId + public Task>> ReadEvents(TAggregateId id) { var type = typeof(TAggregateId); @@ -42,10 +42,10 @@ public Task>> ReadEvents(TAgg return Task.FromResult((IEnumerable>)new List>()); } - return Task.FromResult(Store[type.FullName!].Cast>().Where(x => x.AggregateId == id)); + return Task.FromResult(Store[type.FullName!].Cast>().Where(x => x.AggregateId!.Equals(id))); } - public Task AppendEvent(SaucyEvent sourceEvent, SaucyAggregateId? performedBy) where TAggregateId : SaucyAggregateId + public Task AppendEvent(SaucyEvent sourceEvent, object? performedBy) { var type = typeof(TAggregateId); diff --git a/EventSauce/SaucyAggregate.cs b/EventSauce/SaucyAggregate.cs index f81c313..0e05696 100644 --- a/EventSauce/SaucyAggregate.cs +++ b/EventSauce/SaucyAggregate.cs @@ -8,26 +8,18 @@ namespace EventSauce { - public abstract class SaucyAggregate where TAggregateId : SaucyAggregateId + public abstract class SaucyAggregate { private readonly ICollection> _uncommittedEvents = new LinkedList>(); internal const long NewAggregateVersion = -1; - public TAggregateId? Id { get; private set; } + public TAggregateId? Id { get; protected internal set; } public long Version { get; private set; } = NewAggregateVersion; internal void ApplyEvent(SaucyEvent saucyEvent) { - // Event was already applied - if (_uncommittedEvents.Any(x => x.Id == saucyEvent.Id)) - { - return; - } - - Id ??= saucyEvent.AggregateId; - // Apparently this is faster // than switch cases ((dynamic)this).Apply((dynamic)saucyEvent); @@ -47,46 +39,28 @@ internal IEnumerable> GetUncommittedEvents() protected void IssueEvent(SaucyEvent saucyEvent) { - var eventWithAggregate = HydrateEvent(saucyEvent); - - ApplyEvent(eventWithAggregate); - - _uncommittedEvents.Add(eventWithAggregate); - } - - private SaucyEvent HydrateEvent(SaucyEvent saucyEvent) - { - var aggregateId = Id ?? saucyEvent.AggregateId; - - if (aggregateId == null || string.IsNullOrEmpty(aggregateId.IdType) || aggregateId.Id == Guid.Empty) - { - throw new EventSauceException($"Aggregate ID {aggregateId} is not valid"); - } - - return saucyEvent with + saucyEvent = saucyEvent with { - AggregateId = aggregateId, + AggregateId = Id, AggregateVersion = Version + 1 }; - } - } - public abstract record SaucyAggregateId(Guid Id) - { - public string IdType => GetType().Name; + // Event was already applied + if (_uncommittedEvents.Any(x => x == saucyEvent)) + { + return; + } - public override string ToString() - { - return $"[{IdType}|{Id}]"; + ApplyEvent(saucyEvent); + + _uncommittedEvents.Add(saucyEvent); } } - public abstract record SaucyEvent where TAggregateId : SaucyAggregateId + public abstract record SaucyEvent { [JsonIgnore] private string IdType => GetType().Name; - [JsonIgnore] public Guid Id { get; init; } = Guid.NewGuid(); - [JsonIgnore] public DateTime Created { get; init; } = DateTime.UtcNow; [JsonIgnore] public TAggregateId? AggregateId { get; init; } @@ -95,7 +69,7 @@ public abstract record SaucyEvent where TAggregateId : SaucyAggreg public override string ToString() { - return $"[{IdType}|{Id}|Aggregate: {AggregateId}|Version: {AggregateVersion}]"; + return $"[{IdType}|Aggregate: {AggregateId}|Version: {AggregateVersion}]"; } } }