diff --git a/.gitignore b/.gitignore index a0199d35b..97a000728 100644 --- a/.gitignore +++ b/.gitignore @@ -213,3 +213,6 @@ FakesAssemblies/ # mkdocs output /docs + +# Mac Files +*.DS_Store \ No newline at end of file diff --git a/EventFlow.sln b/EventFlow.sln index 04312f60b..9d3c7fa62 100644 --- a/EventFlow.sln +++ b/EventFlow.sln @@ -63,6 +63,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.SQLite", "Source\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.SQLite.Tests", "Source\EventFlow.SQLite.Tests\EventFlow.SQLite.Tests.csproj", "{8FAC191C-340D-47C6-B8AE-3D57783749C4}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EntityFramework", "EntityFramework", "{21A29AA0-EF82-4717-89CE-729257C05C7D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.EntityFramework", "Source\EventFlow.EntityFramework\EventFlow.EntityFramework.csproj", "{ED4AA905-A208-4E67-8B40-CC8E1B8440B8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.EntityFramework.Tests", "Source\EventFlow.EntityFramework.Tests\EventFlow.EntityFramework.Tests.csproj", "{02B1E8B4-CC53-4BAB-BC2C-AD0B06760AE2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -145,6 +151,14 @@ Global {8FAC191C-340D-47C6-B8AE-3D57783749C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {8FAC191C-340D-47C6-B8AE-3D57783749C4}.Release|Any CPU.ActiveCfg = Release|Any CPU {8FAC191C-340D-47C6-B8AE-3D57783749C4}.Release|Any CPU.Build.0 = Release|Any CPU + {ED4AA905-A208-4E67-8B40-CC8E1B8440B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED4AA905-A208-4E67-8B40-CC8E1B8440B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED4AA905-A208-4E67-8B40-CC8E1B8440B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED4AA905-A208-4E67-8B40-CC8E1B8440B8}.Release|Any CPU.Build.0 = Release|Any CPU + {02B1E8B4-CC53-4BAB-BC2C-AD0B06760AE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02B1E8B4-CC53-4BAB-BC2C-AD0B06760AE2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02B1E8B4-CC53-4BAB-BC2C-AD0B06760AE2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02B1E8B4-CC53-4BAB-BC2C-AD0B06760AE2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -175,6 +189,9 @@ Global {74EFCDE2-CB0F-49B1-9CEC-BE748EB1FBF7} = {88359036-4F35-487C-BF2C-4F31C7BC92D8} {D2B5B5CA-57C2-4354-ADB7-47A6D81AD521} = {74EFCDE2-CB0F-49B1-9CEC-BE748EB1FBF7} {8FAC191C-340D-47C6-B8AE-3D57783749C4} = {74EFCDE2-CB0F-49B1-9CEC-BE748EB1FBF7} + {21A29AA0-EF82-4717-89CE-729257C05C7D} = {5EE323DE-E69B-451A-8AC3-22DD6A004FBA} + {ED4AA905-A208-4E67-8B40-CC8E1B8440B8} = {21A29AA0-EF82-4717-89CE-729257C05C7D} + {02B1E8B4-CC53-4BAB-BC2C-AD0B06760AE2} = {21A29AA0-EF82-4717-89CE-729257C05C7D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17607E2C-4E8E-45A2-85BD-0A5808E1C0F3} diff --git a/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs b/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs index 7991eef53..b7b77e57a 100644 --- a/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs +++ b/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs @@ -26,6 +26,7 @@ using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.ReadModels; using EventFlow.Extensions; using EventFlow.TestHelpers.Aggregates.Entities; +using Microsoft.Extensions.DependencyInjection; namespace EventFlow.EntityFramework.Tests { @@ -46,7 +47,7 @@ public static IEventFlowOptions ConfigureForSnapshotStoreTest(this IEventFlowOpt public static IEventFlowOptions ConfigureForReadStoreTest(this IEventFlowOptions options) { return options - .RegisterServices(sr => sr.RegisterType(typeof(ThingyMessageLocator))) + .RegisterServices(sr => sr.AddTransient(typeof(ThingyMessageLocator))) .UseEntityFrameworkReadModel() .UseEntityFrameworkReadModel() .AddQueryHandlers( diff --git a/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj b/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj index 6f293aba1..efaad38f5 100644 --- a/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj +++ b/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj @@ -1,18 +1,18 @@  - netcoreapp3.1 + net8.0 False - - - + + + - - - + + + diff --git a/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryEventStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryEventStoreTests.cs index ec2ededc8..81cbb4dfe 100644 --- a/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryEventStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryEventStoreTests.cs @@ -20,12 +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.Threading.Tasks; -using EventFlow.Configuration; +using System; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.InMemory @@ -33,13 +33,16 @@ namespace EventFlow.EntityFramework.Tests.InMemory [Category(Categories.Integration)] public class EfInMemoryEventStoreTests : TestSuiteForEventStore { - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { - return eventFlowOptions + eventFlowOptions .ConfigureEntityFramework(EntityFrameworkConfiguration.New) - .AddDbContextProvider(Lifetime.Singleton) - .ConfigureForEventStoreTest() - .CreateResolver(); + .AddDbContextProvider(ServiceLifetime.Singleton) + .ConfigureForEventStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryReadStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryReadStoreTests.cs index d786ea14d..11a6406c6 100644 --- a/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryReadStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemoryReadStoreTests.cs @@ -26,6 +26,7 @@ using EventFlow.EntityFramework.Tests.Model; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.InMemory @@ -35,13 +36,16 @@ public class EfInMemoryReadStoreTests : TestSuiteForReadModelStore { protected override Type ReadModelType => typeof(ThingyReadModelEntity); - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { - return eventFlowOptions + eventFlowOptions .ConfigureEntityFramework(EntityFrameworkConfiguration.New) - .AddDbContextProvider(Lifetime.Singleton) - .ConfigureForReadStoreTest() - .CreateResolver(); + .AddDbContextProvider(ServiceLifetime.Singleton) + .ConfigureForReadStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemorySnapshotTests.cs b/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemorySnapshotTests.cs index 9636135ee..11c90c1bb 100644 --- a/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemorySnapshotTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/InMemory/EfInMemorySnapshotTests.cs @@ -20,11 +20,13 @@ // 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.Configuration; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.InMemory @@ -32,13 +34,16 @@ namespace EventFlow.EntityFramework.Tests.InMemory [Category(Categories.Integration)] public class EfInMemorySnapshotTests : TestSuiteForSnapshotStore { - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { - return eventFlowOptions + eventFlowOptions .ConfigureEntityFramework(EntityFrameworkConfiguration.New) - .AddDbContextProvider(Lifetime.Singleton) - .ConfigureForSnapshotStoreTest() - .CreateResolver(); + .AddDbContextProvider(ServiceLifetime.Singleton) + .ConfigureForSnapshotStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/InMemory/InMemoryDbContextProvider.cs b/Source/EventFlow.EntityFramework.Tests/InMemory/InMemoryDbContextProvider.cs index 59c3f8356..40e2d918d 100644 --- a/Source/EventFlow.EntityFramework.Tests/InMemory/InMemoryDbContextProvider.cs +++ b/Source/EventFlow.EntityFramework.Tests/InMemory/InMemoryDbContextProvider.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 System; +using System.Diagnostics.CodeAnalysis; using EventFlow.EntityFramework.Tests.InMemory.Infrastructure; using EventFlow.EntityFramework.Tests.Model; using Microsoft.EntityFrameworkCore; @@ -27,14 +29,15 @@ namespace EventFlow.EntityFramework.Tests.InMemory { + [SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Only for tests")] public class InMemoryDbContextProvider : IDbContextProvider { private readonly DbContextOptions _options; - + public InMemoryDbContextProvider() { _options = new DbContextOptionsBuilder() - .UseInMemoryDatabase("EventFlowTest") + .UseInMemoryDatabase($"EventFlowTest-{Guid.NewGuid()}") .ReplaceService() .Options; } diff --git a/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTable.cs b/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTable.cs index 9e8ceb864..935095b39 100644 --- a/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTable.cs +++ b/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTable.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; using Microsoft.EntityFrameworkCore.InMemory.ValueGeneration.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -39,6 +40,12 @@ public class IndexingInMemoryTable : IInMemoryTable private readonly HashSet[] _indexes; private readonly IInMemoryTable _innerTable; + public IEnumerable Rows => _innerTable.Rows; + + public IInMemoryTable BaseTable => _innerTable; + + public IEntityType EntityType => throw new InvalidOperationException("Property deprecated in newer versions so not used anymore."); + public IndexingInMemoryTable(IInMemoryTable innerTable, IIndex[] indexDefinitions) { _innerTable = innerTable; @@ -51,34 +58,39 @@ public IReadOnlyList SnapshotRows() return _innerTable.SnapshotRows(); } - public void Create(IUpdateEntry entry) - { + public void Create(IUpdateEntry entry, IDiagnosticsLogger updateLogger) + { var indexEntries = _indexDefinitions .Select(d => d.Properties.Select(entry.GetCurrentValue).ToArray()) .Select(values => new IndexEntry(values)) .ToArray(); - + if (indexEntries.Select((item, i) => _indexes[i].Contains(item)).Any(contains => contains)) throw new DbUpdateException("Error while updating.", new Exception("Unique constraint violated.")); + + _innerTable.Create(entry, updateLogger); + + _ = indexEntries.Select((item, i) => _indexes[i].Add(item)).ToArray(); + } - _innerTable.Create(entry); - - indexEntries.Select((item, i) => _indexes[i].Add(item)).ToArray(); + public void Delete(IUpdateEntry entry, IDiagnosticsLogger updateLogger) + { + _innerTable.Delete(entry, updateLogger); } - public void Delete(IUpdateEntry entry) + public void Update(IUpdateEntry entry, IDiagnosticsLogger updateLogger) { - _innerTable.Delete(entry); + _innerTable.Update(entry, updateLogger); } - public void Update(IUpdateEntry entry) + public InMemoryIntegerValueGenerator GetIntegerValueGenerator(IProperty property, IReadOnlyList tables) { - _innerTable.Update(entry); + return _innerTable.GetIntegerValueGenerator(property, tables); } - public InMemoryIntegerValueGenerator GetIntegerValueGenerator(IProperty property) + public void BumpValueGenerators(object[] row) { - return _innerTable.GetIntegerValueGenerator(property); + _innerTable.BumpValueGenerators(row); } private struct IndexEntry diff --git a/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTableFactory.cs b/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTableFactory.cs index 385bf1837..957bd0eb2 100644 --- a/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTableFactory.cs +++ b/Source/EventFlow.EntityFramework.Tests/InMemory/Infrastructure/IndexingInMemoryTableFactory.cs @@ -22,6 +22,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; using Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -30,13 +31,13 @@ namespace EventFlow.EntityFramework.Tests.InMemory.Infrastructure [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Only for test")] public class IndexingInMemoryTableFactory : InMemoryTableFactory { - public IndexingInMemoryTableFactory(ILoggingOptions loggingOptions) : base(loggingOptions) + public IndexingInMemoryTableFactory(ILoggingOptions loggingOptions, IInMemorySingletonOptions option) : base(loggingOptions, option) { } - public override IInMemoryTable Create(IEntityType entityType) + public override IInMemoryTable Create(IEntityType entityType, IInMemoryTable baseTable) { - var innerTable = base.Create(entityType); + var innerTable = base.Create(entityType, baseTable); var uniqueIndexes = entityType.GetIndexes().Where(i => i.IsUnique).ToArray(); return uniqueIndexes.Any() diff --git a/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs b/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs index edc551470..25b966af9 100644 --- a/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs +++ b/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs @@ -56,7 +56,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .Property(e => e.AggregateId) .ValueGeneratedOnAdd(); - + modelBuilder.Entity() .Property(e => e.AddressId) .ValueGeneratedNever(); diff --git a/Source/EventFlow.EntityFramework.Tests/Model/ThingyMessageReadModelEntity.cs b/Source/EventFlow.EntityFramework.Tests/Model/ThingyMessageReadModelEntity.cs index 4cb7a79c4..f28f4cf20 100644 --- a/Source/EventFlow.EntityFramework.Tests/Model/ThingyMessageReadModelEntity.cs +++ b/Source/EventFlow.EntityFramework.Tests/Model/ThingyMessageReadModelEntity.cs @@ -22,6 +22,8 @@ using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.ReadStores; using EventFlow.TestHelpers.Aggregates; @@ -41,19 +43,21 @@ public class ThingyMessageReadModelEntity : IReadModel, public string Message { get; set; } - public void Apply(IReadModelContext context, IDomainEvent domainEvent) + public Task ApplyAsync(IReadModelContext context, IDomainEvent domainEvent, CancellationToken cancellationToken) { ThingyId = domainEvent.AggregateIdentity.Value; Message = domainEvent.AggregateEvent.ThingyMessage.Message; + return Task.CompletedTask; } - public void Apply(IReadModelContext context, IDomainEvent domainEvent) + public Task ApplyAsync(IReadModelContext context, IDomainEvent domainEvent, CancellationToken cancellationToken) { ThingyId = domainEvent.AggregateIdentity.Value; var messageId = new ThingyMessageId(context.ReadModelId); var thingyMessage = domainEvent.AggregateEvent.ThingyMessages.Single(m => m.Id == messageId); Message = thingyMessage.Message; + return Task.CompletedTask; } public ThingyMessage ToThingyMessage() diff --git a/Source/EventFlow.EntityFramework.Tests/Model/ThingyReadModelEntity.cs b/Source/EventFlow.EntityFramework.Tests/Model/ThingyReadModelEntity.cs index 90673c8c5..f049f8374 100644 --- a/Source/EventFlow.EntityFramework.Tests/Model/ThingyReadModelEntity.cs +++ b/Source/EventFlow.EntityFramework.Tests/Model/ThingyReadModelEntity.cs @@ -21,6 +21,8 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System.ComponentModel.DataAnnotations; +using System.Threading; +using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.ReadStores; using EventFlow.TestHelpers.Aggregates; @@ -42,22 +44,25 @@ public class ThingyReadModelEntity : IReadModel, [ConcurrencyCheck] public long Version { get; set; } - public void Apply(IReadModelContext context, - IDomainEvent domainEvent) + public Task ApplyAsync(IReadModelContext context, + IDomainEvent domainEvent, CancellationToken cancellationToken) { context.MarkForDeletion(); + return Task.CompletedTask; } - public void Apply(IReadModelContext context, - IDomainEvent domainEvent) + public Task ApplyAsync(IReadModelContext context, + IDomainEvent domainEvent, CancellationToken cancellationToken) { DomainErrorAfterFirstReceived = true; + return Task.CompletedTask; } - public void Apply(IReadModelContext context, - IDomainEvent domainEvent) + public Task ApplyAsync(IReadModelContext context, + IDomainEvent domainEvent, CancellationToken cancellationToken) { PingsReceived++; + return Task.CompletedTask; } public Thingy ToThingy() diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlEventStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlEventStoreTests.cs index b5cf9b25e..b15081f17 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlEventStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlEventStoreTests.cs @@ -20,14 +20,14 @@ // 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.Tasks; -using EventFlow.Configuration; +using System; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.MsSql; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.MsSql @@ -37,22 +37,25 @@ public class EfMsSqlEventStoreTests : TestSuiteForEventStore { private IMsSqlDatabase _testDatabase; - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { _testDatabase = MsSqlHelpz.CreateDatabase("eventflow"); - return eventFlowOptions - .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(c => _testDatabase.ConnectionString)) .ConfigureEntityFramework(EntityFrameworkConfiguration.New) .AddDbContextProvider() - .ConfigureForEventStoreTest() - .CreateResolver(); + .ConfigureForEventStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } [TearDown] public void TearDown() { - _testDatabase.DisposeSafe("Failed to delete database"); + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs index 74b92cace..a73f2d312 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.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. +using System; using System.Threading; using System.Threading.Tasks; -using EventFlow.Configuration; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.EntityFramework.Tests.MsSql.IncludeTests; @@ -32,6 +32,7 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.MsSql; using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.MsSql @@ -41,23 +42,26 @@ public class EfMsSqlReadStoreIncludeTests : IntegrationTest { private IMsSqlDatabase _testDatabase; - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { _testDatabase = MsSqlHelpz.CreateDatabase("eventflow"); - return eventFlowOptions - .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(c => _testDatabase.ConnectionString)) .ConfigureEntityFramework(EntityFrameworkConfiguration.New) .AddDbContextProvider() .ConfigureForReadStoreIncludeTest() - .AddDefaults(typeof(EfMsSqlReadStoreIncludeTests).Assembly) - .CreateResolver(); + .AddDefaults(typeof(EfMsSqlReadStoreIncludeTests).Assembly); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } [TearDown] public void TearDown() { - _testDatabase.DisposeSafe("Failed to delete database"); + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); } [Test] diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreTests.cs index b6eb90dfa..654c78431 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreTests.cs @@ -28,6 +28,7 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.MsSql; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.MsSql @@ -39,22 +40,25 @@ public class EfMsSqlReadStoreTests : TestSuiteForReadModelStore protected override Type ReadModelType => typeof(ThingyReadModelEntity); - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { _testDatabase = MsSqlHelpz.CreateDatabase("eventflow"); - return eventFlowOptions - .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(c => _testDatabase.ConnectionString)) .ConfigureEntityFramework(EntityFrameworkConfiguration.New) .AddDbContextProvider() - .ConfigureForReadStoreTest() - .CreateResolver(); + .ConfigureForReadStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } [TearDown] public void TearDown() { - _testDatabase.DisposeSafe("Failed to delete database"); + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlSnapshotTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlSnapshotTests.cs index aaabe9e54..755499a70 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlSnapshotTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlSnapshotTests.cs @@ -20,14 +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 EventFlow.Configuration; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.MsSql; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using System; namespace EventFlow.EntityFramework.Tests.MsSql { @@ -36,22 +37,25 @@ public class EfMsSqlSnapshotTests : TestSuiteForSnapshotStore { private IMsSqlDatabase _testDatabase; - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { _testDatabase = MsSqlHelpz.CreateDatabase("eventflow-snapshots"); - return eventFlowOptions - .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(c => _testDatabase.ConnectionString)) .ConfigureEntityFramework(EntityFrameworkConfiguration.New) .AddDbContextProvider() - .ConfigureForSnapshotStoreTest() - .CreateResolver(); + .ConfigureForSnapshotStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } [TearDown] public void TearDown() { - _testDatabase.DisposeSafe("Failed to delete database"); + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/PersonReadModelEntity.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/PersonReadModelEntity.cs index 5a59fdc70..002cf892d 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/PersonReadModelEntity.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/PersonReadModelEntity.cs @@ -23,6 +23,8 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using EventFlow.Aggregates; using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Events; using EventFlow.ReadStores; @@ -43,14 +45,14 @@ public class PersonReadModelEntity : IReadModel, public virtual ICollection Addresses { get; set; } = new List(); - public void Apply(IReadModelContext context, - IDomainEvent domainEvent) + public Task ApplyAsync(IReadModelContext context, IDomainEvent domainEvent, CancellationToken cancellationToken) { Name = domainEvent.AggregateEvent.Name; + + return Task.CompletedTask; } - public void Apply(IReadModelContext context, - IDomainEvent domainEvent) + public Task ApplyAsync(IReadModelContext context, IDomainEvent domainEvent, CancellationToken cancellationToken) { var address = domainEvent.AggregateEvent.Address; Addresses.Add(new AddressReadModelEntity @@ -64,6 +66,8 @@ public void Apply(IReadModelContext context, }); NumberOfAddresses = Addresses.Count; + + return Task.CompletedTask; } public Person ToPerson() => diff --git a/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlEventStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlEventStoreTests.cs index 7b01ec634..040dc860a 100644 --- a/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlEventStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlEventStoreTests.cs @@ -20,14 +20,14 @@ // 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.Tasks; -using EventFlow.Configuration; +using System; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.Extensions; using EventFlow.PostgreSql.TestsHelpers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.PostgreSql @@ -37,22 +37,25 @@ public class EfPostgreSqlEventStoreTests : TestSuiteForEventStore { private IPostgreSqlDatabase _testDatabase; - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { _testDatabase = PostgreSqlHelpz.CreateDatabase("eventflow"); - return eventFlowOptions - .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(c => _testDatabase.ConnectionString)) .ConfigureEntityFramework(EntityFrameworkConfiguration.New) .AddDbContextProvider() - .ConfigureForEventStoreTest() - .CreateResolver(); + .ConfigureForEventStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } [TearDown] public void TearDown() { - _testDatabase.DisposeSafe("Failed to delete database"); + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlReadStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlReadStoreTests.cs index e17ead3d3..cd25a1d01 100644 --- a/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlReadStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlReadStoreTests.cs @@ -21,13 +21,13 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; -using EventFlow.Configuration; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.Extensions; using EventFlow.PostgreSql.TestsHelpers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.PostgreSql @@ -39,22 +39,25 @@ public class EfPostgreSqlReadStoreTests : TestSuiteForReadModelStore protected override Type ReadModelType => typeof(ThingyReadModelEntity); - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { _testDatabase = PostgreSqlHelpz.CreateDatabase("eventflow"); - return eventFlowOptions - .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(c => _testDatabase.ConnectionString)) .ConfigureEntityFramework(EntityFrameworkConfiguration.New) .AddDbContextProvider() - .ConfigureForReadStoreTest() - .CreateResolver(); + .ConfigureForReadStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } [TearDown] public void TearDown() { - _testDatabase.DisposeSafe("Failed to delete database"); + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlSnapshotTests.cs b/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlSnapshotTests.cs index 17c011957..0ca5073c2 100644 --- a/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlSnapshotTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/PostgreSql/EfPostgreSqlSnapshotTests.cs @@ -20,14 +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 EventFlow.Configuration; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.Extensions; using EventFlow.PostgreSql.TestsHelpers; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using System; namespace EventFlow.EntityFramework.Tests.PostgreSql { @@ -36,22 +37,25 @@ public class EfPostgreSqlSnapshotTests : TestSuiteForSnapshotStore { private IPostgreSqlDatabase _testDatabase; - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { - _testDatabase = PostgreSqlHelpz.CreateDatabase("eventflow-snapshots"); + _testDatabase = PostgreSqlHelpz.CreateDatabase("snapshots"); - return eventFlowOptions - .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + eventFlowOptions + .RegisterServices(sr => sr.AddTransient(c => _testDatabase.ConnectionString)) .ConfigureEntityFramework(EntityFrameworkConfiguration.New) .AddDbContextProvider() - .ConfigureForSnapshotStoreTest() - .CreateResolver(); + .ConfigureForSnapshotStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } [TearDown] public void TearDown() { - _testDatabase.DisposeSafe("Failed to delete database"); + _testDatabase.DisposeSafe(Logger, "Failed to delete database"); } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteEventStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteEventStoreTests.cs index 68212d2ec..a1cd8a2be 100644 --- a/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteEventStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteEventStoreTests.cs @@ -20,12 +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.Threading.Tasks; -using EventFlow.Configuration; +using System; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.SQLite @@ -33,13 +33,16 @@ namespace EventFlow.EntityFramework.Tests.SQLite [Category(Categories.Integration)] public class EfSqliteEventStoreTests : TestSuiteForEventStore { - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { - return eventFlowOptions + eventFlowOptions .ConfigureEntityFramework(EntityFrameworkConfiguration.New) - .AddDbContextProvider(Lifetime.Singleton) - .ConfigureForEventStoreTest() - .CreateResolver(); + .AddDbContextProvider(ServiceLifetime.Singleton) + .ConfigureForEventStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteReadStoreTests.cs b/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteReadStoreTests.cs index 3f2fb665f..07121f7af 100644 --- a/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteReadStoreTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteReadStoreTests.cs @@ -21,11 +21,11 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; -using EventFlow.Configuration; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.SQLite @@ -35,13 +35,16 @@ public class EfSqliteReadStoreTests : TestSuiteForReadModelStore { protected override Type ReadModelType => typeof(ThingyReadModelEntity); - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { - return eventFlowOptions + eventFlowOptions .ConfigureEntityFramework(EntityFrameworkConfiguration.New) - .AddDbContextProvider(Lifetime.Singleton) - .ConfigureForReadStoreTest() - .CreateResolver(); + .AddDbContextProvider(ServiceLifetime.Singleton) + .ConfigureForReadStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/SQLite/EfInMemorySnapshotTests.cs b/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteSnapshotTests.cs similarity index 81% rename from Source/EventFlow.EntityFramework.Tests/SQLite/EfInMemorySnapshotTests.cs rename to Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteSnapshotTests.cs index 89cd8b13f..7ec907141 100644 --- a/Source/EventFlow.EntityFramework.Tests/SQLite/EfInMemorySnapshotTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/SQLite/EfSqliteSnapshotTests.cs @@ -20,11 +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 EventFlow.Configuration; +using System; using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Suites; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; namespace EventFlow.EntityFramework.Tests.SQLite @@ -32,13 +33,16 @@ namespace EventFlow.EntityFramework.Tests.SQLite [Category(Categories.Integration)] public class EfSqliteSnapshotTests : TestSuiteForSnapshotStore { - protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) { - return eventFlowOptions + eventFlowOptions .ConfigureEntityFramework(EntityFrameworkConfiguration.New) - .AddDbContextProvider(Lifetime.Singleton) - .ConfigureForSnapshotStoreTest() - .CreateResolver(); + .AddDbContextProvider(ServiceLifetime.Singleton) + .ConfigureForSnapshotStoreTest(); + + var serviceProvider = base.Configure(eventFlowOptions); + + return serviceProvider; } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/EntityFrameworkConfiguration.cs b/Source/EventFlow.EntityFramework/EntityFrameworkConfiguration.cs index b0dde880f..e3ed8a2df 100644 --- a/Source/EventFlow.EntityFramework/EntityFrameworkConfiguration.cs +++ b/Source/EventFlow.EntityFramework/EntityFrameworkConfiguration.cs @@ -19,44 +19,41 @@ // 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 EventFlow.Configuration; +using Microsoft.Extensions.DependencyInjection; -namespace EventFlow.EntityFramework -{ - public class EntityFrameworkConfiguration : IEntityFrameworkConfiguration - { - private Action _registerUniqueConstraintDetectionStrategy; - private Action _registerBulkOperationConfiguration; +namespace EventFlow.EntityFramework; - public static EntityFrameworkConfiguration New => new EntityFrameworkConfiguration(); +public class EntityFrameworkConfiguration : IEntityFrameworkConfiguration +{ + private Action? _registerBulkOperationConfiguration; + private Action? _registerUniqueConstraintDetectionStrategy; + public static EntityFrameworkConfiguration New => new(); - private EntityFrameworkConfiguration() - { - UseUniqueConstraintDetectionStrategy(); - UseBulkOperationConfiguration(); - } + private EntityFrameworkConfiguration() + { + UseUniqueConstraintDetectionStrategy(); + UseBulkOperationConfiguration(); + } - void IEntityFrameworkConfiguration.Apply(IServiceRegistration serviceRegistration) - { - serviceRegistration.Register(s => this); - _registerUniqueConstraintDetectionStrategy(serviceRegistration); - _registerBulkOperationConfiguration(serviceRegistration); - } + public EntityFrameworkConfiguration UseBulkOperationConfiguration() + where T : class, IBulkOperationConfiguration + { + _registerBulkOperationConfiguration = s => s.AddTransient(); + return this; + } - public EntityFrameworkConfiguration UseBulkOperationConfiguration() - where T : class, IBulkOperationConfiguration - { - _registerBulkOperationConfiguration = s => s.Register(); - return this; - } + public EntityFrameworkConfiguration UseUniqueConstraintDetectionStrategy() + where T : class, IUniqueConstraintDetectionStrategy + { + _registerUniqueConstraintDetectionStrategy = s => s.AddTransient(); + return this; + } - public EntityFrameworkConfiguration UseUniqueConstraintDetectionStrategy() - where T : class, IUniqueConstraintDetectionStrategy - { - _registerUniqueConstraintDetectionStrategy = s => s.Register(); - return this; - } + void IEntityFrameworkConfiguration.Apply(IServiceCollection serviceCollection) + { + serviceCollection.AddTransient(_ => this); + _registerUniqueConstraintDetectionStrategy!(serviceCollection); + _registerBulkOperationConfiguration!(serviceCollection); } -} \ No newline at end of file +} diff --git a/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj b/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj index 4a67c176c..57355e2af 100644 --- a/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj +++ b/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj @@ -1,15 +1,11 @@  - - netstandard2.0;netcoreapp3.1 - True - True - False + net8.0 True EventFlow.EntityFramework Frank Ebersoll Rasmus Mikkelsen - Copyright (c) Rasmus Mikkelsen 2015 - 2021 + Copyright (c) Rasmus Mikkelsen 2015 - 2025 Entity Framework Core support for EventFlow CQRS ES event sourcing EF Entity Framework Core git @@ -18,17 +14,13 @@ MIT en-US UPDATED BY BUILD + enable - - - 3.1.5 - - - + - 3.1.1 + 8.0.11 diff --git a/Source/EventFlow.EntityFramework/EventStores/EntityFrameworkEventPersistence.cs b/Source/EventFlow.EntityFramework/EventStores/EntityFrameworkEventPersistence.cs index a536b0e37..e3c9db7b0 100644 --- a/Source/EventFlow.EntityFramework/EventStores/EntityFrameworkEventPersistence.cs +++ b/Source/EventFlow.EntityFramework/EventStores/EntityFrameworkEventPersistence.cs @@ -19,7 +19,6 @@ // 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; @@ -30,131 +29,147 @@ using EventFlow.EntityFramework.Extensions; using EventFlow.EventStores; using EventFlow.Exceptions; -using EventFlow.Logs; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace EventFlow.EntityFramework.EventStores; -namespace EventFlow.EntityFramework.EventStores +public class EntityFrameworkEventPersistence : IEventPersistence + where TDbContext : DbContext { - public class EntityFrameworkEventPersistence : IEventPersistence - where TDbContext : DbContext + private readonly IDbContextProvider _contextProvider; + private readonly ILogger> _logger; + private readonly IUniqueConstraintDetectionStrategy _strategy; + + public EntityFrameworkEventPersistence(ILogger> logger, + IDbContextProvider contextProvider, + IUniqueConstraintDetectionStrategy strategy) { - private readonly IDbContextProvider _contextProvider; - private readonly ILog _log; - private readonly IUniqueConstraintDetectionStrategy _strategy; - - public EntityFrameworkEventPersistence( - ILog log, - IDbContextProvider contextProvider, - IUniqueConstraintDetectionStrategy strategy - ) - { - _log = log; - _contextProvider = contextProvider; - _strategy = strategy; - } + _logger = logger; + _contextProvider = contextProvider; + _strategy = strategy; + } - public async Task LoadAllCommittedEvents(GlobalPosition globalPosition, int pageSize, - CancellationToken cancellationToken) + public async Task> CommitEventsAsync(IIdentity id, + IReadOnlyCollection serializedEvents, + CancellationToken cancellationToken) + { + if (!serializedEvents.Any()) { - var startPosition = globalPosition.IsStart - ? 0 - : long.Parse(globalPosition.Value); - - using (var context = _contextProvider.CreateContext()) - { - var entities = await context - .Set() - .OrderBy(e => e.GlobalSequenceNumber) - .Where(e => e.GlobalSequenceNumber >= startPosition) - .Take(pageSize) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - - var nextPosition = entities.Any() - ? entities.Max(e => e.GlobalSequenceNumber) + 1 - : startPosition; - - return new AllCommittedEventsPage(new GlobalPosition(nextPosition.ToString()), entities); - } + return Array.Empty(); } - public async Task> CommitEventsAsync(IIdentity id, - IReadOnlyCollection serializedEvents, CancellationToken cancellationToken) + var entities = serializedEvents + .Select((e, _) => new EventEntity + { + AggregateId = id.Value, + AggregateName = e.Metadata[MetadataKeys.AggregateName], + BatchId = Guid.Parse(e.Metadata[MetadataKeys.BatchId]), + Data = e.SerializedData, + Metadata = e.SerializedMetadata, + AggregateSequenceNumber = e.AggregateSequenceNumber + }) + .ToList(); + + _logger.LogTrace( + "Committing {Count} events to EntityFramework event store for entity with ID '{Id}'", + entities.Count, + id); + + try { - if (!serializedEvents.Any()) - return new ICommittedDomainEvent[0]; - - var entities = serializedEvents - .Select((e, i) => new EventEntity - { - AggregateId = id.Value, - AggregateName = e.Metadata[MetadataKeys.AggregateName], - BatchId = Guid.Parse(e.Metadata[MetadataKeys.BatchId]), - Data = e.SerializedData, - Metadata = e.SerializedMetadata, - AggregateSequenceNumber = e.AggregateSequenceNumber - }) - .ToList(); - - _log.Verbose( - "Committing {0} events to EntityFramework event store for entity with ID '{1}'", - entities.Count, - id); - - try - { - using (var context = _contextProvider.CreateContext()) - { - await context.AddRangeAsync(entities, cancellationToken); - await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - } - catch (DbUpdateException ex) when (ex.IsUniqueConstraintViolation(_strategy)) - { - _log.Verbose( - "Entity Framework event insert detected an optimistic concurrency " + - "exception for entity with ID '{0}'", id); - throw new OptimisticConcurrencyException(ex.Message, ex); - } - - return entities; + await using var context = _contextProvider.CreateContext(); + await context.AddRangeAsync(entities, cancellationToken); + await context.SaveChangesAsync(cancellationToken) + .ConfigureAwait(false); } - - public async Task> LoadCommittedEventsAsync(IIdentity id, - int fromEventSequenceNumber, CancellationToken cancellationToken) + catch (DbUpdateException ex) when (ex.IsUniqueConstraintViolation(_strategy)) { - using (var context = _contextProvider.CreateContext()) - { - var entities = await context - .Set() - .Where(e => e.AggregateId == id.Value - && e.AggregateSequenceNumber >= fromEventSequenceNumber) - .OrderBy(e => e.AggregateSequenceNumber) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - - return entities; - } + _logger.LogTrace(ex, + "Entity Framework event insert detected an optimistic concurrency " + + "exception for entity with ID '{Id}'", + id); +#pragma warning disable Wintellect010 + throw new OptimisticConcurrencyException(ex.Message, ex); +#pragma warning restore Wintellect010 } - public async Task DeleteEventsAsync(IIdentity id, CancellationToken cancellationToken) - { - using (var context = _contextProvider.CreateContext()) - { - var entities = await context.Set() - .Where(e => e.AggregateId == id.Value) - .Select(e => new EventEntity {GlobalSequenceNumber = e.GlobalSequenceNumber}) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - - context.RemoveRange(entities); - var rowsAffected = await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - - _log.Verbose( - "Deleted entity with ID '{0}' by deleting all of its {1} events", - id, - rowsAffected); - } - } + return entities; + } + + public async Task DeleteEventsAsync(IIdentity id, CancellationToken cancellationToken) + { + await using var context = _contextProvider.CreateContext(); + + var entities = await context.Set() + .Where(e => e.AggregateId == id.Value) + .Select(e => new EventEntity { GlobalSequenceNumber = e.GlobalSequenceNumber }) + .ToListAsync(cancellationToken); + + context.RemoveRange(entities); + var rowsAffected = await context.SaveChangesAsync(cancellationToken) + .ConfigureAwait(false); + + _logger.LogTrace( + "Deleted entity with ID {Id} by deleting all of its {NumberOfEvents} events", + id, + rowsAffected); + } + + public async Task LoadAllCommittedEvents(GlobalPosition globalPosition, + int pageSize, + CancellationToken cancellationToken) + { + var startPosition = globalPosition.IsStart + ? 0 + : long.Parse(globalPosition.Value); + + await using var context = _contextProvider.CreateContext(); + + var entities = await context + .Set() + .OrderBy(e => e.GlobalSequenceNumber) + .Where(e => e.GlobalSequenceNumber >= startPosition) + .Take(pageSize) + .ToListAsync(cancellationToken); + + var nextPosition = entities.Any() + ? entities.Max(e => e.GlobalSequenceNumber) + 1 + : startPosition; + + return new AllCommittedEventsPage(new GlobalPosition(nextPosition.ToString()), entities); + } + + public async Task> LoadCommittedEventsAsync(IIdentity id, + int fromEventSequenceNumber, + CancellationToken cancellationToken) + { + await using var context = _contextProvider.CreateContext(); + + var entities = await context + .Set() + .Where(e => e.AggregateId == id.Value + && e.AggregateSequenceNumber >= fromEventSequenceNumber) + .OrderBy(e => e.AggregateSequenceNumber) + .ToListAsync(cancellationToken); + + return entities; + } + + public async Task> LoadCommittedEventsAsync(IIdentity id, + int fromEventSequenceNumber, + int toEventSequenceNumber, + CancellationToken cancellationToken) + { + await using var context = _contextProvider.CreateContext(); + + var entities = await context + .Set() + .Where(e => e.AggregateId == id.Value + && e.AggregateSequenceNumber >= fromEventSequenceNumber && e.AggregateSequenceNumber <= toEventSequenceNumber) + .OrderBy(e => e.AggregateSequenceNumber) + .ToListAsync(cancellationToken); + + return entities; } -} \ No newline at end of file +} diff --git a/Source/EventFlow.EntityFramework/EventStores/EventEntity.cs b/Source/EventFlow.EntityFramework/EventStores/EventEntity.cs index 0c0eb75a4..b5c53e651 100644 --- a/Source/EventFlow.EntityFramework/EventStores/EventEntity.cs +++ b/Source/EventFlow.EntityFramework/EventStores/EventEntity.cs @@ -19,20 +19,19 @@ // 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.ComponentModel.DataAnnotations.Schema; using EventFlow.EventStores; -namespace EventFlow.EntityFramework.EventStores +namespace EventFlow.EntityFramework.EventStores; + +public class EventEntity : ICommittedDomainEvent { - public class EventEntity : ICommittedDomainEvent - { - public long GlobalSequenceNumber { get; set; } - public Guid BatchId { get; set; } - public string AggregateName { get; set; } - public string AggregateId { get; set; } - public string Data { get; set; } - public string Metadata { get; set; } - public int AggregateSequenceNumber { get; set; } - } + public string AggregateId { get; init; } = default!; + public string AggregateName { get; init; } = default!; + public int AggregateSequenceNumber { get; init; } + public Guid BatchId { get; init; } + public string Data { get; init; } = default!; + public long GlobalSequenceNumber { get; init; } + public string Metadata { get; init; } = default!; } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/Extensions/DbContextExtensions.cs b/Source/EventFlow.EntityFramework/Extensions/DbContextExtensions.cs index ec1a58eac..06186d0a8 100644 --- a/Source/EventFlow.EntityFramework/Extensions/DbContextExtensions.cs +++ b/Source/EventFlow.EntityFramework/Extensions/DbContextExtensions.cs @@ -19,7 +19,6 @@ // 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; @@ -29,62 +28,67 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; -namespace EventFlow.EntityFramework.Extensions +namespace EventFlow.EntityFramework.Extensions; + +public static class Bulk { - public static class Bulk + public static async Task DeleteAsync(IDbContextProvider contextProvider, + int batchSize, + CancellationToken cancellationToken, + Expression> projection, + Expression> orderBy, + Expression>? condition = null, + Action>? setProperties = null) + where TContext : DbContext + where TEntity : class, new() { - public static async Task Delete(IDbContextProvider contextProvider, - int batchSize, - CancellationToken cancellationToken, - Expression> projection, - Expression> condition = null, - Action> setProperties = null) - where TContext : DbContext - where TEntity : class, new() - { - int rowsAffected = 0; + var rowsAffected = 0; - while (!cancellationToken.IsCancellationRequested) - using (var dbContext = contextProvider.CreateContext()) - { - IQueryable query = dbContext + while (!cancellationToken.IsCancellationRequested) + { + await using var dbContext = contextProvider.CreateContext(); + var query = dbContext .Set() .AsNoTracking(); - if (condition != null) - { - query = query.Where(condition); - } + if (condition != null) + { + query = query.Where(condition); + } - IEnumerable items = await query - .Take(batchSize) - .Select(projection) - .ToArrayAsync(cancellationToken) - .ConfigureAwait(false); + IEnumerable items = await query + .OrderBy(orderBy) + .Take(batchSize) + .Select(projection) + .ToArrayAsync(cancellationToken) + .ConfigureAwait(false); - if (!items.Any()) - return rowsAffected; + if (!items.Any()) + { + return rowsAffected; + } - if (setProperties == null) - { - dbContext.RemoveRange((IEnumerable) items); - } - else - { - foreach (var item in items) - { - var entity = new TEntity(); - var entry = dbContext.Attach(entity); - setProperties.Invoke(item, entry); - entry.State = EntityState.Deleted; - } - } + if (setProperties == null) + { + dbContext.RemoveRange((IEnumerable)items); + } + else + { + foreach (var item in items) + { + var entity = new TEntity(); + var entityEntry = dbContext.Entry(entity); + setProperties.Invoke(item, entityEntry); - rowsAffected += await dbContext.SaveChangesAsync(cancellationToken) - .ConfigureAwait(false); + var entry = dbContext.Attach(entityEntry.Entity); + entry.State = EntityState.Deleted; } + } - return rowsAffected; + rowsAffected += await dbContext.SaveChangesAsync(cancellationToken) + .ConfigureAwait(false); } + + return rowsAffected; } } diff --git a/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs b/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs index 05674f31e..3b7e58934 100644 --- a/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs +++ b/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs @@ -21,156 +21,156 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using System; -using EventFlow.Configuration; using EventFlow.EntityFramework.EventStores; using EventFlow.EntityFramework.ReadStores; using EventFlow.EntityFramework.ReadStores.Configuration; -using EventFlow.EntityFramework.ReadStores.Configuration.Includes; using EventFlow.EntityFramework.SnapshotStores; using EventFlow.Extensions; using EventFlow.ReadStores; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; -namespace EventFlow.EntityFramework.Extensions +namespace EventFlow.EntityFramework.Extensions; + +public static class EventFlowOptionsEntityFrameworkExtensions { - public static class EventFlowOptionsEntityFrameworkExtensions + public static IEventFlowOptions UseEntityFrameworkReadModel( + this IEventFlowOptions eventFlowOptions) + where TDbContext : DbContext + where TReadModel : class, IReadModel, new() { - public static IEventFlowOptions ConfigureEntityFramework( - this IEventFlowOptions eventFlowOptions, - IEntityFrameworkConfiguration configuration) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - return eventFlowOptions.RegisterServices(configuration.Apply); - } + return eventFlowOptions + .RegisterServices(f => + { + f.TryAddTransient, + EntityFrameworkReadModelStore>(); + f.TryAddSingleton>(_ => + new EntityFrameworkReadModelConfiguration()); + f.TryAddTransient>(r => + r.GetRequiredService>()); + }) + .UseReadStoreFor, TReadModel>(); + } - public static IEventFlowOptions UseEntityFrameworkEventStore( - this IEventFlowOptions eventFlowOptions) - where TDbContext : DbContext - { - return eventFlowOptions - .UseEventStore>(); - } + /// + /// Configures the read model. Can be used for eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. + /// + /// The read model's entity type + /// The database context type + /// The read model locator type + /// + /// Function to configure eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. + /// Avoid navigation properties if you create read models for both, the parent entity and the child entity. Otherwise there is a risk of a ordering problem when saving aggregates and updating read modules independently (FOREIGN-KEY constraint) + public static IEventFlowOptions UseEntityFrameworkReadModel( + this IEventFlowOptions eventFlowOptions, + Func, IApplyQueryableConfiguration> configure) + where TDbContext : DbContext + where TReadModel : class, IReadModel, new() + where TReadModelLocator : IReadModelLocator + { + return eventFlowOptions + .RegisterServices(f => + { + f.TryAddTransient, + EntityFrameworkReadModelStore>(); + f.TryAddSingleton(_ => + { + var readModelConfig = new EntityFrameworkReadModelConfiguration(); + return configure != null + ? configure(readModelConfig) + : readModelConfig; + }); + f.TryAddTransient>(r => + r.GetRequiredService>()); + }) + .UseReadStoreFor, TReadModel, TReadModelLocator>(); + } - public static IEventFlowOptions UseEntityFrameworkSnapshotStore( - this IEventFlowOptions eventFlowOptions) - where TDbContext : DbContext - { - return eventFlowOptions - .UseSnapshotStore>(); - } + public static IEventFlowOptions UseEntityFrameworkReadModel( + this IEventFlowOptions eventFlowOptions) + where TDbContext : DbContext + where TReadModel : class, IReadModel, new() + where TReadModelLocator : IReadModelLocator + { + return eventFlowOptions + .RegisterServices(f => + { + f.TryAddTransient, + EntityFrameworkReadModelStore>(); + f.TryAddSingleton>(_ => + new EntityFrameworkReadModelConfiguration()); + f.TryAddTransient>(r => + r.GetRequiredService>()); + }) + .UseReadStoreFor, TReadModel, TReadModelLocator>(); + } - public static IEventFlowOptions UseEntityFrameworkReadModel( - this IEventFlowOptions eventFlowOptions) - where TDbContext : DbContext - where TReadModel : class, IReadModel, new() - { - return eventFlowOptions - .RegisterServices(f => + /// + /// Configures the read model. Can be used for eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. + /// + /// The read model's entity type + /// The database context type + /// + /// Function to configure eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. + /// Avoid navigation properties if you create read models for both, the parent entity and the child entity. Otherwise there is a risk of a ordering problem when saving aggregates and updating read modules independently (FOREIGN-KEY constraint) + public static IEventFlowOptions UseEntityFrameworkReadModel( + this IEventFlowOptions eventFlowOptions, + Func, IApplyQueryableConfiguration> configure) + where TDbContext : DbContext + where TReadModel : class, IReadModel, new() + { + return eventFlowOptions + .RegisterServices(f => + { + f.TryAddTransient, + EntityFrameworkReadModelStore>(); + f.TryAddSingleton(_ => { - f.Register, - EntityFrameworkReadModelStore>(); - f.Register>(_ => - new EntityFrameworkReadModelConfiguration(), Lifetime.Singleton); - f.Register>(r => - r.Resolver.Resolve>()); - }) - .UseReadStoreFor, TReadModel>(); - } + var readModelConfig = new EntityFrameworkReadModelConfiguration(); + return configure != null + ? configure(readModelConfig) + : readModelConfig; + }); + f.TryAddTransient>(r => + r.GetRequiredService>()); + }) + .UseReadStoreFor, TReadModel>(); + } - /// - /// Configures the read model. Can be used for eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. - /// - /// The read model's entity type - /// The database context type - /// - /// Function to configure eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. - /// Avoid navigation properties if you create read models for both, the parent entity and the child entity. Otherwise there is a risk of a ordering problem when saving aggregates and updating read modules independently (FOREIGN-KEY constraint) - public static IEventFlowOptions UseEntityFrameworkReadModel( - this IEventFlowOptions eventFlowOptions, - Func,IApplyQueryableConfiguration> configure) - where TDbContext : DbContext - where TReadModel : class, IReadModel, new() - { - return eventFlowOptions - .RegisterServices(f => - { - f.Register, - EntityFrameworkReadModelStore>(); - f.Register(_ => - { - var readModelConfig = new EntityFrameworkReadModelConfiguration(); - return configure != null - ? configure(readModelConfig) - : readModelConfig; + public static IEventFlowOptions UseEntityFrameworkSnapshotStore(this IEventFlowOptions eventFlowOptions) + where TDbContext : DbContext + { + return eventFlowOptions + .UseSnapshotPersistence>(ServiceLifetime.Transient); + } - }, Lifetime.Singleton); - f.Register>(r => - r.Resolver.Resolve>()); - }) - .UseReadStoreFor, TReadModel>(); - } + public static IEventFlowOptions AddDbContextProvider( + this IEventFlowOptions eventFlowOptions, + ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + where TContextProvider : class, IDbContextProvider + where TDbContext : DbContext + { + return eventFlowOptions.RegisterServices(s => + s.Replace(ServiceDescriptor.Describe(typeof(IDbContextProvider), + typeof(TContextProvider), + serviceLifetime))); + } - /// - /// Configures the read model. Can be used for eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. - /// - /// The read model's entity type - /// The database context type - /// The read model locator type - /// - /// Function to configure eager loading of related data by appending .Include(..) / .ThenInclude(..) statements. - /// Avoid navigation properties if you create read models for both, the parent entity and the child entity. Otherwise there is a risk of a ordering problem when saving aggregates and updating read modules independently (FOREIGN-KEY constraint) - public static IEventFlowOptions UseEntityFrameworkReadModel( - this IEventFlowOptions eventFlowOptions, - Func,IApplyQueryableConfiguration> configure) - where TDbContext : DbContext - where TReadModel : class, IReadModel, new() - where TReadModelLocator : IReadModelLocator + public static IEventFlowOptions ConfigureEntityFramework(this IEventFlowOptions eventFlowOptions, + IEntityFrameworkConfiguration configuration) + { + if (configuration == null) { - return eventFlowOptions - .RegisterServices(f => - { - f.Register, - EntityFrameworkReadModelStore>(); - f.Register(_ => - { - var readModelConfig = new EntityFrameworkReadModelConfiguration(); - return configure != null - ? configure(readModelConfig) - : readModelConfig; - }, Lifetime.Singleton); - f.Register>(r => - r.Resolver.Resolve>()); - }) - .UseReadStoreFor, TReadModel, TReadModelLocator>(); + throw new ArgumentNullException(nameof(configuration)); } - public static IEventFlowOptions UseEntityFrameworkReadModel( - this IEventFlowOptions eventFlowOptions) - where TDbContext : DbContext - where TReadModel : class, IReadModel, new() - where TReadModelLocator : IReadModelLocator - { - return eventFlowOptions - .RegisterServices(f => - { - f.Register, - EntityFrameworkReadModelStore>(); - f.Register>(_ => - new EntityFrameworkReadModelConfiguration(), Lifetime.Singleton); - f.Register>(r => - r.Resolver.Resolve>()); - }) - .UseReadStoreFor, TReadModel, TReadModelLocator>(); - } + return eventFlowOptions.RegisterServices(configuration.Apply); + } - public static IEventFlowOptions AddDbContextProvider( - this IEventFlowOptions eventFlowOptions, - Lifetime lifetime = Lifetime.AlwaysUnique) - where TContextProvider : class, IDbContextProvider - where TDbContext : DbContext - { - return eventFlowOptions.RegisterServices(s => - s.Register, TContextProvider>(lifetime)); - } + public static IEventFlowOptions UseEntityFrameworkEventStore(this IEventFlowOptions eventFlowOptions) + where TDbContext : DbContext + { + return eventFlowOptions.UseEventPersistence>(); } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/Extensions/ModelBuilderExtensions.cs b/Source/EventFlow.EntityFramework/Extensions/ModelBuilderExtensions.cs index 640ff28eb..9d4b6d150 100644 --- a/Source/EventFlow.EntityFramework/Extensions/ModelBuilderExtensions.cs +++ b/Source/EventFlow.EntityFramework/Extensions/ModelBuilderExtensions.cs @@ -20,25 +20,32 @@ // 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.EntityFramework.EventStores; using EventFlow.EntityFramework.SnapshotStores; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace EventFlow.EntityFramework.Extensions { public static class ModelBuilderExtensions { - public static ModelBuilder AddEventFlowEvents(this ModelBuilder modelBuilder) + public static ModelBuilder AddEventFlowEvents(this ModelBuilder modelBuilder, Action>? dob = null) { var eventEntity = modelBuilder.Entity(); + dob?.Invoke(eventEntity); + eventEntity.HasKey(e => e.GlobalSequenceNumber); eventEntity.HasIndex(e => new {e.AggregateId, e.AggregateSequenceNumber}).IsUnique(); + return modelBuilder; } - public static ModelBuilder AddEventFlowSnapshots(this ModelBuilder modelBuilder) + public static ModelBuilder AddEventFlowSnapshots(this ModelBuilder modelBuilder, Action>? dob = null) { var eventEntity = modelBuilder.Entity(); + dob?.Invoke(eventEntity); + eventEntity.HasKey(e => e.Id); eventEntity.HasIndex(e => new {e.AggregateName, e.AggregateId, e.AggregateSequenceNumber}).IsUnique(); return modelBuilder; diff --git a/Source/EventFlow.EntityFramework/IEntityFrameworkConfiguration.cs b/Source/EventFlow.EntityFramework/IEntityFrameworkConfiguration.cs index 45aab780b..13dab305c 100644 --- a/Source/EventFlow.EntityFramework/IEntityFrameworkConfiguration.cs +++ b/Source/EventFlow.EntityFramework/IEntityFrameworkConfiguration.cs @@ -20,12 +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 EventFlow.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace EventFlow.EntityFramework { public interface IEntityFrameworkConfiguration { - void Apply(IServiceRegistration serviceRegistration); + void Apply(IServiceCollection serviceCollection); } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs b/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs index 3e9502ad8..6d51df7bd 100644 --- a/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs +++ b/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs @@ -19,7 +19,6 @@ // 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.Concurrent; using System.Collections.Generic; @@ -33,354 +32,373 @@ using EventFlow.EntityFramework.ReadStores.Configuration; using EventFlow.Exceptions; using EventFlow.Extensions; -using EventFlow.Logs; using EventFlow.ReadStores; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.Logging; + +namespace EventFlow.EntityFramework.ReadStores; -namespace EventFlow.EntityFramework.ReadStores +public class EntityFrameworkReadModelStore : + ReadModelStore, + IEntityFrameworkReadModelStore + where TReadModel : class, IReadModel, new() + where TDbContext : DbContext { - public class EntityFrameworkReadModelStore : - ReadModelStore, - IEntityFrameworkReadModelStore - where TReadModel : class, IReadModel, new() - where TDbContext : DbContext + protected static readonly ConcurrentDictionary Descriptors = new(); + protected static readonly string ReadModelNameLowerCase = typeof(TReadModel).Name.ToLowerInvariant(); + protected readonly IApplyQueryableConfiguration QueryableConfiguration; + private readonly IDbContextProvider _contextProvider; + private readonly int _deletionBatchSize; + private readonly IReadModelFactory _readModelFactory; + private readonly ITransientFaultHandler _transientFaultHandler; + + public EntityFrameworkReadModelStore(IBulkOperationConfiguration bulkOperationConfiguration, + ILogger> logger, + IReadModelFactory readModelFactory, + IApplyQueryableConfiguration queryableConfiguration, + IDbContextProvider contextProvider, + ITransientFaultHandler transientFaultHandler) + : base(logger) { - private static readonly ConcurrentDictionary Descriptors - = new ConcurrentDictionary(); - - private static readonly string ReadModelNameLowerCase = typeof(TReadModel).Name.ToLowerInvariant(); - - private readonly IDbContextProvider _contextProvider; - private readonly int _deletionBatchSize; - private readonly IReadModelFactory _readModelFactory; - private readonly IApplyQueryableConfiguration _queryableConfiguration; - private readonly ITransientFaultHandler _transientFaultHandler; - - public EntityFrameworkReadModelStore( - IBulkOperationConfiguration bulkOperationConfiguration, - ILog log, - IReadModelFactory readModelFactory, - IApplyQueryableConfiguration queryableConfiguration, - IDbContextProvider contextProvider, - ITransientFaultHandler transientFaultHandler) - : base(log) - { - _readModelFactory = readModelFactory; - _queryableConfiguration = queryableConfiguration; - _contextProvider = contextProvider; - _transientFaultHandler = transientFaultHandler; - _deletionBatchSize = bulkOperationConfiguration.DeletionBatchSize; - } + _readModelFactory = readModelFactory; + QueryableConfiguration = queryableConfiguration; + _contextProvider = contextProvider; + _transientFaultHandler = transientFaultHandler; + _deletionBatchSize = bulkOperationConfiguration.DeletionBatchSize; + } - public override async Task UpdateAsync(IReadOnlyCollection readModelUpdates, - IReadModelContextFactory readModelContextFactory, - Func, ReadModelEnvelope, CancellationToken, - Task>> updateReadModel, - CancellationToken cancellationToken) - { - using (var dbContext = _contextProvider.CreateContext()) - { - foreach (var readModelUpdate in readModelUpdates) - { - await _transientFaultHandler.TryAsync( - c => UpdateReadModelAsync( - // ReSharper disable once AccessToDisposedClosure - dbContext, - readModelContextFactory, - updateReadModel, - c, - readModelUpdate), - Label.Named("efcore-read-model-update"), - cancellationToken) - .ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - } - } - } + public override async Task DeleteAllAsync(CancellationToken cancellationToken) + { + var readModelName = typeof(TReadModel).Name; - public override async Task> GetAsync(string id, - CancellationToken cancellationToken) + EntityDescriptor descriptor; + await using (var dbContext = _contextProvider.CreateContext()) { - using (var dbContext = _contextProvider.CreateContext()) - { - return await GetAsync(dbContext, id, cancellationToken).ConfigureAwait(false); - } + descriptor = GetDescriptor(dbContext, QueryableConfiguration); } - public override async Task DeleteAsync( - string id, - CancellationToken cancellationToken) + var rowsAffected = await Bulk.DeleteAsync( + _contextProvider, + _deletionBatchSize, + cancellationToken, + entity => new BulkDeletionModel + { + Id = EF.Property(entity, descriptor.Key), + Version = EF.Property(entity, + descriptor.Version!) + }, + entity => EF.Property(entity, descriptor.Key), + setProperties: (model, entry) => + { + descriptor.SetId(entry, model.Id); + descriptor.SetVersion(entry, model.Version); + }) + .ConfigureAwait(false); + + Logger.LogTrace( + "Purge {0} read models of type '{1}'", + rowsAffected, + readModelName); + } + + public override async Task DeleteAsync(string id, + CancellationToken cancellationToken) + { + await using var dbContext = _contextProvider.CreateContext(); + await DeleteAsync(dbContext, id, cancellationToken) + .ConfigureAwait(false); + } + + public override async Task> GetAsync(string id, + CancellationToken cancellationToken) + { + await using var dbContext = _contextProvider.CreateContext(); + return await GetAsync(dbContext, id, cancellationToken) + .ConfigureAwait(false); + } + + public override async Task>> UpdateAsync(IReadOnlyCollection readModelUpdates, + IReadModelContextFactory readModelContextFactory, + Func, + ReadModelEnvelope, CancellationToken, + Task>> updateReadModel, + CancellationToken cancellationToken) + { + var readModelUpdateResults = new List>(); + + await using var dbContext = _contextProvider.CreateContext(); + foreach (var readModelUpdate in readModelUpdates) { - using (var dbContext = _contextProvider.CreateContext()) - { - await DeleteAsync(dbContext, id, cancellationToken).ConfigureAwait(false); - } + var result = await _transientFaultHandler.TryAsync( + c => UpdateReadModelAsync( + // ReSharper disable once AccessToDisposedClosure + dbContext, + readModelContextFactory, + updateReadModel, + c, + readModelUpdate), + Label.Named("efcore-read-model-update"), + cancellationToken) + .ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + + readModelUpdateResults.Add(result); } - private class BulkDeletionModel + return readModelUpdateResults; + } + + protected virtual async Task DeleteAsync(TDbContext dbContext, string id, CancellationToken cancellationToken) + { + var entity = await dbContext.Set() + .FindAsync([id], cancellationToken: cancellationToken) + .ConfigureAwait(false); + if (entity == null) { - public string Id { get; set; } - public long? Version { get; set; } + return; } - public override async Task DeleteAllAsync(CancellationToken cancellationToken) - { - var readModelName = typeof(TReadModel).Name; + dbContext.Remove(entity); + var rowsAffected = await dbContext.SaveChangesAsync(cancellationToken) + .ConfigureAwait(false); - EntityDescriptor descriptor; - using (var dbContext = _contextProvider.CreateContext()) - { - descriptor = GetDescriptor(dbContext, _queryableConfiguration); - } + if (rowsAffected != 0) + { + Logger.LogTrace($"Deleted Entity Framework read model '{id}' of type '{ReadModelNameLowerCase}'"); + } + } - var rowsAffected = await Bulk.Delete( - _contextProvider, - _deletionBatchSize, - cancellationToken, - entity => new BulkDeletionModel - { - Id = EF.Property(entity, descriptor.Key), - Version = EF.Property(entity, descriptor.Version) - }, - setProperties: (model, entry) => - { - descriptor.SetId(entry, model.Id); - descriptor.SetVersion(entry, model.Version); - }) - .ConfigureAwait(false); + private async Task> GetAsync(TDbContext dbContext, + string id, + CancellationToken cancellationToken, + bool tracking = false) + { + var readModelType = typeof(TReadModel); + var descriptor = GetDescriptor(dbContext, QueryableConfiguration); + var entity = await descriptor.Query(dbContext, id, cancellationToken, tracking) + .ConfigureAwait(false); - Log.Verbose( - "Purge {0} read models of type '{1}'", - rowsAffected, - readModelName); + if (entity == null) + { + Logger.LogTrace($"Could not find any Entity Framework read model '{readModelType.PrettyPrint()}' with ID '{id}'"); + return ReadModelEnvelope.Empty(id); } - private async Task> GetAsync(TDbContext dbContext, - string id, - CancellationToken cancellationToken, - bool tracking = false) - { - var readModelType = typeof(TReadModel); - var descriptor = GetDescriptor(dbContext, _queryableConfiguration); - var entity = await descriptor.Query(dbContext, id, cancellationToken, tracking) - .ConfigureAwait(false); + var entry = dbContext.Entry(entity); + var version = descriptor.GetVersion(entry); - if (entity == null) - { - Log.Verbose(() => $"Could not find any Entity Framework read model '{readModelType.PrettyPrint()}' with ID '{id}'"); - return ReadModelEnvelope.Empty(id); - } + Logger.LogTrace($"Found Entity Framework read model '{readModelType.PrettyPrint()}' with ID '{id}' and version '{version}'"); - var entry = dbContext.Entry(entity); - var version = descriptor.GetVersion(entry); + return version.HasValue + ? ReadModelEnvelope.With(id, entity, version.Value) + : ReadModelEnvelope.With(id, entity); + } - Log.Verbose(() => $"Found Entity Framework read model '{readModelType.PrettyPrint()}' with ID '{id}' and version '{version}'"); + private async Task> UpdateReadModelAsync(TDbContext dbContext, + IReadModelContextFactory readModelContextFactory, + Func, + ReadModelEnvelope, CancellationToken, + Task>> updateReadModel, + CancellationToken cancellationToken, + ReadModelUpdate readModelUpdate) + { + var readModelId = readModelUpdate.ReadModelId; + var readModelEnvelope = await GetAsync(dbContext, readModelId, cancellationToken, true) + .ConfigureAwait(false); - return version.HasValue - ? ReadModelEnvelope.With(id, entity, version.Value) - : ReadModelEnvelope.With(id, entity); - } + var entity = readModelEnvelope.ReadModel; + var isNew = entity == null; - private async Task DeleteAsync(TDbContext dbContext, string id, CancellationToken cancellationToken) + if (entity == null) { - var entity = await dbContext.Set().FindAsync(id).ConfigureAwait(false); - if (entity == null) - return; - dbContext.Remove(entity); - var rowsAffected = await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + entity = await _readModelFactory.CreateAsync(readModelId, cancellationToken) + .ConfigureAwait(false); + readModelEnvelope = ReadModelEnvelope.With(readModelId, entity); + } - if (rowsAffected != 0) - { - Log.Verbose($"Deleted Entity Framework read model '{id}' of type '{ReadModelNameLowerCase}'"); - } + var readModelContext = readModelContextFactory.Create(readModelId, isNew); + var originalVersion = readModelEnvelope.Version; + var updateResult = await updateReadModel( + readModelContext, + readModelUpdate.DomainEvents, + readModelEnvelope, + cancellationToken) + .ConfigureAwait(false); + + if (!updateResult.IsModified) + { + return updateResult; } - private async Task UpdateReadModelAsync(TDbContext dbContext, IReadModelContextFactory readModelContextFactory, - Func, ReadModelEnvelope, CancellationToken, Task>> updateReadModel, - CancellationToken cancellationToken, - ReadModelUpdate readModelUpdate) + if (readModelContext.IsMarkedForDeletion) { - var readModelId = readModelUpdate.ReadModelId; - var readModelEnvelope = await GetAsync(dbContext, readModelId, cancellationToken, true) + await DeleteAsync(dbContext, readModelId, cancellationToken) .ConfigureAwait(false); + return ReadModelUpdateResult.With(readModelEnvelope, true); + } - var entity = readModelEnvelope.ReadModel; - var isNew = entity == null; - - if (entity == null) - { - entity = await _readModelFactory.CreateAsync(readModelId, cancellationToken).ConfigureAwait(false); - readModelEnvelope = ReadModelEnvelope.With(readModelId, entity); - } + readModelEnvelope = updateResult.Envelope; + entity = readModelEnvelope.ReadModel; - var readModelContext = readModelContextFactory.Create(readModelId, isNew); - var originalVersion = readModelEnvelope.Version; - var updateResult = await updateReadModel( - readModelContext, - readModelUpdate.DomainEvents, - readModelEnvelope, - cancellationToken) - .ConfigureAwait(false); + var descriptor = GetDescriptor(dbContext, QueryableConfiguration); + var entry = isNew + ? dbContext.Add(entity) + : dbContext.Entry(entity); + descriptor.SetId(entry, readModelId); + descriptor.SetVersion(entry, originalVersion, readModelEnvelope.Version); + try + { + await dbContext.SaveChangesAsync(cancellationToken) + .ConfigureAwait(false); + } + catch (DbUpdateConcurrencyException e) + { + var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken) + .ConfigureAwait(false); + entry.CurrentValues.SetValues(databaseValues!); + throw new OptimisticConcurrencyException(e.Message, e); + } - if (!updateResult.IsModified) - return; + Logger.LogTrace($"Updated Entity Framework read model {typeof(TReadModel).PrettyPrint()} with ID '{readModelId}' to version '{readModelEnvelope.Version}'"); + return updateResult; + } - if (readModelContext.IsMarkedForDeletion) - { - await DeleteAsync(dbContext, readModelId, cancellationToken).ConfigureAwait(false); - return; - } + protected static EntityDescriptor GetDescriptor(DbContext context, + IApplyQueryableConfiguration queryableConfiguration) + { + return Descriptors.GetOrAdd(context.Database.ProviderName!, + _ => + new EntityDescriptor(context, queryableConfiguration)); + } - readModelEnvelope = updateResult.Envelope; - entity = readModelEnvelope.ReadModel; + protected sealed class EntityDescriptor + { + private readonly IProperty _key; + private readonly Func> _queryByIdNoTracking; + private readonly Func> _queryByIdTracking; + private readonly IProperty? _version; + public string Key => _key.Name; + public string? Version => _version?.Name; + + public EntityDescriptor(DbContext context, + IApplyQueryableConfiguration queryableConfiguration) + { + var entityType = context.Model.FindEntityType(typeof(TReadModel)); + _key = GetKeyProperty(entityType!); + _version = GetVersionProperty(entityType!); + _queryByIdTracking = CompileQueryById(queryableConfiguration, true); + _queryByIdNoTracking = CompileQueryById(queryableConfiguration, false); + } - var descriptor = GetDescriptor(dbContext, _queryableConfiguration); - var entry = isNew - ? dbContext.Add(entity) - : dbContext.Entry(entity); - descriptor.SetId(entry, readModelId); - descriptor.SetVersion(entry, originalVersion, readModelEnvelope.Version); - try - { - await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - catch (DbUpdateConcurrencyException e) + public long? GetVersion(EntityEntry entry) + { + if (_version == null) { - var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken) - .ConfigureAwait(false); - entry.CurrentValues.SetValues(databaseValues); - throw new OptimisticConcurrencyException(e.Message, e); + return null; } - Log.Verbose(() => $"Updated Entity Framework read model {typeof(TReadModel).PrettyPrint()} with ID '{readModelId}' to version '{readModelEnvelope.Version}'"); + var property = entry.Property(_version.Name); + return (long?)property.CurrentValue; + } + + public Task Query(DbContext context, string id, CancellationToken t, bool tracking = false) + { + return tracking + ? _queryByIdTracking(context, t, id) + : _queryByIdNoTracking(context, t, id); } - private static EntityDescriptor GetDescriptor( - DbContext context, - IApplyQueryableConfiguration queryableConfiguration) + public void SetId(EntityEntry entry, string id) { - return Descriptors.GetOrAdd(context.Database.ProviderName, s => - new EntityDescriptor(context, queryableConfiguration)); + var property = entry.Property(_key.Name); + property.CurrentValue = id; } - private class EntityDescriptor + public void SetVersion(EntityEntry entry, long? originalVersion, long? currentVersion = null) { - private readonly IProperty _key; - private readonly Func> _queryByIdNoTracking; - private readonly Func> _queryByIdTracking; - private readonly IProperty _version; - - public EntityDescriptor( - DbContext context, - IApplyQueryableConfiguration queryableConfiguration) + if (_version == null) { - var entityType = context.Model.FindEntityType(typeof(TReadModel)); - _key = GetKeyProperty(entityType); - _version = GetVersionProperty(entityType); - _queryByIdTracking = CompileQueryById(queryableConfiguration, true); - _queryByIdNoTracking = CompileQueryById(queryableConfiguration, false); + return; } - public string Key => _key.Name; - public string Version => _version.Name; + var property = entry.Property(_version.Name); + property.OriginalValue = originalVersion ?? 0; + property.CurrentValue = currentVersion ?? 0; + } - public Task Query(DbContext context, string id, CancellationToken t, bool tracking = false) - { - return tracking - ? _queryByIdTracking(context, t, id) - : _queryByIdNoTracking(context, t, id); - } + private Func> CompileQueryById(IApplyQueryableConfiguration queryableConfiguration, + bool tracking) + { + return tracking + ? EF.CompileAsyncQuery((DbContext dbContext, CancellationToken t, string id) => + queryableConfiguration.Apply(dbContext + .Set() + .AsTracking()) + .SingleOrDefault(e => EF.Property(e, Key) == id)) + : EF.CompileAsyncQuery((DbContext dbContext, CancellationToken t, string id) => + queryableConfiguration.Apply( + dbContext + .Set() + .AsNoTracking()) + .SingleOrDefault(e => EF.Property(e, Key) == id)); + } - public void SetId(EntityEntry entry, string id) + private static IProperty GetKeyProperty(IEntityType entityType) + { + IProperty key; + var keyProperties = entityType.FindPrimaryKey() ?? + throw new InvalidOperationException("Primary key not found"); + try { - var property = entry.Property(_key.Name); - property.CurrentValue = id; + key = keyProperties.Properties.Single(); } - - public long? GetVersion(EntityEntry entry) + catch (InvalidOperationException e) { - if (_version == null) return null; - - var property = entry.Property(_version.Name); - return (long?) property.CurrentValue; + throw new InvalidOperationException("Read store doesn't support composite primary keys.", e); } - public void SetVersion(EntityEntry entry, long? originalVersion, long? currentVersion = null) - { - if (_version == null) return; + return key; + } - var property = entry.Property(_version.Name); - property.OriginalValue = originalVersion ?? 0; - property.CurrentValue = currentVersion ?? 0; - } + private static IProperty? GetVersionProperty(IEntityType entityType) + { + IProperty? version; + var concurrencyProperties = entityType + .GetProperties() + .Where(IsConcurrencyProperty) + .ToList(); - private bool IsConcurrencyProperty(IProperty p) + if (concurrencyProperties.Count > 1) { - return p.IsConcurrencyToken && (p.ClrType == typeof(long) || p.ClrType == typeof(byte[])); + concurrencyProperties = concurrencyProperties + .Where(p => p.Name.IndexOf("version", StringComparison.OrdinalIgnoreCase) >= 0) + .ToList(); } - private static IProperty GetKeyProperty(IEntityType entityType) + try { - IProperty key; - var keyProperties = entityType.FindPrimaryKey() ?? - throw new InvalidOperationException("Primary key not found"); - try - { - key = keyProperties.Properties.Single(); - } - catch (InvalidOperationException e) - { - throw new InvalidOperationException("Read store doesn't support composite primary keys.", e); - } - - return key; + version = concurrencyProperties.SingleOrDefault(); } - - private IProperty GetVersionProperty(IEntityType entityType) + catch (InvalidOperationException e) { - IProperty version; - var concurrencyProperties = entityType - .GetProperties() - .Where(IsConcurrencyProperty) - .ToList(); - - if (concurrencyProperties.Count > 1) - concurrencyProperties = concurrencyProperties - .Where(p => p.Name.IndexOf("version", StringComparison.OrdinalIgnoreCase) >= 0) - .ToList(); - - try - { - version = concurrencyProperties.SingleOrDefault(); - } - catch (InvalidOperationException e) - { - throw new InvalidOperationException("Couldn't determine row version property.", e); - } - - return version; + throw new InvalidOperationException("Couldn't determine row version property.", e); } - private Func> CompileQueryById( - IApplyQueryableConfiguration queryableConfiguration, - bool tracking) - { - return tracking - ? EF.CompileAsyncQuery((DbContext dbContext, CancellationToken t, string id) => - queryableConfiguration.Apply(dbContext - .Set() - .AsTracking()) - .SingleOrDefault(e => EF.Property(e, Key) == id)) - : EF.CompileAsyncQuery((DbContext dbContext, CancellationToken t, string id) => - queryableConfiguration.Apply( - dbContext - .Set() - .AsNoTracking()) - .SingleOrDefault(e => EF.Property(e, Key) == id)); - } + return version; } + + private static bool IsConcurrencyProperty(IProperty p) + { + return p.IsConcurrencyToken && (p.ClrType == typeof(long) || p.ClrType == typeof(byte[])); + } + } + + private sealed class BulkDeletionModel + { + public required string Id { get; set; } + public long? Version { get; set; } } -} \ No newline at end of file +} diff --git a/Source/EventFlow.EntityFramework/SnapshotStores/EntityFrameworkSnapshotPersistence.cs b/Source/EventFlow.EntityFramework/SnapshotStores/EntityFrameworkSnapshotPersistence.cs index d5f67c8ad..6773d83df 100644 --- a/Source/EventFlow.EntityFramework/SnapshotStores/EntityFrameworkSnapshotPersistence.cs +++ b/Source/EventFlow.EntityFramework/SnapshotStores/EntityFrameworkSnapshotPersistence.cs @@ -19,7 +19,6 @@ // 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.Linq; using System.Threading; @@ -31,122 +30,119 @@ using EventFlow.Snapshots.Stores; using Microsoft.EntityFrameworkCore; -namespace EventFlow.EntityFramework.SnapshotStores +namespace EventFlow.EntityFramework.SnapshotStores; + +public class EntityFrameworkSnapshotPersistence : ISnapshotPersistence + where TDbContext : DbContext { - public class EntityFrameworkSnapshotPersistence : ISnapshotPersistence - where TDbContext : DbContext - { - private readonly IDbContextProvider _contextProvider; - private readonly IUniqueConstraintDetectionStrategy _strategy; - private readonly int _deletionBatchSize; + private readonly IDbContextProvider _contextProvider; + private readonly int _deletionBatchSize; + private readonly IUniqueConstraintDetectionStrategy _strategy; - public EntityFrameworkSnapshotPersistence( - IBulkOperationConfiguration bulkOperationConfiguration, - IDbContextProvider contextProvider, - IUniqueConstraintDetectionStrategy strategy - ) - { - _contextProvider = contextProvider; - _strategy = strategy; - _deletionBatchSize = bulkOperationConfiguration.DeletionBatchSize; - } + public EntityFrameworkSnapshotPersistence(IBulkOperationConfiguration bulkOperationConfiguration, + IDbContextProvider contextProvider, + IUniqueConstraintDetectionStrategy strategy) + { + _contextProvider = contextProvider; + _strategy = strategy; + _deletionBatchSize = bulkOperationConfiguration.DeletionBatchSize; + } - public async Task GetSnapshotAsync( - Type aggregateType, - IIdentity identity, - CancellationToken cancellationToken) - { - var aggregateName = aggregateType.GetAggregateName().Value; + public Task DeleteSnapshotAsync(Type aggregateType, + IIdentity identity, + CancellationToken cancellationToken) + { + var aggregateName = aggregateType.GetAggregateName() + .Value; + var aggregateId = identity.Value; + return Bulk.DeleteAsync( + _contextProvider, + _deletionBatchSize, + cancellationToken, + e => new SnapshotEntity { Id = e.Id }, + e => e.AggregateId, + e => e.AggregateName == aggregateName + && e.AggregateId == aggregateId); + } - using (var dbContext = _contextProvider.CreateContext()) - { - var snapshot = await dbContext.Set() - .AsNoTracking() - .Where(s => s.AggregateName == aggregateName - && s.AggregateId == identity.Value) - .OrderByDescending(s => s.AggregateSequenceNumber) - .Select(s => new SnapshotEntity - { - Metadata = s.Metadata, - Data = s.Data - }) - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); + public async Task GetSnapshotAsync(Type aggregateType, + IIdentity identity, + CancellationToken cancellationToken) + { + var aggregateName = aggregateType.GetAggregateName() + .Value; - return snapshot == null - ? null - : new CommittedSnapshot(snapshot.Metadata, snapshot.Data); - } - } + await using var dbContext = _contextProvider.CreateContext(); + var snapshot = await dbContext.Set() + .AsNoTracking() + .Where(s => s.AggregateName == aggregateName + && s.AggregateId == identity.Value) + .OrderByDescending(s => s.AggregateSequenceNumber) + .Select(s => new SnapshotEntity + { + Metadata = s.Metadata, + Data = s.Data + }) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); - public async Task SetSnapshotAsync( - Type aggregateType, - IIdentity identity, - SerializedSnapshot serializedSnapshot, - CancellationToken cancellationToken) - { - var entity = new SnapshotEntity - { - AggregateId = identity.Value, - AggregateName = aggregateType.GetAggregateName().Value, - AggregateSequenceNumber = serializedSnapshot.Metadata.AggregateSequenceNumber, - Metadata = serializedSnapshot.SerializedMetadata, - Data = serializedSnapshot.SerializedData - }; + return snapshot == null + ? null + : new CommittedSnapshot(snapshot.Metadata, snapshot.Data); + } - using (var dbContext = _contextProvider.CreateContext()) - { - dbContext.Add(entity); + public Task PurgeSnapshotsAsync(Type aggregateType, + CancellationToken cancellationToken) + { + var aggregateName = aggregateType.GetAggregateName() + .Value; + return Bulk.DeleteAsync( + _contextProvider, + _deletionBatchSize, + cancellationToken, + e => new SnapshotEntity { Id = e.Id }, + e => e.AggregateId, + e => e.AggregateName == aggregateName); + } - try - { - await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); - } - catch (DbUpdateException ex) when (ex.IsUniqueConstraintViolation(_strategy)) - { - // If we have a duplicate key exception, then the snapshot has already been created - } - } - } + public Task PurgeSnapshotsAsync(CancellationToken cancellationToken) + { + return Bulk.DeleteAsync( + _contextProvider, + _deletionBatchSize, + cancellationToken, + e => new SnapshotEntity { Id = e.Id }, + e => e.AggregateId); + } - public Task DeleteSnapshotAsync( - Type aggregateType, - IIdentity identity, - CancellationToken cancellationToken) - { - var aggregateName = aggregateType.GetAggregateName().Value; - var aggregateId = identity.Value; - return Bulk.Delete( - _contextProvider, - _deletionBatchSize, - cancellationToken, - e => new SnapshotEntity {Id = e.Id}, - e => e.AggregateName == aggregateName - && e.AggregateId == aggregateId); - } + public async Task SetSnapshotAsync(Type aggregateType, + IIdentity identity, + SerializedSnapshot serializedSnapshot, + CancellationToken cancellationToken) + { + var entity = new SnapshotEntity + { + AggregateId = identity.Value, + AggregateName = aggregateType.GetAggregateName() + .Value, + AggregateSequenceNumber = serializedSnapshot.Metadata.AggregateSequenceNumber, + Metadata = serializedSnapshot.SerializedMetadata, + Data = serializedSnapshot.SerializedData + }; - public Task PurgeSnapshotsAsync( - Type aggregateType, - CancellationToken cancellationToken) + using (var dbContext = _contextProvider.CreateContext()) { - var aggregateName = aggregateType.GetAggregateName().Value; - return Bulk.Delete( - _contextProvider, - _deletionBatchSize, - cancellationToken, - e => new SnapshotEntity {Id = e.Id}, - e => e.AggregateName == aggregateName); - } + dbContext.Add(entity); - public Task PurgeSnapshotsAsync( - CancellationToken cancellationToken) - { - return Bulk.Delete( - _contextProvider, - _deletionBatchSize, - cancellationToken, - e => new SnapshotEntity { Id = e.Id }); + try + { + await dbContext.SaveChangesAsync(cancellationToken) + .ConfigureAwait(false); + } + catch (DbUpdateException ex) when (ex.IsUniqueConstraintViolation(_strategy)) + { + // If we have a duplicate key exception, then the snapshot has already been created + } } } } - diff --git a/Source/EventFlow.EntityFramework/SnapshotStores/SnapshotEntity.cs b/Source/EventFlow.EntityFramework/SnapshotStores/SnapshotEntity.cs index fa6ea0a68..d6e1f3941 100644 --- a/Source/EventFlow.EntityFramework/SnapshotStores/SnapshotEntity.cs +++ b/Source/EventFlow.EntityFramework/SnapshotStores/SnapshotEntity.cs @@ -19,16 +19,14 @@ // 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.EntityFramework.SnapshotStores; -namespace EventFlow.EntityFramework.SnapshotStores +public class SnapshotEntity { - public class SnapshotEntity - { - public long Id { get; set; } - public string AggregateId { get; set; } - public string AggregateName { get; set; } - public int AggregateSequenceNumber { get; set; } - public string Data { get; set; } - public string Metadata { get; set; } - } + public string AggregateId { get; init; } = default!; + public string AggregateName { get; init; } = default!; + public int AggregateSequenceNumber { get; init; } + public string Data { get; init; } = default!; + public long Id { get; init; } + public string Metadata { get; init; } = default!; } \ No newline at end of file diff --git a/Source/EventFlow.PostgreSql.Tests/IntegrationTests/ReadStores/PostgresSqlReadModelStoreTests.cs b/Source/EventFlow.PostgreSql.Tests/IntegrationTests/ReadStores/PostgresSqlReadModelStoreTests.cs index 20e8629b6..2a6ef1b3e 100644 --- a/Source/EventFlow.PostgreSql.Tests/IntegrationTests/ReadStores/PostgresSqlReadModelStoreTests.cs +++ b/Source/EventFlow.PostgreSql.Tests/IntegrationTests/ReadStores/PostgresSqlReadModelStoreTests.cs @@ -22,7 +22,6 @@ using System; using System.Threading; -using EventFlow.Configuration; using EventFlow.Extensions; using EventFlow.PostgreSql.Connections; using EventFlow.PostgreSql.EventStores;