From 46f347ba6f443867d1d254b72ae452070650cef7 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 19 May 2015 20:44:55 +0200 Subject: [PATCH 01/47] Read models can now be purged --- .../MsSqlIntegrationTestConfiguration.cs | 9 +++- .../IReadModelSqlGenerator.cs | 3 ++ .../MssqlReadModelStore.cs | 30 +++++++++++- .../ReadModelSqlGenerator.cs | 21 +++++++-- .../IntegrationTestConfiguration.cs | 4 +- .../Suites/ReadModelStoreSuite.cs | 17 ++++++- .../EventStores/FilesEventStoreTests.cs | 10 +++- .../IntegrationTests/InMemoryConfiguration.cs | 8 +++- Source/EventFlow/Core/Label.cs | 7 ++- Source/EventFlow/EventFlow.csproj | 2 + Source/EventFlow/EventFlowOptions.cs | 1 + .../ReadStores/IReadModelPopulator.cs | 33 +++++++++++++ .../EventFlow/ReadStores/IReadModelStore.cs | 5 +- .../InMemory/InMemoryReadModelStore.cs | 14 +++++- .../ReadStores/ReadModelPopulator.cs | 46 +++++++++++++++++++ Source/EventFlow/ReadStores/ReadModelStore.cs | 4 ++ 16 files changed, 201 insertions(+), 13 deletions(-) create mode 100644 Source/EventFlow/ReadStores/IReadModelPopulator.cs create mode 100644 Source/EventFlow/ReadStores/ReadModelPopulator.cs diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs index 661482e95..cdc08269f 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs @@ -44,6 +44,7 @@ public class MsSqlIntegrationTestConfiguration : IntegrationTestConfiguration protected ITestDatabase TestDatabase { get; private set; } protected IMsSqlConnection MsSqlConnection { get; private set; } protected IReadModelSqlGenerator ReadModelSqlGenerator { get; private set; } + protected IReadModelPopulator ReadModelPopulator { get; private set; } public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) { @@ -57,6 +58,7 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio MsSqlConnection = resolver.Resolve(); ReadModelSqlGenerator = resolver.Resolve(); + ReadModelPopulator = resolver.Resolve(); var databaseMigrator = resolver.Resolve(); EventFlowEventStoresMsSql.MigrateDatabase(databaseMigrator); @@ -65,7 +67,7 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio return resolver; } - public override async Task GetTestAggregateReadModel(IIdentity id) + public override async Task GetTestAggregateReadModelAsync(IIdentity id) { var sql = ReadModelSqlGenerator.CreateSelectSql(); var readModels = await MsSqlConnection.QueryAsync( @@ -77,6 +79,11 @@ public override async Task GetTestAggregateReadModel(II return readModels.SingleOrDefault(); } + public override Task PurgeTestAggregateReadModelAsync() + { + return ReadModelPopulator.PurgeAsync(CancellationToken.None); + } + public override void TearDown() { TestDatabase.Dispose(); diff --git a/Source/EventFlow.ReadStores.MsSql/IReadModelSqlGenerator.cs b/Source/EventFlow.ReadStores.MsSql/IReadModelSqlGenerator.cs index c4cf7e657..b0b7c6f56 100644 --- a/Source/EventFlow.ReadStores.MsSql/IReadModelSqlGenerator.cs +++ b/Source/EventFlow.ReadStores.MsSql/IReadModelSqlGenerator.cs @@ -32,5 +32,8 @@ string CreateSelectSql() string CreateUpdateSql() where TReadModel : IMssqlReadModel; + + string CreatePurgeSql() + where TReadModel : IReadModel; } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index 7631b9828..7476833c4 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -99,13 +100,38 @@ private async Task UpdateReadModelAsync( : _readModelSqlGenerator.CreateUpdateSql(); await _connection.ExecuteAsync( - Label.Named(string.Format("mssql-store-read-model-{0}", readModelNameLowerCased)), + Label.Named("mssql-store-read-model", readModelNameLowerCased), cancellationToken, sql, readModel).ConfigureAwait(false); } - protected override Task UpdateReadModelsAsync(IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, CancellationToken cancellationToken) + public override async Task PurgeAsync(CancellationToken cancellationToken) + { + if (typeof (TReadModel) != typeof(TReadModelToPurge)) + { + return; + } + + var sql = _readModelSqlGenerator.CreatePurgeSql(); + var readModelName = typeof (TReadModelToPurge).Name; + + var rowsAffected = await _connection.ExecuteAsync( + Label.Named("mssql-purge-read-model", readModelName), + cancellationToken, + sql) + .ConfigureAwait(false); + + Log.Verbose( + "Purge {0} read models of type '{1}'", + rowsAffected, + readModelName); + } + + protected override Task UpdateReadModelsAsync( + IReadOnlyCollection readModelUpdates, + IReadModelContext readModelContext, + CancellationToken cancellationToken) { var updateTasks = readModelUpdates .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, rmu.DomainEvents, readModelContext, cancellationToken)); diff --git a/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs b/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs index b955f38d0..651c87765 100644 --- a/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs +++ b/Source/EventFlow.ReadStores.MsSql/ReadModelSqlGenerator.cs @@ -26,6 +26,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Reflection; +using EventFlow.Extensions; namespace EventFlow.ReadStores.MsSql { @@ -34,6 +35,7 @@ public class ReadModelSqlGenerator : IReadModelSqlGenerator private readonly Dictionary _insertSqls = new Dictionary(); private readonly Dictionary _selectSqls = new Dictionary(); private readonly Dictionary _updateSqls = new Dictionary(); + private readonly Dictionary _purgeSqls = new Dictionary(); private static readonly ConcurrentDictionary TableNames = new ConcurrentDictionary(); public string CreateInsertSql() @@ -91,6 +93,14 @@ public string CreateUpdateSql() return sql; } + public string CreatePurgeSql() + where TReadModel : IReadModel + { + return _purgeSqls.GetOrCreate( + typeof (TReadModel), + t => string.Format("DELETE FROM {0}", GetTableName(t))); + } + protected IEnumerable GetInsertColumns() where TReadModel : IMssqlReadModel { @@ -108,17 +118,22 @@ protected IEnumerable GetUpdateColumns() .Where(c => c != "AggregateId"); } - public virtual string GetTableName() + public string GetTableName() where TReadModel : IMssqlReadModel + { + return GetTableName(typeof(TReadModel)); + } + + protected virtual string GetTableName(Type readModelType) { return TableNames.GetOrAdd( - typeof (TReadModel), + readModelType, t => { var tableAttribute = t.GetCustomAttribute(false); return tableAttribute != null ? string.Format("[{0}]", tableAttribute.Name) - : string.Format("[ReadModel-{0}]", typeof(TReadModel).Name.Replace("ReadModel", string.Empty)); + : string.Format("[ReadModel-{0}]", t.Name.Replace("ReadModel", string.Empty)); }); } } diff --git a/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs b/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs index 45dcf0aac..5b7354b19 100644 --- a/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs +++ b/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs @@ -31,7 +31,9 @@ public abstract class IntegrationTestConfiguration { public abstract IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions); - public abstract Task GetTestAggregateReadModel(IIdentity id); + public abstract Task GetTestAggregateReadModelAsync(IIdentity id); + + public abstract Task PurgeTestAggregateReadModelAsync(); public abstract void TearDown(); } diff --git a/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs index f9bf626e0..3dc26047c 100644 --- a/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs @@ -41,11 +41,26 @@ public async Task ReadModelReceivesEvent() // Act await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); - var readModel = await Configuration.GetTestAggregateReadModel(id).ConfigureAwait(false); + var readModel = await Configuration.GetTestAggregateReadModelAsync(id).ConfigureAwait(false); // Assert readModel.Should().NotBeNull(); readModel.PingsReceived.Should().Be(1); } + + [Test] + public async Task PurgeRemoveReadModels() + { + // Arrange + var id = TestId.New; + await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); + + // Act + await Configuration.PurgeTestAggregateReadModelAsync().ConfigureAwait(false); + var readModel = await Configuration.GetTestAggregateReadModelAsync(id).ConfigureAwait(false); + + // Assert + readModel.Should().BeNull(); + } } } diff --git a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs index 94abb396f..6757bfc53 100644 --- a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs @@ -22,6 +22,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Configuration; @@ -41,6 +42,7 @@ public class FilesConfiguration : IntegrationTestConfiguration { private IInMemoryReadModelStore _inMemoryReadModelStore; private IFilesEventStoreConfiguration _configuration; + private IReadModelPopulator _readModelPopulator; public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) { @@ -56,15 +58,21 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio _inMemoryReadModelStore = resolver.Resolve>(); _configuration = resolver.Resolve(); + _readModelPopulator = resolver.Resolve(); return resolver; } - public override Task GetTestAggregateReadModel(IIdentity id) + public override Task GetTestAggregateReadModelAsync(IIdentity id) { return Task.FromResult(_inMemoryReadModelStore.Get(id)); } + public override Task PurgeTestAggregateReadModelAsync() + { + return _readModelPopulator.PurgeAsync(CancellationToken.None); + } + public override void TearDown() { Directory.Delete(_configuration.StorePath, true); diff --git a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs index f953b40ee..dffb9c7b6 100644 --- a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs +++ b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Configuration; @@ -46,11 +47,16 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio return resolver; } - public override Task GetTestAggregateReadModel(IIdentity id) + public override Task GetTestAggregateReadModelAsync(IIdentity id) { return Task.FromResult(_inMemoryReadModelStore.Get(id)); } + public override Task PurgeTestAggregateReadModelAsync() + { + return _inMemoryReadModelStore.PurgeAsync(CancellationToken.None); + } + public override void TearDown() { } diff --git a/Source/EventFlow/Core/Label.cs b/Source/EventFlow/Core/Label.cs index f98a2f004..53c09ae32 100644 --- a/Source/EventFlow/Core/Label.cs +++ b/Source/EventFlow/Core/Label.cs @@ -29,7 +29,12 @@ public class Label { private static readonly Regex NameValidator = new Regex(@"^[a-z0-9\-]{3,}$", RegexOptions.Compiled); - public static Label Named(string name) { return new Label(name); } + public static Label Named(string name) { return new Label(name.ToLowerInvariant()); } + + public static Label Named(params string[] parts) + { + return Named(string.Join("-", parts)); + } public string Name { get; private set; } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 010796e9f..7009e9508 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -120,9 +120,11 @@ + + diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index 4e64cdcc4..3e3c7b200 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -95,6 +95,7 @@ public IRootResolver CreateResolver(bool validateRegistrations = true) RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); RegisterIfMissing(services); + RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); diff --git a/Source/EventFlow/ReadStores/IReadModelPopulator.cs b/Source/EventFlow/ReadStores/IReadModelPopulator.cs new file mode 100644 index 000000000..41aedfbfe --- /dev/null +++ b/Source/EventFlow/ReadStores/IReadModelPopulator.cs @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; + +namespace EventFlow.ReadStores +{ + public interface IReadModelPopulator + { + Task PurgeAsync(CancellationToken cancellationToken) + where TReadModel : IReadModel; + } +} diff --git a/Source/EventFlow/ReadStores/IReadModelStore.cs b/Source/EventFlow/ReadStores/IReadModelStore.cs index 9c91fd026..a3ebde741 100644 --- a/Source/EventFlow/ReadStores/IReadModelStore.cs +++ b/Source/EventFlow/ReadStores/IReadModelStore.cs @@ -30,5 +30,8 @@ namespace EventFlow.ReadStores public interface IReadModelStore { Task ApplyDomainEventsAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken); + + Task PurgeAsync(CancellationToken cancellationToken) + where TReadModelToPurge : IReadModel; } -} \ No newline at end of file +} diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs index 4702582fc..534e008f0 100644 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs @@ -85,7 +85,19 @@ public IEnumerable Find(Func predicate) .Where(predicate); } - protected override Task UpdateReadModelsAsync(IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, CancellationToken cancellationToken) + public override Task PurgeAsync(CancellationToken cancellationToken) + { + if (typeof (TReadModel) == typeof(TReadModelToPurge)) + { + _readModels.Clear(); + } + return Task.FromResult(0); + } + + protected override Task UpdateReadModelsAsync( + IReadOnlyCollection readModelUpdates, + IReadModelContext readModelContext, + CancellationToken cancellationToken) { var updateTasks = readModelUpdates .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, rmu.DomainEvents, readModelContext, cancellationToken)); diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs new file mode 100644 index 000000000..96305dd6e --- /dev/null +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -0,0 +1,46 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace EventFlow.ReadStores +{ + public class ReadModelPopulator : IReadModelPopulator + { + private readonly IReadOnlyCollection _readModelStores; + + public ReadModelPopulator( + IEnumerable readModelStores) + { + _readModelStores = readModelStores.ToList(); + } + + public Task PurgeAsync(CancellationToken cancellationToken) where TReadModel : IReadModel + { + var purgeTasks = _readModelStores.Select(s => s.PurgeAsync(cancellationToken)); + return Task.WhenAll(purgeTasks); + } + } +} diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index 803358f05..fa7f25c3c 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -67,6 +68,9 @@ group de by rid into g return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); } + public abstract Task PurgeAsync(CancellationToken cancellationToken) + where TReadModelToPurge : IReadModel; + protected abstract Task UpdateReadModelsAsync( IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, From 77bdfd39d154c88e975c80f6c91e350fe9b53388 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 19 May 2015 21:12:20 +0200 Subject: [PATCH 02/47] Max global sequence number can now be fetched from the event store --- .../MssqlEventStore.cs | 11 +++++++++++ .../Suites/EventStoreSuite.cs | 18 ++++++++++++++++++ Source/EventFlow/EventStores/EventStore.cs | 2 ++ .../EventStores/Files/FilesEventStore.cs | 5 +++++ Source/EventFlow/EventStores/IEventStore.cs | 2 ++ .../EventStores/InMemory/InMemoryEventStore.cs | 6 ++++++ .../ReadStores/IReadModelPopulator.cs | 3 +++ .../EventFlow/ReadStores/ReadModelPopulator.cs | 10 ++++++++++ 8 files changed, 57 insertions(+) diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index 21b3640ee..d85756404 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -190,5 +190,16 @@ ORDER BY .ConfigureAwait(false); return eventDataModels; } + + public override async Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken) + { + const string sql = "SELECT MAX(GlobalSequenceNumber) FROM EventFlow"; + var globalSeuqnceNumber = await _connection.QueryAsync( + Label.Named("mssql-fetch-max--global-sequence-number"), + cancellationToken, + sql) + .ConfigureAwait(false); + return globalSeuqnceNumber.Single(); + } } } diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index f75ac1539..98f5f5a77 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -170,6 +170,24 @@ public async Task DomainEventCanBeLoaded() domainEvents.Count.Should().Be(2); } + [Test] + public async Task MaxGlobalSequenceNumberCanBeFetched() + { + // Arrange + var id = TestId.New; + var aggregate = await EventStore.LoadAggregateAsync(id, CancellationToken.None).ConfigureAwait(false); + aggregate.Ping(PingId.New); + await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + aggregate.Ping(PingId.New); + await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + + // Act + var maxGlobalSequenceNumber = await EventStore.GetMaxGlobalSequenceNumberAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + maxGlobalSequenceNumber.Should().Be(2); + } + [Test] public async Task OptimisticConcurrency() { diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index 021a4a6ab..c84d26764 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -242,5 +242,7 @@ public virtual TAggregate LoadAggregate( } return aggregate; } + + public abstract Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index a29478e75..29fd5901a 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -220,6 +220,11 @@ protected override async Task> LoadCo return committedDomainEvents; } + public override Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_globalSequenceNumber); + } + private EventStoreLog RecreateEventStoreLog(string path) { var directory = Directory.GetDirectories(path) diff --git a/Source/EventFlow/EventStores/IEventStore.cs b/Source/EventFlow/EventStores/IEventStore.cs index 82dbc564e..a16d762e1 100644 --- a/Source/EventFlow/EventStores/IEventStore.cs +++ b/Source/EventFlow/EventStores/IEventStore.cs @@ -67,5 +67,7 @@ TAggregate LoadAggregate( CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity; + + Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 34b261592..12040c1a6 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -154,6 +154,12 @@ protected override Task> LoadCommitte return Task.FromResult>(committedDomainEvents); } + public override Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken) + { + var globalSequencenUmber = (long) _eventStore.Values.SelectMany(e => e).Count(); + return Task.FromResult(globalSequencenUmber); + } + public void Dispose() { _asyncLock.Dispose(); diff --git a/Source/EventFlow/ReadStores/IReadModelPopulator.cs b/Source/EventFlow/ReadStores/IReadModelPopulator.cs index 41aedfbfe..13ccd7082 100644 --- a/Source/EventFlow/ReadStores/IReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/IReadModelPopulator.cs @@ -29,5 +29,8 @@ public interface IReadModelPopulator { Task PurgeAsync(CancellationToken cancellationToken) where TReadModel : IReadModel; + + Task PopulateAsync(CancellationToken cancellationToken) + where TReadModel : IReadModel; } } diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 96305dd6e..55147b742 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -20,20 +20,25 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventFlow.EventStores; namespace EventFlow.ReadStores { public class ReadModelPopulator : IReadModelPopulator { + private readonly IEventStore _eventStore; private readonly IReadOnlyCollection _readModelStores; public ReadModelPopulator( + IEventStore eventStore, IEnumerable readModelStores) { + _eventStore = eventStore; _readModelStores = readModelStores.ToList(); } @@ -42,5 +47,10 @@ public Task PurgeAsync(CancellationToken cancellationToken) where TR var purgeTasks = _readModelStores.Select(s => s.PurgeAsync(cancellationToken)); return Task.WhenAll(purgeTasks); } + + public Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel + { + throw new NotImplementedException(); + } } } From 7bc875c808a501708d57a45f0e996a04bb3099e8 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 19 May 2015 21:34:13 +0200 Subject: [PATCH 03/47] Global sequence number range can now calculate batches --- Source/EventFlow.Tests/EventFlow.Tests.csproj | 1 + .../GlobalSequenceNumberRangeTests.cs | 53 +++++++++++++++++++ .../EventStores/GlobalSequenceNumberRange.cs | 16 ++++++ 3 files changed, 70 insertions(+) create mode 100644 Source/EventFlow.Tests/UnitTests/EventStores/GlobalSequenceNumberRangeTests.cs diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 9b81e983f..d60ba4ce1 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -66,6 +66,7 @@ + diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/GlobalSequenceNumberRangeTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/GlobalSequenceNumberRangeTests.cs new file mode 100644 index 000000000..cc15dc058 --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/EventStores/GlobalSequenceNumberRangeTests.cs @@ -0,0 +1,53 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Linq; +using EventFlow.EventStores; +using EventFlow.TestHelpers; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.Tests.UnitTests.EventStores +{ + [Timeout(5000)] + public class GlobalSequenceNumberRangeTests : Test + { + [TestCase(1, 1, 1, 1)] + [TestCase(1, 2, 1, 2)] + [TestCase(1, 3, 2, 2)] + [TestCase(2, 3, 1, 2)] + [TestCase(3, 8, 3, 2)] + [TestCase(8, 8, 8, 1)] + public void BatchesAreCorrect(long from, long to, long batchSize, int expectedNumberOfBatches) + { + // Act + var batches = GlobalSequenceNumberRange.Batches(from, to, batchSize).ToList(); + + // Assert + batches.Count.Should().Be(expectedNumberOfBatches); + batches.Min(r => r.From).Should().Be(from); + batches.Max(r => r.To).Should().Be(to); + + batches.Zip(batches.Skip(1), (r1, r2) => r2.From - r1.To).Sum().Should().Be(expectedNumberOfBatches - 1); + } + } +} diff --git a/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs b/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs index 4a91b141a..822f95aef 100644 --- a/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs +++ b/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs @@ -33,6 +33,22 @@ public static GlobalSequenceNumberRange Range(long from, long to) return new GlobalSequenceNumberRange(from, to); } + public static IEnumerable Batches(long from, long to, long batchSize) + { + if (from <= 0) throw new ArgumentOutOfRangeException("from"); + if (to <= 0) throw new ArgumentOutOfRangeException("to"); + if (from > to) throw new ArgumentException(string.Format( + "The 'from' value ({0}) must be less or equal to the 'to' value ({1})", + from, + to)); + if (batchSize <= 0) throw new ArgumentOutOfRangeException("batchSize"); + + for (var start = from; start <= to; start = start + batchSize) + { + yield return Range(start, Math.Min(to, start + batchSize - 1)); + } + } + public long From { get; private set; } public long To { get; private set; } public long Count { get { return To - From + 1; } } From 37265d8194759ba4053397d4948151f210603e12 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 19 May 2015 21:37:47 +0200 Subject: [PATCH 04/47] Started on populating read models --- .../EventFlow/ReadStores/ReadModelPopulator.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 55147b742..7338e7e5f 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -20,7 +20,6 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -48,9 +47,20 @@ public Task PurgeAsync(CancellationToken cancellationToken) where TR return Task.WhenAll(purgeTasks); } - public Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel + public async Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel { - throw new NotImplementedException(); + var maxGlobalSequenceNumber = await _eventStore.GetMaxGlobalSequenceNumberAsync(cancellationToken).ConfigureAwait(false); + if (maxGlobalSequenceNumber < 1) + { + return; + } + + foreach (var globalSequenceNumberRange in GlobalSequenceNumberRange.Batches(1, maxGlobalSequenceNumber, 10)) + { + var domainEvents = await _eventStore.LoadEventsAsync(globalSequenceNumberRange, cancellationToken).ConfigureAwait(false); + + // TODO: Do stuff + } } } } From 9c15bdf03fca1f4c9de476b85fece9c85ae81762 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 06:55:35 +0200 Subject: [PATCH 05/47] Version is now 0.8 --- RELEASE_NOTES.md | 6 +++++- appveyor.yml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 685d3a73d..f0eb6dce1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,8 @@ -### New in 0.7 (not released yet) +### New in 0.8 (not released yet) + + * _Nothing yet_ + +### New in 0.7.481 (released 2015-05-22) * New: EventFlow now includes a `IQueryProcessor` that enables you to implement queries and query handlers in a structure manner. EventFlow ships with two diff --git a/appveyor.yml b/appveyor.yml index c600232a7..649548ddd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ init: - git config --global core.autocrlf input -version: 0.7.{build} +version: 0.8.{build} skip_tags: true From 462b9212bf1229191216dcc9490a16d33b218bb3 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 18:35:23 +0200 Subject: [PATCH 06/47] Remove global sequence number --- .../MssqlEventStore.cs | 25 ----- ...- Create table ReadModel-TestAggregate.sql | 1 - .../ReadModels/ReadModelSqlGeneratorTests.cs | 6 +- .../IMssqlReadModel.cs | 1 - .../MssqlReadModel.cs | 4 +- .../MssqlReadModelStore.cs | 1 - .../Suites/EventStoreSuite.cs | 45 --------- Source/EventFlow.TestHelpers/Test.cs | 1 - .../BackwardCompatibilityTests.cs | 10 -- .../TestData/FilesEventStore/Log.store | 3 - Source/EventFlow/Aggregates/DomainEvent.cs | 3 - Source/EventFlow/Aggregates/IDomainEvent.cs | 1 - Source/EventFlow/EventFlow.csproj | 1 - .../EventStores/DomainEventFactory.cs | 5 - .../EventStores/EventJsonSerializer.cs | 1 - Source/EventFlow/EventStores/EventStore.cs | 28 ------ .../EventStores/EventUpgradeManager.cs | 2 +- .../EventStores/Files/FilesEventStore.cs | 93 ------------------- .../EventStores/GlobalSequenceNumberRange.cs | 66 ------------- .../EventStores/ICommittedDomainEvent.cs | 1 - .../EventStores/IDomainEventFactory.cs | 2 - Source/EventFlow/EventStores/IEventStore.cs | 8 -- .../InMemory/InMemoryEventStore.cs | 13 --- .../EventFlow/ReadStores/IReadModelContext.cs | 1 - .../EventFlow/ReadStores/ReadModelContext.cs | 6 -- Source/EventFlow/ReadStores/ReadModelStore.cs | 5 +- 26 files changed, 6 insertions(+), 327 deletions(-) delete mode 100644 Source/EventFlow.Tests/TestData/FilesEventStore/Log.store delete mode 100644 Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index 21b3640ee..00eac97f6 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -165,30 +165,5 @@ ORDER BY .ConfigureAwait(false); return eventDataModels; } - - protected override async Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - const string sql = @" - SELECT - GlobalSequenceNumber, BatchId, AggregateId, AggregateName, Data, Metadata, AggregateSequenceNumber - FROM EventFlow - WHERE - GlobalSequenceNumber >= @FromId AND GlobalSequenceNumber <= @ToId - ORDER BY - GlobalSequenceNumber ASC"; - var eventDataModels = await _connection.QueryAsync( - Label.Named("mssql-fetch-events"), - cancellationToken, - sql, - new - { - FromId = globalSequenceNumberRange.From, - ToId = globalSequenceNumberRange.To, - }) - .ConfigureAwait(false); - return eventDataModels; - } } } diff --git a/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql b/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql index b8ad30ac6..67f08b39c 100644 --- a/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql +++ b/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql @@ -8,7 +8,6 @@ [CreateTime] [datetimeoffset](7) NOT NULL, [UpdatedTime] [datetimeoffset](7) NOT NULL, [LastAggregateSequenceNumber] [int] NOT NULL, - [LastGlobalSequenceNumber] [bigint] NOT NULL, CONSTRAINT [PK_ReadModel-TestAggregate] PRIMARY KEY CLUSTERED ( [Id] ASC diff --git a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs index b2d21c418..9773a83f6 100644 --- a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs +++ b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs @@ -43,9 +43,9 @@ public void CreateInsertSql_ProducesCorrectSql() // Assert sql.Should().Be( "INSERT INTO [ReadModel-TestAggregate] " + - "(AggregateId, CreateTime, DomainErrorAfterFirstReceived, LastAggregateSequenceNumber, LastGlobalSequenceNumber, PingsReceived, UpdatedTime) " + + "(AggregateId, CreateTime, DomainErrorAfterFirstReceived, LastAggregateSequenceNumber, PingsReceived, UpdatedTime) " + "VALUES " + - "(@AggregateId, @CreateTime, @DomainErrorAfterFirstReceived, @LastAggregateSequenceNumber, @LastGlobalSequenceNumber, @PingsReceived, @UpdatedTime)"); + "(@AggregateId, @CreateTime, @DomainErrorAfterFirstReceived, @LastAggregateSequenceNumber, @PingsReceived, @UpdatedTime)"); } [Test] @@ -58,7 +58,7 @@ public void CreateUpdateSql_ProducesCorrectSql() sql.Should().Be( "UPDATE [ReadModel-TestAggregate] SET " + "CreateTime = @CreateTime, DomainErrorAfterFirstReceived = @DomainErrorAfterFirstReceived, " + - "LastAggregateSequenceNumber = @LastAggregateSequenceNumber, LastGlobalSequenceNumber = @LastGlobalSequenceNumber, " + + "LastAggregateSequenceNumber = @LastAggregateSequenceNumber, " + "PingsReceived = @PingsReceived, UpdatedTime = @UpdatedTime " + "WHERE AggregateId = @AggregateId"); } diff --git a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs index 94820e43a..9be825d78 100644 --- a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs +++ b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs @@ -30,6 +30,5 @@ public interface IMssqlReadModel : IReadModel DateTimeOffset CreateTime { get; set; } DateTimeOffset UpdatedTime { get; set; } int LastAggregateSequenceNumber { get; set; } - long LastGlobalSequenceNumber { get; set; } } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs index 5dd4e117b..6164ad376 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs @@ -30,15 +30,13 @@ public abstract class MssqlReadModel : IMssqlReadModel public DateTimeOffset CreateTime { get; set; } public DateTimeOffset UpdatedTime { get; set; } public int LastAggregateSequenceNumber { get; set; } - public long LastGlobalSequenceNumber { get; set; } public override string ToString() { return string.Format( - "Read model '{0}' for '{1} ({2}/{3}'", + "Read model '{0}' for '{1} v{2}'", GetType().Name, AggregateId, - LastGlobalSequenceNumber, LastAggregateSequenceNumber); } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index eac40fc54..67fdf8c7f 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -89,7 +89,6 @@ private async Task UpdateReadModelAsync( var lastDomainEvent = domainEvents.Last(); readModel.UpdatedTime = lastDomainEvent.Timestamp; readModel.LastAggregateSequenceNumber = lastDomainEvent.AggregateSequenceNumber; - readModel.LastGlobalSequenceNumber = lastDomainEvent.GlobalSequenceNumber; var sql = isNew ? _readModelSqlGenerator.CreateInsertSql() diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index f75ac1539..7f1ca8acf 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -25,7 +25,6 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.EventStores; using EventFlow.Exceptions; using EventFlow.TestHelpers.Aggregates.Test; using EventFlow.TestHelpers.Aggregates.Test.Events; @@ -69,7 +68,6 @@ public async Task EventsCanBeStored() pingEvent.AggregateType.Should().Be(typeof (TestAggregate)); pingEvent.BatchId.Should().NotBe(default(Guid)); pingEvent.EventType.Should().Be(typeof (PingEvent)); - pingEvent.GlobalSequenceNumber.Should().Be(1); pingEvent.Timestamp.Should().NotBe(default(DateTimeOffset)); pingEvent.Metadata.Count.Should().BeGreaterThan(0); } @@ -93,27 +91,6 @@ public async Task AggregatesCanBeLoaded() loadedTestAggregate.PingsReceived.Count.Should().Be(1); } - [Test] - public async Task GlobalSequenceNumberIncrements() - { - // Arrange - var id1 = TestId.New; - var id2 = TestId.New; - var aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); - var aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); - aggregate1.Ping(PingId.New); - aggregate2.Ping(PingId.New); - - // Act - await aggregate1.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - var domainEvents = await aggregate2.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - - // Assert - var pingEvent = domainEvents.SingleOrDefault(); - pingEvent.Should().NotBeNull(); - pingEvent.GlobalSequenceNumber.Should().Be(2); - } - [Test] public async Task AggregateEventStreamsAreSeperate() { @@ -148,28 +125,6 @@ public async Task NoEventsEmittedIsOk() await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); } - [Test] - public async Task DomainEventCanBeLoaded() - { - // Arrange - var id1 = TestId.New; - var id2 = TestId.New; - var pingId1 = PingId.New; - var pingId2 = PingId.New; - var aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); - var aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); - aggregate1.Ping(pingId1); - aggregate2.Ping(pingId2); - await aggregate1.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - await aggregate2.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - - // Act - var domainEvents = await EventStore.LoadEventsAsync(GlobalSequenceNumberRange.Range(1, 2), CancellationToken.None).ConfigureAwait(false); - - // Assert - domainEvents.Count.Should().Be(2); - } - [Test] public async Task OptimisticConcurrency() { diff --git a/Source/EventFlow.TestHelpers/Test.cs b/Source/EventFlow.TestHelpers/Test.cs index a33c7c6f9..9258e7849 100644 --- a/Source/EventFlow.TestHelpers/Test.cs +++ b/Source/EventFlow.TestHelpers/Test.cs @@ -79,7 +79,6 @@ protected IDomainEvent ToDomainEvent( return DomainEventFactory.Create( aggregateEvent, metadata, - A(), A(), A(), A()); diff --git a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs index 1a9a68e5a..573e73cb9 100644 --- a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs @@ -72,16 +72,6 @@ public void ValidateTestAggregate() testAggregate.PingsReceived.Should().Contain(PingId.With("2352d09b-4712-48cc-bb4f-5560d7c52558")); } - [Test] - public void DomainEventsCanBeLoaded() - { - // Act - var domainEvents = _eventStore.LoadEvents(GlobalSequenceNumberRange.Range(1, 2), CancellationToken.None); - - // Assert - domainEvents.Count.Should().Be(2); - } - [Test, Explicit] public void CreateEventHelper() { diff --git a/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store b/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store deleted file mode 100644 index 0eb2aaa06..000000000 --- a/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store +++ /dev/null @@ -1,3 +0,0 @@ -{ - "GlobalSequenceNumber": 2 -} \ No newline at end of file diff --git a/Source/EventFlow/Aggregates/DomainEvent.cs b/Source/EventFlow/Aggregates/DomainEvent.cs index fbf68e57f..cc0005e46 100644 --- a/Source/EventFlow/Aggregates/DomainEvent.cs +++ b/Source/EventFlow/Aggregates/DomainEvent.cs @@ -35,7 +35,6 @@ public class DomainEvent : IDomainEvent< public int AggregateSequenceNumber { get; private set; } public Guid BatchId { get; private set; } public TAggregateEvent AggregateEvent { get; private set; } - public long GlobalSequenceNumber { get; private set; } public TIdentity AggregateIdentity { get; private set; } public IMetadata Metadata { get; private set; } public DateTimeOffset Timestamp { get; private set; } @@ -44,7 +43,6 @@ public DomainEvent( TAggregateEvent aggregateEvent, IMetadata metadata, DateTimeOffset timestamp, - long globalSequenceNumber, TIdentity aggregateIdentity, int aggregateSequenceNumber, Guid batchId) @@ -52,7 +50,6 @@ public DomainEvent( AggregateEvent = aggregateEvent; Metadata = metadata; Timestamp = timestamp; - GlobalSequenceNumber = globalSequenceNumber; AggregateIdentity = aggregateIdentity; AggregateSequenceNumber = aggregateSequenceNumber; BatchId = batchId; diff --git a/Source/EventFlow/Aggregates/IDomainEvent.cs b/Source/EventFlow/Aggregates/IDomainEvent.cs index 07940aa55..5bd786a44 100644 --- a/Source/EventFlow/Aggregates/IDomainEvent.cs +++ b/Source/EventFlow/Aggregates/IDomainEvent.cs @@ -30,7 +30,6 @@ public interface IDomainEvent Type EventType { get; } int AggregateSequenceNumber { get; } Guid BatchId { get; } - long GlobalSequenceNumber { get; } IMetadata Metadata { get; } DateTimeOffset Timestamp { get; } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 3caee3d4b..9a3ddf389 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -100,7 +100,6 @@ - diff --git a/Source/EventFlow/EventStores/DomainEventFactory.cs b/Source/EventFlow/EventStores/DomainEventFactory.cs index 6284156e1..f5e8586f7 100644 --- a/Source/EventFlow/EventStores/DomainEventFactory.cs +++ b/Source/EventFlow/EventStores/DomainEventFactory.cs @@ -35,7 +35,6 @@ public class DomainEventFactory : IDomainEventFactory public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, string aggregateIdentity, int aggregateSequenceNumber, Guid batchId) @@ -49,7 +48,6 @@ public IDomainEvent Create( aggregateEvent, metadata, metadata.Timestamp, - globalSequenceNumber, identity, aggregateSequenceNumber, batchId); @@ -60,7 +58,6 @@ public IDomainEvent Create( public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, TIdentity id, int aggregateSequenceNumber, Guid batchId) @@ -70,7 +67,6 @@ public IDomainEvent Create( return (IDomainEvent)Create( aggregateEvent, metadata, - globalSequenceNumber, id.Value, aggregateSequenceNumber, batchId); @@ -85,7 +81,6 @@ public IDomainEvent Upgrade( return Create( aggregateEvent, domainEvent.Metadata, - domainEvent.GlobalSequenceNumber, (TIdentity) domainEvent.GetIdentity(), domainEvent.AggregateSequenceNumber, domainEvent.BatchId); diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index e3c7ad3e5..c16fc6266 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -76,7 +76,6 @@ public IDomainEvent Deserialize(ICommittedDomainEvent committedDomainEvent) var domainEvent = _domainEventFactory.Create( aggregateEvent, metadata, - committedDomainEvent.GlobalSequenceNumber, committedDomainEvent.AggregateId, committedDomainEvent.AggregateSequenceNumber, committedDomainEvent.BatchId); diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index 021a4a6ab..75aedc1ef 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -133,10 +133,6 @@ protected abstract Task> LoadCommitte where TAggregate : IAggregateRoot where TIdentity : IIdentity; - protected abstract Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken); - public virtual async Task>> LoadEventsAsync( TIdentity id, CancellationToken cancellationToken) @@ -179,30 +175,6 @@ public IReadOnlyCollection> LoadEvents> LoadEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - var committedDomainEvents = await LoadCommittedEventsAsync(globalSequenceNumberRange, cancellationToken).ConfigureAwait(false); - var domainEvents = (IReadOnlyCollection) committedDomainEvents - .Select(e => EventJsonSerializer.Deserialize(e)) - .ToList(); - domainEvents = EventUpgradeManager.Upgrade(domainEvents); - return domainEvents; - } - - public IReadOnlyCollection LoadEvents( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - IReadOnlyCollection domainEvents = null; - using (var a = AsyncHelper.Wait) - { - a.Run(LoadEventsAsync(globalSequenceNumberRange, cancellationToken), d => domainEvents = d); - } - return domainEvents; - } - public virtual async Task LoadAggregateAsync( TIdentity id, CancellationToken cancellationToken) diff --git a/Source/EventFlow/EventStores/EventUpgradeManager.cs b/Source/EventFlow/EventStores/EventUpgradeManager.cs index 95cd400ff..eae1ebd86 100644 --- a/Source/EventFlow/EventStores/EventUpgradeManager.cs +++ b/Source/EventFlow/EventStores/EventUpgradeManager.cs @@ -102,7 +102,7 @@ private IEnumerable Upgrade(IEnumerable domainEvents (IEnumerable) new[] {e}, (de, up) => de.SelectMany(ee => a.Upgrade(up, ee))); }) - .OrderBy(d => d.GlobalSequenceNumber); + .OrderBy(d => d.AggregateSequenceNumber); } public IReadOnlyCollection> Upgrade( diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index a29478e75..575e09bc0 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -23,7 +23,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -39,13 +38,9 @@ public class FilesEventStore : EventStore private readonly IJsonSerializer _jsonSerializer; private readonly IFilesEventStoreConfiguration _configuration; private readonly AsyncLock _asyncLock = new AsyncLock(); - private readonly string _logFilePath; - private long _globalSequenceNumber; - private Dictionary _log; public class FileEventData : ICommittedDomainEvent { - public long GlobalSequenceNumber { get; set; } public Guid BatchId { get; set; } public string AggregateId { get; set; } public string AggregateName { get; set; } @@ -54,12 +49,6 @@ public class FileEventData : ICommittedDomainEvent public int AggregateSequenceNumber { get; set; } } - public class EventStoreLog - { - public long GlobalSequenceNumber { get; set; } - public Dictionary Log { get; set; } - } - public FilesEventStore( ILog log, IAggregateFactory aggregateFactory, @@ -73,26 +62,6 @@ public FilesEventStore( { _jsonSerializer = jsonSerializer; _configuration = configuration; - _logFilePath = Path.Combine(_configuration.StorePath, "Log.store"); - - if (File.Exists(_logFilePath)) - { - var json = File.ReadAllText(_logFilePath); - var eventStoreLog = _jsonSerializer.Deserialize(json); - _globalSequenceNumber = eventStoreLog.GlobalSequenceNumber; - _log = eventStoreLog.Log ?? new Dictionary(); - - if (_log.Count != _globalSequenceNumber) - { - eventStoreLog = RecreateEventStoreLog(_configuration.StorePath); - _globalSequenceNumber = eventStoreLog.GlobalSequenceNumber; - _log = eventStoreLog.Log; - } - } - else - { - _log = new Dictionary(); - } } protected override async Task> CommitEventsAsync( @@ -115,8 +84,6 @@ protected override async Task> Commit foreach (var serializedEvent in serializedEvents) { var eventPath = GetEventPath(aggregateType, id, serializedEvent.AggregateSequenceNumber); - _globalSequenceNumber++; - _log[_globalSequenceNumber] = eventPath; var fileEventData = new FileEventData { @@ -125,7 +92,6 @@ protected override async Task> Commit AggregateSequenceNumber = serializedEvent.AggregateSequenceNumber, BatchId = batchId, Data = serializedEvent.Data, - GlobalSequenceNumber = _globalSequenceNumber, Metadata = serializedEvent.Meta, }; @@ -150,22 +116,6 @@ protected override async Task> Commit committedDomainEvents.Add(fileEventData); } - using (var streamWriter = File.CreateText(_logFilePath)) - { - Log.Verbose( - "Writing global sequence number '{0}' to '{1}'", - _globalSequenceNumber, - _logFilePath); - var json = _jsonSerializer.Serialize( - new EventStoreLog - { - GlobalSequenceNumber = _globalSequenceNumber, - Log = _log, - }, - true); - await streamWriter.WriteAsync(json).ConfigureAwait(false); - } - return committedDomainEvents; } } @@ -201,49 +151,6 @@ private async Task LoadFileEventDataFile(string eventPath) } } - protected override async Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - var paths = Enumerable.Range((int) globalSequenceNumberRange.From, (int) globalSequenceNumberRange.Count) - .TakeWhile(g => _log.ContainsKey(g)) - .Select(g => _log[g]) - .ToList(); - - var committedDomainEvents = new List(); - foreach (var path in paths) - { - var committedDomainEvent = await LoadFileEventDataFile(path).ConfigureAwait(false); - committedDomainEvents.Add(committedDomainEvent); - } - - return committedDomainEvents; - } - - private EventStoreLog RecreateEventStoreLog(string path) - { - var directory = Directory.GetDirectories(path) - .SelectMany(Directory.GetDirectories) - .SelectMany(Directory.GetFiles) - .Select(f => - { - Console.WriteLine(f); - using (var streamReader = File.OpenText(f)) - { - var json = streamReader.ReadToEnd(); - var fileEventData = _jsonSerializer.Deserialize(json); - return new {fileEventData.GlobalSequenceNumber, Path = f}; - } - }) - .ToDictionary(a => a.GlobalSequenceNumber, a => a.Path); - - return new EventStoreLog - { - GlobalSequenceNumber = directory.Keys.Any() ? directory.Keys.Max() : 0, - Log = directory, - }; - } - private string GetAggregatePath(Type aggregateType, IIdentity id) { return Path.Combine( diff --git a/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs b/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs deleted file mode 100644 index 4a91b141a..000000000 --- a/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs +++ /dev/null @@ -1,66 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using EventFlow.ValueObjects; - -namespace EventFlow.EventStores -{ - public class GlobalSequenceNumberRange : ValueObject - { - public static GlobalSequenceNumberRange Range(long from, long to) - { - return new GlobalSequenceNumberRange(from, to); - } - - public long From { get; private set; } - public long To { get; private set; } - public long Count { get { return To - From + 1; } } - - private GlobalSequenceNumberRange( - long from, - long to) - { - if (from <= 0) throw new ArgumentOutOfRangeException("from"); - if (to <= 0) throw new ArgumentOutOfRangeException("to"); - if (from > to) throw new ArgumentException(string.Format( - "The 'from' value ({0}) must be less or equal to the 'to' value ({1})", - from, - to)); - - From = from; - To = to; - } - - protected override IEnumerable GetEqualityComponents() - { - yield return From; - yield return To; - } - - public override string ToString() - { - return string.Format("[{0},{1}]", From, To); - } - } -} diff --git a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs index d651b63fd..738fcd223 100644 --- a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs +++ b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs @@ -26,7 +26,6 @@ namespace EventFlow.EventStores { public interface ICommittedDomainEvent { - long GlobalSequenceNumber { get; set; } Guid BatchId { get; set; } string AggregateId { get; set; } string AggregateName { get; set; } diff --git a/Source/EventFlow/EventStores/IDomainEventFactory.cs b/Source/EventFlow/EventStores/IDomainEventFactory.cs index a2208987a..d035465d8 100644 --- a/Source/EventFlow/EventStores/IDomainEventFactory.cs +++ b/Source/EventFlow/EventStores/IDomainEventFactory.cs @@ -30,7 +30,6 @@ public interface IDomainEventFactory IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, string aggregateIdentity, int aggregateSequenceNumber, Guid batchId); @@ -38,7 +37,6 @@ IDomainEvent Create( IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, TIdentity id, int aggregateSequenceNumber, Guid batchId) diff --git a/Source/EventFlow/EventStores/IEventStore.cs b/Source/EventFlow/EventStores/IEventStore.cs index 82dbc564e..f9286c9ba 100644 --- a/Source/EventFlow/EventStores/IEventStore.cs +++ b/Source/EventFlow/EventStores/IEventStore.cs @@ -48,14 +48,6 @@ IReadOnlyCollection> LoadEvents where TIdentity : IIdentity; - Task> LoadEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken); - - IReadOnlyCollection LoadEvents( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken); - Task LoadAggregateAsync( TIdentity id, CancellationToken cancellationToken) diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 34b261592..d38676009 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -43,7 +43,6 @@ public class InMemoryEventStore : EventStore, IDisposable private class InMemoryCommittedDomainEvent : ICommittedDomainEvent { - public long GlobalSequenceNumber { get; set; } public Guid BatchId { get; set; } public string AggregateId { get; set; } public string AggregateName { get; set; } @@ -111,7 +110,6 @@ protected async override Task> Commit BatchId = batchId, Data = e.Data, Metadata = e.Meta, - GlobalSequenceNumber = globalCount + i + 1 }; Log.Verbose("Committing event {0}{1}", Environment.NewLine, committedDomainEvent.ToString()); return committedDomainEvent; @@ -143,17 +141,6 @@ protected override async Task> LoadCo } } - protected override Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - var committedDomainEvents = _eventStore - .SelectMany(kv => kv.Value) - .Where(e => e.GlobalSequenceNumber >= globalSequenceNumberRange.From && e.GlobalSequenceNumber <= globalSequenceNumberRange.To) - .ToList(); - return Task.FromResult>(committedDomainEvents); - } - public void Dispose() { _asyncLock.Dispose(); diff --git a/Source/EventFlow/ReadStores/IReadModelContext.cs b/Source/EventFlow/ReadStores/IReadModelContext.cs index 3e85b811f..a0587feb1 100644 --- a/Source/EventFlow/ReadStores/IReadModelContext.cs +++ b/Source/EventFlow/ReadStores/IReadModelContext.cs @@ -24,6 +24,5 @@ namespace EventFlow.ReadStores { public interface IReadModelContext { - long GlobalSequenceNumber { get; } } } diff --git a/Source/EventFlow/ReadStores/ReadModelContext.cs b/Source/EventFlow/ReadStores/ReadModelContext.cs index 8dfe00b29..9a1c32c1d 100644 --- a/Source/EventFlow/ReadStores/ReadModelContext.cs +++ b/Source/EventFlow/ReadStores/ReadModelContext.cs @@ -24,11 +24,5 @@ namespace EventFlow.ReadStores { public class ReadModelContext : IReadModelContext { - public long GlobalSequenceNumber { get; private set; } - - public ReadModelContext(long globalSequenceNumber) - { - GlobalSequenceNumber = globalSequenceNumber; - } } } diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index 14bc9ce8b..bf38cdeed 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -59,10 +59,7 @@ group de by rid into g select new ReadModelUpdate(g.Key, g.ToList()) ).ToList(); - var globalSequenceNumber = domainEvents.Max(de => de.GlobalSequenceNumber); - - var readModelContext = new ReadModelContext( - globalSequenceNumber); + var readModelContext = new ReadModelContext(); return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); } From 5ff615eb36c37269ebe793b6635080e39d34ecf2 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 18:40:14 +0200 Subject: [PATCH 07/47] Updated release notes --- RELEASE_NOTES.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f0eb6dce1..9fcecf5ec 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,14 @@ ### New in 0.8 (not released yet) - * _Nothing yet_ + * Breaking: Remove all functionality related to global sequence + numbers as it proved problematic. It also matches this quote: + + > Order is only assured per a handler within an aggregate root + > boundary. There is no assurance of order between handlers or + > between aggregates. Trying to provide those things leads to + > the dark side. + + Greg Young ### New in 0.7.481 (released 2015-05-22) From 24f62b4f166e544aecb5eff110fcf8ed96292de4 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 19:33:55 +0200 Subject: [PATCH 08/47] Release note updates --- RELEASE_NOTES.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9fcecf5ec..56f163d28 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,14 +1,21 @@ ### New in 0.8 (not released yet) - * Breaking: Remove all functionality related to global sequence - numbers as it proved problematic. It also matches this quote: + * Breaking: Remove _all_ functionality related to global sequence + numbers as it proved problematic to maintain. It also matches this + quote: > Order is only assured per a handler within an aggregate root > boundary. There is no assurance of order between handlers or > between aggregates. Trying to provide those things leads to > the dark side. + >> Greg Young - Greg Young + - If you use a MSSQL read store, be sure to delete the + `LastGlobalSequenceNumber` column during update, or set it to + default `NULL` + - `IDomainEvent.GlobalSequenceNumber` removed + - `IEventStore.LoadEventsAsync` and `IEventStore.LoadEvents` taking + a `GlobalSequenceNumberRange` removed ### New in 0.7.481 (released 2015-05-22) From 2d68d017441e8a0708f132a2f3c5f1a79bba57fb Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 19:37:24 +0200 Subject: [PATCH 09/47] Fixed string.Format --- Source/EventFlow/Aggregates/AggregateRoot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/EventFlow/Aggregates/AggregateRoot.cs b/Source/EventFlow/Aggregates/AggregateRoot.cs index aacf69148..458180db2 100644 --- a/Source/EventFlow/Aggregates/AggregateRoot.cs +++ b/Source/EventFlow/Aggregates/AggregateRoot.cs @@ -108,7 +108,7 @@ public void ApplyEvents(IEnumerable aggregateEvents) if (e == null) { throw new ArgumentException(string.Format( - "Aggregate event of type '{0}' does not belong with aggregate '{1}'," + + "Aggregate event of type '{0}' does not belong with aggregate '{1}',", aggregateEvent.GetType(), this)); } From 173b57959887a1375820c878c52050a85ead47d8 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 19:48:13 +0200 Subject: [PATCH 10/47] Fixed problem when event upgraders removed events from the event stream and the resulting aggregate version was lower than the committed making it impossible to commit any new events --- RELEASE_NOTES.md | 3 +++ Source/EventFlow.TestHelpers/Test.cs | 14 ++++++++--- .../Aggregates/AggregateRootTests.cs | 25 +++++++++++++++++++ Source/EventFlow/Aggregates/AggregateRoot.cs | 11 ++++++++ Source/EventFlow/Aggregates/IAggregateRoot.cs | 1 + Source/EventFlow/EventStores/EventStore.cs | 2 +- 6 files changed, 51 insertions(+), 5 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 56f163d28..23b2af321 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -16,6 +16,9 @@ - `IDomainEvent.GlobalSequenceNumber` removed - `IEventStore.LoadEventsAsync` and `IEventStore.LoadEvents` taking a `GlobalSequenceNumberRange` removed + * Fixed: `AggregateRoot<>` now reads the aggregate version from + domain events applied during aggregate load. This resolves an issue + for when an `IEventUpgrader` removed events from the event stream ### New in 0.7.481 (released 2015-05-22) diff --git a/Source/EventFlow.TestHelpers/Test.cs b/Source/EventFlow.TestHelpers/Test.cs index 9258e7849..34a055c95 100644 --- a/Source/EventFlow.TestHelpers/Test.cs +++ b/Source/EventFlow.TestHelpers/Test.cs @@ -68,19 +68,25 @@ protected Mock InjectMock() } protected IDomainEvent ToDomainEvent( - TAggregateEvent aggregateEvent) + TAggregateEvent aggregateEvent, + int aggregateSequenceNumber = 0) where TAggregateEvent : IAggregateEvent { var metadata = new Metadata + { + Timestamp = A() + }; + + if (aggregateSequenceNumber == 0) { - Timestamp = A() - }; + aggregateSequenceNumber = A(); + } return DomainEventFactory.Create( aggregateEvent, metadata, A(), - A(), + aggregateSequenceNumber, A()); } diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs index 96d7ba9d0..2fceed4df 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs @@ -21,6 +21,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Linq; +using EventFlow.Aggregates; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Test; using EventFlow.TestHelpers.Aggregates.Test.Events; @@ -70,5 +71,29 @@ public void EventsCanBeApplied() Sut.PingsReceived.Count.Should().Be(2); Sut.UncommittedEvents.Count().Should().Be(0); } + + [Test] + public void EmptyListCanBeApplied() + { + // Act + Sut.ApplyEvents(new IDomainEvent[]{}); + + // Assert + Sut.Version.Should().Be(0); + } + + [Test] + public void ApplyEventsReadsAggregateSequenceNumber() + { + // Arrange + const int expectedVersion = 7; + var domainEvent = ToDomainEvent(A(), expectedVersion); + + // Act + Sut.ApplyEvents(new []{ domainEvent }); + + // Assert + Sut.Version.Should().Be(expectedVersion); + } } } diff --git a/Source/EventFlow/Aggregates/AggregateRoot.cs b/Source/EventFlow/Aggregates/AggregateRoot.cs index 458180db2..faf0586e8 100644 --- a/Source/EventFlow/Aggregates/AggregateRoot.cs +++ b/Source/EventFlow/Aggregates/AggregateRoot.cs @@ -92,6 +92,17 @@ public async Task> CommitAsync(IEventStore eve return domainEvents; } + public void ApplyEvents(IReadOnlyCollection domainEvents) + { + if (!domainEvents.Any()) + { + return; + } + + ApplyEvents(domainEvents.Select(e => e.GetAggregateEvent())); + Version = domainEvents.Max(e => e.AggregateSequenceNumber); + } + public void ApplyEvents(IEnumerable aggregateEvents) { if (Version > 0) diff --git a/Source/EventFlow/Aggregates/IAggregateRoot.cs b/Source/EventFlow/Aggregates/IAggregateRoot.cs index f2ee3bfc4..eb1d45e95 100644 --- a/Source/EventFlow/Aggregates/IAggregateRoot.cs +++ b/Source/EventFlow/Aggregates/IAggregateRoot.cs @@ -37,5 +37,6 @@ public interface IAggregateRoot Task> CommitAsync(IEventStore eventStore, CancellationToken cancellationToken); void ApplyEvents(IEnumerable aggregateEvents); + void ApplyEvents(IReadOnlyCollection domainEvents); } } diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index 75aedc1ef..5bc4d18b2 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -190,7 +190,7 @@ public virtual async Task LoadAggregateAsync( var domainEvents = await LoadEventsAsync(id, cancellationToken).ConfigureAwait(false); var aggregate = await AggregateFactory.CreateNewAggregateAsync(id).ConfigureAwait(false); - aggregate.ApplyEvents(domainEvents.Select(e => e.GetAggregateEvent())); + aggregate.ApplyEvents(domainEvents); Log.Verbose( "Done loading aggregate '{0}' with ID '{1}' after applying {2} events", From 2fc80119b77933e9841305a051e8730ebf71413e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 20:04:54 +0200 Subject: [PATCH 11/47] Aggregates can now be deleted --- RELEASE_NOTES.md | 3 +++ .../MssqlEventStore.cs | 19 +++++++++++++++ .../Suites/EventStoreSuite.cs | 24 +++++++++++++++++++ Source/EventFlow/EventStores/EventStore.cs | 6 +++++ .../EventStores/Files/FilesEventStore.cs | 14 +++++++++++ Source/EventFlow/EventStores/IEventStore.cs | 6 +++++ .../InMemory/InMemoryEventStore.cs | 18 ++++++++++++++ 7 files changed, 90 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 23b2af321..4816424e4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -16,6 +16,9 @@ - `IDomainEvent.GlobalSequenceNumber` removed - `IEventStore.LoadEventsAsync` and `IEventStore.LoadEvents` taking a `GlobalSequenceNumberRange` removed + * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate + stream. Please consider carefully if you really want to use it. Storage + might be cheaper than the historic knowledge within your events * Fixed: `AggregateRoot<>` now reads the aggregate version from domain events applied during aggregate load. This resolves an issue for when an `IEventUpgrader` removed events from the event stream diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index 00eac97f6..3b6cc5690 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -165,5 +165,24 @@ ORDER BY .ConfigureAwait(false); return eventDataModels; } + + public override async Task DeleteAggregateAsync( + TIdentity id, + CancellationToken cancellationToken) + { + const string sql = @"DELETE FROM EventFlow WHERE AggregateId = @AggregateId"; + var affectedRows = await _connection.ExecuteAsync( + Label.Named("mssql-delete-aggregate"), + cancellationToken, + sql, + new {AggregateId = id.Value}) + .ConfigureAwait(false); + + Log.Verbose( + "Deleted aggregate '{0}' with ID '{1}' by deleting all of its {2} events", + typeof(TAggregate).Name, + id, + affectedRows); + } } } diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index 7f1ca8acf..ea513d1a9 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -114,6 +114,30 @@ public async Task AggregateEventStreamsAreSeperate() aggregate2.Version.Should().Be(2); } + [Test] + public async Task AggregateEventStreamsCanBeDeleted() + { + // Arrange + var id1 = TestId.New; + var id2 = TestId.New; + var aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); + var aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); + aggregate1.Ping(PingId.New); + aggregate2.Ping(PingId.New); + aggregate2.Ping(PingId.New); + await aggregate1.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + await aggregate2.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + + // Act + await EventStore.DeleteAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); + + // Assert + aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); + aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); + aggregate1.Version.Should().Be(1); + aggregate2.Version.Should().Be(0); + } + [Test] public async Task NoEventsEmittedIsOk() { diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index 5bc4d18b2..c18edec94 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -214,5 +214,11 @@ public virtual TAggregate LoadAggregate( } return aggregate; } + + public abstract Task DeleteAggregateAsync( + TIdentity id, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity; } } diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index 575e09bc0..f55a01bfa 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -142,6 +142,20 @@ protected override async Task> LoadCo } } + public override Task DeleteAggregateAsync( + TIdentity id, + CancellationToken cancellationToken) + { + var aggregateType = typeof (TAggregate); + Log.Verbose( + "Deleting aggregate '{0}' with ID '{1}'", + aggregateType.Name, + id); + var path = GetAggregatePath(aggregateType, id); + Directory.Delete(path, true); + return Task.FromResult(0); + } + private async Task LoadFileEventDataFile(string eventPath) { using (var streamReader = File.OpenText(eventPath)) diff --git a/Source/EventFlow/EventStores/IEventStore.cs b/Source/EventFlow/EventStores/IEventStore.cs index f9286c9ba..b6007b9fc 100644 --- a/Source/EventFlow/EventStores/IEventStore.cs +++ b/Source/EventFlow/EventStores/IEventStore.cs @@ -59,5 +59,11 @@ TAggregate LoadAggregate( CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity; + + Task DeleteAggregateAsync( + TIdentity id, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity; } } diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index d38676009..5b6ca19a1 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -141,6 +141,24 @@ protected override async Task> LoadCo } } + public override Task DeleteAggregateAsync( + TIdentity id, + CancellationToken cancellationToken) + { + if (_eventStore.ContainsKey(id.Value)) + { + List committedDomainEvents; + _eventStore.TryRemove(id.Value, out committedDomainEvents); + Log.Verbose( + "Deleted aggregate '{0}' with ID '{1}' by deleting all of its {2} events", + typeof(TAggregate).Name, + id, + committedDomainEvents.Count); + } + + return Task.FromResult(0); + } + public void Dispose() { _asyncLock.Dispose(); From d6431af28e3d42ca43226c9fcdb65533b95cbee7 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 22:47:16 +0200 Subject: [PATCH 12/47] Reverted removal of the global sequence number as it really makes everything really cumbersome. Besides, http://geteventstore.com has support for it, so it can't be that bad --- RELEASE_NOTES.md | 16 ---- .../MssqlEventStore.cs | 26 ++++++ ...- Create table ReadModel-TestAggregate.sql | 1 + .../ReadModels/ReadModelSqlGeneratorTests.cs | 6 +- .../IMssqlReadModel.cs | 1 + .../MssqlReadModel.cs | 4 +- .../MssqlReadModelStore.cs | 1 + .../Suites/EventStoreSuite.cs | 45 +++++++++ Source/EventFlow.TestHelpers/Test.cs | 1 + .../BackwardCompatibilityTests.cs | 10 ++ .../TestData/FilesEventStore/Log.store | 3 + Source/EventFlow/Aggregates/DomainEvent.cs | 3 + Source/EventFlow/Aggregates/IDomainEvent.cs | 1 + Source/EventFlow/EventFlow.csproj | 1 + .../EventStores/DomainEventFactory.cs | 5 + .../EventStores/EventJsonSerializer.cs | 1 + Source/EventFlow/EventStores/EventStore.cs | 28 ++++++ .../EventStores/EventUpgradeManager.cs | 2 +- .../EventStores/Files/FilesEventStore.cs | 93 +++++++++++++++++++ .../EventStores/GlobalSequenceNumberRange.cs | 66 +++++++++++++ .../EventStores/ICommittedDomainEvent.cs | 1 + .../EventStores/IDomainEventFactory.cs | 2 + Source/EventFlow/EventStores/IEventStore.cs | 8 ++ .../InMemory/InMemoryEventStore.cs | 12 +++ .../EventFlow/ReadStores/IReadModelContext.cs | 1 + .../EventFlow/ReadStores/ReadModelContext.cs | 6 ++ Source/EventFlow/ReadStores/ReadModelStore.cs | 5 +- 27 files changed, 327 insertions(+), 22 deletions(-) create mode 100644 Source/EventFlow.Tests/TestData/FilesEventStore/Log.store create mode 100644 Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4816424e4..b52811daa 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,21 +1,5 @@ ### New in 0.8 (not released yet) - * Breaking: Remove _all_ functionality related to global sequence - numbers as it proved problematic to maintain. It also matches this - quote: - - > Order is only assured per a handler within an aggregate root - > boundary. There is no assurance of order between handlers or - > between aggregates. Trying to provide those things leads to - > the dark side. - >> Greg Young - - - If you use a MSSQL read store, be sure to delete the - `LastGlobalSequenceNumber` column during update, or set it to - default `NULL` - - `IDomainEvent.GlobalSequenceNumber` removed - - `IEventStore.LoadEventsAsync` and `IEventStore.LoadEvents` taking - a `GlobalSequenceNumberRange` removed * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate stream. Please consider carefully if you really want to use it. Storage might be cheaper than the historic knowledge within your events diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index 3b6cc5690..2e2870819 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -166,6 +166,31 @@ ORDER BY return eventDataModels; } + protected override async Task> LoadCommittedEventsAsync( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken) + { + const string sql = @" + SELECT + GlobalSequenceNumber, BatchId, AggregateId, AggregateName, Data, Metadata, AggregateSequenceNumber + FROM EventFlow + WHERE + GlobalSequenceNumber >= @FromId AND GlobalSequenceNumber <= @ToId + ORDER BY + GlobalSequenceNumber ASC"; + var eventDataModels = await _connection.QueryAsync( + Label.Named("mssql-fetch-events"), + cancellationToken, + sql, + new + { + FromId = globalSequenceNumberRange.From, + ToId = globalSequenceNumberRange.To, + }) + .ConfigureAwait(false); + return eventDataModels; + } + public override async Task DeleteAggregateAsync( TIdentity id, CancellationToken cancellationToken) @@ -185,4 +210,5 @@ public override async Task DeleteAggregateAsync( affectedRows); } } + } diff --git a/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql b/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql index 67f08b39c..b8ad30ac6 100644 --- a/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql +++ b/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql @@ -8,6 +8,7 @@ [CreateTime] [datetimeoffset](7) NOT NULL, [UpdatedTime] [datetimeoffset](7) NOT NULL, [LastAggregateSequenceNumber] [int] NOT NULL, + [LastGlobalSequenceNumber] [bigint] NOT NULL, CONSTRAINT [PK_ReadModel-TestAggregate] PRIMARY KEY CLUSTERED ( [Id] ASC diff --git a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs index 9773a83f6..b2d21c418 100644 --- a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs +++ b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs @@ -43,9 +43,9 @@ public void CreateInsertSql_ProducesCorrectSql() // Assert sql.Should().Be( "INSERT INTO [ReadModel-TestAggregate] " + - "(AggregateId, CreateTime, DomainErrorAfterFirstReceived, LastAggregateSequenceNumber, PingsReceived, UpdatedTime) " + + "(AggregateId, CreateTime, DomainErrorAfterFirstReceived, LastAggregateSequenceNumber, LastGlobalSequenceNumber, PingsReceived, UpdatedTime) " + "VALUES " + - "(@AggregateId, @CreateTime, @DomainErrorAfterFirstReceived, @LastAggregateSequenceNumber, @PingsReceived, @UpdatedTime)"); + "(@AggregateId, @CreateTime, @DomainErrorAfterFirstReceived, @LastAggregateSequenceNumber, @LastGlobalSequenceNumber, @PingsReceived, @UpdatedTime)"); } [Test] @@ -58,7 +58,7 @@ public void CreateUpdateSql_ProducesCorrectSql() sql.Should().Be( "UPDATE [ReadModel-TestAggregate] SET " + "CreateTime = @CreateTime, DomainErrorAfterFirstReceived = @DomainErrorAfterFirstReceived, " + - "LastAggregateSequenceNumber = @LastAggregateSequenceNumber, " + + "LastAggregateSequenceNumber = @LastAggregateSequenceNumber, LastGlobalSequenceNumber = @LastGlobalSequenceNumber, " + "PingsReceived = @PingsReceived, UpdatedTime = @UpdatedTime " + "WHERE AggregateId = @AggregateId"); } diff --git a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs index 9be825d78..94820e43a 100644 --- a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs +++ b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs @@ -30,5 +30,6 @@ public interface IMssqlReadModel : IReadModel DateTimeOffset CreateTime { get; set; } DateTimeOffset UpdatedTime { get; set; } int LastAggregateSequenceNumber { get; set; } + long LastGlobalSequenceNumber { get; set; } } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs index 6164ad376..5dd4e117b 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs @@ -30,13 +30,15 @@ public abstract class MssqlReadModel : IMssqlReadModel public DateTimeOffset CreateTime { get; set; } public DateTimeOffset UpdatedTime { get; set; } public int LastAggregateSequenceNumber { get; set; } + public long LastGlobalSequenceNumber { get; set; } public override string ToString() { return string.Format( - "Read model '{0}' for '{1} v{2}'", + "Read model '{0}' for '{1} ({2}/{3}'", GetType().Name, AggregateId, + LastGlobalSequenceNumber, LastAggregateSequenceNumber); } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index 67fdf8c7f..eac40fc54 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -89,6 +89,7 @@ private async Task UpdateReadModelAsync( var lastDomainEvent = domainEvents.Last(); readModel.UpdatedTime = lastDomainEvent.Timestamp; readModel.LastAggregateSequenceNumber = lastDomainEvent.AggregateSequenceNumber; + readModel.LastGlobalSequenceNumber = lastDomainEvent.GlobalSequenceNumber; var sql = isNew ? _readModelSqlGenerator.CreateInsertSql() diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index ea513d1a9..6821be37d 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -25,6 +25,7 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.EventStores; using EventFlow.Exceptions; using EventFlow.TestHelpers.Aggregates.Test; using EventFlow.TestHelpers.Aggregates.Test.Events; @@ -68,6 +69,7 @@ public async Task EventsCanBeStored() pingEvent.AggregateType.Should().Be(typeof (TestAggregate)); pingEvent.BatchId.Should().NotBe(default(Guid)); pingEvent.EventType.Should().Be(typeof (PingEvent)); + pingEvent.GlobalSequenceNumber.Should().Be(1); pingEvent.Timestamp.Should().NotBe(default(DateTimeOffset)); pingEvent.Metadata.Count.Should().BeGreaterThan(0); } @@ -91,6 +93,27 @@ public async Task AggregatesCanBeLoaded() loadedTestAggregate.PingsReceived.Count.Should().Be(1); } + [Test] + public async Task GlobalSequenceNumberIncrements() + { + // Arrange + var id1 = TestId.New; + var id2 = TestId.New; + var aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); + var aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); + aggregate1.Ping(PingId.New); + aggregate2.Ping(PingId.New); + + // Act + await aggregate1.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + var domainEvents = await aggregate2.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + + // Assert + var pingEvent = domainEvents.SingleOrDefault(); + pingEvent.Should().NotBeNull(); + pingEvent.GlobalSequenceNumber.Should().Be(2); + } + [Test] public async Task AggregateEventStreamsAreSeperate() { @@ -149,6 +172,28 @@ public async Task NoEventsEmittedIsOk() await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); } + [Test] + public async Task DomainEventCanBeLoaded() + { + // Arrange + var id1 = TestId.New; + var id2 = TestId.New; + var pingId1 = PingId.New; + var pingId2 = PingId.New; + var aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); + var aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); + aggregate1.Ping(pingId1); + aggregate2.Ping(pingId2); + await aggregate1.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + await aggregate2.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + + // Act + var domainEvents = await EventStore.LoadEventsAsync(GlobalSequenceNumberRange.Range(1, 2), CancellationToken.None).ConfigureAwait(false); + + // Assert + domainEvents.Count.Should().Be(2); + } + [Test] public async Task OptimisticConcurrency() { diff --git a/Source/EventFlow.TestHelpers/Test.cs b/Source/EventFlow.TestHelpers/Test.cs index 34a055c95..3d6bb0efc 100644 --- a/Source/EventFlow.TestHelpers/Test.cs +++ b/Source/EventFlow.TestHelpers/Test.cs @@ -85,6 +85,7 @@ protected IDomainEvent ToDomainEvent( return DomainEventFactory.Create( aggregateEvent, metadata, + A(), A(), aggregateSequenceNumber, A()); diff --git a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs index 573e73cb9..1a9a68e5a 100644 --- a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs @@ -72,6 +72,16 @@ public void ValidateTestAggregate() testAggregate.PingsReceived.Should().Contain(PingId.With("2352d09b-4712-48cc-bb4f-5560d7c52558")); } + [Test] + public void DomainEventsCanBeLoaded() + { + // Act + var domainEvents = _eventStore.LoadEvents(GlobalSequenceNumberRange.Range(1, 2), CancellationToken.None); + + // Assert + domainEvents.Count.Should().Be(2); + } + [Test, Explicit] public void CreateEventHelper() { diff --git a/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store b/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store new file mode 100644 index 000000000..0eb2aaa06 --- /dev/null +++ b/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store @@ -0,0 +1,3 @@ +{ + "GlobalSequenceNumber": 2 +} \ No newline at end of file diff --git a/Source/EventFlow/Aggregates/DomainEvent.cs b/Source/EventFlow/Aggregates/DomainEvent.cs index cc0005e46..fbf68e57f 100644 --- a/Source/EventFlow/Aggregates/DomainEvent.cs +++ b/Source/EventFlow/Aggregates/DomainEvent.cs @@ -35,6 +35,7 @@ public class DomainEvent : IDomainEvent< public int AggregateSequenceNumber { get; private set; } public Guid BatchId { get; private set; } public TAggregateEvent AggregateEvent { get; private set; } + public long GlobalSequenceNumber { get; private set; } public TIdentity AggregateIdentity { get; private set; } public IMetadata Metadata { get; private set; } public DateTimeOffset Timestamp { get; private set; } @@ -43,6 +44,7 @@ public DomainEvent( TAggregateEvent aggregateEvent, IMetadata metadata, DateTimeOffset timestamp, + long globalSequenceNumber, TIdentity aggregateIdentity, int aggregateSequenceNumber, Guid batchId) @@ -50,6 +52,7 @@ public DomainEvent( AggregateEvent = aggregateEvent; Metadata = metadata; Timestamp = timestamp; + GlobalSequenceNumber = globalSequenceNumber; AggregateIdentity = aggregateIdentity; AggregateSequenceNumber = aggregateSequenceNumber; BatchId = batchId; diff --git a/Source/EventFlow/Aggregates/IDomainEvent.cs b/Source/EventFlow/Aggregates/IDomainEvent.cs index 5bd786a44..07940aa55 100644 --- a/Source/EventFlow/Aggregates/IDomainEvent.cs +++ b/Source/EventFlow/Aggregates/IDomainEvent.cs @@ -30,6 +30,7 @@ public interface IDomainEvent Type EventType { get; } int AggregateSequenceNumber { get; } Guid BatchId { get; } + long GlobalSequenceNumber { get; } IMetadata Metadata { get; } DateTimeOffset Timestamp { get; } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 9a3ddf389..3caee3d4b 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -100,6 +100,7 @@ + diff --git a/Source/EventFlow/EventStores/DomainEventFactory.cs b/Source/EventFlow/EventStores/DomainEventFactory.cs index f5e8586f7..6284156e1 100644 --- a/Source/EventFlow/EventStores/DomainEventFactory.cs +++ b/Source/EventFlow/EventStores/DomainEventFactory.cs @@ -35,6 +35,7 @@ public class DomainEventFactory : IDomainEventFactory public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, + long globalSequenceNumber, string aggregateIdentity, int aggregateSequenceNumber, Guid batchId) @@ -48,6 +49,7 @@ public IDomainEvent Create( aggregateEvent, metadata, metadata.Timestamp, + globalSequenceNumber, identity, aggregateSequenceNumber, batchId); @@ -58,6 +60,7 @@ public IDomainEvent Create( public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, + long globalSequenceNumber, TIdentity id, int aggregateSequenceNumber, Guid batchId) @@ -67,6 +70,7 @@ public IDomainEvent Create( return (IDomainEvent)Create( aggregateEvent, metadata, + globalSequenceNumber, id.Value, aggregateSequenceNumber, batchId); @@ -81,6 +85,7 @@ public IDomainEvent Upgrade( return Create( aggregateEvent, domainEvent.Metadata, + domainEvent.GlobalSequenceNumber, (TIdentity) domainEvent.GetIdentity(), domainEvent.AggregateSequenceNumber, domainEvent.BatchId); diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index c16fc6266..e3c7ad3e5 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -76,6 +76,7 @@ public IDomainEvent Deserialize(ICommittedDomainEvent committedDomainEvent) var domainEvent = _domainEventFactory.Create( aggregateEvent, metadata, + committedDomainEvent.GlobalSequenceNumber, committedDomainEvent.AggregateId, committedDomainEvent.AggregateSequenceNumber, committedDomainEvent.BatchId); diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index c18edec94..7737cb2c0 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -133,6 +133,10 @@ protected abstract Task> LoadCommitte where TAggregate : IAggregateRoot where TIdentity : IIdentity; + protected abstract Task> LoadCommittedEventsAsync( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken); + public virtual async Task>> LoadEventsAsync( TIdentity id, CancellationToken cancellationToken) @@ -175,6 +179,30 @@ public IReadOnlyCollection> LoadEvents> LoadEventsAsync( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken) + { + var committedDomainEvents = await LoadCommittedEventsAsync(globalSequenceNumberRange, cancellationToken).ConfigureAwait(false); + var domainEvents = (IReadOnlyCollection) committedDomainEvents + .Select(e => EventJsonSerializer.Deserialize(e)) + .ToList(); + domainEvents = EventUpgradeManager.Upgrade(domainEvents); + return domainEvents; + } + + public IReadOnlyCollection LoadEvents( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken) + { + IReadOnlyCollection domainEvents = null; + using (var a = AsyncHelper.Wait) + { + a.Run(LoadEventsAsync(globalSequenceNumberRange, cancellationToken), d => domainEvents = d); + } + return domainEvents; + } + public virtual async Task LoadAggregateAsync( TIdentity id, CancellationToken cancellationToken) diff --git a/Source/EventFlow/EventStores/EventUpgradeManager.cs b/Source/EventFlow/EventStores/EventUpgradeManager.cs index eae1ebd86..95cd400ff 100644 --- a/Source/EventFlow/EventStores/EventUpgradeManager.cs +++ b/Source/EventFlow/EventStores/EventUpgradeManager.cs @@ -102,7 +102,7 @@ private IEnumerable Upgrade(IEnumerable domainEvents (IEnumerable) new[] {e}, (de, up) => de.SelectMany(ee => a.Upgrade(up, ee))); }) - .OrderBy(d => d.AggregateSequenceNumber); + .OrderBy(d => d.GlobalSequenceNumber); } public IReadOnlyCollection> Upgrade( diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index f55a01bfa..ac71ba640 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -38,9 +39,13 @@ public class FilesEventStore : EventStore private readonly IJsonSerializer _jsonSerializer; private readonly IFilesEventStoreConfiguration _configuration; private readonly AsyncLock _asyncLock = new AsyncLock(); + private readonly string _logFilePath; + private long _globalSequenceNumber; + private Dictionary _log; public class FileEventData : ICommittedDomainEvent { + public long GlobalSequenceNumber { get; set; } public Guid BatchId { get; set; } public string AggregateId { get; set; } public string AggregateName { get; set; } @@ -49,6 +54,12 @@ public class FileEventData : ICommittedDomainEvent public int AggregateSequenceNumber { get; set; } } + public class EventStoreLog + { + public long GlobalSequenceNumber { get; set; } + public Dictionary Log { get; set; } + } + public FilesEventStore( ILog log, IAggregateFactory aggregateFactory, @@ -62,6 +73,26 @@ public FilesEventStore( { _jsonSerializer = jsonSerializer; _configuration = configuration; + _logFilePath = Path.Combine(_configuration.StorePath, "Log.store"); + + if (File.Exists(_logFilePath)) + { + var json = File.ReadAllText(_logFilePath); + var eventStoreLog = _jsonSerializer.Deserialize(json); + _globalSequenceNumber = eventStoreLog.GlobalSequenceNumber; + _log = eventStoreLog.Log ?? new Dictionary(); + + if (_log.Count != _globalSequenceNumber) + { + eventStoreLog = RecreateEventStoreLog(_configuration.StorePath); + _globalSequenceNumber = eventStoreLog.GlobalSequenceNumber; + _log = eventStoreLog.Log; + } + } + else + { + _log = new Dictionary(); + } } protected override async Task> CommitEventsAsync( @@ -84,6 +115,8 @@ protected override async Task> Commit foreach (var serializedEvent in serializedEvents) { var eventPath = GetEventPath(aggregateType, id, serializedEvent.AggregateSequenceNumber); + _globalSequenceNumber++; + _log[_globalSequenceNumber] = eventPath; var fileEventData = new FileEventData { @@ -92,6 +125,7 @@ protected override async Task> Commit AggregateSequenceNumber = serializedEvent.AggregateSequenceNumber, BatchId = batchId, Data = serializedEvent.Data, + GlobalSequenceNumber = _globalSequenceNumber, Metadata = serializedEvent.Meta, }; @@ -116,6 +150,22 @@ protected override async Task> Commit committedDomainEvents.Add(fileEventData); } + using (var streamWriter = File.CreateText(_logFilePath)) + { + Log.Verbose( + "Writing global sequence number '{0}' to '{1}'", + _globalSequenceNumber, + _logFilePath); + var json = _jsonSerializer.Serialize( + new EventStoreLog + { + GlobalSequenceNumber = _globalSequenceNumber, + Log = _log, + }, + true); + await streamWriter.WriteAsync(json).ConfigureAwait(false); + } + return committedDomainEvents; } } @@ -165,6 +215,49 @@ private async Task LoadFileEventDataFile(string eventPath) } } + protected override async Task> LoadCommittedEventsAsync( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken) + { + var paths = Enumerable.Range((int) globalSequenceNumberRange.From, (int) globalSequenceNumberRange.Count) + .TakeWhile(g => _log.ContainsKey(g)) + .Select(g => _log[g]) + .ToList(); + + var committedDomainEvents = new List(); + foreach (var path in paths) + { + var committedDomainEvent = await LoadFileEventDataFile(path).ConfigureAwait(false); + committedDomainEvents.Add(committedDomainEvent); + } + + return committedDomainEvents; + } + + private EventStoreLog RecreateEventStoreLog(string path) + { + var directory = Directory.GetDirectories(path) + .SelectMany(Directory.GetDirectories) + .SelectMany(Directory.GetFiles) + .Select(f => + { + Console.WriteLine(f); + using (var streamReader = File.OpenText(f)) + { + var json = streamReader.ReadToEnd(); + var fileEventData = _jsonSerializer.Deserialize(json); + return new {fileEventData.GlobalSequenceNumber, Path = f}; + } + }) + .ToDictionary(a => a.GlobalSequenceNumber, a => a.Path); + + return new EventStoreLog + { + GlobalSequenceNumber = directory.Keys.Any() ? directory.Keys.Max() : 0, + Log = directory, + }; + } + private string GetAggregatePath(Type aggregateType, IIdentity id) { return Path.Combine( diff --git a/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs b/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs new file mode 100644 index 000000000..4a91b141a --- /dev/null +++ b/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs @@ -0,0 +1,66 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using EventFlow.ValueObjects; + +namespace EventFlow.EventStores +{ + public class GlobalSequenceNumberRange : ValueObject + { + public static GlobalSequenceNumberRange Range(long from, long to) + { + return new GlobalSequenceNumberRange(from, to); + } + + public long From { get; private set; } + public long To { get; private set; } + public long Count { get { return To - From + 1; } } + + private GlobalSequenceNumberRange( + long from, + long to) + { + if (from <= 0) throw new ArgumentOutOfRangeException("from"); + if (to <= 0) throw new ArgumentOutOfRangeException("to"); + if (from > to) throw new ArgumentException(string.Format( + "The 'from' value ({0}) must be less or equal to the 'to' value ({1})", + from, + to)); + + From = from; + To = to; + } + + protected override IEnumerable GetEqualityComponents() + { + yield return From; + yield return To; + } + + public override string ToString() + { + return string.Format("[{0},{1}]", From, To); + } + } +} diff --git a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs index 738fcd223..d651b63fd 100644 --- a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs +++ b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs @@ -26,6 +26,7 @@ namespace EventFlow.EventStores { public interface ICommittedDomainEvent { + long GlobalSequenceNumber { get; set; } Guid BatchId { get; set; } string AggregateId { get; set; } string AggregateName { get; set; } diff --git a/Source/EventFlow/EventStores/IDomainEventFactory.cs b/Source/EventFlow/EventStores/IDomainEventFactory.cs index d035465d8..a2208987a 100644 --- a/Source/EventFlow/EventStores/IDomainEventFactory.cs +++ b/Source/EventFlow/EventStores/IDomainEventFactory.cs @@ -30,6 +30,7 @@ public interface IDomainEventFactory IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, + long globalSequenceNumber, string aggregateIdentity, int aggregateSequenceNumber, Guid batchId); @@ -37,6 +38,7 @@ IDomainEvent Create( IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, + long globalSequenceNumber, TIdentity id, int aggregateSequenceNumber, Guid batchId) diff --git a/Source/EventFlow/EventStores/IEventStore.cs b/Source/EventFlow/EventStores/IEventStore.cs index b6007b9fc..51adf50b0 100644 --- a/Source/EventFlow/EventStores/IEventStore.cs +++ b/Source/EventFlow/EventStores/IEventStore.cs @@ -48,6 +48,14 @@ IReadOnlyCollection> LoadEvents where TIdentity : IIdentity; + Task> LoadEventsAsync( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken); + + IReadOnlyCollection LoadEvents( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken); + Task LoadAggregateAsync( TIdentity id, CancellationToken cancellationToken) diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 5b6ca19a1..dfee52d02 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -43,6 +43,7 @@ public class InMemoryEventStore : EventStore, IDisposable private class InMemoryCommittedDomainEvent : ICommittedDomainEvent { + public long GlobalSequenceNumber { get; set; } public Guid BatchId { get; set; } public string AggregateId { get; set; } public string AggregateName { get; set; } @@ -110,6 +111,7 @@ protected async override Task> Commit BatchId = batchId, Data = e.Data, Metadata = e.Meta, + GlobalSequenceNumber = globalCount + i + 1 }; Log.Verbose("Committing event {0}{1}", Environment.NewLine, committedDomainEvent.ToString()); return committedDomainEvent; @@ -158,6 +160,16 @@ public override Task DeleteAggregateAsync( return Task.FromResult(0); } + protected override Task> LoadCommittedEventsAsync( + GlobalSequenceNumberRange globalSequenceNumberRange, + CancellationToken cancellationToken) + { + var committedDomainEvents = _eventStore + .SelectMany(kv => kv.Value) + .Where(e => e.GlobalSequenceNumber >= globalSequenceNumberRange.From && e.GlobalSequenceNumber <= globalSequenceNumberRange.To) + .ToList(); + return Task.FromResult>(committedDomainEvents); + } public void Dispose() { diff --git a/Source/EventFlow/ReadStores/IReadModelContext.cs b/Source/EventFlow/ReadStores/IReadModelContext.cs index a0587feb1..3e85b811f 100644 --- a/Source/EventFlow/ReadStores/IReadModelContext.cs +++ b/Source/EventFlow/ReadStores/IReadModelContext.cs @@ -24,5 +24,6 @@ namespace EventFlow.ReadStores { public interface IReadModelContext { + long GlobalSequenceNumber { get; } } } diff --git a/Source/EventFlow/ReadStores/ReadModelContext.cs b/Source/EventFlow/ReadStores/ReadModelContext.cs index 9a1c32c1d..8dfe00b29 100644 --- a/Source/EventFlow/ReadStores/ReadModelContext.cs +++ b/Source/EventFlow/ReadStores/ReadModelContext.cs @@ -24,5 +24,11 @@ namespace EventFlow.ReadStores { public class ReadModelContext : IReadModelContext { + public long GlobalSequenceNumber { get; private set; } + + public ReadModelContext(long globalSequenceNumber) + { + GlobalSequenceNumber = globalSequenceNumber; + } } } diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index bf38cdeed..14bc9ce8b 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -59,7 +59,10 @@ group de by rid into g select new ReadModelUpdate(g.Key, g.ToList()) ).ToList(); - var readModelContext = new ReadModelContext(); + var globalSequenceNumber = domainEvents.Max(de => de.GlobalSequenceNumber); + + var readModelContext = new ReadModelContext( + globalSequenceNumber); return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); } From c7bb4996469e08ae496e55b4eed7556343b617f8 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 22:55:49 +0200 Subject: [PATCH 13/47] Remove fetching of max global sequence number --- .../MssqlEventStore.cs | 11 ----------- .../Suites/EventStoreSuite.cs | 18 ------------------ Source/EventFlow/EventStores/EventStore.cs | 2 -- .../EventStores/Files/FilesEventStore.cs | 5 ----- Source/EventFlow/EventStores/IEventStore.cs | 2 -- .../EventStores/InMemory/InMemoryEventStore.cs | 6 ------ 6 files changed, 44 deletions(-) diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index d85756404..21b3640ee 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -190,16 +190,5 @@ ORDER BY .ConfigureAwait(false); return eventDataModels; } - - public override async Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken) - { - const string sql = "SELECT MAX(GlobalSequenceNumber) FROM EventFlow"; - var globalSeuqnceNumber = await _connection.QueryAsync( - Label.Named("mssql-fetch-max--global-sequence-number"), - cancellationToken, - sql) - .ConfigureAwait(false); - return globalSeuqnceNumber.Single(); - } } } diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index 98f5f5a77..f75ac1539 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -170,24 +170,6 @@ public async Task DomainEventCanBeLoaded() domainEvents.Count.Should().Be(2); } - [Test] - public async Task MaxGlobalSequenceNumberCanBeFetched() - { - // Arrange - var id = TestId.New; - var aggregate = await EventStore.LoadAggregateAsync(id, CancellationToken.None).ConfigureAwait(false); - aggregate.Ping(PingId.New); - await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - aggregate.Ping(PingId.New); - await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - - // Act - var maxGlobalSequenceNumber = await EventStore.GetMaxGlobalSequenceNumberAsync(CancellationToken.None).ConfigureAwait(false); - - // Assert - maxGlobalSequenceNumber.Should().Be(2); - } - [Test] public async Task OptimisticConcurrency() { diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index c84d26764..021a4a6ab 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -242,7 +242,5 @@ public virtual TAggregate LoadAggregate( } return aggregate; } - - public abstract Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index 29fd5901a..a29478e75 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -220,11 +220,6 @@ protected override async Task> LoadCo return committedDomainEvents; } - public override Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken) - { - return Task.FromResult(_globalSequenceNumber); - } - private EventStoreLog RecreateEventStoreLog(string path) { var directory = Directory.GetDirectories(path) diff --git a/Source/EventFlow/EventStores/IEventStore.cs b/Source/EventFlow/EventStores/IEventStore.cs index a16d762e1..82dbc564e 100644 --- a/Source/EventFlow/EventStores/IEventStore.cs +++ b/Source/EventFlow/EventStores/IEventStore.cs @@ -67,7 +67,5 @@ TAggregate LoadAggregate( CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity; - - Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 12040c1a6..34b261592 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -154,12 +154,6 @@ protected override Task> LoadCommitte return Task.FromResult>(committedDomainEvents); } - public override Task GetMaxGlobalSequenceNumberAsync(CancellationToken cancellationToken) - { - var globalSequencenUmber = (long) _eventStore.Values.SelectMany(e => e).Count(); - return Task.FromResult(globalSequencenUmber); - } - public void Dispose() { _asyncLock.Dispose(); From a2ca8d2934c727d3789c89abd3467145c3b6c0ee Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 23:24:43 +0200 Subject: [PATCH 14/47] Read models can now be populated --- .../MsSqlIntegrationTestConfiguration.cs | 5 ++ .../MssqlReadModelStore.cs | 1 - .../EventFlow.TestHelpers/IntegrationTest.cs | 3 ++ .../IntegrationTestConfiguration.cs | 2 + .../Suites/ReadModelStoreSuite.cs | 20 +++++++- .../EventStores/FilesEventStoreTests.cs | 5 ++ .../IntegrationTests/InMemoryConfiguration.cs | 9 +++- .../EventStores/EventUpgradeManager.cs | 2 +- .../EventFlow/ReadStores/IReadModelStore.cs | 3 ++ .../ReadStores/ReadModelPopulator.cs | 47 +++++++++++++++---- Source/EventFlow/ReadStores/ReadModelStore.cs | 10 ++++ 11 files changed, 94 insertions(+), 13 deletions(-) diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs index cdc08269f..196f4a109 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs @@ -84,6 +84,11 @@ public override Task PurgeTestAggregateReadModelAsync() return ReadModelPopulator.PurgeAsync(CancellationToken.None); } + public override Task PopulateTestAggregateReadModelAsync() + { + return ReadModelPopulator.PopulateAsync(CancellationToken.None); + } + public override void TearDown() { TestDatabase.Dispose(); diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index 7476833c4..cc0767391 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -20,7 +20,6 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; diff --git a/Source/EventFlow.TestHelpers/IntegrationTest.cs b/Source/EventFlow.TestHelpers/IntegrationTest.cs index ee3e25429..2a2180df6 100644 --- a/Source/EventFlow.TestHelpers/IntegrationTest.cs +++ b/Source/EventFlow.TestHelpers/IntegrationTest.cs @@ -23,6 +23,7 @@ using EventFlow.Configuration; using EventFlow.EventStores; using EventFlow.Extensions; +using EventFlow.ReadStores; using NUnit.Framework; namespace EventFlow.TestHelpers @@ -33,6 +34,7 @@ public abstract class IntegrationTest : Test protected IRootResolver Resolver { get; private set; } protected IEventStore EventStore { get; private set; } protected ICommandBus CommandBus { get; private set; } + protected IReadModelPopulator ReadModelPopulator { get; private set; } protected TIntegrationTestConfiguration Configuration { get; private set; } [SetUp] @@ -47,6 +49,7 @@ public void SetUpIntegrationTest() Resolver = Configuration.CreateRootResolver(eventFlowOptions); EventStore = Resolver.Resolve(); CommandBus = Resolver.Resolve(); + ReadModelPopulator = Resolver.Resolve(); } [TearDown] diff --git a/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs b/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs index 5b7354b19..c5bea986b 100644 --- a/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs +++ b/Source/EventFlow.TestHelpers/IntegrationTestConfiguration.cs @@ -35,6 +35,8 @@ public abstract class IntegrationTestConfiguration public abstract Task PurgeTestAggregateReadModelAsync(); + public abstract Task PopulateTestAggregateReadModelAsync(); + public abstract void TearDown(); } } diff --git a/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs index 3dc26047c..e9cbf9357 100644 --- a/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs @@ -49,7 +49,7 @@ public async Task ReadModelReceivesEvent() } [Test] - public async Task PurgeRemoveReadModels() + public async Task PurgeRemovesReadModels() { // Arrange var id = TestId.New; @@ -62,5 +62,23 @@ public async Task PurgeRemoveReadModels() // Assert readModel.Should().BeNull(); } + + [Test] + public async Task PopulateCreatesReadModels() + { + // Arrange + var id = TestId.New; + await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); + await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); + await Configuration.PurgeTestAggregateReadModelAsync().ConfigureAwait(false); + + // Act + await Configuration.PopulateTestAggregateReadModelAsync().ConfigureAwait(false); + var readModel = await Configuration.GetTestAggregateReadModelAsync(id).ConfigureAwait(false); + + // Assert + readModel.Should().NotBeNull(); + readModel.PingsReceived.Should().Be(2); + } } } diff --git a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs index 6757bfc53..52cfac4b1 100644 --- a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs @@ -73,6 +73,11 @@ public override Task PurgeTestAggregateReadModelAsync() return _readModelPopulator.PurgeAsync(CancellationToken.None); } + public override Task PopulateTestAggregateReadModelAsync() + { + return _readModelPopulator.PopulateAsync(CancellationToken.None); + } + public override void TearDown() { Directory.Delete(_configuration.StorePath, true); diff --git a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs index dffb9c7b6..571a247fa 100644 --- a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs +++ b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs @@ -35,6 +35,7 @@ namespace EventFlow.Tests.IntegrationTests public class InMemoryConfiguration : IntegrationTestConfiguration { private IInMemoryReadModelStore _inMemoryReadModelStore; + private IReadModelPopulator _readModelPopulator; public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) { @@ -43,6 +44,7 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio .CreateResolver(); _inMemoryReadModelStore = resolver.Resolve>(); + _readModelPopulator = resolver.Resolve(); return resolver; } @@ -54,7 +56,12 @@ public override Task GetTestAggregateReadModelAsync(IId public override Task PurgeTestAggregateReadModelAsync() { - return _inMemoryReadModelStore.PurgeAsync(CancellationToken.None); + return _readModelPopulator.PurgeAsync(CancellationToken.None); + } + + public override Task PopulateTestAggregateReadModelAsync() + { + return _readModelPopulator.PopulateAsync(CancellationToken.None); } public override void TearDown() diff --git a/Source/EventFlow/EventStores/EventUpgradeManager.cs b/Source/EventFlow/EventStores/EventUpgradeManager.cs index 95cd400ff..b06ea0212 100644 --- a/Source/EventFlow/EventStores/EventUpgradeManager.cs +++ b/Source/EventFlow/EventStores/EventUpgradeManager.cs @@ -92,7 +92,7 @@ private IEnumerable Upgrade(IEnumerable domainEvents _log.Verbose(() => string.Format( "Upgrading {0} events and found these event upgraders to use: {1}", domainEventList.Count, - string.Join(", ", eventUpgraders.Values.Select(e => e.GetType().Name)))); + string.Join(", ", eventUpgraders.Values.SelectMany(a => a.EventUpgraders.Select(e => e.GetType().Name))))); return domainEventList .SelectMany(e => diff --git a/Source/EventFlow/ReadStores/IReadModelStore.cs b/Source/EventFlow/ReadStores/IReadModelStore.cs index a3ebde741..7f4fb56ff 100644 --- a/Source/EventFlow/ReadStores/IReadModelStore.cs +++ b/Source/EventFlow/ReadStores/IReadModelStore.cs @@ -31,6 +31,9 @@ public interface IReadModelStore { Task ApplyDomainEventsAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken); + Task ApplyDomainEventsAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + where TReadModelToPopulate : IReadModel; + Task PurgeAsync(CancellationToken cancellationToken) where TReadModelToPurge : IReadModel; } diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 7338e7e5f..73739ad96 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -20,46 +20,75 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.EventStores; +using EventFlow.Logs; namespace EventFlow.ReadStores { public class ReadModelPopulator : IReadModelPopulator { + private readonly ILog _log; private readonly IEventStore _eventStore; private readonly IReadOnlyCollection _readModelStores; public ReadModelPopulator( + ILog log, IEventStore eventStore, IEnumerable readModelStores) { + _log = log; _eventStore = eventStore; _readModelStores = readModelStores.ToList(); } - public Task PurgeAsync(CancellationToken cancellationToken) where TReadModel : IReadModel + public Task PurgeAsync(CancellationToken cancellationToken) + where TReadModel : IReadModel { var purgeTasks = _readModelStores.Select(s => s.PurgeAsync(cancellationToken)); return Task.WhenAll(purgeTasks); } - public async Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel + public async Task PopulateAsync(CancellationToken cancellationToken) + where TReadModel : IReadModel { - var maxGlobalSequenceNumber = await _eventStore.GetMaxGlobalSequenceNumberAsync(cancellationToken).ConfigureAwait(false); - if (maxGlobalSequenceNumber < 1) - { - return; - } + var readModelType = typeof (TReadModel); + var aggregateEventTypes = new HashSet(readModelType + .GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IAmReadModelFor<,,>)) + .Select(i => i.GetGenericArguments()[2])); + + _log.Verbose(() => string.Format( + "Read model '{0}' is interested in these aggregate events: {1}", + readModelType.Name, + string.Join(", ", aggregateEventTypes.Select(e => e.Name).OrderBy(s => s)))); - foreach (var globalSequenceNumberRange in GlobalSequenceNumberRange.Batches(1, maxGlobalSequenceNumber, 10)) + foreach (var globalSequenceNumberRange in GlobalSequenceNumberRange.Batches(1, long.MaxValue, 1)) { + _log.Verbose("Loading domain events from global position {0}", globalSequenceNumberRange); + var domainEvents = await _eventStore.LoadEventsAsync(globalSequenceNumberRange, cancellationToken).ConfigureAwait(false); + if (!domainEvents.Any()) + { + _log.Verbose("No more events in event store, stopping population of read model '{0}'", readModelType.Name); + return; + } + + domainEvents = domainEvents + .Where(e => aggregateEventTypes.Contains(e.EventType)) + .ToList(); + if (!domainEvents.Any()) + { + continue; + } - // TODO: Do stuff + var applyTasks = _readModelStores + .Select(rms => rms.ApplyDomainEventsAsync(domainEvents, cancellationToken)); + await Task.WhenAll(applyTasks).ConfigureAwait(false); } } } diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index fa7f25c3c..6792cb83b 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -68,6 +68,16 @@ group de by rid into g return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); } + public Task ApplyDomainEventsAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + where TReadModelToPopulate : IReadModel + { + return (typeof (TReadModel) == typeof (TReadModelToPopulate)) + ? ApplyDomainEventsAsync(domainEvents, cancellationToken) + : Task.FromResult(0); + } + public abstract Task PurgeAsync(CancellationToken cancellationToken) where TReadModelToPurge : IReadModel; From 2f0abf43bd27773b6726b53901113bdb4621e00c Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 22 May 2015 23:41:44 +0200 Subject: [PATCH 15/47] Logging and non-async methods --- .../ReadStores/IReadModelPopulator.cs | 6 +++ .../ReadStores/ReadModelPopulator.cs | 39 ++++++++++++++++++- Source/EventFlow/ReadStores/ReadModelStore.cs | 1 - 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/Source/EventFlow/ReadStores/IReadModelPopulator.cs b/Source/EventFlow/ReadStores/IReadModelPopulator.cs index 13ccd7082..8fa50d7bd 100644 --- a/Source/EventFlow/ReadStores/IReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/IReadModelPopulator.cs @@ -30,7 +30,13 @@ public interface IReadModelPopulator Task PurgeAsync(CancellationToken cancellationToken) where TReadModel : IReadModel; + void Purge(CancellationToken cancellationToken) + where TReadModel : IReadModel; + Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel; + + void Populate(CancellationToken cancellationToken) + where TReadModel : IReadModel; } } diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 73739ad96..e30991b3e 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -22,9 +22,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventFlow.Core; using EventFlow.EventStores; using EventFlow.Logs; @@ -53,9 +55,20 @@ public Task PurgeAsync(CancellationToken cancellationToken) return Task.WhenAll(purgeTasks); } + public void Purge(CancellationToken cancellationToken) + where TReadModel : IReadModel + { + using (var a = AsyncHelper.Wait) + { + a.Run(PurgeAsync(cancellationToken)); + } + } + public async Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel { + var stopwatch = Stopwatch.StartNew(); + var readModelType = typeof (TReadModel); var aggregateEventTypes = new HashSet(readModelType .GetInterfaces() @@ -67,20 +80,27 @@ public async Task PopulateAsync(CancellationToken cancellationToken) readModelType.Name, string.Join(", ", aggregateEventTypes.Select(e => e.Name).OrderBy(s => s)))); + long totalEvents = 0; + long relevantEvents = 0; + foreach (var globalSequenceNumberRange in GlobalSequenceNumberRange.Batches(1, long.MaxValue, 1)) { _log.Verbose("Loading domain events from global position {0}", globalSequenceNumberRange); var domainEvents = await _eventStore.LoadEventsAsync(globalSequenceNumberRange, cancellationToken).ConfigureAwait(false); + totalEvents += domainEvents.Count; + if (!domainEvents.Any()) { _log.Verbose("No more events in event store, stopping population of read model '{0}'", readModelType.Name); - return; + break; } domainEvents = domainEvents .Where(e => aggregateEventTypes.Contains(e.EventType)) .ToList(); + relevantEvents += domainEvents.Count; + if (!domainEvents.Any()) { continue; @@ -90,6 +110,23 @@ public async Task PopulateAsync(CancellationToken cancellationToken) .Select(rms => rms.ApplyDomainEventsAsync(domainEvents, cancellationToken)); await Task.WhenAll(applyTasks).ConfigureAwait(false); } + + stopwatch.Stop(); + _log.Information( + "Population of read model '{0}' took {1:0.###} seconds, in which {2} events was loaded and {3} was relevant", + readModelType.Name, + stopwatch.Elapsed.TotalSeconds, + totalEvents, + relevantEvents); + } + + public void Populate(CancellationToken cancellationToken) + where TReadModel : IReadModel + { + using (var a = AsyncHelper.Wait) + { + a.Run(PopulateAsync(cancellationToken)); + } } } } diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index 83d271e0e..2b8dc3cd9 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -20,7 +20,6 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; From 8d094c9138337f14510fa551fe1dc6ab9bd9dc2b Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 24 May 2015 23:40:21 +0200 Subject: [PATCH 16/47] Revert "Reverted removal of the global sequence number as it really makes everything really cumbersome. Besides, http://geteventstore.com has support for it, so it can't be that bad" This reverts commit d6431af28e3d42ca43226c9fcdb65533b95cbee7. --- RELEASE_NOTES.md | 16 ++++ .../MssqlEventStore.cs | 26 ------ ...- Create table ReadModel-TestAggregate.sql | 1 - .../ReadModels/ReadModelSqlGeneratorTests.cs | 6 +- .../IMssqlReadModel.cs | 1 - .../MssqlReadModel.cs | 4 +- .../MssqlReadModelStore.cs | 1 - .../Suites/EventStoreSuite.cs | 45 --------- Source/EventFlow.TestHelpers/Test.cs | 1 - .../BackwardCompatibilityTests.cs | 10 -- .../TestData/FilesEventStore/Log.store | 3 - Source/EventFlow/Aggregates/DomainEvent.cs | 3 - Source/EventFlow/Aggregates/IDomainEvent.cs | 1 - Source/EventFlow/EventFlow.csproj | 1 - .../EventStores/DomainEventFactory.cs | 5 - .../EventStores/EventJsonSerializer.cs | 1 - Source/EventFlow/EventStores/EventStore.cs | 28 ------ .../EventStores/EventUpgradeManager.cs | 2 +- .../EventStores/Files/FilesEventStore.cs | 93 ------------------- .../EventStores/GlobalSequenceNumberRange.cs | 66 ------------- .../EventStores/ICommittedDomainEvent.cs | 1 - .../EventStores/IDomainEventFactory.cs | 2 - Source/EventFlow/EventStores/IEventStore.cs | 8 -- .../InMemory/InMemoryEventStore.cs | 12 --- .../EventFlow/ReadStores/IReadModelContext.cs | 1 - .../EventFlow/ReadStores/ReadModelContext.cs | 6 -- Source/EventFlow/ReadStores/ReadModelStore.cs | 5 +- 27 files changed, 22 insertions(+), 327 deletions(-) delete mode 100644 Source/EventFlow.Tests/TestData/FilesEventStore/Log.store delete mode 100644 Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b52811daa..4816424e4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,21 @@ ### New in 0.8 (not released yet) + * Breaking: Remove _all_ functionality related to global sequence + numbers as it proved problematic to maintain. It also matches this + quote: + + > Order is only assured per a handler within an aggregate root + > boundary. There is no assurance of order between handlers or + > between aggregates. Trying to provide those things leads to + > the dark side. + >> Greg Young + + - If you use a MSSQL read store, be sure to delete the + `LastGlobalSequenceNumber` column during update, or set it to + default `NULL` + - `IDomainEvent.GlobalSequenceNumber` removed + - `IEventStore.LoadEventsAsync` and `IEventStore.LoadEvents` taking + a `GlobalSequenceNumberRange` removed * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate stream. Please consider carefully if you really want to use it. Storage might be cheaper than the historic knowledge within your events diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index 2e2870819..3b6cc5690 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -166,31 +166,6 @@ ORDER BY return eventDataModels; } - protected override async Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - const string sql = @" - SELECT - GlobalSequenceNumber, BatchId, AggregateId, AggregateName, Data, Metadata, AggregateSequenceNumber - FROM EventFlow - WHERE - GlobalSequenceNumber >= @FromId AND GlobalSequenceNumber <= @ToId - ORDER BY - GlobalSequenceNumber ASC"; - var eventDataModels = await _connection.QueryAsync( - Label.Named("mssql-fetch-events"), - cancellationToken, - sql, - new - { - FromId = globalSequenceNumberRange.From, - ToId = globalSequenceNumberRange.To, - }) - .ConfigureAwait(false); - return eventDataModels; - } - public override async Task DeleteAggregateAsync( TIdentity id, CancellationToken cancellationToken) @@ -210,5 +185,4 @@ public override async Task DeleteAggregateAsync( affectedRows); } } - } diff --git a/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql b/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql index b8ad30ac6..67f08b39c 100644 --- a/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql +++ b/Source/EventFlow.MsSql.Tests/Scripts/0001 - Create table ReadModel-TestAggregate.sql @@ -8,7 +8,6 @@ [CreateTime] [datetimeoffset](7) NOT NULL, [UpdatedTime] [datetimeoffset](7) NOT NULL, [LastAggregateSequenceNumber] [int] NOT NULL, - [LastGlobalSequenceNumber] [bigint] NOT NULL, CONSTRAINT [PK_ReadModel-TestAggregate] PRIMARY KEY CLUSTERED ( [Id] ASC diff --git a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs index b2d21c418..9773a83f6 100644 --- a/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs +++ b/Source/EventFlow.MsSql.Tests/UnitTests/ReadModels/ReadModelSqlGeneratorTests.cs @@ -43,9 +43,9 @@ public void CreateInsertSql_ProducesCorrectSql() // Assert sql.Should().Be( "INSERT INTO [ReadModel-TestAggregate] " + - "(AggregateId, CreateTime, DomainErrorAfterFirstReceived, LastAggregateSequenceNumber, LastGlobalSequenceNumber, PingsReceived, UpdatedTime) " + + "(AggregateId, CreateTime, DomainErrorAfterFirstReceived, LastAggregateSequenceNumber, PingsReceived, UpdatedTime) " + "VALUES " + - "(@AggregateId, @CreateTime, @DomainErrorAfterFirstReceived, @LastAggregateSequenceNumber, @LastGlobalSequenceNumber, @PingsReceived, @UpdatedTime)"); + "(@AggregateId, @CreateTime, @DomainErrorAfterFirstReceived, @LastAggregateSequenceNumber, @PingsReceived, @UpdatedTime)"); } [Test] @@ -58,7 +58,7 @@ public void CreateUpdateSql_ProducesCorrectSql() sql.Should().Be( "UPDATE [ReadModel-TestAggregate] SET " + "CreateTime = @CreateTime, DomainErrorAfterFirstReceived = @DomainErrorAfterFirstReceived, " + - "LastAggregateSequenceNumber = @LastAggregateSequenceNumber, LastGlobalSequenceNumber = @LastGlobalSequenceNumber, " + + "LastAggregateSequenceNumber = @LastAggregateSequenceNumber, " + "PingsReceived = @PingsReceived, UpdatedTime = @UpdatedTime " + "WHERE AggregateId = @AggregateId"); } diff --git a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs index 94820e43a..9be825d78 100644 --- a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs +++ b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModel.cs @@ -30,6 +30,5 @@ public interface IMssqlReadModel : IReadModel DateTimeOffset CreateTime { get; set; } DateTimeOffset UpdatedTime { get; set; } int LastAggregateSequenceNumber { get; set; } - long LastGlobalSequenceNumber { get; set; } } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs index 5dd4e117b..6164ad376 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModel.cs @@ -30,15 +30,13 @@ public abstract class MssqlReadModel : IMssqlReadModel public DateTimeOffset CreateTime { get; set; } public DateTimeOffset UpdatedTime { get; set; } public int LastAggregateSequenceNumber { get; set; } - public long LastGlobalSequenceNumber { get; set; } public override string ToString() { return string.Format( - "Read model '{0}' for '{1} ({2}/{3}'", + "Read model '{0}' for '{1} v{2}'", GetType().Name, AggregateId, - LastGlobalSequenceNumber, LastAggregateSequenceNumber); } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index eac40fc54..67fdf8c7f 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -89,7 +89,6 @@ private async Task UpdateReadModelAsync( var lastDomainEvent = domainEvents.Last(); readModel.UpdatedTime = lastDomainEvent.Timestamp; readModel.LastAggregateSequenceNumber = lastDomainEvent.AggregateSequenceNumber; - readModel.LastGlobalSequenceNumber = lastDomainEvent.GlobalSequenceNumber; var sql = isNew ? _readModelSqlGenerator.CreateInsertSql() diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index 6821be37d..ea513d1a9 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -25,7 +25,6 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.EventStores; using EventFlow.Exceptions; using EventFlow.TestHelpers.Aggregates.Test; using EventFlow.TestHelpers.Aggregates.Test.Events; @@ -69,7 +68,6 @@ public async Task EventsCanBeStored() pingEvent.AggregateType.Should().Be(typeof (TestAggregate)); pingEvent.BatchId.Should().NotBe(default(Guid)); pingEvent.EventType.Should().Be(typeof (PingEvent)); - pingEvent.GlobalSequenceNumber.Should().Be(1); pingEvent.Timestamp.Should().NotBe(default(DateTimeOffset)); pingEvent.Metadata.Count.Should().BeGreaterThan(0); } @@ -93,27 +91,6 @@ public async Task AggregatesCanBeLoaded() loadedTestAggregate.PingsReceived.Count.Should().Be(1); } - [Test] - public async Task GlobalSequenceNumberIncrements() - { - // Arrange - var id1 = TestId.New; - var id2 = TestId.New; - var aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); - var aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); - aggregate1.Ping(PingId.New); - aggregate2.Ping(PingId.New); - - // Act - await aggregate1.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - var domainEvents = await aggregate2.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - - // Assert - var pingEvent = domainEvents.SingleOrDefault(); - pingEvent.Should().NotBeNull(); - pingEvent.GlobalSequenceNumber.Should().Be(2); - } - [Test] public async Task AggregateEventStreamsAreSeperate() { @@ -172,28 +149,6 @@ public async Task NoEventsEmittedIsOk() await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); } - [Test] - public async Task DomainEventCanBeLoaded() - { - // Arrange - var id1 = TestId.New; - var id2 = TestId.New; - var pingId1 = PingId.New; - var pingId2 = PingId.New; - var aggregate1 = await EventStore.LoadAggregateAsync(id1, CancellationToken.None).ConfigureAwait(false); - var aggregate2 = await EventStore.LoadAggregateAsync(id2, CancellationToken.None).ConfigureAwait(false); - aggregate1.Ping(pingId1); - aggregate2.Ping(pingId2); - await aggregate1.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - await aggregate2.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); - - // Act - var domainEvents = await EventStore.LoadEventsAsync(GlobalSequenceNumberRange.Range(1, 2), CancellationToken.None).ConfigureAwait(false); - - // Assert - domainEvents.Count.Should().Be(2); - } - [Test] public async Task OptimisticConcurrency() { diff --git a/Source/EventFlow.TestHelpers/Test.cs b/Source/EventFlow.TestHelpers/Test.cs index 3d6bb0efc..34a055c95 100644 --- a/Source/EventFlow.TestHelpers/Test.cs +++ b/Source/EventFlow.TestHelpers/Test.cs @@ -85,7 +85,6 @@ protected IDomainEvent ToDomainEvent( return DomainEventFactory.Create( aggregateEvent, metadata, - A(), A(), aggregateSequenceNumber, A()); diff --git a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs index 1a9a68e5a..573e73cb9 100644 --- a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs @@ -72,16 +72,6 @@ public void ValidateTestAggregate() testAggregate.PingsReceived.Should().Contain(PingId.With("2352d09b-4712-48cc-bb4f-5560d7c52558")); } - [Test] - public void DomainEventsCanBeLoaded() - { - // Act - var domainEvents = _eventStore.LoadEvents(GlobalSequenceNumberRange.Range(1, 2), CancellationToken.None); - - // Assert - domainEvents.Count.Should().Be(2); - } - [Test, Explicit] public void CreateEventHelper() { diff --git a/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store b/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store deleted file mode 100644 index 0eb2aaa06..000000000 --- a/Source/EventFlow.Tests/TestData/FilesEventStore/Log.store +++ /dev/null @@ -1,3 +0,0 @@ -{ - "GlobalSequenceNumber": 2 -} \ No newline at end of file diff --git a/Source/EventFlow/Aggregates/DomainEvent.cs b/Source/EventFlow/Aggregates/DomainEvent.cs index fbf68e57f..cc0005e46 100644 --- a/Source/EventFlow/Aggregates/DomainEvent.cs +++ b/Source/EventFlow/Aggregates/DomainEvent.cs @@ -35,7 +35,6 @@ public class DomainEvent : IDomainEvent< public int AggregateSequenceNumber { get; private set; } public Guid BatchId { get; private set; } public TAggregateEvent AggregateEvent { get; private set; } - public long GlobalSequenceNumber { get; private set; } public TIdentity AggregateIdentity { get; private set; } public IMetadata Metadata { get; private set; } public DateTimeOffset Timestamp { get; private set; } @@ -44,7 +43,6 @@ public DomainEvent( TAggregateEvent aggregateEvent, IMetadata metadata, DateTimeOffset timestamp, - long globalSequenceNumber, TIdentity aggregateIdentity, int aggregateSequenceNumber, Guid batchId) @@ -52,7 +50,6 @@ public DomainEvent( AggregateEvent = aggregateEvent; Metadata = metadata; Timestamp = timestamp; - GlobalSequenceNumber = globalSequenceNumber; AggregateIdentity = aggregateIdentity; AggregateSequenceNumber = aggregateSequenceNumber; BatchId = batchId; diff --git a/Source/EventFlow/Aggregates/IDomainEvent.cs b/Source/EventFlow/Aggregates/IDomainEvent.cs index 07940aa55..5bd786a44 100644 --- a/Source/EventFlow/Aggregates/IDomainEvent.cs +++ b/Source/EventFlow/Aggregates/IDomainEvent.cs @@ -30,7 +30,6 @@ public interface IDomainEvent Type EventType { get; } int AggregateSequenceNumber { get; } Guid BatchId { get; } - long GlobalSequenceNumber { get; } IMetadata Metadata { get; } DateTimeOffset Timestamp { get; } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 3caee3d4b..9a3ddf389 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -100,7 +100,6 @@ - diff --git a/Source/EventFlow/EventStores/DomainEventFactory.cs b/Source/EventFlow/EventStores/DomainEventFactory.cs index 6284156e1..f5e8586f7 100644 --- a/Source/EventFlow/EventStores/DomainEventFactory.cs +++ b/Source/EventFlow/EventStores/DomainEventFactory.cs @@ -35,7 +35,6 @@ public class DomainEventFactory : IDomainEventFactory public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, string aggregateIdentity, int aggregateSequenceNumber, Guid batchId) @@ -49,7 +48,6 @@ public IDomainEvent Create( aggregateEvent, metadata, metadata.Timestamp, - globalSequenceNumber, identity, aggregateSequenceNumber, batchId); @@ -60,7 +58,6 @@ public IDomainEvent Create( public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, TIdentity id, int aggregateSequenceNumber, Guid batchId) @@ -70,7 +67,6 @@ public IDomainEvent Create( return (IDomainEvent)Create( aggregateEvent, metadata, - globalSequenceNumber, id.Value, aggregateSequenceNumber, batchId); @@ -85,7 +81,6 @@ public IDomainEvent Upgrade( return Create( aggregateEvent, domainEvent.Metadata, - domainEvent.GlobalSequenceNumber, (TIdentity) domainEvent.GetIdentity(), domainEvent.AggregateSequenceNumber, domainEvent.BatchId); diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index e3c7ad3e5..c16fc6266 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -76,7 +76,6 @@ public IDomainEvent Deserialize(ICommittedDomainEvent committedDomainEvent) var domainEvent = _domainEventFactory.Create( aggregateEvent, metadata, - committedDomainEvent.GlobalSequenceNumber, committedDomainEvent.AggregateId, committedDomainEvent.AggregateSequenceNumber, committedDomainEvent.BatchId); diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index 7737cb2c0..c18edec94 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -133,10 +133,6 @@ protected abstract Task> LoadCommitte where TAggregate : IAggregateRoot where TIdentity : IIdentity; - protected abstract Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken); - public virtual async Task>> LoadEventsAsync( TIdentity id, CancellationToken cancellationToken) @@ -179,30 +175,6 @@ public IReadOnlyCollection> LoadEvents> LoadEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - var committedDomainEvents = await LoadCommittedEventsAsync(globalSequenceNumberRange, cancellationToken).ConfigureAwait(false); - var domainEvents = (IReadOnlyCollection) committedDomainEvents - .Select(e => EventJsonSerializer.Deserialize(e)) - .ToList(); - domainEvents = EventUpgradeManager.Upgrade(domainEvents); - return domainEvents; - } - - public IReadOnlyCollection LoadEvents( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - IReadOnlyCollection domainEvents = null; - using (var a = AsyncHelper.Wait) - { - a.Run(LoadEventsAsync(globalSequenceNumberRange, cancellationToken), d => domainEvents = d); - } - return domainEvents; - } - public virtual async Task LoadAggregateAsync( TIdentity id, CancellationToken cancellationToken) diff --git a/Source/EventFlow/EventStores/EventUpgradeManager.cs b/Source/EventFlow/EventStores/EventUpgradeManager.cs index 95cd400ff..eae1ebd86 100644 --- a/Source/EventFlow/EventStores/EventUpgradeManager.cs +++ b/Source/EventFlow/EventStores/EventUpgradeManager.cs @@ -102,7 +102,7 @@ private IEnumerable Upgrade(IEnumerable domainEvents (IEnumerable) new[] {e}, (de, up) => de.SelectMany(ee => a.Upgrade(up, ee))); }) - .OrderBy(d => d.GlobalSequenceNumber); + .OrderBy(d => d.AggregateSequenceNumber); } public IReadOnlyCollection> Upgrade( diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index ac71ba640..f55a01bfa 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -23,7 +23,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -39,13 +38,9 @@ public class FilesEventStore : EventStore private readonly IJsonSerializer _jsonSerializer; private readonly IFilesEventStoreConfiguration _configuration; private readonly AsyncLock _asyncLock = new AsyncLock(); - private readonly string _logFilePath; - private long _globalSequenceNumber; - private Dictionary _log; public class FileEventData : ICommittedDomainEvent { - public long GlobalSequenceNumber { get; set; } public Guid BatchId { get; set; } public string AggregateId { get; set; } public string AggregateName { get; set; } @@ -54,12 +49,6 @@ public class FileEventData : ICommittedDomainEvent public int AggregateSequenceNumber { get; set; } } - public class EventStoreLog - { - public long GlobalSequenceNumber { get; set; } - public Dictionary Log { get; set; } - } - public FilesEventStore( ILog log, IAggregateFactory aggregateFactory, @@ -73,26 +62,6 @@ public FilesEventStore( { _jsonSerializer = jsonSerializer; _configuration = configuration; - _logFilePath = Path.Combine(_configuration.StorePath, "Log.store"); - - if (File.Exists(_logFilePath)) - { - var json = File.ReadAllText(_logFilePath); - var eventStoreLog = _jsonSerializer.Deserialize(json); - _globalSequenceNumber = eventStoreLog.GlobalSequenceNumber; - _log = eventStoreLog.Log ?? new Dictionary(); - - if (_log.Count != _globalSequenceNumber) - { - eventStoreLog = RecreateEventStoreLog(_configuration.StorePath); - _globalSequenceNumber = eventStoreLog.GlobalSequenceNumber; - _log = eventStoreLog.Log; - } - } - else - { - _log = new Dictionary(); - } } protected override async Task> CommitEventsAsync( @@ -115,8 +84,6 @@ protected override async Task> Commit foreach (var serializedEvent in serializedEvents) { var eventPath = GetEventPath(aggregateType, id, serializedEvent.AggregateSequenceNumber); - _globalSequenceNumber++; - _log[_globalSequenceNumber] = eventPath; var fileEventData = new FileEventData { @@ -125,7 +92,6 @@ protected override async Task> Commit AggregateSequenceNumber = serializedEvent.AggregateSequenceNumber, BatchId = batchId, Data = serializedEvent.Data, - GlobalSequenceNumber = _globalSequenceNumber, Metadata = serializedEvent.Meta, }; @@ -150,22 +116,6 @@ protected override async Task> Commit committedDomainEvents.Add(fileEventData); } - using (var streamWriter = File.CreateText(_logFilePath)) - { - Log.Verbose( - "Writing global sequence number '{0}' to '{1}'", - _globalSequenceNumber, - _logFilePath); - var json = _jsonSerializer.Serialize( - new EventStoreLog - { - GlobalSequenceNumber = _globalSequenceNumber, - Log = _log, - }, - true); - await streamWriter.WriteAsync(json).ConfigureAwait(false); - } - return committedDomainEvents; } } @@ -215,49 +165,6 @@ private async Task LoadFileEventDataFile(string eventPath) } } - protected override async Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - var paths = Enumerable.Range((int) globalSequenceNumberRange.From, (int) globalSequenceNumberRange.Count) - .TakeWhile(g => _log.ContainsKey(g)) - .Select(g => _log[g]) - .ToList(); - - var committedDomainEvents = new List(); - foreach (var path in paths) - { - var committedDomainEvent = await LoadFileEventDataFile(path).ConfigureAwait(false); - committedDomainEvents.Add(committedDomainEvent); - } - - return committedDomainEvents; - } - - private EventStoreLog RecreateEventStoreLog(string path) - { - var directory = Directory.GetDirectories(path) - .SelectMany(Directory.GetDirectories) - .SelectMany(Directory.GetFiles) - .Select(f => - { - Console.WriteLine(f); - using (var streamReader = File.OpenText(f)) - { - var json = streamReader.ReadToEnd(); - var fileEventData = _jsonSerializer.Deserialize(json); - return new {fileEventData.GlobalSequenceNumber, Path = f}; - } - }) - .ToDictionary(a => a.GlobalSequenceNumber, a => a.Path); - - return new EventStoreLog - { - GlobalSequenceNumber = directory.Keys.Any() ? directory.Keys.Max() : 0, - Log = directory, - }; - } - private string GetAggregatePath(Type aggregateType, IIdentity id) { return Path.Combine( diff --git a/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs b/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs deleted file mode 100644 index 4a91b141a..000000000 --- a/Source/EventFlow/EventStores/GlobalSequenceNumberRange.cs +++ /dev/null @@ -1,66 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using EventFlow.ValueObjects; - -namespace EventFlow.EventStores -{ - public class GlobalSequenceNumberRange : ValueObject - { - public static GlobalSequenceNumberRange Range(long from, long to) - { - return new GlobalSequenceNumberRange(from, to); - } - - public long From { get; private set; } - public long To { get; private set; } - public long Count { get { return To - From + 1; } } - - private GlobalSequenceNumberRange( - long from, - long to) - { - if (from <= 0) throw new ArgumentOutOfRangeException("from"); - if (to <= 0) throw new ArgumentOutOfRangeException("to"); - if (from > to) throw new ArgumentException(string.Format( - "The 'from' value ({0}) must be less or equal to the 'to' value ({1})", - from, - to)); - - From = from; - To = to; - } - - protected override IEnumerable GetEqualityComponents() - { - yield return From; - yield return To; - } - - public override string ToString() - { - return string.Format("[{0},{1}]", From, To); - } - } -} diff --git a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs index d651b63fd..738fcd223 100644 --- a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs +++ b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs @@ -26,7 +26,6 @@ namespace EventFlow.EventStores { public interface ICommittedDomainEvent { - long GlobalSequenceNumber { get; set; } Guid BatchId { get; set; } string AggregateId { get; set; } string AggregateName { get; set; } diff --git a/Source/EventFlow/EventStores/IDomainEventFactory.cs b/Source/EventFlow/EventStores/IDomainEventFactory.cs index a2208987a..d035465d8 100644 --- a/Source/EventFlow/EventStores/IDomainEventFactory.cs +++ b/Source/EventFlow/EventStores/IDomainEventFactory.cs @@ -30,7 +30,6 @@ public interface IDomainEventFactory IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, string aggregateIdentity, int aggregateSequenceNumber, Guid batchId); @@ -38,7 +37,6 @@ IDomainEvent Create( IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, - long globalSequenceNumber, TIdentity id, int aggregateSequenceNumber, Guid batchId) diff --git a/Source/EventFlow/EventStores/IEventStore.cs b/Source/EventFlow/EventStores/IEventStore.cs index 51adf50b0..b6007b9fc 100644 --- a/Source/EventFlow/EventStores/IEventStore.cs +++ b/Source/EventFlow/EventStores/IEventStore.cs @@ -48,14 +48,6 @@ IReadOnlyCollection> LoadEvents where TIdentity : IIdentity; - Task> LoadEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken); - - IReadOnlyCollection LoadEvents( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken); - Task LoadAggregateAsync( TIdentity id, CancellationToken cancellationToken) diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index dfee52d02..5b6ca19a1 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -43,7 +43,6 @@ public class InMemoryEventStore : EventStore, IDisposable private class InMemoryCommittedDomainEvent : ICommittedDomainEvent { - public long GlobalSequenceNumber { get; set; } public Guid BatchId { get; set; } public string AggregateId { get; set; } public string AggregateName { get; set; } @@ -111,7 +110,6 @@ protected async override Task> Commit BatchId = batchId, Data = e.Data, Metadata = e.Meta, - GlobalSequenceNumber = globalCount + i + 1 }; Log.Verbose("Committing event {0}{1}", Environment.NewLine, committedDomainEvent.ToString()); return committedDomainEvent; @@ -160,16 +158,6 @@ public override Task DeleteAggregateAsync( return Task.FromResult(0); } - protected override Task> LoadCommittedEventsAsync( - GlobalSequenceNumberRange globalSequenceNumberRange, - CancellationToken cancellationToken) - { - var committedDomainEvents = _eventStore - .SelectMany(kv => kv.Value) - .Where(e => e.GlobalSequenceNumber >= globalSequenceNumberRange.From && e.GlobalSequenceNumber <= globalSequenceNumberRange.To) - .ToList(); - return Task.FromResult>(committedDomainEvents); - } public void Dispose() { diff --git a/Source/EventFlow/ReadStores/IReadModelContext.cs b/Source/EventFlow/ReadStores/IReadModelContext.cs index 3e85b811f..a0587feb1 100644 --- a/Source/EventFlow/ReadStores/IReadModelContext.cs +++ b/Source/EventFlow/ReadStores/IReadModelContext.cs @@ -24,6 +24,5 @@ namespace EventFlow.ReadStores { public interface IReadModelContext { - long GlobalSequenceNumber { get; } } } diff --git a/Source/EventFlow/ReadStores/ReadModelContext.cs b/Source/EventFlow/ReadStores/ReadModelContext.cs index 8dfe00b29..9a1c32c1d 100644 --- a/Source/EventFlow/ReadStores/ReadModelContext.cs +++ b/Source/EventFlow/ReadStores/ReadModelContext.cs @@ -24,11 +24,5 @@ namespace EventFlow.ReadStores { public class ReadModelContext : IReadModelContext { - public long GlobalSequenceNumber { get; private set; } - - public ReadModelContext(long globalSequenceNumber) - { - GlobalSequenceNumber = globalSequenceNumber; - } } } diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index 14bc9ce8b..bf38cdeed 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -59,10 +59,7 @@ group de by rid into g select new ReadModelUpdate(g.Key, g.ToList()) ).ToList(); - var globalSequenceNumber = domainEvents.Max(de => de.GlobalSequenceNumber); - - var readModelContext = new ReadModelContext( - globalSequenceNumber); + var readModelContext = new ReadModelContext(); return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); } From 0057f2d5983b2061d12e4fddd7429a3d429698f3 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 00:26:38 +0200 Subject: [PATCH 17/47] Removed event caches --- RELEASE_NOTES.md | 2 + .../MssqlEventStore.cs | 4 +- Source/EventFlow.Tests/EventFlow.Tests.csproj | 1 - .../EventCaches/InMemoryEventCacheTests.cs | 115 --------------- .../UnitTests/EventStores/EventStoreTests.cs | 19 --- Source/EventFlow/EventCaches/IEventCache.cs | 51 ------- .../InMemory/InMemoryEventCache.cs | 132 ------------------ .../EventCaches/Null/NullEventCache.cs | 60 -------- Source/EventFlow/EventFlow.csproj | 4 - Source/EventFlow/EventFlowOptions.cs | 3 - Source/EventFlow/EventStores/EventStore.cs | 46 +----- .../EventStores/Files/FilesEventStore.cs | 4 +- .../InMemory/InMemoryEventStore.cs | 4 +- .../EventFlowOptionsEventCachesExtensions.cs | 46 ------ 14 files changed, 11 insertions(+), 480 deletions(-) delete mode 100644 Source/EventFlow.Tests/UnitTests/EventCaches/InMemoryEventCacheTests.cs delete mode 100644 Source/EventFlow/EventCaches/IEventCache.cs delete mode 100644 Source/EventFlow/EventCaches/InMemory/InMemoryEventCache.cs delete mode 100644 Source/EventFlow/EventCaches/Null/NullEventCache.cs delete mode 100644 Source/EventFlow/Extensions/EventFlowOptionsEventCachesExtensions.cs diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4816424e4..9ecb91eef 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -16,6 +16,8 @@ - `IDomainEvent.GlobalSequenceNumber` removed - `IEventStore.LoadEventsAsync` and `IEventStore.LoadEvents` taking a `GlobalSequenceNumberRange` removed + * Breaking: Remove the concept of event caches. If you really need this + then implement it by registering a decorator for `IEventStore` * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate stream. Please consider carefully if you really want to use it. Storage might be cheaper than the historic knowledge within your events diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index 3b6cc5690..ab9259f9d 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -28,7 +28,6 @@ using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Core; -using EventFlow.EventCaches; using EventFlow.Exceptions; using EventFlow.Logs; using EventFlow.MsSql; @@ -56,9 +55,8 @@ public MsSqlEventStore( IEventJsonSerializer eventJsonSerializer, IEventUpgradeManager eventUpgradeManager, IEnumerable metadataProviders, - IEventCache eventCache, IMsSqlConnection connection) - : base(log, aggregateFactory, eventJsonSerializer, eventCache, eventUpgradeManager, metadataProviders) + : base(log, aggregateFactory, eventJsonSerializer, eventUpgradeManager, metadataProviders) { _connection = connection; } diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 061f5e74b..0a604e9d1 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -76,7 +76,6 @@ - diff --git a/Source/EventFlow.Tests/UnitTests/EventCaches/InMemoryEventCacheTests.cs b/Source/EventFlow.Tests/UnitTests/EventCaches/InMemoryEventCacheTests.cs deleted file mode 100644 index c51a5b50a..000000000 --- a/Source/EventFlow.Tests/UnitTests/EventCaches/InMemoryEventCacheTests.cs +++ /dev/null @@ -1,115 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.EventCaches.InMemory; -using EventFlow.TestHelpers; -using EventFlow.TestHelpers.Aggregates.Test; -using EventFlow.TestHelpers.Aggregates.Test.Events; -using FluentAssertions; -using NUnit.Framework; - -namespace EventFlow.Tests.UnitTests.EventCaches -{ - public class InMemoryEventCacheTests : TestsFor - { - [Test] - public void InsertNullThrowsException() - { - // Act - Assert.Throws( - async () => await Sut.InsertAsync(TestId.New, null, CancellationToken.None).ConfigureAwait(false)); - } - - [Test] - public void EmptyListThrowsException() - { - // Act - Assert.Throws( - async () => await Sut.InsertAsync(TestId.New, new List>(), CancellationToken.None).ConfigureAwait(false)); - } - - [Test] - public async Task NoneExistingReturnsNull() - { - // Arrange - var id = TestId.New; - - // Act - var domainEvents = await Sut.GetAsync(id, CancellationToken.None).ConfigureAwait(false); - - // Assert - domainEvents.Should().BeNull(); - } - - [Test] - public async Task StreamCanBeUpdated() - { - // Arrange - var id = TestId.New; - - // Act - await Sut.InsertAsync(id, CreateStream(), CancellationToken.None).ConfigureAwait(false); - await Sut.InsertAsync(id, CreateStream(), CancellationToken.None).ConfigureAwait(false); - } - - [Test] - public async Task InsertAndGetWorks() - { - // Arrange - var id = TestId.New; - var domainEvents = CreateStream(); - - // Act - await Sut.InsertAsync(id, domainEvents, CancellationToken.None).ConfigureAwait(false); - var storedDomainEvents = await Sut.GetAsync(id, CancellationToken.None).ConfigureAwait(false); - - // Assert - storedDomainEvents.Should().BeSameAs(domainEvents); - } - - [Test] - public async Task InvalidateRemoves() - { - // Arrange - var id = TestId.New; - var domainEvents = CreateStream(); - - // Act - await Sut.InsertAsync(id, domainEvents, CancellationToken.None).ConfigureAwait(false); - await Sut.InvalidateAsync(id, CancellationToken.None).ConfigureAwait(false); - var storedEvents = await Sut.GetAsync(id, CancellationToken.None).ConfigureAwait(false); - - // Assert - storedEvents.Should().BeNull(); - } - - private IReadOnlyCollection> CreateStream() - { - return Many>(); - } - } -} diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs index f32eb11fe..50c05fd83 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs @@ -20,13 +20,9 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.EventCaches; using EventFlow.EventStores; using EventFlow.EventStores.InMemory; using EventFlow.TestHelpers; @@ -40,7 +36,6 @@ namespace EventFlow.Tests.UnitTests.EventStores { public class EventStoreTests : TestsFor { - private Mock _eventCacheMock; private Mock _eventUpgradeManagerMock; private Mock _eventJsonSerializerMock; @@ -50,7 +45,6 @@ public void SetUp() Fixture.Inject(Enumerable.Empty()); _eventJsonSerializerMock = InjectMock(); - _eventCacheMock = InjectMock(); _eventUpgradeManagerMock = InjectMock(); _eventUpgradeManagerMock @@ -65,19 +59,6 @@ public void SetUp() int.Parse(m.Single(kv => kv.Key == MetadataKeys.AggregateSequenceNumber).Value))); } - [Test] - public async Task CacheIsInvalidatedOnStore() - { - // Arrange - var ss = ManyUncommittedEvents(1); - - // Act - await Sut.StoreAsync(TestId.New, ss, CancellationToken.None).ConfigureAwait(false); - - // Assert - _eventCacheMock.Verify(c => c.InvalidateAsync(It.IsAny(), It.IsAny()), Times.Once); - } - private List ManyUncommittedEvents(int count = 3) { return Many(count) diff --git a/Source/EventFlow/EventCaches/IEventCache.cs b/Source/EventFlow/EventCaches/IEventCache.cs deleted file mode 100644 index 4974797de..000000000 --- a/Source/EventFlow/EventCaches/IEventCache.cs +++ /dev/null @@ -1,51 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; - -namespace EventFlow.EventCaches -{ - public interface IEventCache - { - Task InsertAsync( - TIdentity id, - IReadOnlyCollection> domainEvents, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity; - - Task InvalidateAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity; - - Task>> GetAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity; - } -} diff --git a/Source/EventFlow/EventCaches/InMemory/InMemoryEventCache.cs b/Source/EventFlow/EventCaches/InMemory/InMemoryEventCache.cs deleted file mode 100644 index 4daa99a12..000000000 --- a/Source/EventFlow/EventCaches/InMemory/InMemoryEventCache.cs +++ /dev/null @@ -1,132 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Caching; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.Logs; - -namespace EventFlow.EventCaches.InMemory -{ - public class InMemoryEventCache : IEventCache, IDisposable - { - private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(5); - private readonly ILog _log; - private readonly MemoryCache _memoryCache = new MemoryCache(string.Format( - "{0}-{1}", - typeof(InMemoryEventCache).FullName, - Guid.NewGuid())); - - public InMemoryEventCache( - ILog log) - { - _log = log; - } - - public Task InsertAsync( - TIdentity id, - IReadOnlyCollection> domainEvents, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { - var aggregateType = typeof (TAggregate); - - if (domainEvents == null) throw new ArgumentNullException("domainEvents"); - if (!domainEvents.Any()) throw new ArgumentException(string.Format( - "You must provide events to cache for aggregate '{0}' with ID '{1}'", - aggregateType.Name, - id)); - - var cacheKey = GetKey(aggregateType, id); - _memoryCache.Set(cacheKey, domainEvents, DateTimeOffset.Now.Add(CacheTime)); - _log.Verbose( - "Added cache key {0} with {1} events to in-memory event store cache. Now it has {2} streams cached.", - cacheKey, - domainEvents.Count, - _memoryCache.GetCount()); - return Task.FromResult(0); - } - - public Task InvalidateAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { - var aggregateType = typeof (TAggregate); - var cacheKey = GetKey(aggregateType, id); - if (_memoryCache.Contains(cacheKey)) - { - _log.Verbose( - "Found and invalidated in-memory cache for aggregate '{0}' with ID '{1}'", - aggregateType.Name, - id); - _memoryCache.Remove(cacheKey); - } - - return Task.FromResult(0); - } - - public Task>> GetAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { - var aggregateType = typeof (TAggregate); - var cacheKey = GetKey(aggregateType, id); - var domainEvents = _memoryCache.Get(cacheKey) as IReadOnlyCollection>; - if (domainEvents == null) - { - _log.Verbose( - "Didn't not find anything in in-memory cache for aggregate '{0}' with ID '{1}'", - aggregateType.Name, - id); - } - else - { - _log.Verbose( - "Found {0} events in in-memory cache for aggregate '{1}' with ID '{2}'", - domainEvents.Count, - aggregateType.Name, - id); - } - - return Task.FromResult(domainEvents); - } - - private static string GetKey(Type aggregateType, IIdentity id) - { - return string.Format("{0} ({1})", aggregateType.FullName, id); - } - - public void Dispose() - { - _memoryCache.Dispose(); - } - } -} diff --git a/Source/EventFlow/EventCaches/Null/NullEventCache.cs b/Source/EventFlow/EventCaches/Null/NullEventCache.cs deleted file mode 100644 index 454595ca9..000000000 --- a/Source/EventFlow/EventCaches/Null/NullEventCache.cs +++ /dev/null @@ -1,60 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; - -namespace EventFlow.EventCaches.Null -{ - public class NullEventCache : IEventCache - { - public Task InsertAsync( - TIdentity id, - IReadOnlyCollection> domainEvents, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { - return Task.FromResult(0); - } - - public Task InvalidateAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { - return Task.FromResult(0); - } - - public Task>> GetAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { - return Task.FromResult(null as IReadOnlyCollection>); - } - } -} diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 9a3ddf389..b75187f1e 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -85,16 +85,13 @@ - - - @@ -110,7 +107,6 @@ - diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index 289216549..7a59ceb2f 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -28,8 +28,6 @@ using EventFlow.Configuration.Registrations; using EventFlow.Core; using EventFlow.Core.RetryStrategies; -using EventFlow.EventCaches; -using EventFlow.EventCaches.InMemory; using EventFlow.EventStores; using EventFlow.EventStores.InMemory; using EventFlow.Logs; @@ -109,7 +107,6 @@ public IRootResolver CreateResolver(bool validateRegistrations = true) RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); - RegisterIfMissing(services, Lifetime.Singleton); RegisterIfMissing(services, f => f.Register(_ => _eventFlowConfiguration)); if (!services.Contains(typeof (ITransientFaultHandler<>))) diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index c18edec94..269e6b426 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -26,8 +26,6 @@ using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Core; -using EventFlow.EventCaches; -using EventFlow.Exceptions; using EventFlow.Logs; namespace EventFlow.EventStores @@ -38,21 +36,18 @@ public abstract class EventStore : IEventStore protected IAggregateFactory AggregateFactory { get; private set; } protected IEventUpgradeManager EventUpgradeManager { get; private set; } protected IEventJsonSerializer EventJsonSerializer { get; private set; } - protected IEventCache EventCache { get; private set; } protected IReadOnlyCollection MetadataProviders { get; private set; } protected EventStore( ILog log, IAggregateFactory aggregateFactory, IEventJsonSerializer eventJsonSerializer, - IEventCache eventCache, IEventUpgradeManager eventUpgradeManager, IEnumerable metadataProviders) { Log = log; AggregateFactory = aggregateFactory; EventJsonSerializer = eventJsonSerializer; - EventCache = eventCache; EventUpgradeManager = eventUpgradeManager; MetadataProviders = metadataProviders.ToList(); } @@ -86,37 +81,16 @@ public virtual async Task committedDomainEvents; - try - { - committedDomainEvents = await CommitEventsAsync( - id, - serializedEvents, - cancellationToken) - .ConfigureAwait(false); - } - catch (OptimisticConcurrencyException) - { - Log.Verbose( - "Detected an optimisting concurrency exception for aggregate '{0}' with ID '{1}', invalidating cache", - aggregateType.Name, - id); - - // TODO: Rework as soon as await is possible within catch - using (var a = AsyncHelper.Wait) - { - a.Run(EventCache.InvalidateAsync(id, cancellationToken)); - } - - throw; - } + var committedDomainEvents = await CommitEventsAsync( + id, + serializedEvents, + cancellationToken) + .ConfigureAwait(false); var domainEvents = committedDomainEvents .Select(e => EventJsonSerializer.Deserialize(id, e)) .ToList(); - await EventCache.InvalidateAsync(id, cancellationToken).ConfigureAwait(false); - return domainEvents; } @@ -139,14 +113,8 @@ public virtual async Task where TIdentity : IIdentity { - var domainEvents = await EventCache.GetAsync(id, cancellationToken).ConfigureAwait(false); - if (domainEvents != null) - { - return domainEvents; - } - var committedDomainEvents = await LoadCommittedEventsAsync(id, cancellationToken).ConfigureAwait(false); - domainEvents = committedDomainEvents + var domainEvents = (IReadOnlyCollection>)committedDomainEvents .Select(e => EventJsonSerializer.Deserialize(id, e)) .ToList(); @@ -157,8 +125,6 @@ public virtual async Task metadataProviders, IEventUpgradeManager eventUpgradeManager, IJsonSerializer jsonSerializer, - IEventCache eventCache, IFilesEventStoreConfiguration configuration) - : base(log, aggregateFactory, eventJsonSerializer, eventCache, eventUpgradeManager, metadataProviders) + : base(log, aggregateFactory, eventJsonSerializer, eventUpgradeManager, metadataProviders) { _jsonSerializer = jsonSerializer; _configuration = configuration; diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 5b6ca19a1..29c0a5952 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -29,7 +29,6 @@ using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Core; -using EventFlow.EventCaches; using EventFlow.Exceptions; using EventFlow.Extensions; using EventFlow.Logs; @@ -66,10 +65,9 @@ public InMemoryEventStore( ILog log, IAggregateFactory aggregateFactory, IEventJsonSerializer eventJsonSerializer, - IEventCache eventCache, IEnumerable metadataProviders, IEventUpgradeManager eventUpgradeManager) - : base(log, aggregateFactory, eventJsonSerializer, eventCache, eventUpgradeManager, metadataProviders) + : base(log, aggregateFactory, eventJsonSerializer, eventUpgradeManager, metadataProviders) { } diff --git a/Source/EventFlow/Extensions/EventFlowOptionsEventCachesExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsEventCachesExtensions.cs deleted file mode 100644 index 95494ac38..000000000 --- a/Source/EventFlow/Extensions/EventFlowOptionsEventCachesExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using EventFlow.Configuration.Registrations; -using EventFlow.EventCaches; -using EventFlow.EventCaches.Null; - -namespace EventFlow.Extensions -{ - public static class EventFlowOptionsEventCachesExtensions - { - public static EventFlowOptions UseNullEventCache(this EventFlowOptions eventFlowOptions) - { - eventFlowOptions.RegisterServices(f => f.Register(Lifetime.Singleton)); - return eventFlowOptions; - } - - public static EventFlowOptions UseEventCache( - this EventFlowOptions eventFlowOptions, - Lifetime lifetime = Lifetime.AlwaysUnique) - where TEventCache : class, IEventCache - { - eventFlowOptions.RegisterServices(f => f.Register(lifetime)); - return eventFlowOptions; - } - } -} From 233c3dfd3a25b5ef30de47d5b626203aacbaab21 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 10:32:05 +0200 Subject: [PATCH 18/47] Testing --- .../EventFlow.TestHelpers/IntegrationTest.cs | 13 +++++++++++ .../Suites/EventStoreSuite.cs | 22 +++++++++++++++++++ .../Suites/ReadModelStoreSuite.cs | 10 +++------ Source/EventFlow/EventStores/EventStore.cs | 6 ++++- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/Source/EventFlow.TestHelpers/IntegrationTest.cs b/Source/EventFlow.TestHelpers/IntegrationTest.cs index 2a2180df6..a2f6670be 100644 --- a/Source/EventFlow.TestHelpers/IntegrationTest.cs +++ b/Source/EventFlow.TestHelpers/IntegrationTest.cs @@ -20,10 +20,15 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System.Threading; +using System.Threading.Tasks; using EventFlow.Configuration; using EventFlow.EventStores; using EventFlow.Extensions; using EventFlow.ReadStores; +using EventFlow.TestHelpers.Aggregates.Test; +using EventFlow.TestHelpers.Aggregates.Test.Commands; +using EventFlow.TestHelpers.Aggregates.Test.ValueObjects; using NUnit.Framework; namespace EventFlow.TestHelpers @@ -58,5 +63,13 @@ public void TearDownIntegrationTest() Configuration.TearDown(); Resolver.Dispose(); } + + protected async Task PublishPingCommandAsync(TestId testId, int count = 1) + { + for (var i = 0; i < count; i++) + { + await CommandBus.PublishAsync(new PingCommand(testId, PingId.New), CancellationToken.None).ConfigureAwait(false); + } + } } } diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index 45c721042..03cfef046 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -171,6 +171,28 @@ public async Task NoEventsEmittedIsOk() await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); } + [Test] + public async Task LoadingFirstPageShouldOnlyLoadCorrectEvents() + { + // Arrange + var id = TestId.New; + var pingIds = new[] {PingId.New, PingId.New, PingId.New}; + var aggregate = await EventStore.LoadAggregateAsync(id, CancellationToken.None).ConfigureAwait(false); + aggregate.Ping(pingIds[0]); + aggregate.Ping(pingIds[1]); + aggregate.Ping(pingIds[2]); + await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + + // Act + var domainEvents = await EventStore.LoadAllEventsAsync(1, 2, CancellationToken.None).ConfigureAwait(false); + + // Assert + domainEvents.NextPosition.Should().Be(3); + domainEvents.DomainEvents.Count.Should().Be(2); + domainEvents.DomainEvents.Should().Contain(e => ((IDomainEvent)e).AggregateEvent.PingId == pingIds[0]); + domainEvents.DomainEvents.Should().Contain(e => ((IDomainEvent)e).AggregateEvent.PingId == pingIds[1]); + } + [Test] public async Task OptimisticConcurrency() { diff --git a/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs index e9cbf9357..02ef0603a 100644 --- a/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/ReadModelStoreSuite.cs @@ -20,11 +20,8 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System.Threading; using System.Threading.Tasks; using EventFlow.TestHelpers.Aggregates.Test; -using EventFlow.TestHelpers.Aggregates.Test.Commands; -using EventFlow.TestHelpers.Aggregates.Test.ValueObjects; using FluentAssertions; using NUnit.Framework; @@ -40,7 +37,7 @@ public async Task ReadModelReceivesEvent() var id = TestId.New; // Act - await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); + await PublishPingCommandAsync(id).ConfigureAwait(false); var readModel = await Configuration.GetTestAggregateReadModelAsync(id).ConfigureAwait(false); // Assert @@ -53,7 +50,7 @@ public async Task PurgeRemovesReadModels() { // Arrange var id = TestId.New; - await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); + await PublishPingCommandAsync(id).ConfigureAwait(false); // Act await Configuration.PurgeTestAggregateReadModelAsync().ConfigureAwait(false); @@ -68,8 +65,7 @@ public async Task PopulateCreatesReadModels() { // Arrange var id = TestId.New; - await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); - await CommandBus.PublishAsync(new PingCommand(id, PingId.New), CancellationToken.None).ConfigureAwait(false); + await PublishPingCommandAsync(id, 2).ConfigureAwait(false); await Configuration.PurgeTestAggregateReadModelAsync().ConfigureAwait(false); // Act diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index 443027ca1..4c41ca182 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -113,9 +114,12 @@ public async Task LoadAllEventsAsync( long pageSize, CancellationToken cancellationToken) { + if (pageSize <= 0) throw new ArgumentOutOfRangeException("pageSize"); + if (startPosition < 0) throw new ArgumentOutOfRangeException("startPosition"); + var allCommittedEventsPage = await LoadAllCommittedDomainEvents( startPosition, - startPosition + pageSize + 1, + startPosition + pageSize - 1, cancellationToken) .ConfigureAwait(false); var domainEvents = (IReadOnlyCollection)allCommittedEventsPage.CommittedDomainEvents From 732a489b17054df32d2be53778e63eb263ff6a80 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 10:47:39 +0200 Subject: [PATCH 19/47] More tests and fixes --- .../MssqlEventStore.cs | 2 +- .../Suites/EventStoreSuite.cs | 33 +++++++++++++++++++ .../EventStores/Files/FilesEventStore.cs | 2 +- .../InMemory/InMemoryEventStore.cs | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index d290f017a..28412d235 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -87,7 +87,7 @@ ORDER BY var nextPosition = eventDataModels.Any() ? eventDataModels.Max(e => e.GlobalSequenceNumber) + 1 - : 1; + : startPostion; return new AllCommittedEventsPage(nextPosition, eventDataModels); } diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index 03cfef046..eb0ef719b 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -171,6 +171,39 @@ public async Task NoEventsEmittedIsOk() await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); } + [Test] + public async Task NextPositionIsIdOfNextEvent() + { + // Arrange + var id = TestId.New; + var aggregate = await EventStore.LoadAggregateAsync(id, CancellationToken.None).ConfigureAwait(false); + aggregate.Ping(PingId.New); + await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + + // Act + var domainEvents = await EventStore.LoadAllEventsAsync(1, 10, CancellationToken.None).ConfigureAwait(false); + + // Assert + domainEvents.NextPosition.Should().Be(2); + } + + [Test] + public async Task NextPositionIsStartIfNoEvents() + { + // Arrange + var id = TestId.New; + var aggregate = await EventStore.LoadAggregateAsync(id, CancellationToken.None).ConfigureAwait(false); + aggregate.Ping(PingId.New); + await aggregate.CommitAsync(EventStore, CancellationToken.None).ConfigureAwait(false); + + // Act + var domainEvents = await EventStore.LoadAllEventsAsync(3, 10, CancellationToken.None).ConfigureAwait(false); + + // Assert + domainEvents.NextPosition.Should().Be(3); + domainEvents.DomainEvents.Should().BeEmpty(); + } + [Test] public async Task LoadingFirstPageShouldOnlyLoadCorrectEvents() { diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index 213ef6b95..136232e0d 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -112,7 +112,7 @@ protected override async Task LoadAllCommittedDomainEven var nextPosition = committedDomainEvents.Any() ? committedDomainEvents.Max(e => e.GlobalSequenceNumber) + 1 - : 1; + : startPostion; return new AllCommittedEventsPage(nextPosition, committedDomainEvents); } diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 420051d96..3bc4749e9 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -84,7 +84,7 @@ protected override Task LoadAllCommittedDomainEvents( var nextPosition = committedDomainEvents.Any() ? committedDomainEvents.Max(e => e.GlobalSequenceNumber) + 1 - : 1; + : startPostion; return Task.FromResult(new AllCommittedEventsPage(nextPosition, committedDomainEvents)); } From b0c0a66c5a3b734b2b4b3de79ef7f6c3afa31e76 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 11:36:06 +0200 Subject: [PATCH 20/47] Move batch ID to meta data --- .../MssqlEventStore.cs | 3 +-- .../Suites/EventStoreSuite.cs | 1 - Source/EventFlow.TestHelpers/Test.cs | 3 +-- .../UnitTests/EventStores/EventStoreTests.cs | 3 ++- Source/EventFlow/Aggregates/DomainEvent.cs | 5 +---- Source/EventFlow/Aggregates/IDomainEvent.cs | 1 - Source/EventFlow/Aggregates/MetadataKeys.cs | 1 + .../EventFlow/EventStores/DomainEventFactory.cs | 15 +++++---------- .../EventFlow/EventStores/EventJsonSerializer.cs | 6 +++--- Source/EventFlow/EventStores/EventStore.cs | 10 +++++++++- .../EventFlow/EventStores/IDomainEventFactory.cs | 7 ++----- Source/EventFlow/EventStores/SerializedEvent.cs | 7 ++++++- 12 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index ab9259f9d..a837a4a83 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -71,7 +71,6 @@ protected override async Task> Commit return new ICommittedDomainEvent[] {}; } - var batchId = Guid.NewGuid(); var aggregateType = typeof(TAggregate); var aggregateName = aggregateType.Name.Replace("Aggregate", string.Empty); var eventDataModels = serializedEvents @@ -79,7 +78,7 @@ protected override async Task> Commit { AggregateId = id.Value, AggregateName = aggregateName, - BatchId = batchId, + BatchId = Guid.Parse(e.Metadata[MetadataKeys.BatchId]), Data = e.Data, Metadata = e.Meta, AggregateSequenceNumber = e.AggregateSequenceNumber, diff --git a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs index ea513d1a9..e162e5dcb 100644 --- a/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs +++ b/Source/EventFlow.TestHelpers/Suites/EventStoreSuite.cs @@ -66,7 +66,6 @@ public async Task EventsCanBeStored() pingEvent.AggregateIdentity.Should().Be(id); pingEvent.AggregateSequenceNumber.Should().Be(1); pingEvent.AggregateType.Should().Be(typeof (TestAggregate)); - pingEvent.BatchId.Should().NotBe(default(Guid)); pingEvent.EventType.Should().Be(typeof (PingEvent)); pingEvent.Timestamp.Should().NotBe(default(DateTimeOffset)); pingEvent.Metadata.Count.Should().BeGreaterThan(0); diff --git a/Source/EventFlow.TestHelpers/Test.cs b/Source/EventFlow.TestHelpers/Test.cs index 34a055c95..ae3bf7953 100644 --- a/Source/EventFlow.TestHelpers/Test.cs +++ b/Source/EventFlow.TestHelpers/Test.cs @@ -86,8 +86,7 @@ protected IDomainEvent ToDomainEvent( aggregateEvent, metadata, A(), - aggregateSequenceNumber, - A()); + aggregateSequenceNumber); } protected Mock> CreateFailingFunction(T result, params Exception[] exceptions) diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs index 50c05fd83..088289815 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/EventStoreTests.cs @@ -56,7 +56,8 @@ public void SetUp() (a, m) => new SerializedEvent( string.Empty, string.Empty, - int.Parse(m.Single(kv => kv.Key == MetadataKeys.AggregateSequenceNumber).Value))); + int.Parse(m.Single(kv => kv.Key == MetadataKeys.AggregateSequenceNumber).Value), + new Metadata())); } private List ManyUncommittedEvents(int count = 3) diff --git a/Source/EventFlow/Aggregates/DomainEvent.cs b/Source/EventFlow/Aggregates/DomainEvent.cs index cc0005e46..17f7007a1 100644 --- a/Source/EventFlow/Aggregates/DomainEvent.cs +++ b/Source/EventFlow/Aggregates/DomainEvent.cs @@ -33,7 +33,6 @@ public class DomainEvent : IDomainEvent< public Type EventType { get { return typeof (TAggregateEvent); } } public int AggregateSequenceNumber { get; private set; } - public Guid BatchId { get; private set; } public TAggregateEvent AggregateEvent { get; private set; } public TIdentity AggregateIdentity { get; private set; } public IMetadata Metadata { get; private set; } @@ -44,15 +43,13 @@ public DomainEvent( IMetadata metadata, DateTimeOffset timestamp, TIdentity aggregateIdentity, - int aggregateSequenceNumber, - Guid batchId) + int aggregateSequenceNumber) { AggregateEvent = aggregateEvent; Metadata = metadata; Timestamp = timestamp; AggregateIdentity = aggregateIdentity; AggregateSequenceNumber = aggregateSequenceNumber; - BatchId = batchId; } public IIdentity GetIdentity() diff --git a/Source/EventFlow/Aggregates/IDomainEvent.cs b/Source/EventFlow/Aggregates/IDomainEvent.cs index 5bd786a44..8a68b45b8 100644 --- a/Source/EventFlow/Aggregates/IDomainEvent.cs +++ b/Source/EventFlow/Aggregates/IDomainEvent.cs @@ -29,7 +29,6 @@ public interface IDomainEvent Type AggregateType { get; } Type EventType { get; } int AggregateSequenceNumber { get; } - Guid BatchId { get; } IMetadata Metadata { get; } DateTimeOffset Timestamp { get; } diff --git a/Source/EventFlow/Aggregates/MetadataKeys.cs b/Source/EventFlow/Aggregates/MetadataKeys.cs index 64d18cfb6..4b27d7022 100644 --- a/Source/EventFlow/Aggregates/MetadataKeys.cs +++ b/Source/EventFlow/Aggregates/MetadataKeys.cs @@ -24,6 +24,7 @@ namespace EventFlow.Aggregates { public sealed class MetadataKeys { + public const string BatchId = "batch_id"; public const string EventName = "event_name"; public const string EventVersion = "event_version"; public const string Timestamp = "timestamp"; diff --git a/Source/EventFlow/EventStores/DomainEventFactory.cs b/Source/EventFlow/EventStores/DomainEventFactory.cs index f5e8586f7..2da134b47 100644 --- a/Source/EventFlow/EventStores/DomainEventFactory.cs +++ b/Source/EventFlow/EventStores/DomainEventFactory.cs @@ -36,8 +36,7 @@ public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, string aggregateIdentity, - int aggregateSequenceNumber, - Guid batchId) + int aggregateSequenceNumber) { var domainEventType = AggregateEventToDomainEventTypeMap.GetOrAdd(aggregateEvent.GetType(), GetDomainEventType); var identityType = DomainEventToIdentityTypeMap.GetOrAdd(domainEventType, GetIdentityType); @@ -49,8 +48,7 @@ public IDomainEvent Create( metadata, metadata.Timestamp, identity, - aggregateSequenceNumber, - batchId); + aggregateSequenceNumber); return domainEvent; } @@ -59,8 +57,7 @@ public IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, TIdentity id, - int aggregateSequenceNumber, - Guid batchId) + int aggregateSequenceNumber) where TAggregate : IAggregateRoot where TIdentity : IIdentity { @@ -68,8 +65,7 @@ public IDomainEvent Create( aggregateEvent, metadata, id.Value, - aggregateSequenceNumber, - batchId); + aggregateSequenceNumber); } public IDomainEvent Upgrade( @@ -82,8 +78,7 @@ public IDomainEvent Upgrade( aggregateEvent, domainEvent.Metadata, (TIdentity) domainEvent.GetIdentity(), - domainEvent.AggregateSequenceNumber, - domainEvent.BatchId); + domainEvent.AggregateSequenceNumber); } private static Type GetIdentityType(Type domainEventType) diff --git a/Source/EventFlow/EventStores/EventJsonSerializer.cs b/Source/EventFlow/EventStores/EventJsonSerializer.cs index c16fc6266..1b898c38f 100644 --- a/Source/EventFlow/EventStores/EventJsonSerializer.cs +++ b/Source/EventFlow/EventStores/EventJsonSerializer.cs @@ -60,7 +60,8 @@ public SerializedEvent Serialize(IAggregateEvent aggregateEvent, IEnumerable(MetadataKeys.BatchId, batchId), + }; + var serializedEvents = uncommittedDomainEvents .Select(e => { var metadata = MetadataProviders .SelectMany(p => p.ProvideMetadata(id, e.AggregateEvent, e.Metadata)) - .Concat(e.Metadata); + .Concat(e.Metadata) + .Concat(storeMetadata); return EventJsonSerializer.Serialize(e.AggregateEvent, metadata); }) .ToList(); diff --git a/Source/EventFlow/EventStores/IDomainEventFactory.cs b/Source/EventFlow/EventStores/IDomainEventFactory.cs index d035465d8..c6fe1e7df 100644 --- a/Source/EventFlow/EventStores/IDomainEventFactory.cs +++ b/Source/EventFlow/EventStores/IDomainEventFactory.cs @@ -20,7 +20,6 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using EventFlow.Aggregates; namespace EventFlow.EventStores @@ -31,15 +30,13 @@ IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, string aggregateIdentity, - int aggregateSequenceNumber, - Guid batchId); + int aggregateSequenceNumber); IDomainEvent Create( IAggregateEvent aggregateEvent, IMetadata metadata, TIdentity id, - int aggregateSequenceNumber, - Guid batchId) + int aggregateSequenceNumber) where TAggregate : IAggregateRoot where TIdentity : IIdentity; diff --git a/Source/EventFlow/EventStores/SerializedEvent.cs b/Source/EventFlow/EventStores/SerializedEvent.cs index e2aa3895d..364b2cdde 100644 --- a/Source/EventFlow/EventStores/SerializedEvent.cs +++ b/Source/EventFlow/EventStores/SerializedEvent.cs @@ -20,6 +20,8 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using EventFlow.Aggregates; + namespace EventFlow.EventStores { public class SerializedEvent @@ -27,15 +29,18 @@ public class SerializedEvent public string Meta { get; private set; } public string Data { get; private set; } public int AggregateSequenceNumber { get; set; } + public IMetadata Metadata { get; private set; } public SerializedEvent( string meta, string data, - int aggregateSequenceNumber) + int aggregateSequenceNumber, + IMetadata metadata) { Meta = meta; Data = data; AggregateSequenceNumber = aggregateSequenceNumber; + Metadata = metadata; } } } From df6fe85073fcf1de0d5fc451fbe72eea8a7ca57a Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 11:38:23 +0200 Subject: [PATCH 21/47] Updated release notes --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9ecb91eef..71af8796e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -18,6 +18,8 @@ a `GlobalSequenceNumberRange` removed * Breaking: Remove the concept of event caches. If you really need this then implement it by registering a decorator for `IEventStore` + * Breaking: Moved `IDomainEvent.BatchId` to metadata and created + `MetadataKeys.BatchId` to help access it * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate stream. Please consider carefully if you really want to use it. Storage might be cheaper than the historic knowledge within your events From 55fe22971ed8a6a97e149675ca2bd670a19e4576 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 12:07:34 +0200 Subject: [PATCH 22/47] Added unit time stamp to metadata --- RELEASE_NOTES.md | 5 +++ Source/EventFlow/Aggregates/AggregateRoot.cs | 5 ++- Source/EventFlow/Aggregates/IMetadata.cs | 1 + Source/EventFlow/Aggregates/Metadata.cs | 12 +++++++ Source/EventFlow/Aggregates/MetadataKeys.cs | 1 + Source/EventFlow/EventFlow.csproj | 1 + .../Extensions/DateTimeOffsetExtensions.cs | 34 +++++++++++++++++++ 7 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 Source/EventFlow/Extensions/DateTimeOffsetExtensions.cs diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9ecb91eef..5c10866e1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -21,6 +21,11 @@ * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate stream. Please consider carefully if you really want to use it. Storage might be cheaper than the historic knowledge within your events + * New: `IMetadata.TimestampEpoch` contains the Unix timestamp version + of `IMetadata.Timestamp`. Also, an additional metadata key + `timestamp_epoch` is added to events containing the same data. Note, + the `TimestampEpoch` on `IMetadata` handles cases in which the + `timestamp_epoch` is not present by using the existing timestamp * Fixed: `AggregateRoot<>` now reads the aggregate version from domain events applied during aggregate load. This resolves an issue for when an `IEventUpgrader` removed events from the event stream diff --git a/Source/EventFlow/Aggregates/AggregateRoot.cs b/Source/EventFlow/Aggregates/AggregateRoot.cs index faf0586e8..c09c9247e 100644 --- a/Source/EventFlow/Aggregates/AggregateRoot.cs +++ b/Source/EventFlow/Aggregates/AggregateRoot.cs @@ -28,6 +28,7 @@ using System.Threading.Tasks; using EventFlow.EventStores; using EventFlow.Exceptions; +using EventFlow.Extensions; namespace EventFlow.Aggregates { @@ -65,9 +66,11 @@ protected void Emit(TEvent aggregateEvent, IMetadata metadata = null) throw new ArgumentNullException("aggregateEvent"); } + var now = DateTimeOffset.Now; var extraMetadata = new Dictionary { - {MetadataKeys.Timestamp, DateTimeOffset.Now.ToString("o")}, + {MetadataKeys.Timestamp, now.ToString("o")}, + {MetadataKeys.TimestampEpoch, now.ToUnixTime().ToString()}, {MetadataKeys.AggregateSequenceNumber, (Version + 1).ToString()} }; diff --git a/Source/EventFlow/Aggregates/IMetadata.cs b/Source/EventFlow/Aggregates/IMetadata.cs index 04d7c0b6e..9507166ce 100644 --- a/Source/EventFlow/Aggregates/IMetadata.cs +++ b/Source/EventFlow/Aggregates/IMetadata.cs @@ -30,6 +30,7 @@ public interface IMetadata : IReadOnlyDictionary string EventName { get; } int EventVersion { get; } DateTimeOffset Timestamp { get; } + long TimestampEpoch { get; } int AggregateSequenceNumber { get; } IMetadata CloneWith(IEnumerable> keyValuePairs); diff --git a/Source/EventFlow/Aggregates/Metadata.cs b/Source/EventFlow/Aggregates/Metadata.cs index c9b2ce6d5..e1b695b1f 100644 --- a/Source/EventFlow/Aggregates/Metadata.cs +++ b/Source/EventFlow/Aggregates/Metadata.cs @@ -23,6 +23,7 @@ using System; using System.Collections.Generic; using System.Linq; +using EventFlow.Extensions; namespace EventFlow.Aggregates { @@ -46,6 +47,17 @@ public DateTimeOffset Timestamp set { this[MetadataKeys.Timestamp] = value.ToString("O"); } } + public long TimestampEpoch + { + get + { + string timestampEpoch; + return TryGetValue(MetadataKeys.TimestampEpoch, out timestampEpoch) + ? long.Parse(this[MetadataKeys.TimestampEpoch]) + : Timestamp.ToUnixTime(); + } + } + public int AggregateSequenceNumber { get { return int.Parse(this[MetadataKeys.AggregateSequenceNumber]); } diff --git a/Source/EventFlow/Aggregates/MetadataKeys.cs b/Source/EventFlow/Aggregates/MetadataKeys.cs index 64d18cfb6..2300ed32d 100644 --- a/Source/EventFlow/Aggregates/MetadataKeys.cs +++ b/Source/EventFlow/Aggregates/MetadataKeys.cs @@ -27,6 +27,7 @@ public sealed class MetadataKeys public const string EventName = "event_name"; public const string EventVersion = "event_version"; public const string Timestamp = "timestamp"; + public const string TimestampEpoch = "timestamp_epoch"; public const string AggregateSequenceNumber = "aggregate_sequence_number"; } } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index b75187f1e..d284f2ede 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -104,6 +104,7 @@ + diff --git a/Source/EventFlow/Extensions/DateTimeOffsetExtensions.cs b/Source/EventFlow/Extensions/DateTimeOffsetExtensions.cs new file mode 100644 index 000000000..fa67fba02 --- /dev/null +++ b/Source/EventFlow/Extensions/DateTimeOffsetExtensions.cs @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; + +namespace EventFlow.Extensions +{ + public static class DateTimeOffsetExtensions + { + public static long ToUnixTime(this DateTimeOffset dateTimeOffset) + { + return Convert.ToInt64((dateTimeOffset.UtcDateTime - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds); + } + } +} From b86272cf8d78670ddfc954a4fb75c4f0591dc198 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 15:21:28 +0200 Subject: [PATCH 23/47] Remove aggregate name from committed event --- Source/EventFlow/EventStores/Files/FilesEventStore.cs | 2 -- Source/EventFlow/EventStores/ICommittedDomainEvent.cs | 1 - Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index 79cba3f1b..ba2699ada 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -42,7 +42,6 @@ public class FileEventData : ICommittedDomainEvent { public Guid BatchId { get; set; } public string AggregateId { get; set; } - public string AggregateName { get; set; } public string Data { get; set; } public string Metadata { get; set; } public int AggregateSequenceNumber { get; set; } @@ -86,7 +85,6 @@ protected override async Task> Commit var fileEventData = new FileEventData { AggregateId = id.Value, - AggregateName = aggregateType.Name, AggregateSequenceNumber = serializedEvent.AggregateSequenceNumber, BatchId = batchId, Data = serializedEvent.Data, diff --git a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs index 738fcd223..72ef60048 100644 --- a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs +++ b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs @@ -28,7 +28,6 @@ public interface ICommittedDomainEvent { Guid BatchId { get; set; } string AggregateId { get; set; } - string AggregateName { get; set; } string Data { get; set; } string Metadata { get; set; } int AggregateSequenceNumber { get; set; } diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 29c0a5952..5f9782253 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -44,7 +44,7 @@ private class InMemoryCommittedDomainEvent : ICommittedDomainEvent { public Guid BatchId { get; set; } public string AggregateId { get; set; } - public string AggregateName { get; set; } + public string AggregateName { private get; set; } public string Data { get; set; } public string Metadata { get; set; } public int AggregateSequenceNumber { get; set; } From ae5d49604968b6de56e878bb0b5da1c331a52022 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 15:27:46 +0200 Subject: [PATCH 24/47] Cleanup of aggregate name and batch id --- Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs | 3 +-- Source/EventFlow/Aggregates/AggregateRoot.cs | 3 ++- Source/EventFlow/Aggregates/MetadataKeys.cs | 1 + Source/EventFlow/EventStores/Files/FilesEventStore.cs | 3 --- Source/EventFlow/EventStores/ICommittedDomainEvent.cs | 3 --- Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs | 5 +---- 6 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs index a837a4a83..b81f1add3 100644 --- a/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs +++ b/Source/EventFlow.EventStores.MsSql/MssqlEventStore.cs @@ -72,12 +72,11 @@ protected override async Task> Commit } var aggregateType = typeof(TAggregate); - var aggregateName = aggregateType.Name.Replace("Aggregate", string.Empty); var eventDataModels = serializedEvents .Select((e, i) => new EventDataModel { AggregateId = id.Value, - AggregateName = aggregateName, + AggregateName = e.Metadata[MetadataKeys.AggregateName], BatchId = Guid.Parse(e.Metadata[MetadataKeys.BatchId]), Data = e.Data, Metadata = e.Meta, diff --git a/Source/EventFlow/Aggregates/AggregateRoot.cs b/Source/EventFlow/Aggregates/AggregateRoot.cs index c09c9247e..7dff46a53 100644 --- a/Source/EventFlow/Aggregates/AggregateRoot.cs +++ b/Source/EventFlow/Aggregates/AggregateRoot.cs @@ -71,7 +71,8 @@ protected void Emit(TEvent aggregateEvent, IMetadata metadata = null) { {MetadataKeys.Timestamp, now.ToString("o")}, {MetadataKeys.TimestampEpoch, now.ToUnixTime().ToString()}, - {MetadataKeys.AggregateSequenceNumber, (Version + 1).ToString()} + {MetadataKeys.AggregateSequenceNumber, (Version + 1).ToString()}, + {MetadataKeys.AggregateName, GetType().Name.Replace("Aggregate", string.Empty)}, }; metadata = metadata == null diff --git a/Source/EventFlow/Aggregates/MetadataKeys.cs b/Source/EventFlow/Aggregates/MetadataKeys.cs index 09a1f5e7e..5d6d14927 100644 --- a/Source/EventFlow/Aggregates/MetadataKeys.cs +++ b/Source/EventFlow/Aggregates/MetadataKeys.cs @@ -30,5 +30,6 @@ public sealed class MetadataKeys public const string Timestamp = "timestamp"; public const string TimestampEpoch = "timestamp_epoch"; public const string AggregateSequenceNumber = "aggregate_sequence_number"; + public const string AggregateName = "aggregate_name"; } } diff --git a/Source/EventFlow/EventStores/Files/FilesEventStore.cs b/Source/EventFlow/EventStores/Files/FilesEventStore.cs index ba2699ada..07d5a3773 100644 --- a/Source/EventFlow/EventStores/Files/FilesEventStore.cs +++ b/Source/EventFlow/EventStores/Files/FilesEventStore.cs @@ -40,7 +40,6 @@ public class FilesEventStore : EventStore public class FileEventData : ICommittedDomainEvent { - public Guid BatchId { get; set; } public string AggregateId { get; set; } public string Data { get; set; } public string Metadata { get; set; } @@ -69,7 +68,6 @@ protected override async Task> Commit using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { var aggregateType = typeof (TAggregate); - var batchId = Guid.NewGuid(); var committedDomainEvents = new List(); var aggregatePath = GetAggregatePath(aggregateType, id); @@ -86,7 +84,6 @@ protected override async Task> Commit { AggregateId = id.Value, AggregateSequenceNumber = serializedEvent.AggregateSequenceNumber, - BatchId = batchId, Data = serializedEvent.Data, Metadata = serializedEvent.Meta, }; diff --git a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs index 72ef60048..9ac27c087 100644 --- a/Source/EventFlow/EventStores/ICommittedDomainEvent.cs +++ b/Source/EventFlow/EventStores/ICommittedDomainEvent.cs @@ -20,13 +20,10 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; - namespace EventFlow.EventStores { public interface ICommittedDomainEvent { - Guid BatchId { get; set; } string AggregateId { get; set; } string Data { get; set; } string Metadata { get; set; } diff --git a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs index 5f9782253..87756287e 100644 --- a/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs +++ b/Source/EventFlow/EventStores/InMemory/InMemoryEventStore.cs @@ -42,7 +42,6 @@ public class InMemoryEventStore : EventStore, IDisposable private class InMemoryCommittedDomainEvent : ICommittedDomainEvent { - public Guid BatchId { get; set; } public string AggregateId { get; set; } public string AggregateName { private get; set; } public string Data { get; set; } @@ -84,7 +83,6 @@ protected async override Task> Commit using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { var globalCount = _eventStore.Values.SelectMany(e => e).Count(); - var batchId = Guid.NewGuid(); List committedDomainEvents; if (_eventStore.ContainsKey(id.Value)) @@ -103,9 +101,8 @@ protected async override Task> Commit var committedDomainEvent = (ICommittedDomainEvent) new InMemoryCommittedDomainEvent { AggregateId = id.Value, - AggregateName = typeof (TAggregate).Name, + AggregateName = e.Metadata[MetadataKeys.AggregateName], AggregateSequenceNumber = e.AggregateSequenceNumber, - BatchId = batchId, Data = e.Data, Metadata = e.Meta, }; From ff4dd223fe928a90654d569c71622d1ef657682d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 15:35:13 +0200 Subject: [PATCH 25/47] Wrote about aggregate sequence numbers --- Documentation/EventUpgrade.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/EventUpgrade.md b/Documentation/EventUpgrade.md index 96b79deb8..5ed1f4b48 100644 --- a/Documentation/EventUpgrade.md +++ b/Documentation/EventUpgrade.md @@ -16,6 +16,10 @@ two upgraders, one upgrade a event from V1 to V2 and then another upgrading V2 to V3. EventFlow orders the event upgraders by name before starting the event upgrade. +**Be careful** if working with event upgraders that return zero or more than one +event, as this have an influence on the aggregate version and you need to make +sure that the aggregate sequence number on upgraded events have a valid value. + ## Example - removing a damaged event To remove an event, simply check and only return the event if its no the event From 42650a219077a3f7490143b9c4da9b8d10a39531 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 15:41:00 +0200 Subject: [PATCH 26/47] Minor log enhancements --- Source/EventFlow/ReadStores/ReadModelPopulator.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 2e97b0c60..1ea2474ab 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -83,10 +83,16 @@ public async Task PopulateAsync(CancellationToken cancellationToken) long totalEvents = 0; long relevantEvents = 0; long currentPosition = 0; + const long pageSize = 100; while (true) { - var allEventsPage = await _eventStore.LoadAllEventsAsync(currentPosition, 100, cancellationToken).ConfigureAwait(false); + _log.Verbose( + "Loading events starting from {0} and the next {1} for populating '{2}'", + currentPosition, + pageSize, + readModelType.Name); + var allEventsPage = await _eventStore.LoadAllEventsAsync(currentPosition, pageSize, cancellationToken).ConfigureAwait(false); totalEvents += allEventsPage.DomainEvents.Count; currentPosition += allEventsPage.NextPosition; @@ -103,7 +109,6 @@ public async Task PopulateAsync(CancellationToken cancellationToken) if (!domainEvents.Any()) { - continue; } From 65afb1e7d8d21c82517cc6f66c95f4757517ebf9 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Mon, 25 May 2015 16:21:55 +0200 Subject: [PATCH 27/47] Test populator --- Source/EventFlow.Tests/EventFlow.Tests.csproj | 1 + .../ReadStores/ReadModelPopulatorTests.cs | 148 ++++++++++++++++++ .../Configuration/EventFlowConfiguration.cs | 2 + .../Configuration/IEventFlowConfiguration.cs | 1 + Source/EventFlow/EventFlowOptions.cs | 6 + .../ReadStores/ReadModelPopulator.cs | 15 +- 6 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 0a604e9d1..0c3883607 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -79,6 +79,7 @@ + diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs new file mode 100644 index 000000000..96c44597c --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs @@ -0,0 +1,148 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Configuration; +using EventFlow.EventStores; +using EventFlow.ReadStores; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates.Test; +using EventFlow.TestHelpers.Aggregates.Test.Events; +using Moq; +using NUnit.Framework; +using Ploeh.AutoFixture; + +namespace EventFlow.Tests.UnitTests.ReadStores +{ + [Timeout(5000)] + public class ReadModelPopulatorTests : TestsFor + { + public class TestReadModel : IReadModel, + IAmReadModelFor + { + public void Apply(IReadModelContext context, IDomainEvent e) + { + } + } + + private Mock _readModelStoreMock; + private Mock _eventFlowConfigurationMock; + private Mock _eventStoreMock; + private List _eventStoreData; + + [SetUp] + public void SetUp() + { + _eventStoreMock = InjectMock(); + _eventStoreData = null; + _readModelStoreMock = new Mock(); + _eventFlowConfigurationMock = InjectMock(); + + Fixture.Inject>(new []{ _readModelStoreMock.Object }); + + _eventFlowConfigurationMock + .Setup(c => c.PopulateReadModelEventPageSize) + .Returns(3); + + _eventStoreMock + .Setup(s => s.LoadAllEventsAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((s, p, c) => Task.FromResult(GetEvents(s, p))); + } + + [Test] + public async Task PurgeIsCalled() + { + // Act + await Sut.PurgeAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + _readModelStoreMock.Verify(s => s.PurgeAsync(It.IsAny()), Times.Once); + } + + [Test] + public async Task PopulateCallsApplyDomainEvents() + { + // Arrange + ArrangeEventStore(Many(6)); + + // Act + await Sut.PopulateAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + _readModelStoreMock.Verify( + s => s.ApplyDomainEventsAsync( + It.Is>(l => l.Count == 3), + It.IsAny()), + Times.Exactly(2)); + } + + [Test] + public async Task UnwantedEventsAreFiltered() + { + // Arrange + var events = new IAggregateEvent[] + { + A(), + A(), + A(), + }; + ArrangeEventStore(events); + + // Act + await Sut.PopulateAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + _readModelStoreMock + .Verify( + s => s.ApplyDomainEventsAsync( + It.Is>(l => l.Count == 2 && l.All(e => e.EventType == typeof(PingEvent))), + It.IsAny()), + Times.Once); + } + + private AllEventsPage GetEvents(long startPosition, long pageSize) + { + var events = _eventStoreData + .Skip((int) Math.Max(startPosition - 1, 0)) + .Take((int)pageSize) + .ToList(); + var nextPosition = Math.Min(Math.Max(startPosition, 1) + pageSize, _eventStoreData.Count + 1); + + return new AllEventsPage(nextPosition, events); + } + + private void ArrangeEventStore(IEnumerable aggregateEvents) + { + ArrangeEventStore(aggregateEvents.Select(e => ToDomainEvent(e))); + } + + private void ArrangeEventStore(IEnumerable domainEvents) + { + _eventStoreData = domainEvents.ToList(); + } + } +} diff --git a/Source/EventFlow/Configuration/EventFlowConfiguration.cs b/Source/EventFlow/Configuration/EventFlowConfiguration.cs index e890de664..e71b45392 100644 --- a/Source/EventFlow/Configuration/EventFlowConfiguration.cs +++ b/Source/EventFlow/Configuration/EventFlowConfiguration.cs @@ -26,11 +26,13 @@ namespace EventFlow.Configuration { public class EventFlowConfiguration : IEventFlowConfiguration { + public long PopulateReadModelEventPageSize { get; set; } public int NumberOfRetriesOnOptimisticConcurrencyExceptions { get; set; } public TimeSpan DelayBeforeRetryOnOptimisticConcurrencyExceptions { get; set; } public EventFlowConfiguration() { + PopulateReadModelEventPageSize = 200; NumberOfRetriesOnOptimisticConcurrencyExceptions = 4; DelayBeforeRetryOnOptimisticConcurrencyExceptions = TimeSpan.FromMilliseconds(100); } diff --git a/Source/EventFlow/Configuration/IEventFlowConfiguration.cs b/Source/EventFlow/Configuration/IEventFlowConfiguration.cs index e9a4a7790..2cc828ad3 100644 --- a/Source/EventFlow/Configuration/IEventFlowConfiguration.cs +++ b/Source/EventFlow/Configuration/IEventFlowConfiguration.cs @@ -26,6 +26,7 @@ namespace EventFlow.Configuration { public interface IEventFlowConfiguration { + long PopulateReadModelEventPageSize { get; } int NumberOfRetriesOnOptimisticConcurrencyExceptions { get; } TimeSpan DelayBeforeRetryOnOptimisticConcurrencyExceptions { get; } } diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index 5f82d5c19..e4d96ace8 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -54,6 +54,12 @@ public EventFlowOptions ConfigureOptimisticConcurrentcyRetry(int retries, TimeSp return this; } + public EventFlowOptions Configure(Action configure) + { + configure(_eventFlowConfiguration); + return this; + } + public EventFlowOptions AddEvents(IEnumerable aggregateEventTypes) { foreach (var aggregateEventType in aggregateEventTypes) diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 1ea2474ab..e4283d942 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -26,6 +26,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventFlow.Configuration; using EventFlow.Core; using EventFlow.EventStores; using EventFlow.Logs; @@ -35,15 +36,18 @@ namespace EventFlow.ReadStores public class ReadModelPopulator : IReadModelPopulator { private readonly ILog _log; + private readonly IEventFlowConfiguration _configuration; private readonly IEventStore _eventStore; private readonly IReadOnlyCollection _readModelStores; public ReadModelPopulator( ILog log, + IEventFlowConfiguration configuration, IEventStore eventStore, IEnumerable readModelStores) { _log = log; + _configuration = configuration; _eventStore = eventStore; _readModelStores = readModelStores.ToList(); } @@ -83,18 +87,21 @@ public async Task PopulateAsync(CancellationToken cancellationToken) long totalEvents = 0; long relevantEvents = 0; long currentPosition = 0; - const long pageSize = 100; while (true) { _log.Verbose( "Loading events starting from {0} and the next {1} for populating '{2}'", currentPosition, - pageSize, + _configuration.PopulateReadModelEventPageSize, readModelType.Name); - var allEventsPage = await _eventStore.LoadAllEventsAsync(currentPosition, pageSize, cancellationToken).ConfigureAwait(false); + var allEventsPage = await _eventStore.LoadAllEventsAsync( + currentPosition, + _configuration.PopulateReadModelEventPageSize, + cancellationToken) + .ConfigureAwait(false); totalEvents += allEventsPage.DomainEvents.Count; - currentPosition += allEventsPage.NextPosition; + currentPosition = allEventsPage.NextPosition; if (!allEventsPage.DomainEvents.Any()) { From 9180b1d3da1fcb7c431e3f5642d01417d768a608 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 26 May 2015 14:26:26 +0200 Subject: [PATCH 28/47] Clarification on how event upgraders work --- Documentation/EventUpgrade.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Documentation/EventUpgrade.md b/Documentation/EventUpgrade.md index 5ed1f4b48..a9d43a079 100644 --- a/Documentation/EventUpgrade.md +++ b/Documentation/EventUpgrade.md @@ -11,6 +11,11 @@ EventFlow event upgraders are invoked whenever the event stream is loaded from the event store. Each event upgrader receives the entire event stream one event at a time. +A new instance of a event upgrader is created each time an aggregate is loaded. +This enables you to store information from previous events on the upgrader +instance to be used later, e.g. to determine an action to take on a event +or provide additional information for a new event. + Note that the _ordering_ of event upgraders is important as you might implement two upgraders, one upgrade a event from V1 to V2 and then another upgrading V2 to V3. EventFlow orders the event upgraders by name before starting the event From 6ea40d970de39e2ed1b08fe2e253ffe97b92855d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 26 May 2015 20:29:20 +0200 Subject: [PATCH 29/47] Ping --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 473ba600d..f619ba028 100644 --- a/README.md +++ b/README.md @@ -105,3 +105,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + From 52143aa0109943f143655147e734835ac5cd3654 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Tue, 26 May 2015 21:15:08 +0200 Subject: [PATCH 30/47] Updates to read model stores --- RELEASE_NOTES.md | 1 + .../MssqlReadModelStore.cs | 22 ++++--- .../ReadStores/ReadModelFactoryTest.cs | 2 +- .../EventFlow/ReadStores/IReadModelFactory.cs | 6 +- .../ReadStores/IReadModelPopulator.cs | 8 +++ .../EventFlow/ReadStores/IReadModelStore.cs | 11 +++- .../InMemory/InMemoryReadModelStore.cs | 49 +++++++++++----- .../ReadStores/ReadModelPopulator.cs | 57 +++++++++++++++++++ Source/EventFlow/ReadStores/ReadModelStore.cs | 10 ++-- 9 files changed, 130 insertions(+), 36 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index cbfa788df..248874372 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -31,6 +31,7 @@ * Fixed: `AggregateRoot<>` now reads the aggregate version from domain events applied during aggregate load. This resolves an issue for when an `IEventUpgrader` removed events from the event stream + * Fixed: `InMemoryReadModelStore<,>` is now thread safe ### New in 0.7.481 (released 2015-05-22) diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index 33878eb28..1e9e57f97 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -58,16 +58,16 @@ public MssqlReadModelStore( private async Task UpdateReadModelAsync( string id, + bool forceNew, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) { var readModelNameLowerCased = typeof (TReadModel).Name.ToLowerInvariant(); var readModel = await GetByIdAsync(id, cancellationToken).ConfigureAwait(false); - var isNew = false; - if (readModel == null) + var isNew = readModel == null; + if (readModel == null || forceNew) { - isNew = true; readModel = new TReadModel { AggregateId = id, @@ -101,6 +101,15 @@ await _connection.ExecuteAsync( readModel).ConfigureAwait(false); } + public override Task PopulateReadModelAsync( + string id, + IReadOnlyCollection domainEvents, + IReadModelContext readModelContext, + CancellationToken cancellationToken) + { + return UpdateReadModelAsync(id, true, domainEvents, readModelContext, cancellationToken); + } + public override Task GetByIdAsync( string id, CancellationToken cancellationToken) @@ -130,13 +139,10 @@ public override async Task PurgeAsync(CancellationToken cance readModelName); } - protected override Task UpdateReadModelsAsync( - IReadOnlyCollection readModelUpdates, - IReadModelContext readModelContext, - CancellationToken cancellationToken) + protected override Task UpdateReadModelsAsync(IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, CancellationToken cancellationToken) { var updateTasks = readModelUpdates - .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, rmu.DomainEvents, readModelContext, cancellationToken)); + .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, false, rmu.DomainEvents, readModelContext, cancellationToken)); return Task.WhenAll(updateTasks); } } diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTest.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTest.cs index c9bf27b69..fbd200b21 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTest.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTest.cs @@ -148,7 +148,7 @@ public void UpdateReturnsFalseIfNoEventsWasApplied() var appliedAny = Sut.UpdateReadModelAsync( new PingReadModel(), events, - A(), + A(), CancellationToken.None) .Result; diff --git a/Source/EventFlow/ReadStores/IReadModelFactory.cs b/Source/EventFlow/ReadStores/IReadModelFactory.cs index b16aee491..ccf096855 100644 --- a/Source/EventFlow/ReadStores/IReadModelFactory.cs +++ b/Source/EventFlow/ReadStores/IReadModelFactory.cs @@ -43,11 +43,7 @@ Task CreateReadModelAsync( CancellationToken cancellationToken) where TReadModel : IReadModel; - Task UpdateReadModelAsync( - TReadModel readModel, - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - CancellationToken cancellationToken) + Task UpdateReadModelAsync(TReadModel readModel, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) where TReadModel : IReadModel; } } diff --git a/Source/EventFlow/ReadStores/IReadModelPopulator.cs b/Source/EventFlow/ReadStores/IReadModelPopulator.cs index 8fa50d7bd..6fcfc3824 100644 --- a/Source/EventFlow/ReadStores/IReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/IReadModelPopulator.cs @@ -22,6 +22,7 @@ using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates; namespace EventFlow.ReadStores { @@ -38,5 +39,12 @@ Task PopulateAsync(CancellationToken cancellationToken) void Populate(CancellationToken cancellationToken) where TReadModel : IReadModel; + + Task PopulateAggregateReadModelAsync( + TIdentity id, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TReadModel : IReadModel; } } diff --git a/Source/EventFlow/ReadStores/IReadModelStore.cs b/Source/EventFlow/ReadStores/IReadModelStore.cs index 7f4fb56ff..db0d4926d 100644 --- a/Source/EventFlow/ReadStores/IReadModelStore.cs +++ b/Source/EventFlow/ReadStores/IReadModelStore.cs @@ -29,12 +29,19 @@ namespace EventFlow.ReadStores { public interface IReadModelStore { - Task ApplyDomainEventsAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken); + Task ApplyDomainEventsAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken); - Task ApplyDomainEventsAsync(IReadOnlyCollection domainEvents, CancellationToken cancellationToken) + Task ApplyDomainEventsAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) where TReadModelToPopulate : IReadModel; Task PurgeAsync(CancellationToken cancellationToken) where TReadModelToPurge : IReadModel; + + Task PopulateReadModelAsync(string id, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) + where TReadModelToPopulate : IReadModel; } } diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs index 046e22bf6..9f62f99c0 100644 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs @@ -26,8 +26,8 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; +using EventFlow.Core; using EventFlow.Logs; -using EventFlow.Queries; namespace EventFlow.ReadStores.InMemory { @@ -37,6 +37,7 @@ public class InMemoryReadModelStore : where TReadModel : IReadModel, new() where TReadModelLocator : IReadModelLocator { + private readonly AsyncLock _asyncLock = new AsyncLock(); private readonly Dictionary _readModels = new Dictionary(); public InMemoryReadModelStore( @@ -47,24 +48,33 @@ public InMemoryReadModelStore( { } - private Task UpdateReadModelAsync( + private async Task UpdateReadModelAsync( string id, + bool forceNew, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) { - TReadModel readModel; - if (_readModels.ContainsKey(id)) - { - readModel = _readModels[id]; - } - else + using (await _asyncLock.WaitAsync(cancellationToken)) { - readModel = new TReadModel(); - _readModels.Add(id, readModel); - } + TReadModel readModel; + if (_readModels.ContainsKey(id) && !forceNew) + { + readModel = _readModels[id]; + } + else + { + readModel = new TReadModel(); + _readModels[id] = readModel; + } - return ReadModelFactory.UpdateReadModelAsync(readModel, domainEvents, readModelContext, cancellationToken); + await ReadModelFactory.UpdateReadModelAsync( + readModel, + domainEvents, + readModelContext, + cancellationToken) + .ConfigureAwait(false); + } } public TReadModel Get(IIdentity id) @@ -84,8 +94,17 @@ public IEnumerable Find(Predicate predicate) { return _readModels.Values.Where(rm => predicate(rm)); } - - public override Task GetByIdAsync(string id, CancellationToken cancellationToken) + + public override Task PopulateReadModelAsync( + string id, + IReadOnlyCollection domainEvents, + IReadModelContext readModelContext, + CancellationToken cancellationToken) + { + return UpdateReadModelAsync(id, true, domainEvents, readModelContext, cancellationToken); + } + + public override Task GetByIdAsync(string id, CancellationToken cancellationToken) { TReadModel readModel; return _readModels.TryGetValue(id, out readModel) @@ -108,7 +127,7 @@ protected override Task UpdateReadModelsAsync( CancellationToken cancellationToken) { var updateTasks = readModelUpdates - .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, rmu.DomainEvents, readModelContext, cancellationToken)); + .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, false, rmu.DomainEvents, readModelContext, cancellationToken)); return Task.WhenAll(updateTasks); } } diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index e4283d942..869dff494 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -26,6 +26,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates; using EventFlow.Configuration; using EventFlow.Core; using EventFlow.EventStores; @@ -133,6 +134,62 @@ public async Task PopulateAsync(CancellationToken cancellationToken) relevantEvents); } + public async Task PopulateAggregateReadModelAsync( + TIdentity id, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TReadModel : IReadModel + { + var readModelType = typeof (TReadModel); + var iAmReadModelForInterfaceTypes = readModelType + .GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IAmReadModelFor<,,>)) + .ToList(); + var aggregateTypes = iAmReadModelForInterfaceTypes + .Select(i => i.GetGenericArguments()[0]) + .Distinct() + .ToList(); + + if (!aggregateTypes.Any()) + { + throw new ArgumentException(string.Format( + "Read model type '{0}' does not implement 'IAmReadModelFor<>'", + readModelType.Name)); + } + if (aggregateTypes.Count != 1) + { + throw new ArgumentException(string.Format( + "Use 'PopulateAsync' to populate read models than registers events from more than one aggregate. '{0}' registers to these: {1}", + readModelType.Name, + string.Join(", ", aggregateTypes.Select(t => t.Name)))); + } + if (aggregateTypes.Single() != typeof (TReadModel)) + { + throw new ArgumentException(string.Format( + "Read model '{0}' registers to aggregate '{1}', but you supplied '{2}' as the generic argument", + readModelType.Name, + aggregateTypes.Single().Name, + typeof(TAggregate).Name)); + } + + var domainEvens = await _eventStore.LoadEventsAsync( + id, + cancellationToken) + .ConfigureAwait(false); + + var readModelContext = new ReadModelContext(); + + var populateTasks = _readModelStores + .Select(s => s.PopulateReadModelAsync( + id.Value, + domainEvens, + readModelContext, + cancellationToken)); + + await Task.WhenAll(populateTasks).ConfigureAwait(false); + } + public void Populate(CancellationToken cancellationToken) where TReadModel : IReadModel { diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index 3e8534aa9..571ca7671 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -77,13 +77,13 @@ public Task ApplyDomainEventsAsync( public abstract Task PurgeAsync(CancellationToken cancellationToken) where TReadModelToPurge : IReadModel; - public abstract Task GetByIdAsync( + public abstract Task PopulateReadModelAsync(string id, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) + where TReadModelToPopulate : IReadModel; + + public abstract Task GetByIdAsync( string id, CancellationToken cancellationToken); - protected abstract Task UpdateReadModelsAsync( - IReadOnlyCollection readModelUpdates, - IReadModelContext readModelContext, - CancellationToken cancellationToken); + protected abstract Task UpdateReadModelsAsync(IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, CancellationToken cancellationToken); } } From 5e2c40d9be1bbb14234f6e4108d132fcf3fca0ac Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 27 May 2015 05:40:18 +0200 Subject: [PATCH 31/47] Class rename --- Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs | 6 +++--- Source/EventFlow.Tests/EventFlow.Tests.csproj | 2 +- ...elFactoryTest.cs => ReadModelDomainEventApplierTests.cs} | 2 +- Source/EventFlow/EventFlow.csproj | 4 ++-- Source/EventFlow/EventFlowOptions.cs | 2 +- ...IReadModelFactory.cs => IReadModelDomainEventApplier.cs} | 2 +- .../EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs | 6 +++--- .../{ReadModelFactory.cs => ReadModelDomainEventApplier.cs} | 2 +- Source/EventFlow/ReadStores/ReadModelStore.cs | 6 +++--- 9 files changed, 16 insertions(+), 16 deletions(-) rename Source/EventFlow.Tests/UnitTests/ReadStores/{ReadModelFactoryTest.cs => ReadModelDomainEventApplierTests.cs} (98%) rename Source/EventFlow/ReadStores/{IReadModelFactory.cs => IReadModelDomainEventApplier.cs} (97%) rename Source/EventFlow/ReadStores/{ReadModelFactory.cs => ReadModelDomainEventApplier.cs} (98%) diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index 1e9e57f97..9d24e65b7 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -45,11 +45,11 @@ public class MssqlReadModelStore : public MssqlReadModelStore( ILog log, TReadModelLocator readModelLocator, - IReadModelFactory readModelFactory, + IReadModelDomainEventApplier readModelDomainEventApplier, IMsSqlConnection connection, IQueryProcessor queryProcessor, IReadModelSqlGenerator readModelSqlGenerator) - : base(log, readModelLocator, readModelFactory) + : base(log, readModelLocator, readModelDomainEventApplier) { _connection = connection; _queryProcessor = queryProcessor; @@ -75,7 +75,7 @@ private async Task UpdateReadModelAsync( }; } - var appliedAny = await ReadModelFactory.UpdateReadModelAsync( + var appliedAny = await ReadModelDomainEventApplier.UpdateReadModelAsync( readModel, domainEvents, readModelContext, diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 0c3883607..1e0366402 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -67,7 +67,7 @@ - + diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTest.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs similarity index 98% rename from Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTest.cs rename to Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs index fbd200b21..a744247ff 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTest.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs @@ -31,7 +31,7 @@ namespace EventFlow.Tests.UnitTests.ReadStores { - public class ReadModelFactoryTest : TestsFor + public class ReadModelDomainEventApplierTests : TestsFor { public class PingReadModel : IReadModel, IAmReadModelFor diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index c85670bd1..6103535ab 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -124,11 +124,11 @@ - + - + diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index e4d96ace8..2145e4128 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -110,7 +110,7 @@ public IRootResolver CreateResolver(bool validateRegistrations = true) RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); RegisterIfMissing(services); - RegisterIfMissing(services); + RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); diff --git a/Source/EventFlow/ReadStores/IReadModelFactory.cs b/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs similarity index 97% rename from Source/EventFlow/ReadStores/IReadModelFactory.cs rename to Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs index ccf096855..87033125c 100644 --- a/Source/EventFlow/ReadStores/IReadModelFactory.cs +++ b/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs @@ -28,7 +28,7 @@ namespace EventFlow.ReadStores { - public interface IReadModelFactory + public interface IReadModelDomainEventApplier { Task CreateReadModelAsync( IReadOnlyCollection domainEvents, diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs index 9f62f99c0..a4092f2b1 100644 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs @@ -43,8 +43,8 @@ public class InMemoryReadModelStore : public InMemoryReadModelStore( ILog log, TReadModelLocator readModelLocator, - IReadModelFactory readModelFactory) - : base(log, readModelLocator, readModelFactory) + IReadModelDomainEventApplier readModelDomainEventApplier) + : base(log, readModelLocator, readModelDomainEventApplier) { } @@ -68,7 +68,7 @@ private async Task UpdateReadModelAsync( _readModels[id] = readModel; } - await ReadModelFactory.UpdateReadModelAsync( + await ReadModelDomainEventApplier.UpdateReadModelAsync( readModel, domainEvents, readModelContext, diff --git a/Source/EventFlow/ReadStores/ReadModelFactory.cs b/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs similarity index 98% rename from Source/EventFlow/ReadStores/ReadModelFactory.cs rename to Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs index c6426fd33..219edc569 100644 --- a/Source/EventFlow/ReadStores/ReadModelFactory.cs +++ b/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs @@ -29,7 +29,7 @@ namespace EventFlow.ReadStores { - public class ReadModelFactory : IReadModelFactory + public class ReadModelDomainEventApplier : IReadModelDomainEventApplier { private static readonly ConcurrentDictionary>> ApplyMethods = new ConcurrentDictionary>>(); diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index 571ca7671..b8f835f8b 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -35,16 +35,16 @@ public abstract class ReadModelStore : IReadModel { protected ILog Log { get; private set; } protected TReadModelLocator ReadModelLocator { get; private set; } - protected IReadModelFactory ReadModelFactory { get; private set; } + protected IReadModelDomainEventApplier ReadModelDomainEventApplier { get; private set; } protected ReadModelStore( ILog log, TReadModelLocator readModelLocator, - IReadModelFactory readModelFactory) + IReadModelDomainEventApplier readModelDomainEventApplier) { Log = log; ReadModelLocator = readModelLocator; - ReadModelFactory = readModelFactory; + ReadModelDomainEventApplier = readModelDomainEventApplier; } public virtual Task ApplyDomainEventsAsync( From 9e8880683a223f44e9d9cd280e1a9a65e32d3cff Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 27 May 2015 05:46:51 +0200 Subject: [PATCH 32/47] Introduced the concept of multi-aggregate read models --- .../MssqlReadModelStore.cs | 2 +- Source/EventFlow/EventFlow.csproj | 3 +- .../InMemory/InMemoryReadModelStore.cs | 2 +- .../MultiAggregateReadModelStore.cs | 82 +++++++++++++++++++ Source/EventFlow/ReadStores/ReadModelStore.cs | 48 +++-------- 5 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index 9d24e65b7..6fbf331be 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -33,7 +33,7 @@ namespace EventFlow.ReadStores.MsSql { public class MssqlReadModelStore : - ReadModelStore, + MultiAggregateReadModelStore, IMssqlReadModelStore where TReadModel : IMssqlReadModel, new() where TReadModelLocator : IReadModelLocator diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 6103535ab..e41afcf51 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -130,6 +130,7 @@ + @@ -149,7 +150,7 @@ - + diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs index a4092f2b1..8564e7bd7 100644 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs @@ -32,7 +32,7 @@ namespace EventFlow.ReadStores.InMemory { public class InMemoryReadModelStore : - ReadModelStore, + MultiAggregateReadModelStore, IInMemoryReadModelStore where TReadModel : IReadModel, new() where TReadModelLocator : IReadModelLocator diff --git a/Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs b/Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs new file mode 100644 index 000000000..5677b7200 --- /dev/null +++ b/Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs @@ -0,0 +1,82 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Logs; + +namespace EventFlow.ReadStores +{ + public abstract class MultiAggregateReadModelStore : ReadModelStore + where TReadModel : IReadModel + where TReadModelLocator : IReadModelLocator + { + protected TReadModelLocator ReadModelLocator { get; private set; } + + protected MultiAggregateReadModelStore( + ILog log, + TReadModelLocator readModelLocator, + IReadModelDomainEventApplier readModelDomainEventApplier) + : base (log, readModelDomainEventApplier) + { + ReadModelLocator = readModelLocator; + } + + public override Task ApplyDomainEventsAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + var readModelUpdates = ( + from de in domainEvents + let readModelIds = ReadModelLocator.GetReadModelIds(de) + from rid in readModelIds + group de by rid into g + select new ReadModelUpdate(g.Key, g.ToList()) + ).ToList(); + + var readModelContext = new ReadModelContext(); + + return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); + } + + public override Task ApplyDomainEventsAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + return (typeof (TReadModel) == typeof (TReadModelToPopulate)) + ? ApplyDomainEventsAsync(domainEvents, cancellationToken) + : Task.FromResult(0); + } + + public abstract Task GetByIdAsync( + string id, + CancellationToken cancellationToken); + + protected abstract Task UpdateReadModelsAsync( + IReadOnlyCollection readModelUpdates, + IReadModelContext readModelContext, + CancellationToken cancellationToken); + } +} diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index b8f835f8b..8fab0d74b 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -21,7 +21,6 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -29,61 +28,38 @@ namespace EventFlow.ReadStores { - public abstract class ReadModelStore : IReadModelStore + public abstract class ReadModelStore : IReadModelStore where TReadModel : IReadModel - where TReadModelLocator : IReadModelLocator { protected ILog Log { get; private set; } - protected TReadModelLocator ReadModelLocator { get; private set; } protected IReadModelDomainEventApplier ReadModelDomainEventApplier { get; private set; } protected ReadModelStore( ILog log, - TReadModelLocator readModelLocator, IReadModelDomainEventApplier readModelDomainEventApplier) { Log = log; - ReadModelLocator = readModelLocator; ReadModelDomainEventApplier = readModelDomainEventApplier; } - public virtual Task ApplyDomainEventsAsync( + public abstract Task ApplyDomainEventsAsync( IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - { - var readModelUpdates = ( - from de in domainEvents - let readModelIds = ReadModelLocator.GetReadModelIds(de) - from rid in readModelIds - group de by rid into g - select new ReadModelUpdate(g.Key, g.ToList()) - ).ToList(); + CancellationToken cancellationToken); - var readModelContext = new ReadModelContext(); - - return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); - } - - public Task ApplyDomainEventsAsync( + public abstract Task ApplyDomainEventsAsync( IReadOnlyCollection domainEvents, CancellationToken cancellationToken) - where TReadModelToPopulate : IReadModel - { - return (typeof (TReadModel) == typeof (TReadModelToPopulate)) - ? ApplyDomainEventsAsync(domainEvents, cancellationToken) - : Task.FromResult(0); - } + where TReadModelToPopulate : IReadModel; - public abstract Task PurgeAsync(CancellationToken cancellationToken) + public abstract Task PurgeAsync( + CancellationToken cancellationToken) where TReadModelToPurge : IReadModel; - public abstract Task PopulateReadModelAsync(string id, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) + public abstract Task PopulateReadModelAsync( + string id, + IReadOnlyCollection domainEvents, + IReadModelContext readModelContext, + CancellationToken cancellationToken) where TReadModelToPopulate : IReadModel; - - public abstract Task GetByIdAsync( - string id, - CancellationToken cancellationToken); - - protected abstract Task UpdateReadModelsAsync(IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, CancellationToken cancellationToken); } } From 74a59aa65be0c82738e82af1db94fbe11f4ea452 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 27 May 2015 05:50:24 +0200 Subject: [PATCH 33/47] Started on the single aggregate read model store --- Source/EventFlow/EventFlow.csproj | 1 + .../SingleAggregateReadModelStore.cs | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index e41afcf51..1671f7a8b 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -132,6 +132,7 @@ + diff --git a/Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs b/Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs new file mode 100644 index 000000000..166a9d449 --- /dev/null +++ b/Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs @@ -0,0 +1,70 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Logs; + +namespace EventFlow.ReadStores +{ + public abstract class SingleAggregateReadModelStore : + ReadModelStore + where TReadModel : IReadModel + { + protected SingleAggregateReadModelStore( + ILog log, + IReadModelDomainEventApplier readModelDomainEventApplier) + : base(log, readModelDomainEventApplier) + { + } + + public override Task ApplyDomainEventsAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + public override Task ApplyDomainEventsAsync( + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + public override Task PurgeAsync(CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + + public override Task PopulateReadModelAsync( + string id, + IReadOnlyCollection domainEvents, + IReadModelContext readModelContext, + CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } +} From 10175e1429c409dc1d973a19d77a9e470b9c49a8 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 27 May 2015 20:18:03 +0200 Subject: [PATCH 34/47] Started on another attempt at populating read models --- Source/EventFlow/EventFlow.csproj | 6 + .../IReadModelDomainEventApplier.cs | 6 +- .../ReadStores/V2/IReadModelStoreV2.cs | 45 +++++++ .../ReadStores/V2/InMemoryReadStoreV2.cs | 83 ++++++++++++ .../ReadStores/V2/ReadModelEnvelope.cs | 49 +++++++ .../ReadStores/V2/ReadModelStoreV2.cs | 54 ++++++++ .../ReadStores/V2/ReadStoreManagerV2.cs | 127 ++++++++++++++++++ .../V2/SingleAggregateReadStoreManager.cs | 49 +++++++ 8 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs create mode 100644 Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs create mode 100644 Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs create mode 100644 Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs create mode 100644 Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs create mode 100644 Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 1671f7a8b..3183a8fbc 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -121,18 +121,24 @@ + + + + + + diff --git a/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs b/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs index 87033125c..93cdd9f60 100644 --- a/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs +++ b/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs @@ -43,7 +43,11 @@ Task CreateReadModelAsync( CancellationToken cancellationToken) where TReadModel : IReadModel; - Task UpdateReadModelAsync(TReadModel readModel, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) + Task UpdateReadModelAsync( + TReadModel readModel, + IReadOnlyCollection domainEvents, + IReadModelContext readModelContext, + CancellationToken cancellationToken) where TReadModel : IReadModel; } } diff --git a/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs new file mode 100644 index 000000000..c3a51d4b2 --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs @@ -0,0 +1,45 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EventFlow.ReadStores.V2 +{ + public interface IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + { + Task DeleteAsync( + string id, + CancellationToken cancellationToken); + + Task DeleteAllAsync( + CancellationToken cancellationToken); + + Task UpdateAsync( + string id, + IReadModelContext readModelContext, + Func, CancellationToken, Task>> updateReadModel, + CancellationToken cancellationToken); + } +} diff --git a/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs b/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs new file mode 100644 index 000000000..983365a6c --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs @@ -0,0 +1,83 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Core; +using EventFlow.Logs; + +namespace EventFlow.ReadStores.V2 +{ + public class InMemoryReadStoreV2 : ReadModelStoreV2 + where TReadModel : class, IReadModel, new() + { + private readonly Dictionary> _readModels = new Dictionary>(); + private readonly AsyncLock _asyncLock = new AsyncLock(); + + public InMemoryReadStoreV2( + ILog log) : base(log) + { + } + + public override async Task DeleteAsync( + string id, + CancellationToken cancellationToken) + { + using (await _asyncLock.WaitAsync(cancellationToken)) + { + if (_readModels.ContainsKey(id)) + { + _readModels.Remove(id); + } + } + } + + public async override Task DeleteAllAsync( + CancellationToken cancellationToken) + { + using (await _asyncLock.WaitAsync(cancellationToken)) + { + _readModels.Clear(); + } + } + + public override async Task UpdateAsync( + string id, + IReadModelContext readModelContext, + Func, CancellationToken, Task>> updateReadModel, + CancellationToken cancellationToken) + { + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + { + ReadModelEnvelope readModelEnvelope; + _readModels.TryGetValue(id, out readModelEnvelope); + + readModelEnvelope = await updateReadModel(readModelContext, readModelEnvelope, cancellationToken).ConfigureAwait(false); + + _readModels[id] = readModelEnvelope; + } + } + } +} diff --git a/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs b/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs new file mode 100644 index 000000000..7b8a442e9 --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs @@ -0,0 +1,49 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +namespace EventFlow.ReadStores.V2 +{ + public class ReadModelEnvelope + where TReadModel : class, IReadModel, new() + { + public static ReadModelEnvelope With(TReadModel readModel) + { + return new ReadModelEnvelope(readModel, null); + } + + public static ReadModelEnvelope With(TReadModel readModel, long version) + { + return new ReadModelEnvelope(readModel, version); + } + + public TReadModel ReadModel { get; private set; } + public long? Version { get; private set; } + + private ReadModelEnvelope( + TReadModel readModel, + long? version) + { + ReadModel = readModel; + Version = version; + } + } +} diff --git a/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs new file mode 100644 index 000000000..50d439a85 --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs @@ -0,0 +1,54 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Logs; + +namespace EventFlow.ReadStores.V2 +{ + public abstract class ReadModelStoreV2 : IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + { + protected ILog Log { get; private set; } + + protected ReadModelStoreV2( + ILog log) + { + Log = log; + } + + public abstract Task DeleteAsync( + string id, + CancellationToken cancellationToken); + + public abstract Task DeleteAllAsync( + CancellationToken cancellationToken); + + public abstract Task UpdateAsync( + string id, + IReadModelContext readModelContext, + Func, CancellationToken, Task>> updateReadModel, + CancellationToken cancellationToken); + } +} diff --git a/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs b/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs new file mode 100644 index 000000000..d2533824f --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs @@ -0,0 +1,127 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Logs; + +namespace EventFlow.ReadStores.V2 +{ + public abstract class ReadStoreManagerV2 : IReadStoreManager + where TReadModelStore : IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + { + // ReSharper disable StaticMemberInGenericType + private static readonly Type ReadModelType = typeof(TReadModel); + private static readonly ISet AggregateTypes; + private static readonly ISet DomainEventTypes; + // ReSharper enable StaticMemberInGenericType + + protected ILog Log { get; private set; } + protected TReadModelStore ReadModelStore { get; private set; } + protected IReadModelDomainEventApplier ReadModelDomainEventApplier { get; private set; } + + protected ISet GetAggregateTypes() { return AggregateTypes; } + protected ISet GetDomainEventTypes() { return DomainEventTypes; } + + static ReadStoreManagerV2() + { + var iAmReadModelForInterfaceTypes = ReadModelType + .GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IAmReadModelFor<,,>)) + .ToList(); + if (!iAmReadModelForInterfaceTypes.Any()) + { + throw new ArgumentException(string.Format( + "Read model type '{0}' does not implement any 'IAmReadModelFor<>'", + ReadModelType.Name)); + } + + AggregateTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetGenericArguments()[0])); + DomainEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => + { + var genericArguments = i.GetGenericArguments(); + return typeof (IDomainEvent<,,>).MakeGenericType(genericArguments); + })); + } + + protected ReadStoreManagerV2( + ILog log, + TReadModelStore readModelStore, + IReadModelDomainEventApplier readModelDomainEventApplier) + { + Log = log; + ReadModelStore = readModelStore; + ReadModelDomainEventApplier = readModelDomainEventApplier; + } + + public async Task UpdateReadStoresAsync( + TIdentity id, + IReadOnlyCollection domainEvents, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + { + var aggregateType = typeof (TAggregate); + if (!AggregateTypes.Contains(aggregateType)) + { + Log.Verbose(() => string.Format( + "Read model does not care about aggregate '{0}' so skipping update, only these: {1}", + ReadModelType.Name, + string.Join(", ", AggregateTypes.Select(t => t.Name)) + )); + return; + } + + var relevantDomainEvents = domainEvents + .Where(e => DomainEventTypes.Contains(e.GetType())) + .ToList(); + if (!relevantDomainEvents.Any()) + { + Log.Verbose(() => string.Format( + "None of these events was relevant for read model '{0}', skipping update: {1}", + ReadModelType.Name, + string.Join(", ", domainEvents.Select(e => e.EventType.Name)) + )); + return; + } + + var readModelContext = new ReadModelContext(); + + await ReadModelStore.UpdateAsync( + id.Value, + readModelContext, + UpdateAsync, + cancellationToken) + .ConfigureAwait(false); + } + + protected abstract Task> UpdateAsync( + IReadModelContext readModelContext, + ReadModelEnvelope readModelEnvelope, + CancellationToken cancellationToken); + } +} diff --git a/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs new file mode 100644 index 000000000..bb8809488 --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs @@ -0,0 +1,49 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Logs; + +namespace EventFlow.ReadStores.V2 +{ + public class SingleAggregateReadStoreManager : ReadStoreManagerV2 + where TReadModelStore : IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + { + public SingleAggregateReadStoreManager( + ILog log, + TReadModelStore readModelStore, + IReadModelDomainEventApplier readModelDomainEventApplier) + : base(log, readModelStore, readModelDomainEventApplier) + { + } + + protected override Task> UpdateAsync( + IReadModelContext readModelContext, + ReadModelEnvelope readModelEnvelope, + CancellationToken cancellationToken) + { + throw new System.NotImplementedException(); + } + } +} From bc75b72618c90f526a5de0d5c8ef4126b38a49ed Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 27 May 2015 20:23:13 +0200 Subject: [PATCH 35/47] More on read models --- Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs | 5 ++++- Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs | 7 ++++--- Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs | 5 ++++- Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs | 2 ++ .../ReadStores/V2/SingleAggregateReadStoreManager.cs | 8 ++++---- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs index c3a51d4b2..c3202a802 100644 --- a/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs +++ b/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs @@ -21,8 +21,10 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates; namespace EventFlow.ReadStores.V2 { @@ -38,8 +40,9 @@ Task DeleteAllAsync( Task UpdateAsync( string id, + IReadOnlyCollection domainEvents, IReadModelContext readModelContext, - Func, CancellationToken, Task>> updateReadModel, + Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs b/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs index 983365a6c..77d7e0773 100644 --- a/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs +++ b/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs @@ -24,6 +24,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates; using EventFlow.Core; using EventFlow.Logs; @@ -64,9 +65,9 @@ public async override Task DeleteAllAsync( public override async Task UpdateAsync( string id, + IReadOnlyCollection domainEvents, IReadModelContext readModelContext, - Func, CancellationToken, Task>> updateReadModel, + Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, CancellationToken cancellationToken) { using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) @@ -74,7 +75,7 @@ public override async Task UpdateAsync( ReadModelEnvelope readModelEnvelope; _readModels.TryGetValue(id, out readModelEnvelope); - readModelEnvelope = await updateReadModel(readModelContext, readModelEnvelope, cancellationToken).ConfigureAwait(false); + readModelEnvelope = await updateReadModel(readModelContext, domainEvents, readModelEnvelope, cancellationToken).ConfigureAwait(false); _readModels[id] = readModelEnvelope; } diff --git a/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs index 50d439a85..d7809c815 100644 --- a/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs +++ b/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs @@ -21,8 +21,10 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates; using EventFlow.Logs; namespace EventFlow.ReadStores.V2 @@ -47,8 +49,9 @@ public abstract Task DeleteAllAsync( public abstract Task UpdateAsync( string id, + IReadOnlyCollection domainEvents, IReadModelContext readModelContext, - Func, CancellationToken, Task>> updateReadModel, + Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs b/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs index d2533824f..057eee091 100644 --- a/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs +++ b/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs @@ -113,6 +113,7 @@ public async Task UpdateReadStoresAsync( await ReadModelStore.UpdateAsync( id.Value, + relevantDomainEvents, readModelContext, UpdateAsync, cancellationToken) @@ -121,6 +122,7 @@ await ReadModelStore.UpdateAsync( protected abstract Task> UpdateAsync( IReadModelContext readModelContext, + IReadOnlyCollection domainEvents, ReadModelEnvelope readModelEnvelope, CancellationToken cancellationToken); } diff --git a/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs index bb8809488..87e5f8870 100644 --- a/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs @@ -20,8 +20,10 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using EventFlow.Aggregates; using EventFlow.Logs; namespace EventFlow.ReadStores.V2 @@ -38,10 +40,8 @@ public SingleAggregateReadStoreManager( { } - protected override Task> UpdateAsync( - IReadModelContext readModelContext, - ReadModelEnvelope readModelEnvelope, - CancellationToken cancellationToken) + protected override Task> UpdateAsync(IReadModelContext readModelContext, IReadOnlyCollection domainEvents, + ReadModelEnvelope readModelEnvelope, CancellationToken cancellationToken) { throw new System.NotImplementedException(); } From a744922ff9b954167f6964825a54c9966ffaa7d7 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 27 May 2015 22:40:21 +0200 Subject: [PATCH 36/47] Got some basic stuff working on read models --- .../IntegrationTests/DomainTests.cs | 10 +-- Source/EventFlow/EventFlow.csproj | 2 + .../EventFlowOptionsReadStoresExtensions.cs | 51 +++++++++++-- .../ReadStores/V2/IInMemoryReadStoreV2.cs | 29 ++++++++ .../ReadStores/V2/IReadModelStoreV2.cs | 7 +- .../ReadStores/V2/InMemoryReadStoreV2.cs | 37 ++++++++-- .../V2/MultipleAggregateReadStoreManager.cs | 73 +++++++++++++++++++ .../ReadStores/V2/ReadModelEnvelope.cs | 7 ++ .../ReadStores/V2/ReadModelStoreV2.cs | 7 +- .../ReadStores/V2/ReadStoreManagerV2.cs | 7 +- .../V2/SingleAggregateReadStoreManager.cs | 32 +++++++- 11 files changed, 235 insertions(+), 27 deletions(-) create mode 100644 Source/EventFlow/ReadStores/V2/IInMemoryReadStoreV2.cs create mode 100644 Source/EventFlow/ReadStores/V2/MultipleAggregateReadStoreManager.cs diff --git a/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs b/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs index 1939a6e1e..ef453fcd9 100644 --- a/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs @@ -54,7 +54,7 @@ public Task HandleAsync(IDomainEvent() .AddMetadataProvider() .AddMetadataProvider() - .UseInMemoryReadStoreFor() + .UseInMemoryReadStoreFor() .AddSubscribers(typeof(Subscriber)) .CreateResolver()) { var commandBus = resolver.Resolve(); var eventStore = resolver.Resolve(); var queryProcessor = resolver.Resolve(); - var readModelStore = resolver.Resolve>(); + //var readModelStore = resolver.Resolve>(); var id = TestId.New; // Act commandBus.Publish(new DomainErrorAfterFirstCommand(id), CancellationToken.None); var testAggregate = eventStore.LoadAggregate(id, CancellationToken.None); - var testReadModelFromStore = await readModelStore.GetByIdAsync(id.Value, CancellationToken.None).ConfigureAwait(false); + //var testReadModelFromStore = await readModelStore.GetByIdAsync(id.Value, CancellationToken.None).ConfigureAwait(false); var testReadModelFromQuery1 = queryProcessor.Process( new ReadModelByIdQuery(id.Value), CancellationToken.None); var testReadModelFromQuery2 = queryProcessor.Process( @@ -86,7 +86,7 @@ public async Task BasicFlow() testAggregate.DomainErrorAfterFirstReceived.Should().BeTrue(); testReadModelFromQuery1.DomainErrorAfterFirstReceived.Should().BeTrue(); testReadModelFromQuery2.Should().NotBeNull(); - testReadModelFromStore.Should().NotBeNull(); + //testReadModelFromStore.Should().NotBeNull(); } } } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 3183a8fbc..ebbceff3b 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -121,6 +121,7 @@ + @@ -131,6 +132,7 @@ + diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs index fe7ca8404..b1c7d1f0d 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs @@ -20,17 +20,58 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System.Collections.Generic; using EventFlow.Configuration.Registrations; -using EventFlow.Queries; using EventFlow.ReadStores; -using EventFlow.ReadStores.InMemory; -using EventFlow.ReadStores.InMemory.Queries; +using EventFlow.ReadStores.V2; namespace EventFlow.Extensions { public static class EventFlowOptionsReadStoresExtensions { + public static EventFlowOptions UseReadStoreFor( + this EventFlowOptions eventFlowOptions) + where TReadStore : class, IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + { + return eventFlowOptions.RegisterServices(f => + { + f.Register>(); + }); + } + + public static EventFlowOptions UseReadStoreFor( + this EventFlowOptions eventFlowOptions) + where TReadStore : class, IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + where TReadModelLocator : IReadModelLocator + { + return eventFlowOptions.RegisterServices(f => + { + f.Register>(); + }); + } + + public static EventFlowOptions UseInMemoryReadStoreFor( + this EventFlowOptions eventFlowOptions) + where TReadModel : class, IReadModel, new() + { + return eventFlowOptions + .RegisterServices(f => f.Register, InMemoryReadStoreV2>(Lifetime.Singleton)) + .UseReadStoreFor, TReadModel>(); + } + + public static EventFlowOptions UseInMemoryReadStoreFor( + this EventFlowOptions eventFlowOptions) + where TReadModel : class, IReadModel, new() + where TReadModelLocator : IReadModelLocator + { + return eventFlowOptions + .RegisterServices(f => f.Register, InMemoryReadStoreV2>(Lifetime.Singleton)) + .UseReadStoreFor, TReadModel, TReadModelLocator>(); + } + + + /* public static EventFlowOptions UseInMemoryReadStoreFor( this EventFlowOptions eventFlowOptions) where TReadModel : IReadModel, new() @@ -61,6 +102,6 @@ public static EventFlowOptions AddReadModelStore( } return eventFlowOptions; - } + }*/ } } diff --git a/Source/EventFlow/ReadStores/V2/IInMemoryReadStoreV2.cs b/Source/EventFlow/ReadStores/V2/IInMemoryReadStoreV2.cs new file mode 100644 index 000000000..80f9e106a --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/IInMemoryReadStoreV2.cs @@ -0,0 +1,29 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +namespace EventFlow.ReadStores.V2 +{ + public interface IInMemoryReadStoreV2 : IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + { + } +} diff --git a/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs index c3202a802..0cfc08cf2 100644 --- a/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs +++ b/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs @@ -31,6 +31,10 @@ namespace EventFlow.ReadStores.V2 public interface IReadModelStoreV2 where TReadModel : class, IReadModel, new() { + Task> GetAsync( + string id, + CancellationToken cancellationToken); + Task DeleteAsync( string id, CancellationToken cancellationToken); @@ -39,8 +43,7 @@ Task DeleteAllAsync( CancellationToken cancellationToken); Task UpdateAsync( - string id, - IReadOnlyCollection domainEvents, + IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, CancellationToken cancellationToken); diff --git a/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs b/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs index 77d7e0773..8b0e523c4 100644 --- a/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs +++ b/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs @@ -30,17 +30,31 @@ namespace EventFlow.ReadStores.V2 { - public class InMemoryReadStoreV2 : ReadModelStoreV2 + public class InMemoryReadStoreV2 : ReadModelStoreV2, IInMemoryReadStoreV2 where TReadModel : class, IReadModel, new() { private readonly Dictionary> _readModels = new Dictionary>(); private readonly AsyncLock _asyncLock = new AsyncLock(); public InMemoryReadStoreV2( - ILog log) : base(log) + ILog log) + : base(log) { } + public override async Task> GetAsync( + string id, + CancellationToken cancellationToken) + { + using (await _asyncLock.WaitAsync(cancellationToken)) + { + ReadModelEnvelope readModelEnvelope; + return _readModels.TryGetValue(id, out readModelEnvelope) + ? readModelEnvelope + : ReadModelEnvelope.Empty; + } + } + public override async Task DeleteAsync( string id, CancellationToken cancellationToken) @@ -64,20 +78,27 @@ public async override Task DeleteAllAsync( } public override async Task UpdateAsync( - string id, - IReadOnlyCollection domainEvents, + IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, CancellationToken cancellationToken) { using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { - ReadModelEnvelope readModelEnvelope; - _readModels.TryGetValue(id, out readModelEnvelope); + foreach (var readModelUpdate in readModelUpdates) + { + ReadModelEnvelope readModelEnvelope; + _readModels.TryGetValue(readModelUpdate.ReadModelId, out readModelEnvelope); - readModelEnvelope = await updateReadModel(readModelContext, domainEvents, readModelEnvelope, cancellationToken).ConfigureAwait(false); + readModelEnvelope = await updateReadModel( + readModelContext, + readModelUpdate.DomainEvents, + readModelEnvelope, + cancellationToken) + .ConfigureAwait(false); - _readModels[id] = readModelEnvelope; + _readModels[readModelUpdate.ReadModelId] = readModelEnvelope; + } } } } diff --git a/Source/EventFlow/ReadStores/V2/MultipleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/V2/MultipleAggregateReadStoreManager.cs new file mode 100644 index 000000000..5404e0f56 --- /dev/null +++ b/Source/EventFlow/ReadStores/V2/MultipleAggregateReadStoreManager.cs @@ -0,0 +1,73 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.Logs; + +namespace EventFlow.ReadStores.V2 +{ + public class MultipleAggregateReadStoreManager : ReadStoreManagerV2 + where TReadStore : IReadModelStoreV2 + where TReadModel : class, IReadModel, new() + where TReadModelLocator : IReadModelLocator + { + private readonly TReadModelLocator _readModelLocator; + + public MultipleAggregateReadStoreManager( + ILog log, + TReadStore readModelStore, + IReadModelDomainEventApplier readModelDomainEventApplier, + TReadModelLocator readModelLocator) + : base(log, readModelStore, readModelDomainEventApplier) + { + _readModelLocator = readModelLocator; + } + + protected override IReadOnlyCollection BuildReadModelUpdates( + IReadOnlyCollection domainEvents) + { + var readModelUpdates = ( + from de in domainEvents + let readModelIds = _readModelLocator.GetReadModelIds(de) + from rid in readModelIds + group de by rid into g + select new ReadModelUpdate(g.Key, g.ToList()) + ).ToList(); + return readModelUpdates; + } + + protected override async Task> UpdateAsync( + IReadModelContext readModelContext, + IReadOnlyCollection domainEvents, + ReadModelEnvelope readModelEnvelope, + CancellationToken cancellationToken) + { + var readModel = readModelEnvelope.ReadModel ?? new TReadModel(); + await ReadModelDomainEventApplier.UpdateReadModelAsync(readModel, domainEvents, readModelContext, cancellationToken).ConfigureAwait(false); + return ReadModelEnvelope.With(readModel); + } + } +} diff --git a/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs b/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs index 7b8a442e9..0a8b46e70 100644 --- a/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs +++ b/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs @@ -25,6 +25,13 @@ namespace EventFlow.ReadStores.V2 public class ReadModelEnvelope where TReadModel : class, IReadModel, new() { + private static readonly ReadModelEnvelope EmptyInstance = new ReadModelEnvelope(null, null); + + public static ReadModelEnvelope Empty + { + get { return EmptyInstance; } + } + public static ReadModelEnvelope With(TReadModel readModel) { return new ReadModelEnvelope(readModel, null); diff --git a/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs index d7809c815..42a808b70 100644 --- a/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs +++ b/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs @@ -40,6 +40,10 @@ protected ReadModelStoreV2( Log = log; } + public abstract Task> GetAsync( + string id, + CancellationToken cancellationToken); + public abstract Task DeleteAsync( string id, CancellationToken cancellationToken); @@ -48,8 +52,7 @@ public abstract Task DeleteAllAsync( CancellationToken cancellationToken); public abstract Task UpdateAsync( - string id, - IReadOnlyCollection domainEvents, + IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, CancellationToken cancellationToken); diff --git a/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs b/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs index 057eee091..8153bf463 100644 --- a/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs +++ b/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs @@ -110,16 +110,19 @@ public async Task UpdateReadStoresAsync( } var readModelContext = new ReadModelContext(); + var readModelUpdates = BuildReadModelUpdates(relevantDomainEvents); await ReadModelStore.UpdateAsync( - id.Value, - relevantDomainEvents, + readModelUpdates, readModelContext, UpdateAsync, cancellationToken) .ConfigureAwait(false); } + protected abstract IReadOnlyCollection BuildReadModelUpdates( + IReadOnlyCollection domainEvents); + protected abstract Task> UpdateAsync( IReadModelContext readModelContext, IReadOnlyCollection domainEvents, diff --git a/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs index 87e5f8870..b46573036 100644 --- a/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs @@ -20,7 +20,9 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -40,10 +42,34 @@ public SingleAggregateReadStoreManager( { } - protected override Task> UpdateAsync(IReadModelContext readModelContext, IReadOnlyCollection domainEvents, - ReadModelEnvelope readModelEnvelope, CancellationToken cancellationToken) + protected override IReadOnlyCollection BuildReadModelUpdates( + IReadOnlyCollection domainEvents) { - throw new System.NotImplementedException(); + var readModelIds = domainEvents + .Select(e => e.GetIdentity().Value) + .Distinct() + .ToList(); + if (readModelIds.Count != 1) + { + throw new ArgumentException("Only domain events from the same aggregate is allowed"); + } + + return new[] {new ReadModelUpdate(readModelIds.Single(), domainEvents)}; + } + + protected override async Task> UpdateAsync( + IReadModelContext readModelContext, + IReadOnlyCollection domainEvents, + ReadModelEnvelope readModelEnvelope, + CancellationToken cancellationToken) + { + var readModel = readModelEnvelope.ReadModel ?? new TReadModel(); + + await ReadModelDomainEventApplier.UpdateReadModelAsync(readModel, domainEvents, readModelContext, cancellationToken).ConfigureAwait(false); + + var readModelVersion = domainEvents.Max(e => e.AggregateSequenceNumber); + + return ReadModelEnvelope.With(readModel, readModelVersion); } } } From c02ceb95fe7c0d097bc31b0e2368a02a808deee8 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 27 May 2015 23:11:11 +0200 Subject: [PATCH 37/47] Getting close to a rework on how read models are updated --- .../EventFlow.ReadStores.MsSql.csproj | 1 - .../Extensions/EventFlowOptionsExtensions.cs | 35 +++-- .../IMssqlReadModelStore.cs | 4 +- .../MssqlReadModelStore.cs | 126 ++++++++-------- .../Queries/MsSqlReadModelByIdQueryHandler.cs | 59 -------- .../EventStores/FilesEventStoreTests.cs | 11 +- .../IntegrationTests/InMemoryConfiguration.cs | 11 +- .../ReadStores/ReadModelPopulatorTests.cs | 6 +- Source/EventFlow/EventFlow.csproj | 23 ++- Source/EventFlow/EventFlowOptions.cs | 1 - .../EventFlowOptionsReadStoresExtensions.cs | 50 ++----- .../EventFlow/Queries/ReadModelByIdQuery.cs | 23 ++- ...ryReadStoreV2.cs => IInMemoryReadStore.cs} | 4 +- .../EventFlow/ReadStores/IReadModelStore.cs | 26 ++-- .../InMemory/IInMemoryReadModelStore.cs | 37 ----- .../InMemory/InMemoryQueryHandler.cs | 4 +- .../InMemory/InMemoryReadModelStore.cs | 134 ------------------ ...oryReadStoreV2.cs => InMemoryReadStore.cs} | 6 +- .../MultiAggregateReadModelStore.cs | 82 ----------- .../MultipleAggregateReadStoreManager.cs | 6 +- .../ReadStores/{V2 => }/ReadModelEnvelope.cs | 2 +- .../ReadStores/ReadModelPopulator.cs | 38 ++++- Source/EventFlow/ReadStores/ReadModelStore.cs | 35 ++--- .../EventFlow/ReadStores/ReadStoreManager.cs | 107 ++++++++++---- .../SingleAggregateReadModelStore.cs | 70 --------- .../SingleAggregateReadStoreManager.cs | 6 +- .../ReadStores/V2/IReadModelStoreV2.cs | 51 ------- .../ReadStores/V2/ReadModelStoreV2.cs | 60 -------- .../ReadStores/V2/ReadStoreManagerV2.cs | 132 ----------------- 29 files changed, 299 insertions(+), 851 deletions(-) delete mode 100644 Source/EventFlow.ReadStores.MsSql/Queries/MsSqlReadModelByIdQueryHandler.cs rename Source/EventFlow/ReadStores/{V2/IInMemoryReadStoreV2.cs => IInMemoryReadStore.cs} (91%) delete mode 100644 Source/EventFlow/ReadStores/InMemory/IInMemoryReadModelStore.cs delete mode 100644 Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs rename Source/EventFlow/ReadStores/{V2/InMemoryReadStoreV2.cs => InMemoryReadStore.cs} (95%) delete mode 100644 Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs rename Source/EventFlow/ReadStores/{V2 => }/MultipleAggregateReadStoreManager.cs (94%) rename Source/EventFlow/ReadStores/{V2 => }/ReadModelEnvelope.cs (98%) delete mode 100644 Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs rename Source/EventFlow/ReadStores/{V2 => }/SingleAggregateReadStoreManager.cs (95%) delete mode 100644 Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs delete mode 100644 Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs delete mode 100644 Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs diff --git a/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj b/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj index 6d42ad9de..0b849c96e 100644 --- a/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj +++ b/Source/EventFlow.ReadStores.MsSql/EventFlow.ReadStores.MsSql.csproj @@ -52,7 +52,6 @@ - diff --git a/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs b/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs index b317e7703..349afa0c9 100644 --- a/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs +++ b/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs @@ -21,26 +21,45 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using EventFlow.Configuration.Registrations; -using EventFlow.Queries; -using EventFlow.ReadStores.MsSql.Queries; +using EventFlow.Extensions; namespace EventFlow.ReadStores.MsSql.Extensions { public static class EventFlowOptionsExtensions { - public static EventFlowOptions UseMssqlReadModel(this EventFlowOptions eventFlowOptions) - where TReadModel : IMssqlReadModel, new() + public static EventFlowOptions UseMssqlReadModel( + this EventFlowOptions eventFlowOptions) + where TReadModel : class, IMssqlReadModel, new() where TReadModelLocator : IReadModelLocator { - eventFlowOptions.RegisterServices(f => + eventFlowOptions + .RegisterServices(f => + { + if (!f.HasRegistrationFor()) + { + f.Register(Lifetime.Singleton); + } + f.Register, MssqlReadModelStore>(); + }) + .UseReadStoreFor, TReadModel, TReadModelLocator>(); + + return eventFlowOptions; + } + + public static EventFlowOptions UseMssqlReadModel( + this EventFlowOptions eventFlowOptions) + where TReadModel : class, IMssqlReadModel, new() + { + eventFlowOptions + .RegisterServices(f => { if (!f.HasRegistrationFor()) { f.Register(Lifetime.Singleton); } - f.Register>(); - f.Register, TReadModel>, MsSqlReadModelByIdQueryHandler>(); - }); + f.Register, MssqlReadModelStore>(); + }) + .UseReadStoreFor, TReadModel>(); return eventFlowOptions; } diff --git a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModelStore.cs index dd7c90a96..213694b21 100644 --- a/Source/EventFlow.ReadStores.MsSql/IMssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/IMssqlReadModelStore.cs @@ -22,8 +22,8 @@ namespace EventFlow.ReadStores.MsSql { - public interface IMssqlReadModelStore : IReadModelStore - where TReadModel : IMssqlReadModel, new() + public interface IMssqlReadModelStore : IReadModelStore + where TReadModel : class, IMssqlReadModel, new() { } } diff --git a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs index 6fbf331be..a48d603b4 100644 --- a/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs +++ b/Source/EventFlow.ReadStores.MsSql/MssqlReadModelStore.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -32,11 +33,10 @@ namespace EventFlow.ReadStores.MsSql { - public class MssqlReadModelStore : - MultiAggregateReadModelStore, + public class MssqlReadModelStore : + ReadModelStore, IMssqlReadModelStore - where TReadModel : IMssqlReadModel, new() - where TReadModelLocator : IReadModelLocator + where TReadModel : class, IMssqlReadModel, new() { private readonly IMsSqlConnection _connection; private readonly IQueryProcessor _queryProcessor; @@ -44,88 +44,87 @@ public class MssqlReadModelStore : public MssqlReadModelStore( ILog log, - TReadModelLocator readModelLocator, - IReadModelDomainEventApplier readModelDomainEventApplier, IMsSqlConnection connection, IQueryProcessor queryProcessor, IReadModelSqlGenerator readModelSqlGenerator) - : base(log, readModelLocator, readModelDomainEventApplier) + : base(log) { _connection = connection; _queryProcessor = queryProcessor; _readModelSqlGenerator = readModelSqlGenerator; } - private async Task UpdateReadModelAsync( - string id, - bool forceNew, - IReadOnlyCollection domainEvents, + public override async Task UpdateAsync( + IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, + Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, CancellationToken cancellationToken) { - var readModelNameLowerCased = typeof (TReadModel).Name.ToLowerInvariant(); - var readModel = await GetByIdAsync(id, cancellationToken).ConfigureAwait(false); - var isNew = readModel == null; - if (readModel == null || forceNew) - { - readModel = new TReadModel - { - AggregateId = id, - CreateTime = domainEvents.First().Timestamp, - }; - } + // TODO: Transaction - var appliedAny = await ReadModelDomainEventApplier.UpdateReadModelAsync( - readModel, - domainEvents, - readModelContext, - cancellationToken) - .ConfigureAwait(false); - if (!appliedAny) + foreach (var readModelUpdate in readModelUpdates) { - return; - } + var readModelNameLowerCased = typeof(TReadModel).Name.ToLowerInvariant(); + var readModelEnvelope = await GetAsync(readModelUpdate.ReadModelId, cancellationToken).ConfigureAwait(false); + var readModel = readModelEnvelope.ReadModel; + var isNew = readModel == null; + if (readModel == null) + { + readModel = new TReadModel + { + AggregateId = readModelUpdate.ReadModelId, + CreateTime = readModelUpdate.DomainEvents.First().Timestamp, + }; + } - var lastDomainEvent = domainEvents.Last(); - readModel.UpdatedTime = lastDomainEvent.Timestamp; - readModel.LastAggregateSequenceNumber = lastDomainEvent.AggregateSequenceNumber; + readModelEnvelope = await updateReadModel( + readModelContext, + readModelUpdate.DomainEvents, + ReadModelEnvelope.With(readModel, readModel.LastAggregateSequenceNumber), + cancellationToken) + .ConfigureAwait(false); - var sql = isNew - ? _readModelSqlGenerator.CreateInsertSql() - : _readModelSqlGenerator.CreateUpdateSql(); + readModel.UpdatedTime = DateTimeOffset.Now; + readModel.LastAggregateSequenceNumber = (int) readModelEnvelope.Version.GetValueOrDefault(); - await _connection.ExecuteAsync( - Label.Named("mssql-store-read-model", readModelNameLowerCased), - cancellationToken, - sql, - readModel).ConfigureAwait(false); + var sql = isNew + ? _readModelSqlGenerator.CreateInsertSql() + : _readModelSqlGenerator.CreateUpdateSql(); + + await _connection.ExecuteAsync( + Label.Named("mssql-store-read-model", readModelNameLowerCased), + cancellationToken, + sql, + readModel).ConfigureAwait(false); + } } - public override Task PopulateReadModelAsync( - string id, - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - CancellationToken cancellationToken) + public override async Task> GetAsync(string id, CancellationToken cancellationToken) { - return UpdateReadModelAsync(id, true, domainEvents, readModelContext, cancellationToken); + var readModelNameLowerCased = typeof(TReadModel).Name.ToLowerInvariant(); + var selectSql = _readModelSqlGenerator.CreateSelectSql(); + var readModels = await _connection.QueryAsync( + Label.Named(string.Format("mssql-fetch-read-model-{0}", readModelNameLowerCased)), + cancellationToken, + selectSql, + new { AggregateId = id }) + .ConfigureAwait(false); + var readModel = readModels.SingleOrDefault(); + + return readModel == null + ? ReadModelEnvelope.Empty + : ReadModelEnvelope.With(readModel, readModel.LastAggregateSequenceNumber); } - public override Task GetByIdAsync( - string id, - CancellationToken cancellationToken) + public override Task DeleteAsync(string id, CancellationToken cancellationToken) { - return _queryProcessor.ProcessAsync(new ReadModelByIdQuery(id), cancellationToken); + throw new NotImplementedException(); } - public override async Task PurgeAsync(CancellationToken cancellationToken) + public override async Task DeleteAllAsync(CancellationToken cancellationToken) { - if (typeof (TReadModel) != typeof(TReadModelToPurge)) - { - return; - } - - var sql = _readModelSqlGenerator.CreatePurgeSql(); - var readModelName = typeof (TReadModelToPurge).Name; + var sql = _readModelSqlGenerator.CreatePurgeSql(); + var readModelName = typeof(TReadModel).Name; var rowsAffected = await _connection.ExecuteAsync( Label.Named("mssql-purge-read-model", readModelName), @@ -138,12 +137,5 @@ public override async Task PurgeAsync(CancellationToken cance rowsAffected, readModelName); } - - protected override Task UpdateReadModelsAsync(IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, CancellationToken cancellationToken) - { - var updateTasks = readModelUpdates - .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, false, rmu.DomainEvents, readModelContext, cancellationToken)); - return Task.WhenAll(updateTasks); - } } } diff --git a/Source/EventFlow.ReadStores.MsSql/Queries/MsSqlReadModelByIdQueryHandler.cs b/Source/EventFlow.ReadStores.MsSql/Queries/MsSqlReadModelByIdQueryHandler.cs deleted file mode 100644 index f3364f7c1..000000000 --- a/Source/EventFlow.ReadStores.MsSql/Queries/MsSqlReadModelByIdQueryHandler.cs +++ /dev/null @@ -1,59 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Core; -using EventFlow.MsSql; -using EventFlow.Queries; - -namespace EventFlow.ReadStores.MsSql.Queries -{ - public class MsSqlReadModelByIdQueryHandler : IQueryHandler, TReadModel> - where TReadModel : IMssqlReadModel - { - private readonly IReadModelSqlGenerator _readModelSqlGenerator; - private readonly IMsSqlConnection _connection; - - public MsSqlReadModelByIdQueryHandler( - IReadModelSqlGenerator readModelSqlGenerator, - IMsSqlConnection connection) - { - _readModelSqlGenerator = readModelSqlGenerator; - _connection = connection; - } - - public async Task ExecuteQueryAsync(ReadModelByIdQuery query, CancellationToken cancellationToken) - { - var readModelNameLowerCased = typeof(TReadModel).Name.ToLowerInvariant(); - var selectSql = _readModelSqlGenerator.CreateSelectSql(); - var readModels = await _connection.QueryAsync( - Label.Named(string.Format("mssql-fetch-read-model-{0}", readModelNameLowerCased)), - cancellationToken, - selectSql, - new { AggregateId = query.Id }) - .ConfigureAwait(false); - return readModels.SingleOrDefault(); - } - } -} diff --git a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs index e028996be..a8496d0b5 100644 --- a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs @@ -28,8 +28,8 @@ using EventFlow.Configuration; using EventFlow.EventStores.Files; using EventFlow.Extensions; +using EventFlow.Queries; using EventFlow.ReadStores; -using EventFlow.ReadStores.InMemory; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Test.ReadModels; using EventFlow.TestHelpers.Suites; @@ -40,9 +40,9 @@ public class FilesEventStoreTests : EventStoreSuite _inMemoryReadModelStore; private IFilesEventStoreConfiguration _configuration; private IReadModelPopulator _readModelPopulator; + private IQueryProcessor _queryProcessor; public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) { @@ -56,16 +56,19 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio .UseFilesEventStore(FilesEventStoreConfiguration.Create(storePath)) .CreateResolver(); - _inMemoryReadModelStore = resolver.Resolve>(); _configuration = resolver.Resolve(); _readModelPopulator = resolver.Resolve(); + _queryProcessor = resolver.Resolve(); return resolver; } public override async Task GetTestAggregateReadModelAsync(IIdentity id) { - return await _inMemoryReadModelStore.GetByIdAsync(id.Value, CancellationToken.None).ConfigureAwait(false); + return await _queryProcessor.ProcessAsync( + new ReadModelByIdQuery(id.Value), + CancellationToken.None) + .ConfigureAwait(false); } public override Task PurgeTestAggregateReadModelAsync() diff --git a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs index b166fd1c9..875a04151 100644 --- a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs +++ b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs @@ -25,8 +25,8 @@ using EventFlow.Aggregates; using EventFlow.Configuration; using EventFlow.Extensions; +using EventFlow.Queries; using EventFlow.ReadStores; -using EventFlow.ReadStores.InMemory; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Test.ReadModels; @@ -34,8 +34,8 @@ namespace EventFlow.Tests.IntegrationTests { public class InMemoryConfiguration : IntegrationTestConfiguration { - private IInMemoryReadModelStore _inMemoryReadModelStore; private IReadModelPopulator _readModelPopulator; + private IQueryProcessor _queryProcessor; public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) { @@ -43,15 +43,18 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio .UseInMemoryReadStoreFor() .CreateResolver(); - _inMemoryReadModelStore = resolver.Resolve>(); _readModelPopulator = resolver.Resolve(); + _queryProcessor = resolver.Resolve(); return resolver; } public override async Task GetTestAggregateReadModelAsync(IIdentity id) { - return await _inMemoryReadModelStore.GetByIdAsync(id.Value, CancellationToken.None).ConfigureAwait(false); + return await _queryProcessor.ProcessAsync( + new ReadModelByIdQuery(id.Value), + CancellationToken.None) + .ConfigureAwait(false); } public override Task PurgeTestAggregateReadModelAsync() diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs index 96c44597c..dbf160411 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs @@ -37,7 +37,7 @@ using Ploeh.AutoFixture; namespace EventFlow.Tests.UnitTests.ReadStores -{ +{/* [Timeout(5000)] public class ReadModelPopulatorTests : TestsFor { @@ -49,7 +49,7 @@ public void Apply(IReadModelContext context, IDomainEvent _readModelStoreMock; + private Mock> _readModelStoreMock; private Mock _eventFlowConfigurationMock; private Mock _eventStoreMock; private List _eventStoreData; @@ -144,5 +144,5 @@ private void ArrangeEventStore(IEnumerable domainEvents) { _eventStoreData = domainEvents.ToList(); } - } + }*/ } diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index ebbceff3b..232825e1d 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -121,26 +121,25 @@ - - + + - + + - - + + - - - - + + @@ -155,12 +154,7 @@ - - - - - @@ -181,7 +175,6 @@ - diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index 2145e4128..4acace6f1 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -105,7 +105,6 @@ public IRootResolver CreateResolver(bool validateRegistrations = true) RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); RegisterIfMissing(services, Lifetime.Singleton); - RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs index b1c7d1f0d..346027b13 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs @@ -21,8 +21,8 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using EventFlow.Configuration.Registrations; +using EventFlow.Queries; using EventFlow.ReadStores; -using EventFlow.ReadStores.V2; namespace EventFlow.Extensions { @@ -30,24 +30,26 @@ public static class EventFlowOptionsReadStoresExtensions { public static EventFlowOptions UseReadStoreFor( this EventFlowOptions eventFlowOptions) - where TReadStore : class, IReadModelStoreV2 + where TReadStore : class, IReadModelStore where TReadModel : class, IReadModel, new() { return eventFlowOptions.RegisterServices(f => { f.Register>(); + f.Register, TReadModel>, ReadModelByIdQueryHandler>(); }); } public static EventFlowOptions UseReadStoreFor( this EventFlowOptions eventFlowOptions) - where TReadStore : class, IReadModelStoreV2 + where TReadStore : class, IReadModelStore where TReadModel : class, IReadModel, new() where TReadModelLocator : IReadModelLocator { return eventFlowOptions.RegisterServices(f => { f.Register>(); + f.Register, TReadModel>, ReadModelByIdQueryHandler>(); }); } @@ -56,8 +58,8 @@ public static EventFlowOptions UseInMemoryReadStoreFor( where TReadModel : class, IReadModel, new() { return eventFlowOptions - .RegisterServices(f => f.Register, InMemoryReadStoreV2>(Lifetime.Singleton)) - .UseReadStoreFor, TReadModel>(); + .RegisterServices(f => f.Register, InMemoryReadStore>(Lifetime.Singleton)) + .UseReadStoreFor, TReadModel>(); } public static EventFlowOptions UseInMemoryReadStoreFor( @@ -66,42 +68,8 @@ public static EventFlowOptions UseInMemoryReadStoreFor f.Register, InMemoryReadStoreV2>(Lifetime.Singleton)) - .UseReadStoreFor, TReadModel, TReadModelLocator>(); + .RegisterServices(f => f.Register, InMemoryReadStore>(Lifetime.Singleton)) + .UseReadStoreFor, TReadModel, TReadModelLocator>(); } - - - /* - public static EventFlowOptions UseInMemoryReadStoreFor( - this EventFlowOptions eventFlowOptions) - where TReadModel : IReadModel, new() - where TReadModelLocator : IReadModelLocator - { - eventFlowOptions.AddReadModelStore>(); - eventFlowOptions.RegisterServices(f => - { - f.Register, InMemoryReadModelStore>(Lifetime.Singleton); - f.Register, IEnumerable>, InMemoryQueryHandler>(); - f.Register, TReadModel>, InMemoryQueryHandler>(); - }); - return eventFlowOptions; - } - - public static EventFlowOptions AddReadModelStore( - this EventFlowOptions eventFlowOptions, - Lifetime lifetime = Lifetime.AlwaysUnique) - where TReadModelStore : class, IReadModelStore - { - if (typeof(TReadModelStore).IsInterface) - { - eventFlowOptions.RegisterServices(f => f.Register(r => r.Resolver.Resolve(), lifetime)); - } - else - { - eventFlowOptions.RegisterServices(f => f.Register(lifetime)); - } - - return eventFlowOptions; - }*/ } } diff --git a/Source/EventFlow/Queries/ReadModelByIdQuery.cs b/Source/EventFlow/Queries/ReadModelByIdQuery.cs index 3e4f7a306..f84263605 100644 --- a/Source/EventFlow/Queries/ReadModelByIdQuery.cs +++ b/Source/EventFlow/Queries/ReadModelByIdQuery.cs @@ -21,12 +21,14 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Threading; +using System.Threading.Tasks; using EventFlow.ReadStores; namespace EventFlow.Queries { public class ReadModelByIdQuery : IQuery - where TReadModel : IReadModel + where TReadModel : class, IReadModel, new() { public string Id { get; private set; } @@ -37,4 +39,23 @@ public ReadModelByIdQuery(string id) Id = id; } } + + public class ReadModelByIdQueryHandler : IQueryHandler, TReadModel> + where TReadStore : IReadModelStore + where TReadModel : class, IReadModel, new() + { + private readonly TReadStore _readStore; + + public ReadModelByIdQueryHandler( + TReadStore readStore) + { + _readStore = readStore; + } + + public async Task ExecuteQueryAsync(ReadModelByIdQuery query, CancellationToken cancellationToken) + { + var readModelEnvelope = await _readStore.GetAsync(query.Id, cancellationToken).ConfigureAwait(false); + return readModelEnvelope.ReadModel; + } + } } diff --git a/Source/EventFlow/ReadStores/V2/IInMemoryReadStoreV2.cs b/Source/EventFlow/ReadStores/IInMemoryReadStore.cs similarity index 91% rename from Source/EventFlow/ReadStores/V2/IInMemoryReadStoreV2.cs rename to Source/EventFlow/ReadStores/IInMemoryReadStore.cs index 80f9e106a..37114a414 100644 --- a/Source/EventFlow/ReadStores/V2/IInMemoryReadStoreV2.cs +++ b/Source/EventFlow/ReadStores/IInMemoryReadStore.cs @@ -20,9 +20,9 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace EventFlow.ReadStores.V2 +namespace EventFlow.ReadStores { - public interface IInMemoryReadStoreV2 : IReadModelStoreV2 + public interface IInMemoryReadStore : IReadModelStore where TReadModel : class, IReadModel, new() { } diff --git a/Source/EventFlow/ReadStores/IReadModelStore.cs b/Source/EventFlow/ReadStores/IReadModelStore.cs index db0d4926d..91ac11e6c 100644 --- a/Source/EventFlow/ReadStores/IReadModelStore.cs +++ b/Source/EventFlow/ReadStores/IReadModelStore.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -27,21 +28,24 @@ namespace EventFlow.ReadStores { - public interface IReadModelStore + public interface IReadModelStore + where TReadModel : class, IReadModel, new() { - Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, + Task> GetAsync( + string id, CancellationToken cancellationToken); - Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - where TReadModelToPopulate : IReadModel; + Task DeleteAsync( + string id, + CancellationToken cancellationToken); - Task PurgeAsync(CancellationToken cancellationToken) - where TReadModelToPurge : IReadModel; + Task DeleteAllAsync( + CancellationToken cancellationToken); - Task PopulateReadModelAsync(string id, IReadOnlyCollection domainEvents, IReadModelContext readModelContext, CancellationToken cancellationToken) - where TReadModelToPopulate : IReadModel; + Task UpdateAsync( + IReadOnlyCollection readModelUpdates, + IReadModelContext readModelContext, + Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, + CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/ReadStores/InMemory/IInMemoryReadModelStore.cs b/Source/EventFlow/ReadStores/InMemory/IInMemoryReadModelStore.cs deleted file mode 100644 index c86f63e3e..000000000 --- a/Source/EventFlow/ReadStores/InMemory/IInMemoryReadModelStore.cs +++ /dev/null @@ -1,37 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace EventFlow.ReadStores.InMemory -{ - public interface IInMemoryReadModelStore : IReadModelStore - where TReadModel : IReadModel, new() - { - Task GetByIdAsync(string id, CancellationToken cancellationToken); - IEnumerable GetAll(); - IEnumerable Find(Predicate predicate); - } -} diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs index f07935e87..64460e9f7 100644 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs @@ -27,7 +27,7 @@ using EventFlow.ReadStores.InMemory.Queries; namespace EventFlow.ReadStores.InMemory -{ +{/* public class InMemoryQueryHandler : IQueryHandler, IEnumerable>, IQueryHandler, TReadModel> @@ -51,5 +51,5 @@ public Task ExecuteQueryAsync(ReadModelByIdQuery query, { return _readModelStore.GetByIdAsync(query.Id, cancellationToken); } - } + }*/ } diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs deleted file mode 100644 index 8564e7bd7..000000000 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryReadModelStore.cs +++ /dev/null @@ -1,134 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.Core; -using EventFlow.Logs; - -namespace EventFlow.ReadStores.InMemory -{ - public class InMemoryReadModelStore : - MultiAggregateReadModelStore, - IInMemoryReadModelStore - where TReadModel : IReadModel, new() - where TReadModelLocator : IReadModelLocator - { - private readonly AsyncLock _asyncLock = new AsyncLock(); - private readonly Dictionary _readModels = new Dictionary(); - - public InMemoryReadModelStore( - ILog log, - TReadModelLocator readModelLocator, - IReadModelDomainEventApplier readModelDomainEventApplier) - : base(log, readModelLocator, readModelDomainEventApplier) - { - } - - private async Task UpdateReadModelAsync( - string id, - bool forceNew, - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - CancellationToken cancellationToken) - { - using (await _asyncLock.WaitAsync(cancellationToken)) - { - TReadModel readModel; - if (_readModels.ContainsKey(id) && !forceNew) - { - readModel = _readModels[id]; - } - else - { - readModel = new TReadModel(); - _readModels[id] = readModel; - } - - await ReadModelDomainEventApplier.UpdateReadModelAsync( - readModel, - domainEvents, - readModelContext, - cancellationToken) - .ConfigureAwait(false); - } - } - - public TReadModel Get(IIdentity id) - { - TReadModel readModel; - return _readModels.TryGetValue(id.Value, out readModel) - ? readModel - : default(TReadModel); - } - - public IEnumerable GetAll() - { - return _readModels.Values; - } - - public IEnumerable Find(Predicate predicate) - { - return _readModels.Values.Where(rm => predicate(rm)); - } - - public override Task PopulateReadModelAsync( - string id, - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - CancellationToken cancellationToken) - { - return UpdateReadModelAsync(id, true, domainEvents, readModelContext, cancellationToken); - } - - public override Task GetByIdAsync(string id, CancellationToken cancellationToken) - { - TReadModel readModel; - return _readModels.TryGetValue(id, out readModel) - ? Task.FromResult(readModel) - : Task.FromResult(default(TReadModel)); - } - - public override Task PurgeAsync(CancellationToken cancellationToken) - { - if (typeof (TReadModel) == typeof(TReadModelToPurge)) - { - _readModels.Clear(); - } - return Task.FromResult(0); - } - - protected override Task UpdateReadModelsAsync( - IReadOnlyCollection readModelUpdates, - IReadModelContext readModelContext, - CancellationToken cancellationToken) - { - var updateTasks = readModelUpdates - .Select(rmu => UpdateReadModelAsync(rmu.ReadModelId, false, rmu.DomainEvents, readModelContext, cancellationToken)); - return Task.WhenAll(updateTasks); - } - } -} diff --git a/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs b/Source/EventFlow/ReadStores/InMemoryReadStore.cs similarity index 95% rename from Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs rename to Source/EventFlow/ReadStores/InMemoryReadStore.cs index 8b0e523c4..d0a8b9549 100644 --- a/Source/EventFlow/ReadStores/V2/InMemoryReadStoreV2.cs +++ b/Source/EventFlow/ReadStores/InMemoryReadStore.cs @@ -28,15 +28,15 @@ using EventFlow.Core; using EventFlow.Logs; -namespace EventFlow.ReadStores.V2 +namespace EventFlow.ReadStores { - public class InMemoryReadStoreV2 : ReadModelStoreV2, IInMemoryReadStoreV2 + public class InMemoryReadStore : ReadModelStore, IInMemoryReadStore where TReadModel : class, IReadModel, new() { private readonly Dictionary> _readModels = new Dictionary>(); private readonly AsyncLock _asyncLock = new AsyncLock(); - public InMemoryReadStoreV2( + public InMemoryReadStore( ILog log) : base(log) { diff --git a/Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs b/Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs deleted file mode 100644 index 5677b7200..000000000 --- a/Source/EventFlow/ReadStores/MultiAggregateReadModelStore.cs +++ /dev/null @@ -1,82 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.Logs; - -namespace EventFlow.ReadStores -{ - public abstract class MultiAggregateReadModelStore : ReadModelStore - where TReadModel : IReadModel - where TReadModelLocator : IReadModelLocator - { - protected TReadModelLocator ReadModelLocator { get; private set; } - - protected MultiAggregateReadModelStore( - ILog log, - TReadModelLocator readModelLocator, - IReadModelDomainEventApplier readModelDomainEventApplier) - : base (log, readModelDomainEventApplier) - { - ReadModelLocator = readModelLocator; - } - - public override Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - { - var readModelUpdates = ( - from de in domainEvents - let readModelIds = ReadModelLocator.GetReadModelIds(de) - from rid in readModelIds - group de by rid into g - select new ReadModelUpdate(g.Key, g.ToList()) - ).ToList(); - - var readModelContext = new ReadModelContext(); - - return UpdateReadModelsAsync(readModelUpdates, readModelContext, cancellationToken); - } - - public override Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - { - return (typeof (TReadModel) == typeof (TReadModelToPopulate)) - ? ApplyDomainEventsAsync(domainEvents, cancellationToken) - : Task.FromResult(0); - } - - public abstract Task GetByIdAsync( - string id, - CancellationToken cancellationToken); - - protected abstract Task UpdateReadModelsAsync( - IReadOnlyCollection readModelUpdates, - IReadModelContext readModelContext, - CancellationToken cancellationToken); - } -} diff --git a/Source/EventFlow/ReadStores/V2/MultipleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/MultipleAggregateReadStoreManager.cs similarity index 94% rename from Source/EventFlow/ReadStores/V2/MultipleAggregateReadStoreManager.cs rename to Source/EventFlow/ReadStores/MultipleAggregateReadStoreManager.cs index 5404e0f56..b0c005e0e 100644 --- a/Source/EventFlow/ReadStores/V2/MultipleAggregateReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/MultipleAggregateReadStoreManager.cs @@ -27,10 +27,10 @@ using EventFlow.Aggregates; using EventFlow.Logs; -namespace EventFlow.ReadStores.V2 +namespace EventFlow.ReadStores { - public class MultipleAggregateReadStoreManager : ReadStoreManagerV2 - where TReadStore : IReadModelStoreV2 + public class MultipleAggregateReadStoreManager : ReadStoreManager + where TReadStore : IReadModelStore where TReadModel : class, IReadModel, new() where TReadModelLocator : IReadModelLocator { diff --git a/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs b/Source/EventFlow/ReadStores/ReadModelEnvelope.cs similarity index 98% rename from Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs rename to Source/EventFlow/ReadStores/ReadModelEnvelope.cs index 0a8b46e70..19acccd7e 100644 --- a/Source/EventFlow/ReadStores/V2/ReadModelEnvelope.cs +++ b/Source/EventFlow/ReadStores/ReadModelEnvelope.cs @@ -20,7 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace EventFlow.ReadStores.V2 +namespace EventFlow.ReadStores { public class ReadModelEnvelope where TReadModel : class, IReadModel, new() diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 869dff494..14c5c0bd4 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -21,19 +21,42 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.Configuration; -using EventFlow.Core; -using EventFlow.EventStores; -using EventFlow.Logs; namespace EventFlow.ReadStores { + public class ReadModelPopulator : IReadModelPopulator + { + public Task PurgeAsync(CancellationToken cancellationToken) where TReadModel : IReadModel + { + throw new NotImplementedException(); + } + + public void Purge(CancellationToken cancellationToken) where TReadModel : IReadModel + { + throw new NotImplementedException(); + } + + public Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel + { + throw new NotImplementedException(); + } + + public void Populate(CancellationToken cancellationToken) where TReadModel : IReadModel + { + throw new NotImplementedException(); + } + + public Task PopulateAggregateReadModelAsync(TIdentity id, + CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity where TReadModel : IReadModel + { + throw new NotImplementedException(); + } + } + + /* public class ReadModelPopulator : IReadModelPopulator { private readonly ILog _log; @@ -199,4 +222,5 @@ public void Populate(CancellationToken cancellationToken) } } } + */ } diff --git a/Source/EventFlow/ReadStores/ReadModelStore.cs b/Source/EventFlow/ReadStores/ReadModelStore.cs index 8fab0d74b..c5c199ff5 100644 --- a/Source/EventFlow/ReadStores/ReadModelStore.cs +++ b/Source/EventFlow/ReadStores/ReadModelStore.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -28,38 +29,32 @@ namespace EventFlow.ReadStores { - public abstract class ReadModelStore : IReadModelStore - where TReadModel : IReadModel + public abstract class ReadModelStore : IReadModelStore + where TReadModel : class, IReadModel, new() { protected ILog Log { get; private set; } - protected IReadModelDomainEventApplier ReadModelDomainEventApplier { get; private set; } protected ReadModelStore( - ILog log, - IReadModelDomainEventApplier readModelDomainEventApplier) + ILog log) { Log = log; - ReadModelDomainEventApplier = readModelDomainEventApplier; } - public abstract Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, + public abstract Task> GetAsync( + string id, CancellationToken cancellationToken); - public abstract Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - where TReadModelToPopulate : IReadModel; + public abstract Task DeleteAsync( + string id, + CancellationToken cancellationToken); - public abstract Task PurgeAsync( - CancellationToken cancellationToken) - where TReadModelToPurge : IReadModel; + public abstract Task DeleteAllAsync( + CancellationToken cancellationToken); - public abstract Task PopulateReadModelAsync( - string id, - IReadOnlyCollection domainEvents, + public abstract Task UpdateAsync( + IReadOnlyCollection readModelUpdates, IReadModelContext readModelContext, - CancellationToken cancellationToken) - where TReadModelToPopulate : IReadModel; + Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, + CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index 35d632204..88116ae82 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -26,22 +26,56 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.Configuration; using EventFlow.Logs; namespace EventFlow.ReadStores { - public class ReadStoreManager : IReadStoreManager + public abstract class ReadStoreManager : IReadStoreManager + where TReadModelStore : IReadModelStore + where TReadModel : class, IReadModel, new() { - private readonly ILog _log; - private readonly IResolver _resolver; + // ReSharper disable StaticMemberInGenericType + private static readonly Type ReadModelType = typeof(TReadModel); + private static readonly ISet AggregateTypes; + private static readonly ISet DomainEventTypes; + // ReSharper enable StaticMemberInGenericType - public ReadStoreManager( + protected ILog Log { get; private set; } + protected TReadModelStore ReadModelStore { get; private set; } + protected IReadModelDomainEventApplier ReadModelDomainEventApplier { get; private set; } + + protected ISet GetAggregateTypes() { return AggregateTypes; } + protected ISet GetDomainEventTypes() { return DomainEventTypes; } + + static ReadStoreManager() + { + var iAmReadModelForInterfaceTypes = ReadModelType + .GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IAmReadModelFor<,,>)) + .ToList(); + if (!iAmReadModelForInterfaceTypes.Any()) + { + throw new ArgumentException(string.Format( + "Read model type '{0}' does not implement any 'IAmReadModelFor<>'", + ReadModelType.Name)); + } + + AggregateTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetGenericArguments()[0])); + DomainEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => + { + var genericArguments = i.GetGenericArguments(); + return typeof (IDomainEvent<,,>).MakeGenericType(genericArguments); + })); + } + + protected ReadStoreManager( ILog log, - IResolver resolver) + TReadModelStore readModelStore, + IReadModelDomainEventApplier readModelDomainEventApplier) { - _log = log; - _resolver = resolver; + Log = log; + ReadModelStore = readModelStore; + ReadModelDomainEventApplier = readModelDomainEventApplier; } public async Task UpdateReadStoresAsync( @@ -51,29 +85,48 @@ public async Task UpdateReadStoresAsync( where TAggregate : IAggregateRoot where TIdentity : IIdentity { - var readModelStores = _resolver.Resolve>().ToList(); - var updateTasks = readModelStores - .Select(s => UpdateReadStoreAsync(s, domainEvents, cancellationToken)) - .ToArray(); - await Task.WhenAll(updateTasks).ConfigureAwait(false); - } - - private async Task UpdateReadStoreAsync( - IReadModelStore readModelStore, - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - { - try + var aggregateType = typeof (TAggregate); + if (!AggregateTypes.Contains(aggregateType)) { - await readModelStore.ApplyDomainEventsAsync(domainEvents, cancellationToken).ConfigureAwait(false); + Log.Verbose(() => string.Format( + "Read model does not care about aggregate '{0}' so skipping update, only these: {1}", + ReadModelType.Name, + string.Join(", ", AggregateTypes.Select(t => t.Name)) + )); + return; } - catch (Exception exception) + + var relevantDomainEvents = domainEvents + .Where(e => DomainEventTypes.Contains(e.GetType())) + .ToList(); + if (!relevantDomainEvents.Any()) { - _log.Error( - exception, - "Failed to updated read model store {0}", - readModelStore.GetType().Name); + Log.Verbose(() => string.Format( + "None of these events was relevant for read model '{0}', skipping update: {1}", + ReadModelType.Name, + string.Join(", ", domainEvents.Select(e => e.EventType.Name)) + )); + return; } + + var readModelContext = new ReadModelContext(); + var readModelUpdates = BuildReadModelUpdates(relevantDomainEvents); + + await ReadModelStore.UpdateAsync( + readModelUpdates, + readModelContext, + UpdateAsync, + cancellationToken) + .ConfigureAwait(false); } + + protected abstract IReadOnlyCollection BuildReadModelUpdates( + IReadOnlyCollection domainEvents); + + protected abstract Task> UpdateAsync( + IReadModelContext readModelContext, + IReadOnlyCollection domainEvents, + ReadModelEnvelope readModelEnvelope, + CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs b/Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs deleted file mode 100644 index 166a9d449..000000000 --- a/Source/EventFlow/ReadStores/SingleAggregateReadModelStore.cs +++ /dev/null @@ -1,70 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.Logs; - -namespace EventFlow.ReadStores -{ - public abstract class SingleAggregateReadModelStore : - ReadModelStore - where TReadModel : IReadModel - { - protected SingleAggregateReadModelStore( - ILog log, - IReadModelDomainEventApplier readModelDomainEventApplier) - : base(log, readModelDomainEventApplier) - { - } - - public override Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); - } - - public override Task ApplyDomainEventsAsync( - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); - } - - public override Task PurgeAsync(CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); - } - - public override Task PopulateReadModelAsync( - string id, - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - CancellationToken cancellationToken) - { - throw new System.NotImplementedException(); - } - } -} diff --git a/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/SingleAggregateReadStoreManager.cs similarity index 95% rename from Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs rename to Source/EventFlow/ReadStores/SingleAggregateReadStoreManager.cs index b46573036..a78d5cc23 100644 --- a/Source/EventFlow/ReadStores/V2/SingleAggregateReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/SingleAggregateReadStoreManager.cs @@ -28,10 +28,10 @@ using EventFlow.Aggregates; using EventFlow.Logs; -namespace EventFlow.ReadStores.V2 +namespace EventFlow.ReadStores { - public class SingleAggregateReadStoreManager : ReadStoreManagerV2 - where TReadModelStore : IReadModelStoreV2 + public class SingleAggregateReadStoreManager : ReadStoreManager + where TReadModelStore : IReadModelStore where TReadModel : class, IReadModel, new() { public SingleAggregateReadStoreManager( diff --git a/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs deleted file mode 100644 index 0cfc08cf2..000000000 --- a/Source/EventFlow/ReadStores/V2/IReadModelStoreV2.cs +++ /dev/null @@ -1,51 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; - -namespace EventFlow.ReadStores.V2 -{ - public interface IReadModelStoreV2 - where TReadModel : class, IReadModel, new() - { - Task> GetAsync( - string id, - CancellationToken cancellationToken); - - Task DeleteAsync( - string id, - CancellationToken cancellationToken); - - Task DeleteAllAsync( - CancellationToken cancellationToken); - - Task UpdateAsync( - IReadOnlyCollection readModelUpdates, - IReadModelContext readModelContext, - Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, - CancellationToken cancellationToken); - } -} diff --git a/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs b/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs deleted file mode 100644 index 42a808b70..000000000 --- a/Source/EventFlow/ReadStores/V2/ReadModelStoreV2.cs +++ /dev/null @@ -1,60 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.Logs; - -namespace EventFlow.ReadStores.V2 -{ - public abstract class ReadModelStoreV2 : IReadModelStoreV2 - where TReadModel : class, IReadModel, new() - { - protected ILog Log { get; private set; } - - protected ReadModelStoreV2( - ILog log) - { - Log = log; - } - - public abstract Task> GetAsync( - string id, - CancellationToken cancellationToken); - - public abstract Task DeleteAsync( - string id, - CancellationToken cancellationToken); - - public abstract Task DeleteAllAsync( - CancellationToken cancellationToken); - - public abstract Task UpdateAsync( - IReadOnlyCollection readModelUpdates, - IReadModelContext readModelContext, - Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, - CancellationToken cancellationToken); - } -} diff --git a/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs b/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs deleted file mode 100644 index 8153bf463..000000000 --- a/Source/EventFlow/ReadStores/V2/ReadStoreManagerV2.cs +++ /dev/null @@ -1,132 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using EventFlow.Aggregates; -using EventFlow.Logs; - -namespace EventFlow.ReadStores.V2 -{ - public abstract class ReadStoreManagerV2 : IReadStoreManager - where TReadModelStore : IReadModelStoreV2 - where TReadModel : class, IReadModel, new() - { - // ReSharper disable StaticMemberInGenericType - private static readonly Type ReadModelType = typeof(TReadModel); - private static readonly ISet AggregateTypes; - private static readonly ISet DomainEventTypes; - // ReSharper enable StaticMemberInGenericType - - protected ILog Log { get; private set; } - protected TReadModelStore ReadModelStore { get; private set; } - protected IReadModelDomainEventApplier ReadModelDomainEventApplier { get; private set; } - - protected ISet GetAggregateTypes() { return AggregateTypes; } - protected ISet GetDomainEventTypes() { return DomainEventTypes; } - - static ReadStoreManagerV2() - { - var iAmReadModelForInterfaceTypes = ReadModelType - .GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IAmReadModelFor<,,>)) - .ToList(); - if (!iAmReadModelForInterfaceTypes.Any()) - { - throw new ArgumentException(string.Format( - "Read model type '{0}' does not implement any 'IAmReadModelFor<>'", - ReadModelType.Name)); - } - - AggregateTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetGenericArguments()[0])); - DomainEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => - { - var genericArguments = i.GetGenericArguments(); - return typeof (IDomainEvent<,,>).MakeGenericType(genericArguments); - })); - } - - protected ReadStoreManagerV2( - ILog log, - TReadModelStore readModelStore, - IReadModelDomainEventApplier readModelDomainEventApplier) - { - Log = log; - ReadModelStore = readModelStore; - ReadModelDomainEventApplier = readModelDomainEventApplier; - } - - public async Task UpdateReadStoresAsync( - TIdentity id, - IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - { - var aggregateType = typeof (TAggregate); - if (!AggregateTypes.Contains(aggregateType)) - { - Log.Verbose(() => string.Format( - "Read model does not care about aggregate '{0}' so skipping update, only these: {1}", - ReadModelType.Name, - string.Join(", ", AggregateTypes.Select(t => t.Name)) - )); - return; - } - - var relevantDomainEvents = domainEvents - .Where(e => DomainEventTypes.Contains(e.GetType())) - .ToList(); - if (!relevantDomainEvents.Any()) - { - Log.Verbose(() => string.Format( - "None of these events was relevant for read model '{0}', skipping update: {1}", - ReadModelType.Name, - string.Join(", ", domainEvents.Select(e => e.EventType.Name)) - )); - return; - } - - var readModelContext = new ReadModelContext(); - var readModelUpdates = BuildReadModelUpdates(relevantDomainEvents); - - await ReadModelStore.UpdateAsync( - readModelUpdates, - readModelContext, - UpdateAsync, - cancellationToken) - .ConfigureAwait(false); - } - - protected abstract IReadOnlyCollection BuildReadModelUpdates( - IReadOnlyCollection domainEvents); - - protected abstract Task> UpdateAsync( - IReadModelContext readModelContext, - IReadOnlyCollection domainEvents, - ReadModelEnvelope readModelEnvelope, - CancellationToken cancellationToken); - } -} From 3963d91be640e59d4b97d11b9ba2fce58fdf3889 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 28 May 2015 05:53:39 +0200 Subject: [PATCH 38/47] Register in-memory query handler --- Source/EventFlow/EventFlow.csproj | 4 +-- .../EventFlowOptionsReadStoresExtensions.cs | 15 +++++++++-- .../{ => InMemory}/IInMemoryReadStore.cs | 10 +++++++- .../InMemory/InMemoryQueryHandler.cs | 25 ++++++++----------- .../{ => InMemory}/InMemoryReadStore.cs | 22 +++++++++++++--- .../InMemory/Queries/InMemoryQuery.cs | 2 +- 6 files changed, 53 insertions(+), 25 deletions(-) rename Source/EventFlow/ReadStores/{ => InMemory}/IInMemoryReadStore.cs (81%) rename Source/EventFlow/ReadStores/{ => InMemory}/InMemoryReadStore.cs (87%) diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 232825e1d..a495df082 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -121,8 +121,8 @@ - - + + diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs index 346027b13..8ae3efe44 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs @@ -20,9 +20,12 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System.Collections.Generic; using EventFlow.Configuration.Registrations; using EventFlow.Queries; using EventFlow.ReadStores; +using EventFlow.ReadStores.InMemory; +using EventFlow.ReadStores.InMemory.Queries; namespace EventFlow.Extensions { @@ -58,7 +61,11 @@ public static EventFlowOptions UseInMemoryReadStoreFor( where TReadModel : class, IReadModel, new() { return eventFlowOptions - .RegisterServices(f => f.Register, InMemoryReadStore>(Lifetime.Singleton)) + .RegisterServices(f => + { + f.Register, InMemoryReadStore>(Lifetime.Singleton); + f.Register, IReadOnlyCollection>, InMemoryQueryHandler>(); + }) .UseReadStoreFor, TReadModel>(); } @@ -68,7 +75,11 @@ public static EventFlowOptions UseInMemoryReadStoreFor f.Register, InMemoryReadStore>(Lifetime.Singleton)) + .RegisterServices(f => + { + f.Register, InMemoryReadStore>(Lifetime.Singleton); + f.Register, IReadOnlyCollection>, InMemoryQueryHandler>(); + }) .UseReadStoreFor, TReadModel, TReadModelLocator>(); } } diff --git a/Source/EventFlow/ReadStores/IInMemoryReadStore.cs b/Source/EventFlow/ReadStores/InMemory/IInMemoryReadStore.cs similarity index 81% rename from Source/EventFlow/ReadStores/IInMemoryReadStore.cs rename to Source/EventFlow/ReadStores/InMemory/IInMemoryReadStore.cs index 37114a414..f10168b75 100644 --- a/Source/EventFlow/ReadStores/IInMemoryReadStore.cs +++ b/Source/EventFlow/ReadStores/InMemory/IInMemoryReadStore.cs @@ -20,10 +20,18 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -namespace EventFlow.ReadStores +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace EventFlow.ReadStores.InMemory { public interface IInMemoryReadStore : IReadModelStore where TReadModel : class, IReadModel, new() { + Task> FindAsync( + Predicate predicate, + CancellationToken cancellationToken); } } diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs index 64460e9f7..45b179273 100644 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryQueryHandler.cs @@ -27,29 +27,24 @@ using EventFlow.ReadStores.InMemory.Queries; namespace EventFlow.ReadStores.InMemory -{/* +{ public class InMemoryQueryHandler : - IQueryHandler, IEnumerable>, - IQueryHandler, TReadModel> - where TReadModel : IReadModel, new() + IQueryHandler, IReadOnlyCollection> + where TReadModel : class, IReadModel, new() { - private readonly IInMemoryReadModelStore _readModelStore; + private readonly IInMemoryReadStore _readModelStore; public InMemoryQueryHandler( - IInMemoryReadModelStore readModelStore) + IInMemoryReadStore readModelStore) { _readModelStore = readModelStore; } - public Task> ExecuteQueryAsync(InMemoryQuery query, CancellationToken cancellationToken) + public Task> ExecuteQueryAsync( + InMemoryQuery query, + CancellationToken cancellationToken) { - var result = _readModelStore.Find(query.Query); - return Task.FromResult(result); + return _readModelStore.FindAsync(query.Query, cancellationToken); } - - public Task ExecuteQueryAsync(ReadModelByIdQuery query, CancellationToken cancellationToken) - { - return _readModelStore.GetByIdAsync(query.Id, cancellationToken); - } - }*/ + } } diff --git a/Source/EventFlow/ReadStores/InMemoryReadStore.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryReadStore.cs similarity index 87% rename from Source/EventFlow/ReadStores/InMemoryReadStore.cs rename to Source/EventFlow/ReadStores/InMemory/InMemoryReadStore.cs index d0a8b9549..44cd5fb6c 100644 --- a/Source/EventFlow/ReadStores/InMemoryReadStore.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryReadStore.cs @@ -22,13 +22,14 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.Core; using EventFlow.Logs; -namespace EventFlow.ReadStores +namespace EventFlow.ReadStores.InMemory { public class InMemoryReadStore : ReadModelStore, IInMemoryReadStore where TReadModel : class, IReadModel, new() @@ -46,7 +47,7 @@ public override async Task> GetAsync( string id, CancellationToken cancellationToken) { - using (await _asyncLock.WaitAsync(cancellationToken)) + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { ReadModelEnvelope readModelEnvelope; return _readModels.TryGetValue(id, out readModelEnvelope) @@ -55,11 +56,24 @@ public override async Task> GetAsync( } } + public async Task> FindAsync( + Predicate predicate, + CancellationToken cancellationToken) + { + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) + { + return _readModels.Values + .Where(e => predicate(e.ReadModel)) + .Select(e => e.ReadModel) + .ToList(); + } + } + public override async Task DeleteAsync( string id, CancellationToken cancellationToken) { - using (await _asyncLock.WaitAsync(cancellationToken)) + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { if (_readModels.ContainsKey(id)) { @@ -71,7 +85,7 @@ public override async Task DeleteAsync( public async override Task DeleteAllAsync( CancellationToken cancellationToken) { - using (await _asyncLock.WaitAsync(cancellationToken)) + using (await _asyncLock.WaitAsync(cancellationToken).ConfigureAwait(false)) { _readModels.Clear(); } diff --git a/Source/EventFlow/ReadStores/InMemory/Queries/InMemoryQuery.cs b/Source/EventFlow/ReadStores/InMemory/Queries/InMemoryQuery.cs index 790fa9831..b194865e3 100644 --- a/Source/EventFlow/ReadStores/InMemory/Queries/InMemoryQuery.cs +++ b/Source/EventFlow/ReadStores/InMemory/Queries/InMemoryQuery.cs @@ -26,7 +26,7 @@ namespace EventFlow.ReadStores.InMemory.Queries { - public class InMemoryQuery : IQuery> + public class InMemoryQuery : IQuery> where TReadModel : IReadModel, new() { public Predicate Query { get; private set; } From e325ec257be2cd2393fac601f0a248c04db99494 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 28 May 2015 19:33:05 +0200 Subject: [PATCH 39/47] Fixes to in-memory read store --- .../ReadStores/InMemory/InMemoryReadStore.cs | 5 ++++- Source/EventFlow/ReadStores/ReadStoreManager.cs | 12 ++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Source/EventFlow/ReadStores/InMemory/InMemoryReadStore.cs b/Source/EventFlow/ReadStores/InMemory/InMemoryReadStore.cs index 44cd5fb6c..c7680c23a 100644 --- a/Source/EventFlow/ReadStores/InMemory/InMemoryReadStore.cs +++ b/Source/EventFlow/ReadStores/InMemory/InMemoryReadStore.cs @@ -102,7 +102,10 @@ public override async Task UpdateAsync( foreach (var readModelUpdate in readModelUpdates) { ReadModelEnvelope readModelEnvelope; - _readModels.TryGetValue(readModelUpdate.ReadModelId, out readModelEnvelope); + if (!_readModels.TryGetValue(readModelUpdate.ReadModelId, out readModelEnvelope)) + { + readModelEnvelope = ReadModelEnvelope.Empty; + } readModelEnvelope = await updateReadModel( readModelContext, diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index 88116ae82..a26465412 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -37,7 +37,7 @@ public abstract class ReadStoreManager : IReadStore // ReSharper disable StaticMemberInGenericType private static readonly Type ReadModelType = typeof(TReadModel); private static readonly ISet AggregateTypes; - private static readonly ISet DomainEventTypes; + private static readonly ISet AggregateEventTypes; // ReSharper enable StaticMemberInGenericType protected ILog Log { get; private set; } @@ -45,7 +45,7 @@ public abstract class ReadStoreManager : IReadStore protected IReadModelDomainEventApplier ReadModelDomainEventApplier { get; private set; } protected ISet GetAggregateTypes() { return AggregateTypes; } - protected ISet GetDomainEventTypes() { return DomainEventTypes; } + protected ISet GetDomainEventTypes() { return AggregateEventTypes; } static ReadStoreManager() { @@ -61,11 +61,7 @@ static ReadStoreManager() } AggregateTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetGenericArguments()[0])); - DomainEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => - { - var genericArguments = i.GetGenericArguments(); - return typeof (IDomainEvent<,,>).MakeGenericType(genericArguments); - })); + AggregateEventTypes = new HashSet(iAmReadModelForInterfaceTypes.Select(i => i.GetGenericArguments()[2])); } protected ReadStoreManager( @@ -97,7 +93,7 @@ public async Task UpdateReadStoresAsync( } var relevantDomainEvents = domainEvents - .Where(e => DomainEventTypes.Contains(e.GetType())) + .Where(e => AggregateEventTypes.Contains(e.EventType)) .ToList(); if (!relevantDomainEvents.Any()) { From a399c16ffb0ba88602a29de2f16b00fd406bb71d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 28 May 2015 19:54:33 +0200 Subject: [PATCH 40/47] Fixed the last few tests --- .../Extensions/EventFlowOptionsExtensions.cs | 2 + .../EventFlowOptionsReadStoresExtensions.cs | 2 + .../ReadStores/IReadModelPopulator.cs | 16 +- .../EventFlow/ReadStores/IReadStoreManager.cs | 12 +- .../MultipleAggregateReadStoreManager.cs | 3 +- .../ReadStores/ReadModelPopulator.cs | 145 ++++++------------ .../EventFlow/ReadStores/ReadStoreManager.cs | 18 +-- .../Subscribers/DomainEventPublisher.cs | 2 +- 8 files changed, 66 insertions(+), 134 deletions(-) diff --git a/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs b/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs index 349afa0c9..99b7c26bb 100644 --- a/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs +++ b/Source/EventFlow.ReadStores.MsSql/Extensions/EventFlowOptionsExtensions.cs @@ -40,6 +40,7 @@ public static EventFlowOptions UseMssqlReadModel( f.Register(Lifetime.Singleton); } f.Register, MssqlReadModelStore>(); + f.Register>(r => r.Resolver.Resolve>()); }) .UseReadStoreFor, TReadModel, TReadModelLocator>(); @@ -58,6 +59,7 @@ public static EventFlowOptions UseMssqlReadModel( f.Register(Lifetime.Singleton); } f.Register, MssqlReadModelStore>(); + f.Register>(r => r.Resolver.Resolve>()); }) .UseReadStoreFor, TReadModel>(); diff --git a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs index 8ae3efe44..bed1bc065 100644 --- a/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs +++ b/Source/EventFlow/Extensions/EventFlowOptionsReadStoresExtensions.cs @@ -64,6 +64,7 @@ public static EventFlowOptions UseInMemoryReadStoreFor( .RegisterServices(f => { f.Register, InMemoryReadStore>(Lifetime.Singleton); + f.Register>(r => r.Resolver.Resolve>()); f.Register, IReadOnlyCollection>, InMemoryQueryHandler>(); }) .UseReadStoreFor, TReadModel>(); @@ -78,6 +79,7 @@ public static EventFlowOptions UseInMemoryReadStoreFor { f.Register, InMemoryReadStore>(Lifetime.Singleton); + f.Register>(r => r.Resolver.Resolve>()); f.Register, IReadOnlyCollection>, InMemoryQueryHandler>(); }) .UseReadStoreFor, TReadModel, TReadModelLocator>(); diff --git a/Source/EventFlow/ReadStores/IReadModelPopulator.cs b/Source/EventFlow/ReadStores/IReadModelPopulator.cs index 6fcfc3824..dafabf2a3 100644 --- a/Source/EventFlow/ReadStores/IReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/IReadModelPopulator.cs @@ -22,29 +22,21 @@ using System.Threading; using System.Threading.Tasks; -using EventFlow.Aggregates; namespace EventFlow.ReadStores { public interface IReadModelPopulator { Task PurgeAsync(CancellationToken cancellationToken) - where TReadModel : IReadModel; + where TReadModel : class, IReadModel, new(); void Purge(CancellationToken cancellationToken) - where TReadModel : IReadModel; + where TReadModel : class, IReadModel, new(); Task PopulateAsync(CancellationToken cancellationToken) - where TReadModel : IReadModel; + where TReadModel : class, IReadModel, new(); void Populate(CancellationToken cancellationToken) - where TReadModel : IReadModel; - - Task PopulateAggregateReadModelAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - where TReadModel : IReadModel; + where TReadModel : class, IReadModel, new(); } } diff --git a/Source/EventFlow/ReadStores/IReadStoreManager.cs b/Source/EventFlow/ReadStores/IReadStoreManager.cs index f7203df22..837224d40 100644 --- a/Source/EventFlow/ReadStores/IReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/IReadStoreManager.cs @@ -29,11 +29,13 @@ namespace EventFlow.ReadStores { public interface IReadStoreManager { - Task UpdateReadStoresAsync( - TIdentity id, + Task UpdateReadStoresAsync( IReadOnlyCollection domainEvents, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity; + CancellationToken cancellationToken); + } + + public interface IReadStoreManager : IReadStoreManager + where TReadModel : class, IReadModel, new() + { } } diff --git a/Source/EventFlow/ReadStores/MultipleAggregateReadStoreManager.cs b/Source/EventFlow/ReadStores/MultipleAggregateReadStoreManager.cs index b0c005e0e..560cd0b08 100644 --- a/Source/EventFlow/ReadStores/MultipleAggregateReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/MultipleAggregateReadStoreManager.cs @@ -29,7 +29,8 @@ namespace EventFlow.ReadStores { - public class MultipleAggregateReadStoreManager : ReadStoreManager + public class MultipleAggregateReadStoreManager : + ReadStoreManager where TReadStore : IReadModelStore where TReadModel : class, IReadModel, new() where TReadModelLocator : IReadModelLocator diff --git a/Source/EventFlow/ReadStores/ReadModelPopulator.cs b/Source/EventFlow/ReadStores/ReadModelPopulator.cs index 14c5c0bd4..0938ad91b 100644 --- a/Source/EventFlow/ReadStores/ReadModelPopulator.cs +++ b/Source/EventFlow/ReadStores/ReadModelPopulator.cs @@ -21,70 +21,54 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using EventFlow.Aggregates; +using EventFlow.Configuration; +using EventFlow.Core; +using EventFlow.EventStores; +using EventFlow.Logs; namespace EventFlow.ReadStores { - public class ReadModelPopulator : IReadModelPopulator - { - public Task PurgeAsync(CancellationToken cancellationToken) where TReadModel : IReadModel - { - throw new NotImplementedException(); - } - - public void Purge(CancellationToken cancellationToken) where TReadModel : IReadModel - { - throw new NotImplementedException(); - } - - public Task PopulateAsync(CancellationToken cancellationToken) where TReadModel : IReadModel - { - throw new NotImplementedException(); - } - - public void Populate(CancellationToken cancellationToken) where TReadModel : IReadModel - { - throw new NotImplementedException(); - } - - public Task PopulateAggregateReadModelAsync(TIdentity id, - CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity where TReadModel : IReadModel - { - throw new NotImplementedException(); - } - } - - /* public class ReadModelPopulator : IReadModelPopulator { private readonly ILog _log; private readonly IEventFlowConfiguration _configuration; private readonly IEventStore _eventStore; - private readonly IReadOnlyCollection _readModelStores; + private readonly IResolver _resolver; public ReadModelPopulator( ILog log, IEventFlowConfiguration configuration, IEventStore eventStore, - IEnumerable readModelStores) + IResolver resolver) { _log = log; _configuration = configuration; _eventStore = eventStore; - _readModelStores = readModelStores.ToList(); + _resolver = resolver; } public Task PurgeAsync(CancellationToken cancellationToken) - where TReadModel : IReadModel + where TReadModel : class, IReadModel, new() { - var purgeTasks = _readModelStores.Select(s => s.PurgeAsync(cancellationToken)); - return Task.WhenAll(purgeTasks); + var readModelStores = _resolver.Resolve>>().ToList(); + if (!readModelStores.Any()) + { + throw new ArgumentException(string.Format( + "Could not find any read stores for read model '{0}'", + typeof(TReadModel).Name)); + } + + var deleteTasks = readModelStores.Select(s => s.DeleteAllAsync(cancellationToken)); + return Task.WhenAll(deleteTasks); } public void Purge(CancellationToken cancellationToken) - where TReadModel : IReadModel + where TReadModel : class, IReadModel, new() { using (var a = AsyncHelper.Wait) { @@ -92,12 +76,14 @@ public void Purge(CancellationToken cancellationToken) } } - public async Task PopulateAsync(CancellationToken cancellationToken) - where TReadModel : IReadModel + public async Task PopulateAsync( + CancellationToken cancellationToken) + where TReadModel : class, IReadModel, new() { var stopwatch = Stopwatch.StartNew(); - var readModelType = typeof (TReadModel); + var readStoreManagers = ResolveReadStoreManager(); + var aggregateEventTypes = new HashSet(readModelType .GetInterfaces() .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IAmReadModelFor<,,>)) @@ -143,8 +129,8 @@ public async Task PopulateAsync(CancellationToken cancellationToken) continue; } - var applyTasks = _readModelStores - .Select(rms => rms.ApplyDomainEventsAsync(domainEvents, cancellationToken)); + var applyTasks = readStoreManagers + .Select(m => m.UpdateReadStoresAsync(domainEvents, cancellationToken)); await Task.WhenAll(applyTasks).ConfigureAwait(false); } @@ -157,70 +143,31 @@ public async Task PopulateAsync(CancellationToken cancellationToken) relevantEvents); } - public async Task PopulateAggregateReadModelAsync( - TIdentity id, - CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity - where TReadModel : IReadModel + public void Populate(CancellationToken cancellationToken) + where TReadModel : class, IReadModel, new() { - var readModelType = typeof (TReadModel); - var iAmReadModelForInterfaceTypes = readModelType - .GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IAmReadModelFor<,,>)) - .ToList(); - var aggregateTypes = iAmReadModelForInterfaceTypes - .Select(i => i.GetGenericArguments()[0]) - .Distinct() - .ToList(); - - if (!aggregateTypes.Any()) - { - throw new ArgumentException(string.Format( - "Read model type '{0}' does not implement 'IAmReadModelFor<>'", - readModelType.Name)); - } - if (aggregateTypes.Count != 1) - { - throw new ArgumentException(string.Format( - "Use 'PopulateAsync' to populate read models than registers events from more than one aggregate. '{0}' registers to these: {1}", - readModelType.Name, - string.Join(", ", aggregateTypes.Select(t => t.Name)))); - } - if (aggregateTypes.Single() != typeof (TReadModel)) + using (var a = AsyncHelper.Wait) { - throw new ArgumentException(string.Format( - "Read model '{0}' registers to aggregate '{1}', but you supplied '{2}' as the generic argument", - readModelType.Name, - aggregateTypes.Single().Name, - typeof(TAggregate).Name)); + a.Run(PopulateAsync(cancellationToken)); } - - var domainEvens = await _eventStore.LoadEventsAsync( - id, - cancellationToken) - .ConfigureAwait(false); - - var readModelContext = new ReadModelContext(); - - var populateTasks = _readModelStores - .Select(s => s.PopulateReadModelAsync( - id.Value, - domainEvens, - readModelContext, - cancellationToken)); - - await Task.WhenAll(populateTasks).ConfigureAwait(false); } - public void Populate(CancellationToken cancellationToken) - where TReadModel : IReadModel + private IReadOnlyCollection> ResolveReadStoreManager() + where TReadModel : class, IReadModel, new() { - using (var a = AsyncHelper.Wait) + var readStoreManagers = _resolver.Resolve>() + .Select(m => m as IReadStoreManager) + .Where(m => m != null) + .ToList(); + + if (!readStoreManagers.Any()) { - a.Run(PopulateAsync(cancellationToken)); + throw new ArgumentException(string.Format( + "Did not find any read store managers for read model type '{0}'", + typeof(TReadModel).Name)); } + + return readStoreManagers; } } - */ } diff --git a/Source/EventFlow/ReadStores/ReadStoreManager.cs b/Source/EventFlow/ReadStores/ReadStoreManager.cs index a26465412..617f4b8c8 100644 --- a/Source/EventFlow/ReadStores/ReadStoreManager.cs +++ b/Source/EventFlow/ReadStores/ReadStoreManager.cs @@ -30,7 +30,7 @@ namespace EventFlow.ReadStores { - public abstract class ReadStoreManager : IReadStoreManager + public abstract class ReadStoreManager : IReadStoreManager where TReadModelStore : IReadModelStore where TReadModel : class, IReadModel, new() { @@ -74,24 +74,10 @@ protected ReadStoreManager( ReadModelDomainEventApplier = readModelDomainEventApplier; } - public async Task UpdateReadStoresAsync( - TIdentity id, + public async Task UpdateReadStoresAsync( IReadOnlyCollection domainEvents, CancellationToken cancellationToken) - where TAggregate : IAggregateRoot - where TIdentity : IIdentity { - var aggregateType = typeof (TAggregate); - if (!AggregateTypes.Contains(aggregateType)) - { - Log.Verbose(() => string.Format( - "Read model does not care about aggregate '{0}' so skipping update, only these: {1}", - ReadModelType.Name, - string.Join(", ", AggregateTypes.Select(t => t.Name)) - )); - return; - } - var relevantDomainEvents = domainEvents .Where(e => AggregateEventTypes.Contains(e.EventType)) .ToList(); diff --git a/Source/EventFlow/Subscribers/DomainEventPublisher.cs b/Source/EventFlow/Subscribers/DomainEventPublisher.cs index 83730dbe0..e90c1b45f 100644 --- a/Source/EventFlow/Subscribers/DomainEventPublisher.cs +++ b/Source/EventFlow/Subscribers/DomainEventPublisher.cs @@ -51,7 +51,7 @@ public async Task PublishAsync( { // ARGH, dilemma, should we pass the cancellation token to read model update or not? var updateReadStoresTasks = _readStoreManagers - .Select(rsm => rsm.UpdateReadStoresAsync(id, domainEvents, CancellationToken.None)); + .Select(rsm => rsm.UpdateReadStoresAsync(domainEvents, CancellationToken.None)); await Task.WhenAll(updateReadStoresTasks).ConfigureAwait(false); // Update subscriptions AFTER read stores have been updated From 09369f2f42188ea2307a1c726d4cf68ac3fd67a9 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 28 May 2015 19:59:53 +0200 Subject: [PATCH 41/47] Fixed read store populator tests --- .../ReadStores/ReadModelPopulatorTests.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs index dbf160411..ac06d8c94 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs @@ -34,10 +34,9 @@ using EventFlow.TestHelpers.Aggregates.Test.Events; using Moq; using NUnit.Framework; -using Ploeh.AutoFixture; namespace EventFlow.Tests.UnitTests.ReadStores -{/* +{ [Timeout(5000)] public class ReadModelPopulatorTests : TestsFor { @@ -49,9 +48,11 @@ public void Apply(IReadModelContext context, IDomainEvent> _readModelStoreMock; + private Mock> _readModelStoreMock; + private Mock> _readStoreManagerMock; private Mock _eventFlowConfigurationMock; private Mock _eventStoreMock; + private Mock _resolverMock; private List _eventStoreData; [SetUp] @@ -59,10 +60,17 @@ public void SetUp() { _eventStoreMock = InjectMock(); _eventStoreData = null; - _readModelStoreMock = new Mock(); + _resolverMock = InjectMock(); + _readModelStoreMock = new Mock>(); + _readStoreManagerMock = new Mock>(); _eventFlowConfigurationMock = InjectMock(); - Fixture.Inject>(new []{ _readModelStoreMock.Object }); + _resolverMock + .Setup(r => r.Resolve>()) + .Returns(new[] {_readStoreManagerMock.Object}); + _resolverMock + .Setup(r => r.Resolve>>()) + .Returns(new[] {_readModelStoreMock.Object}); _eventFlowConfigurationMock .Setup(c => c.PopulateReadModelEventPageSize) @@ -80,7 +88,7 @@ public async Task PurgeIsCalled() await Sut.PurgeAsync(CancellationToken.None).ConfigureAwait(false); // Assert - _readModelStoreMock.Verify(s => s.PurgeAsync(It.IsAny()), Times.Once); + _readModelStoreMock.Verify(s => s.DeleteAllAsync(It.IsAny()), Times.Once); } [Test] @@ -93,8 +101,8 @@ public async Task PopulateCallsApplyDomainEvents() await Sut.PopulateAsync(CancellationToken.None).ConfigureAwait(false); // Assert - _readModelStoreMock.Verify( - s => s.ApplyDomainEventsAsync( + _readStoreManagerMock.Verify( + s => s.UpdateReadStoresAsync( It.Is>(l => l.Count == 3), It.IsAny()), Times.Exactly(2)); @@ -116,9 +124,9 @@ public async Task UnwantedEventsAreFiltered() await Sut.PopulateAsync(CancellationToken.None).ConfigureAwait(false); // Assert - _readModelStoreMock + _readStoreManagerMock .Verify( - s => s.ApplyDomainEventsAsync( + s => s.UpdateReadStoresAsync( It.Is>(l => l.Count == 2 && l.All(e => e.EventType == typeof(PingEvent))), It.IsAny()), Times.Once); @@ -144,5 +152,5 @@ private void ArrangeEventStore(IEnumerable domainEvents) { _eventStoreData = domainEvents.ToList(); } - }*/ + } } From eee7676b6d9dd2b6e3848e60b1e0bd1eb6a0082d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 28 May 2015 20:14:06 +0200 Subject: [PATCH 42/47] Added test --- Source/EventFlow.Tests/EventFlow.Tests.csproj | 1 + .../ReadStores/ReadStoreManagerTests.cs | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTests.cs diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 1e0366402..ed9a6a0b6 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -80,6 +80,7 @@ + diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTests.cs new file mode 100644 index 000000000..d021cb40c --- /dev/null +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadStoreManagerTests.cs @@ -0,0 +1,78 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Aggregates; +using EventFlow.ReadStores; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates.Test; +using EventFlow.TestHelpers.Aggregates.Test.Events; +using Moq; +using NUnit.Framework; + +namespace EventFlow.Tests.UnitTests.ReadStores +{ + public class ReadStoreManagerTests : TestsFor, ReadStoreManagerTests.TestReadModel>> + { + public class TestReadModel : IReadModel, + IAmReadModelFor + { + public void Apply(IReadModelContext context, IDomainEvent e) + { + } + } + + private Mock> _readModelStoreMock; + + [SetUp] + public void SetUp() + { + _readModelStoreMock = InjectMock>(); + } + + [Test] + public async Task ReadStoreIsUpdatedWithRelevantEvents() + { + // Arrange + var events = new [] + { + ToDomainEvent(A()), + ToDomainEvent(A()), + }; + + // Act + await Sut.UpdateReadStoresAsync(events, CancellationToken.None).ConfigureAwait(false); + + // Assert + _readModelStoreMock.Verify( + s => s.UpdateAsync( + It.Is>(l => l.Count == 1), + It.IsAny(), + It.IsAny, ReadModelEnvelope, CancellationToken, Task>>>(), + It.IsAny()), + Times.Once); + } + } +} From 6bb0e1d5d24757acb453d9a260cc34b9c841f806 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 28 May 2015 22:56:05 +0200 Subject: [PATCH 43/47] Testing and log --- .../IntegrationTests/DomainTests.cs | 40 +++++++++++++++++-- Source/EventFlow/Queries/QueryProcessor.cs | 13 +++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs b/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs index ef453fcd9..206d44394 100644 --- a/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/DomainTests.cs @@ -21,6 +21,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; @@ -29,13 +30,13 @@ using EventFlow.MetadataProviders; using EventFlow.Queries; using EventFlow.ReadStores; -using EventFlow.ReadStores.InMemory; using EventFlow.ReadStores.InMemory.Queries; using EventFlow.Subscribers; using EventFlow.TestHelpers.Aggregates.Test; using EventFlow.TestHelpers.Aggregates.Test.Commands; using EventFlow.TestHelpers.Aggregates.Test.Events; using EventFlow.TestHelpers.Aggregates.Test.ReadModels; +using EventFlow.TestHelpers.Aggregates.Test.ValueObjects; using FluentAssertions; using NUnit.Framework; @@ -53,6 +54,33 @@ public Task HandleAsync(IDomainEvent + { + public PingId Id { get; private set; } + + public void Apply(IReadModelContext context, IDomainEvent e) + { + Id = e.AggregateEvent.PingId; + } + } + + public interface IPingReadModelLocator : IReadModelLocator { } + + public class PingReadModelLocator : IPingReadModelLocator + { + public IEnumerable GetReadModelIds(IDomainEvent domainEvent) + { + var pingEvent = domainEvent as IDomainEvent; + if (pingEvent == null) + { + yield break; + } + yield return pingEvent.AggregateEvent.PingId.Value; + } + } + [Test] public void BasicFlow() { @@ -60,33 +88,37 @@ public void BasicFlow() using (var resolver = EventFlowOptions.New .AddEvents(typeof (TestAggregate).Assembly) .AddCommandHandlers(typeof(TestAggregate).Assembly) + .RegisterServices(f => f.Register()) .AddMetadataProvider() .AddMetadataProvider() .AddMetadataProvider() .UseInMemoryReadStoreFor() + .UseInMemoryReadStoreFor() .AddSubscribers(typeof(Subscriber)) .CreateResolver()) { var commandBus = resolver.Resolve(); var eventStore = resolver.Resolve(); var queryProcessor = resolver.Resolve(); - //var readModelStore = resolver.Resolve>(); var id = TestId.New; // Act commandBus.Publish(new DomainErrorAfterFirstCommand(id), CancellationToken.None); + commandBus.Publish(new PingCommand(id, PingId.New), CancellationToken.None); + commandBus.Publish(new PingCommand(id, PingId.New), CancellationToken.None); var testAggregate = eventStore.LoadAggregate(id, CancellationToken.None); - //var testReadModelFromStore = await readModelStore.GetByIdAsync(id.Value, CancellationToken.None).ConfigureAwait(false); var testReadModelFromQuery1 = queryProcessor.Process( new ReadModelByIdQuery(id.Value), CancellationToken.None); var testReadModelFromQuery2 = queryProcessor.Process( new InMemoryQuery(rm => rm.DomainErrorAfterFirstReceived), CancellationToken.None); + var pingReadModels = queryProcessor.Process( + new InMemoryQuery(m => true), CancellationToken.None); // Assert + pingReadModels.Should().HaveCount(2); testAggregate.DomainErrorAfterFirstReceived.Should().BeTrue(); testReadModelFromQuery1.DomainErrorAfterFirstReceived.Should().BeTrue(); testReadModelFromQuery2.Should().NotBeNull(); - //testReadModelFromStore.Should().NotBeNull(); } } } diff --git a/Source/EventFlow/Queries/QueryProcessor.cs b/Source/EventFlow/Queries/QueryProcessor.cs index a0fc67706..4f653f47c 100644 --- a/Source/EventFlow/Queries/QueryProcessor.cs +++ b/Source/EventFlow/Queries/QueryProcessor.cs @@ -27,6 +27,8 @@ using System.Threading.Tasks; using EventFlow.Configuration; using EventFlow.Core; +using EventFlow.Extensions; +using EventFlow.Logs; namespace EventFlow.Queries { @@ -38,22 +40,31 @@ private class CacheItem public Func HandlerFunc { get; set; } } + private readonly ILog _log; private readonly IResolver _resolver; private readonly ConcurrentDictionary _cacheItems = new ConcurrentDictionary(); public QueryProcessor( + ILog log, IResolver resolver) { + _log = log; _resolver = resolver; } public async Task ProcessAsync(IQuery query, CancellationToken cancellationToken) { + var queryType = query.GetType(); var cacheItem = _cacheItems.GetOrAdd( - query.GetType(), + queryType, CreateCacheItem); var queryHandler = _resolver.Resolve(cacheItem.QueryHandlerType); + _log.Verbose( + "Executing query '{0}' by using query handler '{1}'", + queryType.Name, + cacheItem.QueryHandlerType.Name); + var task = (Task) cacheItem.HandlerFunc(queryHandler, query, cancellationToken); return await task.ConfigureAwait(false); From b039e121256634f4e15b91dde88b80e723fd287d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 28 May 2015 23:01:35 +0200 Subject: [PATCH 44/47] Removed LocateByAggregateId read model locator as it isn't needed any more --- .../MsSqlIntegrationTestConfiguration.cs | 2 +- .../EventStores/FilesEventStoreTests.cs | 2 +- .../IntegrationTests/InMemoryConfiguration.cs | 2 +- Source/EventFlow/EventFlow.csproj | 2 -- Source/EventFlow/EventFlowOptions.cs | 1 - .../ReadStores/ILocateByAggregateId.cs | 28 --------------- .../ReadStores/LocateByAggregateId.cs | 35 ------------------- 7 files changed, 3 insertions(+), 69 deletions(-) delete mode 100644 Source/EventFlow/ReadStores/ILocateByAggregateId.cs delete mode 100644 Source/EventFlow/ReadStores/LocateByAggregateId.cs diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs index e0e3d9e09..0b9ad9d9f 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs @@ -53,7 +53,7 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio var resolver = eventFlowOptions .ConfigureMsSql(MsSqlConfiguration.New.SetConnectionString(TestDatabase.ConnectionString)) .UseEventStore() - .UseMssqlReadModel() + .UseMssqlReadModel() .CreateResolver(); MsSqlConnection = resolver.Resolve(); diff --git a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs index a8496d0b5..d6f19bdfb 100644 --- a/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/EventStores/FilesEventStoreTests.cs @@ -52,7 +52,7 @@ public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptio Directory.CreateDirectory(storePath); var resolver = eventFlowOptions - .UseInMemoryReadStoreFor() + .UseInMemoryReadStoreFor() .UseFilesEventStore(FilesEventStoreConfiguration.Create(storePath)) .CreateResolver(); diff --git a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs index 875a04151..0cec1ebfa 100644 --- a/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs +++ b/Source/EventFlow.Tests/IntegrationTests/InMemoryConfiguration.cs @@ -40,7 +40,7 @@ public class InMemoryConfiguration : IntegrationTestConfiguration public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) { var resolver = eventFlowOptions - .UseInMemoryReadStoreFor() + .UseInMemoryReadStoreFor() .CreateResolver(); _readModelPopulator = resolver.Resolve(); diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index a495df082..eca0378ef 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -125,12 +125,10 @@ - - diff --git a/Source/EventFlow/EventFlowOptions.cs b/Source/EventFlow/EventFlowOptions.cs index 4acace6f1..bfeed1496 100644 --- a/Source/EventFlow/EventFlowOptions.cs +++ b/Source/EventFlow/EventFlowOptions.cs @@ -101,7 +101,6 @@ public IRootResolver CreateResolver(bool validateRegistrations = true) RegisterIfMissing(services, Lifetime.Singleton); RegisterIfMissing(services); RegisterIfMissing(services); - RegisterIfMissing(services); RegisterIfMissing(services); RegisterIfMissing(services, Lifetime.Singleton); RegisterIfMissing(services, Lifetime.Singleton); diff --git a/Source/EventFlow/ReadStores/ILocateByAggregateId.cs b/Source/EventFlow/ReadStores/ILocateByAggregateId.cs deleted file mode 100644 index 9df9ebec5..000000000 --- a/Source/EventFlow/ReadStores/ILocateByAggregateId.cs +++ /dev/null @@ -1,28 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -namespace EventFlow.ReadStores -{ - public interface ILocateByAggregateId : IReadModelLocator - { - } -} diff --git a/Source/EventFlow/ReadStores/LocateByAggregateId.cs b/Source/EventFlow/ReadStores/LocateByAggregateId.cs deleted file mode 100644 index ce7893bf3..000000000 --- a/Source/EventFlow/ReadStores/LocateByAggregateId.cs +++ /dev/null @@ -1,35 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System.Collections.Generic; -using EventFlow.Aggregates; - -namespace EventFlow.ReadStores -{ - public class LocateByAggregateId : ILocateByAggregateId - { - public IEnumerable GetReadModelIds(IDomainEvent domainEvent) - { - yield return domainEvent.GetIdentity().Value; - } - } -} From e28bfc8beb24dfef38f1c50967506aaaf5f0072d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 29 May 2015 05:47:58 +0200 Subject: [PATCH 45/47] Cleanup --- RELEASE_NOTES.md | 2 ++ Source/EventFlow/EventStores/EventStore.cs | 2 +- Source/EventFlow/EventStores/IEventStore.cs | 10 +++++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 248874372..aede6363d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -23,6 +23,8 @@ * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate stream. Please consider carefully if you really want to use it. Storage might be cheaper than the historic knowledge within your events + * New: `IEventStore` now has `LoadAllEventsAsync` and `LoadAllEvents` that + enables you to load all events in the event store a few at a time. * New: `IMetadata.TimestampEpoch` contains the Unix timestamp version of `IMetadata.Timestamp`. Also, an additional metadata key `timestamp_epoch` is added to events containing the same data. Note, diff --git a/Source/EventFlow/EventStores/EventStore.cs b/Source/EventFlow/EventStores/EventStore.cs index 5a4a46edc..4925717ec 100644 --- a/Source/EventFlow/EventStores/EventStore.cs +++ b/Source/EventFlow/EventStores/EventStore.cs @@ -136,7 +136,7 @@ public async Task LoadAllEventsAsync( return new AllEventsPage(allCommittedEventsPage.NextPosition, domainEvents); } - public AllEventsPage LoadEvents( + public AllEventsPage LoadAllEvents( long startPosition, long pageSize, CancellationToken cancellationToken) diff --git a/Source/EventFlow/EventStores/IEventStore.cs b/Source/EventFlow/EventStores/IEventStore.cs index 890d22b40..fff2fb58b 100644 --- a/Source/EventFlow/EventStores/IEventStore.cs +++ b/Source/EventFlow/EventStores/IEventStore.cs @@ -41,6 +41,11 @@ Task LoadAllEventsAsync( long pageSize, CancellationToken cancellationToken); + AllEventsPage LoadAllEvents( + long startPosition, + long pageSize, + CancellationToken cancellationToken); + Task>> LoadEventsAsync( TIdentity id, CancellationToken cancellationToken) @@ -70,10 +75,5 @@ Task DeleteAggregateAsync( CancellationToken cancellationToken) where TAggregate : IAggregateRoot where TIdentity : IIdentity; - - AllEventsPage LoadEvents( - long startPosition, - long pageSize, - CancellationToken cancellationToken); } } From a730e85800979ab7e23713b5e4901340ad10b335 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 29 May 2015 05:55:11 +0200 Subject: [PATCH 46/47] Removed unused methods --- .../ReadModelDomainEventApplierTests.cs | 44 +++++++++++++------ .../IReadModelDomainEventApplier.cs | 14 ------ .../ReadStores/ReadModelDomainEventApplier.cs | 21 --------- 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs index a744247ff..ae1f09acb 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs @@ -21,6 +21,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.Threading; +using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.ReadStores; using EventFlow.TestHelpers; @@ -67,41 +68,46 @@ public void Apply(IReadModelContext context, IDomainEvent()), }; + var readModel = new PingReadModel(); // Act - var readModel = Sut.CreateReadModelAsync(events, A(), CancellationToken.None).Result; + await Sut.UpdateReadModelAsync(readModel, events, A(), CancellationToken.None).ConfigureAwait(false); // Assert readModel.PingEventsReceived.Should().BeFalse(); } [Test] - public void DifferentReadModelsCanSubscribeToSameEvent() + public async Task DifferentReadModelsCanSubscribeToSameEvent() { // Arrange var events = new[] { ToDomainEvent(A()), }; + var pingReadModel = new PingReadModel(); + var theOtherPingReadModel = new TheOtherPingReadModel(); // Act - var pingReadModel = Sut.CreateReadModelAsync( + await Sut.UpdateReadModelAsync( + pingReadModel, events, A(), CancellationToken.None) - .Result; - var theOtherPingReadModel = Sut.CreateReadModelAsync( + .ConfigureAwait(false); + await Sut.UpdateReadModelAsync( + theOtherPingReadModel, events, A(), CancellationToken.None) - .Result; + .ConfigureAwait(false); // Assert pingReadModel.PingEventsReceived.Should().BeTrue(); @@ -109,7 +115,7 @@ public void DifferentReadModelsCanSubscribeToSameEvent() } [Test] - public void DifferentReadModelsCanBeUpdated() + public async Task DifferentReadModelsCanBeUpdated() { // Arrange var events = new[] @@ -117,18 +123,22 @@ public void DifferentReadModelsCanBeUpdated() ToDomainEvent(A()), ToDomainEvent(A()), }; + var pingReadModel = new PingReadModel(); + var domainErrorAfterFirstReadModel = new DomainErrorAfterFirstReadModel(); // Act - var pingReadModel = Sut.CreateReadModelAsync( + await Sut.UpdateReadModelAsync( + pingReadModel, events, A(), CancellationToken.None) - .Result; - var domainErrorAfterFirstReadModel = Sut.CreateReadModelAsync( + .ConfigureAwait(false); + await Sut.UpdateReadModelAsync( + domainErrorAfterFirstReadModel, events, A(), CancellationToken.None) - .Result; + .ConfigureAwait(false); // Assert pingReadModel.PingEventsReceived.Should().BeTrue(); @@ -178,16 +188,22 @@ public void UpdateReturnsTrueIfEventsWereApplied() } [Test] - public void ReadModelReceivesEvent() + public async Task ReadModelReceivesEvent() { // Arrange var events = new[] { ToDomainEvent(A()), }; + var readModel = new PingReadModel(); // Act - var readModel = Sut.CreateReadModelAsync(events, A(), CancellationToken.None).Result; + await Sut.UpdateReadModelAsync( + readModel, + events, + A(), + CancellationToken.None) + .ConfigureAwait(false); // Assert readModel.PingEventsReceived.Should().BeTrue(); diff --git a/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs b/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs index 93cdd9f60..6d413176f 100644 --- a/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs +++ b/Source/EventFlow/ReadStores/IReadModelDomainEventApplier.cs @@ -20,7 +20,6 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -30,19 +29,6 @@ namespace EventFlow.ReadStores { public interface IReadModelDomainEventApplier { - Task CreateReadModelAsync( - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - CancellationToken cancellationToken) - where TReadModel : IReadModel, new(); - - Task CreateReadModelAsync( - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - Func readModelCreator, - CancellationToken cancellationToken) - where TReadModel : IReadModel; - Task UpdateReadModelAsync( TReadModel readModel, IReadOnlyCollection domainEvents, diff --git a/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs b/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs index 219edc569..a3475cc81 100644 --- a/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs +++ b/Source/EventFlow/ReadStores/ReadModelDomainEventApplier.cs @@ -33,27 +33,6 @@ public class ReadModelDomainEventApplier : IReadModelDomainEventApplier { private static readonly ConcurrentDictionary>> ApplyMethods = new ConcurrentDictionary>>(); - public Task CreateReadModelAsync( - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - CancellationToken cancellationToken) - where TReadModel : IReadModel, new() - { - return CreateReadModelAsync(domainEvents, readModelContext, () => new TReadModel(), cancellationToken); - } - - public async Task CreateReadModelAsync( - IReadOnlyCollection domainEvents, - IReadModelContext readModelContext, - Func readModelCreator, - CancellationToken cancellationToken) - where TReadModel : IReadModel - { - var readModel = readModelCreator(); - await UpdateReadModelAsync(readModel, domainEvents, readModelContext, cancellationToken).ConfigureAwait(false); - return readModel; - } - public Task UpdateReadModelAsync( TReadModel readModel, IReadOnlyCollection domainEvents, From 532eaf07f41bc71f8bee6595a8ad583099b35880 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Fri, 29 May 2015 06:53:50 +0200 Subject: [PATCH 47/47] Updated release notes --- RELEASE_NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index aede6363d..cde424dbf 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -23,6 +23,9 @@ * New: `IEventStore.DeleteAggregateAsync` to delete an entire aggregate stream. Please consider carefully if you really want to use it. Storage might be cheaper than the historic knowledge within your events + * New: `IReadModelPopulator` is new and enables you to both purge and + populate read models by going though the entire event store. Currently + its only basic functionality, but more will be added * New: `IEventStore` now has `LoadAllEventsAsync` and `LoadAllEvents` that enables you to load all events in the event store a few at a time. * New: `IMetadata.TimestampEpoch` contains the Unix timestamp version