diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ea32df159..d30960310 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,18 @@ * Fix: Source IDs are now added to snapshots * Fix: InMemoryReadStore will not break on unmodified update result * Fix: Allow the use of explicitly implemented interfaces in the read model +* New: added extension methods to the `EventFlow.EntityFramework` package that allow + us to configure [eager loading of related data](https://docs.microsoft.com/en-us/ef/core/querying/related-data/eager). Example usage: + ```csharp + public static IEventFlowOptions Configure(this IEventFlowOptions options) + { + return options + .UseEntityFrameworkReadModel( + cfg => cfg.Include(x => x.SomeProperty) + .ThenInclude(y => y.SomeOtherProperty) + ); + } + ``` ### New in 0.81.4483 (released 2020-12-14) diff --git a/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs b/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs index 1540d6269..274af2c65 100644 --- a/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs +++ b/Source/EventFlow.EntityFramework.Tests/EntityFrameworkTestExtensions.cs @@ -23,6 +23,8 @@ using EventFlow.EntityFramework.Extensions; using EventFlow.EntityFramework.Tests.Model; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Queries; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.ReadModels; using EventFlow.Extensions; using EventFlow.TestHelpers.Aggregates.Entities; @@ -53,5 +55,12 @@ public static IEventFlowOptions ConfigureForReadStoreTest(this IEventFlowOptions typeof(EfThingyGetVersionQueryHandler), typeof(EfThingyGetMessagesQueryHandler)); } + + public static IEventFlowOptions ConfigureForReadStoreIncludeTest(this IEventFlowOptions options) + { + return options + .UseEntityFrameworkReadModel(cfg => cfg.Include(x => x.Addresses)) + .AddQueryHandlers(typeof(PersonGetQueryHandler)); + } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs b/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs index 6ca799796..81f4b769f 100644 --- a/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs +++ b/Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs @@ -22,6 +22,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using EventFlow.EntityFramework.Extensions; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.ReadModels; using Microsoft.EntityFrameworkCore; namespace EventFlow.EntityFramework.Tests.Model @@ -35,6 +36,10 @@ public TestDbContext(DbContextOptions options) : base(options) public DbSet Thingys { get; set; } public DbSet ThingyMessages { get; set; } + // Include tests + public DbSet Persons { get; set; } + public DbSet Addresses { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder @@ -48,6 +53,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .Property(e => e.AggregateId) .ValueGeneratedOnAdd(); + + modelBuilder.Entity() + .Property(e => e.AggregateId) + .ValueGeneratedOnAdd(); + + modelBuilder.Entity() + .Property(e => e.AddressId) + .ValueGeneratedNever(); } } } \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs new file mode 100644 index 000000000..b71ab940c --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs @@ -0,0 +1,121 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Configuration; +using EventFlow.EntityFramework.Extensions; +using EventFlow.EntityFramework.Tests.Model; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Commands; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Queries; +using EventFlow.Extensions; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.MsSql; +using FluentAssertions; +using NUnit.Framework; + +namespace EventFlow.EntityFramework.Tests.MsSql +{ + [Category(Categories.Integration)] + public class EfMsSqlReadStoreIncludeTests : IntegrationTest + { + private IMsSqlDatabase _testDatabase; + + protected override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) + { + _testDatabase = MsSqlHelpz.CreateDatabase("eventflow"); + + return eventFlowOptions + .RegisterServices(sr => sr.Register(c => _testDatabase.ConnectionString)) + .ConfigureEntityFramework(EntityFrameworkConfiguration.New) + .AddDbContextProvider() + .ConfigureForReadStoreIncludeTest() + .AddDefaults(typeof(EfMsSqlReadStoreIncludeTests).Assembly) + .CreateResolver(); + } + + [TearDown] + public void TearDown() + { + _testDatabase.DisposeSafe("Failed to delete database"); + } + + [Test] + public async Task ReadModelContainsPersonNameAfterCreation() + { + // Arrange + var id = PersonId.New; + + // Act + await CommandBus + .PublishAsync(new CreatePersonCommand(id, "Bob"), CancellationToken.None) + .ConfigureAwait(false); + + var readModel = await QueryProcessor + .ProcessAsync(new PersonGetQuery(id), CancellationToken.None) + .ConfigureAwait(false); + + // Assert + readModel.Should().NotBeNull(); + readModel.Name.Should().Be("Bob"); + readModel.Addresses.Should().BeNullOrEmpty(); + } + + [Test] + public async Task ReadModelContainsPersonAddressesAfterAdd() + { + // Arrange + var id = PersonId.New; + await CommandBus + .PublishAsync(new CreatePersonCommand(id, "Bob"), CancellationToken.None) + .ConfigureAwait(false); + + // Act + var address1 = new Address(AddressId.New, "Smith street 4.", "1234", "New York", "US"); + await CommandBus + .PublishAsync(new AddAddressCommand(id, + address1), + CancellationToken.None) + .ConfigureAwait(false); + + var address2 = new Address(AddressId.New, "Musterstraße 42.", "6541", "Berlin", "DE"); + await CommandBus + .PublishAsync(new AddAddressCommand(id, + address2), + CancellationToken.None) + .ConfigureAwait(false); + + var readModel = await QueryProcessor + .ProcessAsync(new PersonGetQuery(id), CancellationToken.None) + .ConfigureAwait(false); + + // Assert + readModel.Should().NotBeNull(); + readModel.NumberOfAddresses.Should().Be(2); + readModel.Addresses.Should().HaveCount(2); + readModel.Addresses.Should().ContainEquivalentOf(address1); + readModel.Addresses.Should().ContainEquivalentOf(address2); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Address.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Address.cs new file mode 100644 index 000000000..0127189d8 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Address.cs @@ -0,0 +1,43 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Entities; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests +{ + public class Address : Entity + { + public string Street { get; set; } + public string PostalCode { get; set; } + public string City { get; set; } + public string Country { get; set; } + + public Address(AddressId id, string street, string postalCode, string city, string country) : base(id) + { + Street = street; + PostalCode = postalCode; + City = city; + Country = country; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/AddressId.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/AddressId.cs new file mode 100644 index 000000000..0cce5133c --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/AddressId.cs @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Core; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests +{ + public class AddressId : Identity + { + public AddressId(string value) : base(value) + { + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Commands/AddAddressCommand.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Commands/AddAddressCommand.cs new file mode 100644 index 000000000..b2a193de1 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Commands/AddAddressCommand.cs @@ -0,0 +1,48 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Commands; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Commands +{ + public class AddAddressCommand : Command + { + public Address PersonAddress { get; } + + public AddAddressCommand(PersonId aggregateId, Address personAddress) : base(aggregateId) + { + PersonAddress = personAddress; + } + } + + public class AddAddressCommandHandler : CommandHandler + { + public override Task ExecuteAsync(PersonAggregate aggregate, AddAddressCommand command, CancellationToken cancellationToken) + { + aggregate.AddAddress(command.PersonAddress); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Commands/CreatePersonCommand.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Commands/CreatePersonCommand.cs new file mode 100644 index 000000000..033a51295 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Commands/CreatePersonCommand.cs @@ -0,0 +1,49 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Commands; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Commands +{ + public class CreatePersonCommand : Command + { + public string Name { get; } + + public CreatePersonCommand(PersonId aggregateId, string name) + :base(aggregateId) + { + Name = name; + } + } + + public class CreatePersonCommandHandler : CommandHandler + { + public override Task ExecuteAsync(PersonAggregate aggregate, CreatePersonCommand command, CancellationToken cancellationToken) + { + aggregate.Create(command.Name); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Events/AddressAddedEvent.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Events/AddressAddedEvent.cs new file mode 100644 index 000000000..1a1dd5378 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Events/AddressAddedEvent.cs @@ -0,0 +1,37 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Aggregates; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Events +{ + public class AddressAddedEvent : AggregateEvent + { + public Address Address { get; set; } + + public AddressAddedEvent(Address address) + { + Address = address; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Events/PersonCreatedEvent.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Events/PersonCreatedEvent.cs new file mode 100644 index 000000000..526502f97 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Events/PersonCreatedEvent.cs @@ -0,0 +1,37 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Aggregates; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Events +{ + public class PersonCreatedEvent : AggregateEvent + { + public string Name { get; set; } + + public PersonCreatedEvent(string name) + { + Name = name; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Person.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Person.cs new file mode 100644 index 000000000..ff7b4283a --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Person.cs @@ -0,0 +1,42 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using EventFlow.Entities; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests +{ + public class Person : Entity + { + public string Name { get; } + public ICollection
Addresses { get; } + public int NumberOfAddresses { get; } + + public Person(PersonId id, string name, ICollection
addresses, int numberOfAddresses) : base(id) + { + Name = name; + Addresses = addresses; + NumberOfAddresses = numberOfAddresses; + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/PersonAggregate.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/PersonAggregate.cs new file mode 100644 index 000000000..16fbeda6a --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/PersonAggregate.cs @@ -0,0 +1,60 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Aggregates; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Events; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests +{ + [AggregateName("Person")] + public class PersonAggregate : AggregateRoot, + IEmit, + IEmit + { + public PersonAggregate(PersonId id) : base(id) + { + } + + public void Create(string name) + { + Emit(new PersonCreatedEvent(name)); + } + + public void AddAddress(Address address) + { + Emit(new AddressAddedEvent(address)); + } + + void IEmit.Apply(PersonCreatedEvent aggregateEvent) + { + // save name into field for later usage + // .. + } + + void IEmit.Apply(AddressAddedEvent aggregateEvent) + { + // save address into field for later usage + // .. + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/PersonId.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/PersonId.cs new file mode 100644 index 000000000..18b8b5ed1 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/PersonId.cs @@ -0,0 +1,34 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Core; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests +{ + public class PersonId : Identity + { + public PersonId(string value) : base(value) + { + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Queries/PersonGetQuery.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Queries/PersonGetQuery.cs new file mode 100644 index 000000000..3a6e5173e --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/Queries/PersonGetQuery.cs @@ -0,0 +1,61 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.EntityFramework.Tests.Model; +using EventFlow.Queries; +using Microsoft.EntityFrameworkCore; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Queries +{ + public class PersonGetQuery : IQuery + { + public PersonId PersonId { get; } + + public PersonGetQuery(PersonId personId) + { + PersonId = personId; + } + } + + public class PersonGetQueryHandler : IQueryHandler + { + private readonly IDbContextProvider _dbContextProvider; + + public PersonGetQueryHandler(IDbContextProvider dbContextProvider) + { + _dbContextProvider = dbContextProvider; + } + + public async Task ExecuteQueryAsync(PersonGetQuery query, CancellationToken cancellationToken) + { + await using var context = _dbContextProvider.CreateContext(); + var entity = await context.Persons + .Include(x => x.Addresses) + .SingleOrDefaultAsync(x => x.AggregateId == query.PersonId.Value, cancellationToken) + .ConfigureAwait(false); + return entity?.ToPerson(); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/AddressReadModelEntity.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/AddressReadModelEntity.cs new file mode 100644 index 000000000..1dfad9ef2 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/AddressReadModelEntity.cs @@ -0,0 +1,51 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests.ReadModels +{ + public class AddressReadModelEntity + { + [Key] + [StringLength(64)] + public string AddressId { get; set; } + + [StringLength(64)] + public string PersonId { get; set; } + + [ForeignKey(nameof(PersonId))] + public virtual PersonReadModelEntity Person { get; set; } + + public string Street { get; set; } + + public string PostalCode { get; set; } + + public string City { get; set; } + + public string Country { get; set; } + + public Address ToAddress() => new Address(IncludeTests.AddressId.With(AddressId), Street, PostalCode, City, Country); + } +} \ 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 new file mode 100644 index 000000000..9cf43d3f7 --- /dev/null +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/IncludeTests/ReadModels/PersonReadModelEntity.cs @@ -0,0 +1,77 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using EventFlow.Aggregates; +using EventFlow.EntityFramework.Tests.MsSql.IncludeTests.Events; +using EventFlow.ReadStores; + +namespace EventFlow.EntityFramework.Tests.MsSql.IncludeTests.ReadModels +{ + public class PersonReadModelEntity : IReadModel, + IAmReadModelFor, + IAmReadModelFor + { + [Key] + [StringLength(64)] + public string AggregateId { get; set; } + + public string Name { get; set; } + + public int NumberOfAddresses { get; set; } + + public virtual ICollection Addresses { get; set; } = new List(); + + public void Apply(IReadModelContext context, + IDomainEvent domainEvent) + { + Name = domainEvent.AggregateEvent.Name; + } + + public void Apply(IReadModelContext context, + IDomainEvent domainEvent) + { + var address = domainEvent.AggregateEvent.Address; + Addresses.Add(new AddressReadModelEntity + { + AddressId = address.Id.Value, + PersonId = domainEvent.AggregateIdentity.Value, + Street = address.Street, + City = address.City, + PostalCode = address.PostalCode, + Country = address.Country + }); + + NumberOfAddresses = Addresses.Count; + } + + public Person ToPerson() => + new Person( + PersonId.With(AggregateId), + Name, + Addresses?.Select(x => x.ToAddress()).ToList(), + NumberOfAddresses); + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/EntityFrameworkReadModelConfiguration.cs b/Source/EventFlow.EntityFramework/EntityFrameworkReadModelConfiguration.cs new file mode 100644 index 000000000..2e0ba6b92 --- /dev/null +++ b/Source/EventFlow.EntityFramework/EntityFrameworkReadModelConfiguration.cs @@ -0,0 +1,37 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Linq; +using EventFlow.EntityFramework.ReadStores.Configuration; +using EventFlow.ReadStores; + +namespace EventFlow.EntityFramework +{ + public sealed class EntityFrameworkReadModelConfiguration : IApplyQueryableConfiguration + where TReadModel : class, IReadModel, new() + { + IQueryable + IApplyQueryableConfiguration.Apply(IQueryable queryable) => + queryable; + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/EntityFrameworkReadModelConfigurationExtensions.cs b/Source/EventFlow.EntityFramework/EntityFrameworkReadModelConfigurationExtensions.cs new file mode 100644 index 000000000..6b26ca518 --- /dev/null +++ b/Source/EventFlow.EntityFramework/EntityFrameworkReadModelConfigurationExtensions.cs @@ -0,0 +1,121 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using EventFlow.EntityFramework.ReadStores.Configuration; +using EventFlow.EntityFramework.ReadStores.Configuration.Includes; +using EventFlow.ReadStores; + +namespace EventFlow.EntityFramework +{ + /// + /// Extensions methods to configure the ReadModel + /// + public static class EntityFrameworkReadModelConfigurationExtensions + { + /// + public static IncludeExpression + Include( + this IApplyQueryableConfiguration source, + Expression> navigationPropertyPath) + where TReadModel : class, IReadModel, new() + { + if (navigationPropertyPath == null) + { + throw new ArgumentNullException(nameof(navigationPropertyPath)); + } + + return new IncludeExpression( + source, + navigationPropertyPath); + } + + /// + public static IncludeString + Include( + this IApplyQueryableConfiguration source, + string navigationPropertyPath) + where TReadModel : class, IReadModel, new() + { + if (navigationPropertyPath == null) + { + throw new ArgumentNullException(nameof(navigationPropertyPath)); + } + + if (string.IsNullOrWhiteSpace(navigationPropertyPath)) + { + throw new ArgumentException("Must not be null or empty", nameof(navigationPropertyPath)); + } + + return new IncludeString( + source, + navigationPropertyPath); + } + + /// + public static ThenIncludeExpression + ThenInclude( + this IApplyQueryableIncludeConfiguration source, + Expression> navigationPropertyPath) + where TEntity : class, IReadModel, new() + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (navigationPropertyPath == null) + { + throw new ArgumentNullException(nameof(navigationPropertyPath)); + } + + return new ThenIncludeExpression( + source, + navigationPropertyPath); + } + + /// + public static ThenIncludeEnumerableExpression + ThenInclude( + this IApplyQueryableIncludeConfiguration> source, + Expression> navigationPropertyPath) + where TEntity : class, IReadModel, new() + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (navigationPropertyPath == null) + { + throw new ArgumentNullException(nameof(navigationPropertyPath)); + } + + return new ThenIncludeEnumerableExpression( + source, + navigationPropertyPath); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs b/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs index 836131e5a..2fe0a220c 100644 --- a/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs +++ b/Source/EventFlow.EntityFramework/Extensions/EventFlowOptionsEntityFrameworkExtensions.cs @@ -25,6 +25,8 @@ 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; @@ -68,12 +70,81 @@ public static IEventFlowOptions UseEntityFrameworkReadModel, EntityFrameworkReadModelStore>(); + f.Register>(_ => + new EntityFrameworkReadModelConfiguration(), Lifetime.Singleton); f.Register>(r => r.Resolver.Resolve>()); }) .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; + + }, Lifetime.Singleton); + f.Register>(r => + r.Resolver.Resolve>()); + }) + .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 + /// 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.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>(); + } + public static IEventFlowOptions UseEntityFrameworkReadModel( this IEventFlowOptions eventFlowOptions) where TDbContext : DbContext @@ -85,6 +156,8 @@ public static IEventFlowOptions UseEntityFrameworkReadModel, EntityFrameworkReadModelStore>(); + f.Register>(_ => + new EntityFrameworkReadModelConfiguration(), Lifetime.Singleton); f.Register>(r => r.Resolver.Resolve>()); }) diff --git a/Source/EventFlow.EntityFramework/ReadStores/Configuration/IApplyQueryableConfiguration.cs b/Source/EventFlow.EntityFramework/ReadStores/Configuration/IApplyQueryableConfiguration.cs new file mode 100644 index 000000000..5e0dcf7d4 --- /dev/null +++ b/Source/EventFlow.EntityFramework/ReadStores/Configuration/IApplyQueryableConfiguration.cs @@ -0,0 +1,43 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Linq; +using EventFlow.ReadStores; + +namespace EventFlow.EntityFramework.ReadStores.Configuration +{ + /// + /// Configures an IQueryable + /// + /// Entity type + public interface IApplyQueryableConfiguration + where TReadModel : class, IReadModel, new() + { + /// + /// Applies the expression to the IQueryable + /// + /// Source + /// The applied IQueryable + IQueryable Apply(IQueryable queryable); + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/ReadStores/Configuration/IApplyQueryableIncludeConfiguration.cs b/Source/EventFlow.EntityFramework/ReadStores/Configuration/IApplyQueryableIncludeConfiguration.cs new file mode 100644 index 000000000..c34681761 --- /dev/null +++ b/Source/EventFlow.EntityFramework/ReadStores/Configuration/IApplyQueryableIncludeConfiguration.cs @@ -0,0 +1,46 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Linq; +using EventFlow.ReadStores; +using Microsoft.EntityFrameworkCore.Query; + +namespace EventFlow.EntityFramework.ReadStores.Configuration +{ + /// + /// Configures an IQueryable + /// + /// Entity type + /// Property type + public interface IApplyQueryableIncludeConfiguration + : IApplyQueryableConfiguration + where TReadModel : class, IReadModel, new() + { + /// + /// Applies the include expression to the IQueryable + /// + /// Source + /// An IIncludableQueryable + new IIncludableQueryable Apply(IQueryable queryable); + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/IncludeExpression.cs b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/IncludeExpression.cs new file mode 100644 index 000000000..fee263ded --- /dev/null +++ b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/IncludeExpression.cs @@ -0,0 +1,59 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Linq.Expressions; +using EventFlow.ReadStores; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace EventFlow.EntityFramework.ReadStores.Configuration.Includes +{ + public sealed class IncludeExpression + : IApplyQueryableIncludeConfiguration + where TReadModel : class, IReadModel, new() + { + private readonly IApplyQueryableConfiguration _source; + private readonly Expression> _navigationPropertyPath; + + internal IncludeExpression( + IApplyQueryableConfiguration source, + Expression> navigationPropertyPath) + { + _source = source; + _navigationPropertyPath = navigationPropertyPath; + } + + IQueryable + IApplyQueryableConfiguration.Apply( + IQueryable queryable) => ApplyInternal(queryable); + + IIncludableQueryable + IApplyQueryableIncludeConfiguration.Apply( + IQueryable queryable) => ApplyInternal(queryable); + + private IIncludableQueryable ApplyInternal(IQueryable queryable) => + _source.Apply(queryable).Include(_navigationPropertyPath); + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/IncludeString.cs b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/IncludeString.cs new file mode 100644 index 000000000..7e573c14b --- /dev/null +++ b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/IncludeString.cs @@ -0,0 +1,49 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Linq; +using EventFlow.ReadStores; +using Microsoft.EntityFrameworkCore; + +namespace EventFlow.EntityFramework.ReadStores.Configuration.Includes +{ + public sealed class IncludeString + : IApplyQueryableConfiguration + where TReadModel : class, IReadModel, new() + { + private readonly IApplyQueryableConfiguration _source; + private readonly string _navigationPropertyPath; + + internal IncludeString( + IApplyQueryableConfiguration source, + string navigationPropertyPath) + { + _source = source; + _navigationPropertyPath = navigationPropertyPath; + } + + IQueryable IApplyQueryableConfiguration.Apply( + IQueryable queryable) => + _source.Apply(queryable).Include(_navigationPropertyPath); + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/ThenIncludeEnumerableExpression.cs b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/ThenIncludeEnumerableExpression.cs new file mode 100644 index 000000000..a5c0fe086 --- /dev/null +++ b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/ThenIncludeEnumerableExpression.cs @@ -0,0 +1,62 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using EventFlow.ReadStores; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace EventFlow.EntityFramework.ReadStores.Configuration.Includes +{ + public sealed class ThenIncludeEnumerableExpression + : IApplyQueryableIncludeConfiguration + where TReadModel : class, IReadModel, new() + { + private readonly IApplyQueryableIncludeConfiguration> _source; + private readonly Expression> _navigationPropertyPath; + + public ThenIncludeEnumerableExpression( + IApplyQueryableIncludeConfiguration> source, + Expression> navigationPropertyPath) + { + _source = source; + _navigationPropertyPath = navigationPropertyPath; + } + + IQueryable + IApplyQueryableConfiguration.Apply( + IQueryable queryable) => ApplyInternal(queryable); + + IIncludableQueryable + IApplyQueryableIncludeConfiguration.Apply( + IQueryable queryable) => ApplyInternal(queryable); + + private IIncludableQueryable ApplyInternal(IQueryable queryable) => + _source + .Apply(queryable) + .ThenInclude(_navigationPropertyPath); + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/ThenIncludeExpression.cs b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/ThenIncludeExpression.cs new file mode 100644 index 000000000..6588ab5be --- /dev/null +++ b/Source/EventFlow.EntityFramework/ReadStores/Configuration/Includes/ThenIncludeExpression.cs @@ -0,0 +1,61 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2020 Rasmus Mikkelsen +// Copyright (c) 2015-2020 eBay Software Foundation +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Linq.Expressions; +using EventFlow.ReadStores; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace EventFlow.EntityFramework.ReadStores.Configuration.Includes +{ + public sealed class ThenIncludeExpression + : IApplyQueryableIncludeConfiguration + where TReadModel : class, IReadModel, new() + { + private readonly IApplyQueryableIncludeConfiguration _source; + private readonly Expression> _navigationPropertyPath; + + internal ThenIncludeExpression( + IApplyQueryableIncludeConfiguration source, + Expression> navigationPropertyPath) + { + _source = source; + _navigationPropertyPath = navigationPropertyPath; + } + + IQueryable + IApplyQueryableConfiguration.Apply( + IQueryable queryable) => ApplyInternal(queryable); + + IIncludableQueryable + IApplyQueryableIncludeConfiguration.Apply( + IQueryable queryable) => ApplyInternal(queryable); + + private IIncludableQueryable ApplyInternal(IQueryable queryable) => + _source + .Apply(queryable) + .ThenInclude(_navigationPropertyPath); + } +} \ No newline at end of file diff --git a/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs b/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs index ab08a2882..babaad656 100644 --- a/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs +++ b/Source/EventFlow.EntityFramework/ReadStores/EntityFrameworkReadModelStore.cs @@ -31,6 +31,7 @@ using EventFlow.Core; using EventFlow.Core.RetryStrategies; using EventFlow.EntityFramework.Extensions; +using EventFlow.EntityFramework.ReadStores.Configuration; using EventFlow.Exceptions; using EventFlow.Extensions; using EventFlow.Logs; @@ -55,17 +56,20 @@ private static readonly ConcurrentDictionary Descripto 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; @@ -130,7 +134,7 @@ public override async Task DeleteAllAsync(CancellationToken cancellationToken) EntityDescriptor descriptor; using (var dbContext = _contextProvider.CreateContext()) { - descriptor = GetDescriptor(dbContext); + descriptor = GetDescriptor(dbContext, _queryableConfiguration); } var rowsAffected = await Bulk.Delete( @@ -161,7 +165,7 @@ private async Task> GetAsync(TDbContext dbContext, bool tracking = false) { var readModelType = typeof(TReadModel); - var descriptor = GetDescriptor(dbContext); + var descriptor = GetDescriptor(dbContext, _queryableConfiguration); var entity = await descriptor.Query(dbContext, id, cancellationToken, tracking) .ConfigureAwait(false); @@ -234,7 +238,7 @@ private async Task UpdateReadModelAsync(TDbContext dbContext, IReadModelContextF readModelEnvelope = updateResult.Envelope; entity = readModelEnvelope.ReadModel; - var descriptor = GetDescriptor(dbContext); + var descriptor = GetDescriptor(dbContext, _queryableConfiguration); var entry = isNew ? dbContext.Add(entity) : dbContext.Entry(entity); @@ -255,10 +259,12 @@ private async Task UpdateReadModelAsync(TDbContext dbContext, IReadModelContextF Log.Verbose(() => $"Updated Entity Framework read model {typeof(TReadModel).PrettyPrint()} with ID '{readModelId}' to version '{readModelEnvelope.Version}'"); } - private static EntityDescriptor GetDescriptor(DbContext context) + private static EntityDescriptor GetDescriptor( + DbContext context, + IApplyQueryableConfiguration queryableConfiguration) { return Descriptors.GetOrAdd(context.Database.ProviderName, s => - new EntityDescriptor(context)); + new EntityDescriptor(context, queryableConfiguration)); } private class EntityDescriptor @@ -268,13 +274,15 @@ private class EntityDescriptor private readonly Func> _queryByIdTracking; private readonly IProperty _version; - public EntityDescriptor(DbContext context) + public EntityDescriptor( + DbContext context, + IApplyQueryableConfiguration queryableConfiguration) { var entityType = context.Model.FindEntityType(typeof(TReadModel)); _key = GetKeyProperty(entityType); _version = GetVersionProperty(entityType); - _queryByIdTracking = CompileQueryById(true); - _queryByIdNoTracking = CompileQueryById(false); + _queryByIdTracking = CompileQueryById(queryableConfiguration, true); + _queryByIdNoTracking = CompileQueryById(queryableConfiguration, false); } public string Key => _key.Name; @@ -357,18 +365,21 @@ private IProperty GetVersionProperty(IEntityType entityType) return version; } - private Func> CompileQueryById(bool tracking) + private Func> CompileQueryById( + IApplyQueryableConfiguration queryableConfiguration, + bool tracking) { return tracking ? EF.CompileAsyncQuery((DbContext dbContext, CancellationToken t, string id) => - dbContext - .Set() - .AsTracking() + queryableConfiguration.Apply(dbContext + .Set() + .AsTracking()) .SingleOrDefault(e => EF.Property(e, Key) == id)) : EF.CompileAsyncQuery((DbContext dbContext, CancellationToken t, string id) => - dbContext - .Set() - .AsNoTracking() + queryableConfiguration.Apply( + dbContext + .Set() + .AsNoTracking()) .SingleOrDefault(e => EF.Property(e, Key) == id)); } }