diff --git a/src/Cnblogs.Architecture.Ddd.Domain.Abstractions/INavigationRepository.cs b/src/Cnblogs.Architecture.Ddd.Domain.Abstractions/INavigationRepository.cs index 69b37c5..124919e 100644 --- a/src/Cnblogs.Architecture.Ddd.Domain.Abstractions/INavigationRepository.cs +++ b/src/Cnblogs.Architecture.Ddd.Domain.Abstractions/INavigationRepository.cs @@ -18,4 +18,12 @@ public interface INavigationRepository : IRepository要额外加载的其他实体。 /// 对应的实体。 Task GetAsync(TKey key, params Expression>[] includes); -} \ No newline at end of file + + /// + /// Get entity by key. + /// + /// The key of entity. + /// Include strings. + /// The entity with key equals to . + Task GetAsync(TKey key, params string[] includes); +} diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/BaseRepository.cs b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/BaseRepository.cs index 98ebe2b..c29c6ed 100644 --- a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/BaseRepository.cs +++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/BaseRepository.cs @@ -77,6 +77,12 @@ public async Task AddRangeAsync(TEnumerable entities) return await Context.Set().AggregateIncludes(includes).FirstOrDefaultAsync(e => e.Id.Equals(key)); } + /// + public async Task GetAsync(TKey key, params string[] includes) + { + return await Context.Set().AggregateIncludes(includes).FirstOrDefaultAsync(e => e.Id.Equals(key)); + } + /// public async Task UpdateAsync(TEntity entity) { @@ -179,4 +185,4 @@ private void CallBeforeUpdate() .ToList(); domainEntities.ForEach(x => x.Entity.BeforeUpdate()); } -} \ No newline at end of file +} diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj index 71e68ff..dc0c219 100644 --- a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj +++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework.csproj @@ -9,11 +9,11 @@ - + - + diff --git a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/QueryableExtensions.cs b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/QueryableExtensions.cs index 3a32d79..b1e382a 100644 --- a/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/QueryableExtensions.cs +++ b/src/Cnblogs.Architecture.Ddd.Infrastructure.EntityFramework/QueryableExtensions.cs @@ -23,4 +23,19 @@ public static IQueryable AggregateIncludes( { return includes.Aggregate(query, (queryable, include) => queryable.Include(include)); } -} \ No newline at end of file + + /// + /// Apply multiple includes to . + /// + /// The source queryable. + /// Include strings. + /// The type of entity. + /// + public static IQueryable AggregateIncludes( + this IQueryable query, + IEnumerable includes) + where TEntity : class + { + return includes.Aggregate(query, (queryable, include) => queryable.Include(include)); + } +} diff --git a/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs b/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs index 2501fbe..e296d75 100644 --- a/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs +++ b/test/Cnblogs.Architecture.UnitTests/Infrastructure/EntityFramework/BaseRepositoryTests.cs @@ -1,19 +1,94 @@ using Cnblogs.Architecture.Ddd.Domain.Abstractions; using Cnblogs.Architecture.TestShared; using Cnblogs.Architecture.UnitTests.Infrastructure.FakeObjects; - using FluentAssertions; - using MediatR; - using Microsoft.EntityFrameworkCore; - using Moq; namespace Cnblogs.Architecture.UnitTests.Infrastructure.EntityFramework; public class BaseRepositoryTests { + [Fact] + public async Task GetEntityAsync_Include_GetEntityAsync() + { + // Arrange + var entity = new EntityGenerator(new FakeBlog()) + .Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)) + .HasManyForEachEntity( + x => x.Posts, + x => x.Blog, + new EntityGenerator(new FakePost()) + .Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1))) + .GenerateSingle(); + var db = new FakeDbContext( + new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options); + db.Add(entity); + await db.SaveChangesAsync(); + var repository = new TestRepository(Mock.Of(), db); + + // Act + var got = await repository.GetAsync(entity.Id, e => e.Posts); + + // Assert + got.Should().NotBeNull(); + got!.Posts.Should().BeEquivalentTo(entity.Posts); + } + + [Fact] + public async Task GetEntityAsync_StringBasedInclude_NotNullAsync() + { + // Arrange + var entity = new EntityGenerator(new FakeBlog()) + .Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)) + .HasManyForEachEntity( + x => x.Posts, + x => x.Blog, + new EntityGenerator(new FakePost()) + .Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1))) + .GenerateSingle(); + var db = new FakeDbContext( + new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options); + db.Add(entity); + await db.SaveChangesAsync(); + var repository = new TestRepository(Mock.Of(), db); + + // Act + var got = await repository.GetAsync(entity.Id, nameof(entity.Posts)); + + // Assert + got.Should().NotBeNull(); + got!.Posts.Should().BeEquivalentTo(entity.Posts); + } + + [Fact] + public async Task GetEntityAsync_ThenInclude_NotNullAsync() + { + // Arrange + var entity = new EntityGenerator(new FakeBlog()) + .Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1)) + .HasManyForEachEntity( + x => x.Posts, + x => x.Blog, + new EntityGenerator(new FakePost()) + .HasManyForEachEntity(x => x.Tags, new EntityGenerator(new FakeTag())) + .Setup(x => x.DateUpdated = DateTimeOffset.Now.AddDays(-1))) + .GenerateSingle(); + var db = new FakeDbContext( + new DbContextOptionsBuilder().UseInMemoryDatabase("inmemory").Options); + db.Add(entity); + await db.SaveChangesAsync(); + var repository = new TestRepository(Mock.Of(), db); + + // Act + var got = await repository.GetAsync(entity.Id, "Posts.Tags"); + + // Assert + got.Should().NotBeNull(); + got!.Posts.Should().BeEquivalentTo(entity.Posts); + } + [Fact] public async Task SaveEntitiesAsync_CallBeforeUpdateForRelatedEntityAsync() { @@ -69,10 +144,14 @@ public async Task SaveEntitiesAsync_DispatchEntityDomainEventsAsync() // Assert mediator.Verify( - x => x.Publish(It.Is(d => ((FakeDomainEvent)d).FakeValue == 1), It.IsAny()), + x => x.Publish( + It.Is(d => ((FakeDomainEvent)d).FakeValue == 1), + It.IsAny()), Times.Once); mediator.Verify( - x => x.Publish(It.Is(d => ((FakeDomainEvent)d).FakeValue == 2), It.IsAny()), + x => x.Publish( + It.Is(d => ((FakeDomainEvent)d).FakeValue == 2), + It.IsAny()), Times.Once); } @@ -104,10 +183,14 @@ public async Task SaveEntitiesAsync_DispatchRelatedEntityDomainEventsAsync() // Assert mediator.Verify( - x => x.Publish(It.Is(d => ((FakeDomainEvent)d).FakeValue == 1), It.IsAny()), + x => x.Publish( + It.Is(d => ((FakeDomainEvent)d).FakeValue == 1), + It.IsAny()), Times.Once); mediator.Verify( - x => x.Publish(It.Is(d => ((FakeDomainEvent)d).FakeValue == 2), It.IsAny()), + x => x.Publish( + It.Is(d => ((FakeDomainEvent)d).FakeValue == 2), + It.IsAny()), Times.Once); } @@ -145,4 +228,4 @@ public async Task SaveEntitiesAsync_DispatchEntityDomainEventsWithGeneratedIdAsy It.IsAny()), Times.Exactly(entity.Posts.Count)); } -} \ No newline at end of file +} diff --git a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs index 0406cc2..3e9953a 100644 --- a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs +++ b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeDbContext.cs @@ -18,5 +18,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasKey(x => x.Id); modelBuilder.Entity().Property(x => x.Id).ValueGeneratedOnAdd(); + modelBuilder.Entity().HasMany(x => x.Tags).WithOne().HasForeignKey(x => x.PostId); + + modelBuilder.Entity().HasKey(x => x.Id); + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedOnAdd(); } -} \ No newline at end of file +} diff --git a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeMongoDbContext.cs b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeMongoDbContext.cs index be9a1ee..31e797c 100644 --- a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeMongoDbContext.cs +++ b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeMongoDbContext.cs @@ -41,6 +41,8 @@ public FakeMongoDbContext(Mock mockOptionsMock, Mock("fakeBlog"); + builder.Entity("fakePost"); + builder.Entity("fakeTag"); } private static Mock MockOptions() @@ -54,4 +56,4 @@ private static Mock MockDatabase(Mock mong mongoContextOptionsMock.Setup(x => x.GetDatabase()).Returns(mock.Object); return mock; } -} \ No newline at end of file +} diff --git a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakePost.cs b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakePost.cs index 005c80f..bad93d8 100644 --- a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakePost.cs +++ b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakePost.cs @@ -9,4 +9,5 @@ public class FakePost : Entity // navigations public FakeBlog Blog { get; set; } = null!; -} \ No newline at end of file + public List Tags { get; set; } = null!; +} diff --git a/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeTag.cs b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeTag.cs new file mode 100644 index 0000000..4c8a447 --- /dev/null +++ b/test/Cnblogs.Architecture.UnitTests/Infrastructure/FakeObjects/FakeTag.cs @@ -0,0 +1,9 @@ +using Cnblogs.Architecture.Ddd.Domain.Abstractions; + +namespace Cnblogs.Architecture.UnitTests.Infrastructure.FakeObjects; + +public class FakeTag : Entity +{ + public int BlogId { get; set; } + public int PostId { get; set; } +}