Skip to content

Commit

Permalink
Merge pull request #890 from danm-de/feature/entity-framework-includes
Browse files Browse the repository at this point in the history
New feature: Entity Framework - Eager loading of related data
  • Loading branch information
rasmus authored Aug 30, 2021
2 parents 3fcc354 + a80626e commit 86a749f
Show file tree
Hide file tree
Showing 26 changed files with 1,305 additions and 15 deletions.
12 changes: 12 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<MyEntity, MyDbContext>(
cfg => cfg.Include(x => x.SomeProperty)
.ThenInclude(y => y.SomeOtherProperty)
);
}
```

### New in 0.81.4483 (released 2020-12-14)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -53,5 +55,12 @@ public static IEventFlowOptions ConfigureForReadStoreTest(this IEventFlowOptions
typeof(EfThingyGetVersionQueryHandler),
typeof(EfThingyGetMessagesQueryHandler));
}

public static IEventFlowOptions ConfigureForReadStoreIncludeTest(this IEventFlowOptions options)
{
return options
.UseEntityFrameworkReadModel<PersonReadModelEntity, TestDbContext>(cfg => cfg.Include(x => x.Addresses))
.AddQueryHandlers(typeof(PersonGetQueryHandler));
}
}
}
13 changes: 13 additions & 0 deletions Source/EventFlow.EntityFramework.Tests/Model/TestDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +36,10 @@ public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
public DbSet<ThingyReadModelEntity> Thingys { get; set; }
public DbSet<ThingyMessageReadModelEntity> ThingyMessages { get; set; }

// Include tests
public DbSet<PersonReadModelEntity> Persons { get; set; }
public DbSet<AddressReadModelEntity> Addresses { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
Expand All @@ -48,6 +53,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity<ThingyReadModelEntity>()
.Property(e => e.AggregateId)
.ValueGeneratedOnAdd();

modelBuilder.Entity<PersonReadModelEntity>()
.Property(e => e.AggregateId)
.ValueGeneratedOnAdd();

modelBuilder.Entity<AddressReadModelEntity>()
.Property(e => e.AddressId)
.ValueGeneratedNever();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TestDbContext, MsSqlDbContextProvider>()
.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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<AddressId>
{
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<AddressId>
{
public AddressId(string value) : base(value)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PersonAggregate, PersonId>
{
public Address PersonAddress { get; }

public AddAddressCommand(PersonId aggregateId, Address personAddress) : base(aggregateId)
{
PersonAddress = personAddress;
}
}

public class AddAddressCommandHandler : CommandHandler<PersonAggregate, PersonId, AddAddressCommand>
{
public override Task ExecuteAsync(PersonAggregate aggregate, AddAddressCommand command, CancellationToken cancellationToken)
{
aggregate.AddAddress(command.PersonAddress);
return Task.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<PersonAggregate,PersonId>
{
public string Name { get; }

public CreatePersonCommand(PersonId aggregateId, string name)
:base(aggregateId)
{
Name = name;
}
}

public class CreatePersonCommandHandler : CommandHandler<PersonAggregate, PersonId, CreatePersonCommand>
{
public override Task ExecuteAsync(PersonAggregate aggregate, CreatePersonCommand command, CancellationToken cancellationToken)
{
aggregate.Create(command.Name);
return Task.CompletedTask;
}
}
}
Loading

0 comments on commit 86a749f

Please sign in to comment.