From da63643e44e9e0d8ed75f31435ef71620b8f7163 Mon Sep 17 00:00:00 2001 From: Jason Summers <3616919+jasonsummers@users.noreply.github.com> Date: Tue, 11 Apr 2023 22:43:16 +0100 Subject: [PATCH] Add two methods for consuming repositories in scenarios where repositories could be longer lived (e.g. Blazor component Injections) (#289) * Add two methods for consuming repositories in scenarios where repositories could be longer lived (e.g. Blazor component Injections) - BREAKING CHANGE - requires support for netstandard2.0 to be dropped from Ardalis.Specification.EntityFrameworkCore.csproj in order to make use of IDbContextFactory - Add IRepositoryFactory interface and EFRepositoryFactory concrete implementation to encapsulate the 'Unit of Work' principle at the repository level, consuming DbContextFactories from DI containers such as those added using the .AddDbContextFactory method, following blazor best practices for managing DbContext lifetimes - Add ContextFactoryRepositoryBaseOfT.cs abstract implementation of IRepositoryBase which again consumes DbContextFactories from DI containers such as those added using the .AddDbContextFactory method but creates a new instance of the DbContext for every method call in the repository. This breaks Entity Framework change tracking so Update and Delete methods will have to be overloaded in concrete implementations using the TrackChanges method on the context. * Add integration tests to validate behaviour of ContextFactoryRepositoryOfT.cs * Add more integration tests for ContextFactoryRepositoryBaseOfT and tests for EFRepositoryFactory --------- Co-authored-by: Jason Summers <3616919+JasonS-Dev@users.noreply.github.com> --- Ardalis.Specification.sln | 7 + ...s.Specification.EntityFrameworkCore.csproj | 2 +- .../ContextFactoryRepositoryBaseOfT.cs | 227 ++++++++++++++++++ .../EFRepositoryFactory.cs | 36 +++ .../IRepositoryFactory.cs | 18 ++ .../ContextFactoryRepositoryBaseOfTTests.cs | 150 ++++++++++++ .../EFRepositoryFactoryTests.cs | 91 +++++++ .../Fixture/ContextFactoryRepositoryOfT.cs | 17 ++ .../Fixture/GetCompanyWithStoresSpec.cs | 12 + .../Fixture/RepositoryOfT.cs | 4 + ...ation.EntityFrameworkCore.UnitTests.csproj | 30 +++ .../EFRepositoryFactoryTests.cs | 27 +++ 12 files changed, 620 insertions(+), 1 deletion(-) create mode 100644 Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs create mode 100644 Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/EFRepositoryFactory.cs create mode 100644 Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/IRepositoryFactory.cs create mode 100644 Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/ContextFactoryRepositoryBaseOfTTests.cs create mode 100644 Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/EFRepositoryFactoryTests.cs create mode 100644 Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/ContextFactoryRepositoryOfT.cs create mode 100644 Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/GetCompanyWithStoresSpec.cs create mode 100644 Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/Ardalis.Specification.EntityFrameworkCore.UnitTests.csproj create mode 100644 Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/EFRepositoryFactoryTests.cs diff --git a/Ardalis.Specification.sln b/Ardalis.Specification.sln index 01b56ed9..96d0d522 100644 --- a/Ardalis.Specification.sln +++ b/Ardalis.Specification.sln @@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specification.EntityFramewo EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specification.EntityFramework6", "Specification.EntityFramework6", "{327AEBD6-C8A6-4851-BA42-632F8014CFC5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ardalis.Specification.EntityFrameworkCore.UnitTests", "Specification.EntityFrameworkCore\tests\Ardalis.Specification.EntityFrameworkCore.UnitTests\Ardalis.Specification.EntityFrameworkCore.UnitTests.csproj", "{53E4FFB4-CAC0-482D-B714-FA657C3244C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +88,10 @@ Global {4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A}.Release|Any CPU.ActiveCfg = Release|Any CPU {4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A}.Release|Any CPU.Build.0 = Release|Any CPU + {53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53E4FFB4-CAC0-482D-B714-FA657C3244C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -100,6 +106,7 @@ Global {5AFD1454-E625-451D-A615-CEB7BB09AA65} = {B19F2F64-4B22-48C2-B2F8-7672F84F758D} {37EC09C7-702D-4539-B98D-F67B15E1E6CE} = {327AEBD6-C8A6-4851-BA42-632F8014CFC5} {4BEB4DC4-DE33-4DF1-8A2F-CE76C1D72A4A} = {327AEBD6-C8A6-4851-BA42-632F8014CFC5} + {53E4FFB4-CAC0-482D-B714-FA657C3244C9} = {B19F2F64-4B22-48C2-B2F8-7672F84F758D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C153A625-42F7-49A7-B99A-6A78B4B866B2} diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj index b7852993..b310acc6 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Ardalis.Specification.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - net6.0;netstandard2.1;netstandard2.0 + net6.0;netstandard2.1 Ardalis.Specification.EntityFrameworkCore Ardalis.Specification.EntityFrameworkCore true diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs new file mode 100644 index 00000000..0c10ec74 --- /dev/null +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Ardalis.Specification.EntityFrameworkCore +{ + public abstract class ContextFactoryRepositoryBaseOfT : IRepositoryBase + where TEntity : class + where TContext : DbContext + { + private IDbContextFactory dbContextFactory; + private ISpecificationEvaluator specificationEvaluator; + + public ContextFactoryRepositoryBaseOfT(IDbContextFactory dbContextFactory) + : this(dbContextFactory, SpecificationEvaluator.Default) + { + } + + public ContextFactoryRepositoryBaseOfT(IDbContextFactory dbContextFactory, + ISpecificationEvaluator specificationEvaluator) + { + this.dbContextFactory = dbContextFactory; + this.specificationEvaluator = specificationEvaluator; + } + + /// + public async Task GetByIdAsync(TId id, CancellationToken cancellationToken = default) where TId : notnull + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await dbContext.Set().FindAsync(new object[] { id }, cancellationToken: cancellationToken); + } + + /// + public async Task GetBySpecAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task GetBySpecAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task FirstOrDefaultAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task FirstOrDefaultAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task SingleOrDefaultAsync(ISingleResultSpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task SingleOrDefaultAsync(ISingleResultSpecification specification, + CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext).FirstOrDefaultAsync(cancellationToken); + } + + /// + public async Task> ListAsync(CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await dbContext.Set().ToListAsync(cancellationToken); + } + + /// + public async Task> ListAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + var queryResult = await ApplySpecification(specification, dbContext).ToListAsync(cancellationToken); + + return specification.PostProcessingAction == null ? queryResult : specification.PostProcessingAction(queryResult).ToList(); + } + + /// + public async Task> ListAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + var queryResult = await ApplySpecification(specification, dbContext).ToListAsync(cancellationToken); + + return specification.PostProcessingAction == null ? queryResult : specification.PostProcessingAction(queryResult).ToList(); + } + + /// + public async Task CountAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext, true).CountAsync(cancellationToken); + } + + /// + public async Task CountAsync(CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await dbContext.Set().CountAsync(cancellationToken); + } + + /// + public async Task AnyAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await ApplySpecification(specification, dbContext, true).AnyAsync(cancellationToken); + } + + /// + public async Task AnyAsync(CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + return await dbContext.Set().AnyAsync(cancellationToken); + } + + /// + public async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + dbContext.Set().Add(entity); + + await SaveChangesAsync(dbContext, cancellationToken); + + return entity; + } + + /// + public async Task> AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + dbContext.Set().AddRange(entities); + + await SaveChangesAsync(dbContext, cancellationToken); + + return entities; + } + + /// + public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + dbContext.Set().Update(entity); + + await SaveChangesAsync(dbContext, cancellationToken); + } + + /// + public async Task UpdateRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + dbContext.Set().UpdateRange(entities); + + await SaveChangesAsync(dbContext, cancellationToken); + } + + /// + public async Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + dbContext.Set().Remove(entity); + + await SaveChangesAsync(dbContext, cancellationToken); + } + + /// + public async Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) + { + await using var dbContext = this.dbContextFactory.CreateDbContext(); + dbContext.Set().RemoveRange(entities); + + await SaveChangesAsync(dbContext, cancellationToken); + } + + /// + public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + throw new InvalidOperationException(); + } + + public async Task SaveChangesAsync(TContext dbContext, CancellationToken cancellationToken = default) + { + return await dbContext.SaveChangesAsync(cancellationToken); + } + + /// + /// Filters the entities of , to those that match the encapsulated query logic of the + /// . + /// + /// The encapsulated query logic. + /// The filtered entities as an . + protected virtual IQueryable ApplySpecification(ISpecification specification, TContext dbContext, bool evaluateCriteriaOnly = false) + { + return specificationEvaluator.GetQuery(dbContext.Set().AsQueryable(), specification, evaluateCriteriaOnly); + } + + /// + /// Filters all entities of , that matches the encapsulated query logic of the + /// , from the database. + /// + /// Projects each entity into a new form, being . + /// + /// + /// The type of the value returned by the projection. + /// The encapsulated query logic. + /// The filtered projected entities as an . + protected virtual IQueryable ApplySpecification(ISpecification specification, TContext dbContext) + { + return specificationEvaluator.GetQuery(dbContext.Set().AsQueryable(), specification); + } + } +} diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/EFRepositoryFactory.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/EFRepositoryFactory.cs new file mode 100644 index 00000000..dba7bc13 --- /dev/null +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/EFRepositoryFactory.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Ardalis.Specification.EntityFrameworkCore +{ + /// + /// + /// + /// The Interface of the repository created by this Factory + /// + /// The Concrete implementation of the repository interface to create + /// + /// The DbContext derived class to support the concrete repository + public class EFRepositoryFactory : IRepositoryFactory + where TConcreteRepository : TRepository + where TContext : DbContext + { + private IDbContextFactory dbContextFactory; + + /// + /// Initialises a new instance of the EFRepositoryFactory + /// + /// The IDbContextFactory to use to generate the TContext + public EFRepositoryFactory(IDbContextFactory dbContextFactory) + { + this.dbContextFactory = dbContextFactory; + } + + /// + public TRepository CreateRepository() + { + var args = new object[] { dbContextFactory.CreateDbContext() }; + return (TRepository)Activator.CreateInstance(typeof(TConcreteRepository), args); + } + } +} diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/IRepositoryFactory.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/IRepositoryFactory.cs new file mode 100644 index 00000000..257b9d27 --- /dev/null +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/IRepositoryFactory.cs @@ -0,0 +1,18 @@ +namespace Ardalis.Specification.EntityFrameworkCore +{ + /// + /// Generates new instances of to encapsulate the 'Unit of Work' pattern + /// in scenarios where injected types may be long-lived (e.g. Blazor) + /// + /// + /// The Interface of the Repository to be generated. + /// + public interface IRepositoryFactory + { + /// + /// Generates a new repository instance + /// + /// The generated repository instance + public TRepository CreateRepository(); + } +} diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/ContextFactoryRepositoryBaseOfTTests.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/ContextFactoryRepositoryBaseOfTTests.cs new file mode 100644 index 00000000..c98c4c4e --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/ContextFactoryRepositoryBaseOfTTests.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; +using Ardalis.Specification.UnitTests.Fixture.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests +{ + public class ContextFactoryRepositoryBaseOfTTests : IClassFixture + { + protected TestDbContext dbContext; + protected IServiceProvider serviceProvider; + protected ContextFactoryRepository repository; + + public ContextFactoryRepositoryBaseOfTTests(SharedDatabaseFixture fixture) + { + dbContext = fixture.CreateContext(); + + serviceProvider = new ServiceCollection() + .AddDbContextFactory((builder => builder.UseSqlServer(fixture.Connection)), + ServiceLifetime.Transient).BuildServiceProvider(); + + var contextFactory = serviceProvider.GetService>(); + repository = new ContextFactoryRepository(contextFactory); + } + + [Fact] + public async Task Saves_new_entity() + { + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company(); + company.Name = "Test save new company name"; + company.CountryId = country.Id; + + await repository.AddAsync(company); + Assert.NotEqual(0, company.Id); + } + + [Fact] + public async Task Updates_existing_entity() + { + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company { Name = "Test update existing company name", CountryId = country.Id }; + await repository.AddAsync(company); + + var existingCompany = await repository.GetByIdAsync(company.Id); + existingCompany.Name = "Updated company name"; + await repository.UpdateAsync(existingCompany); + + var validationCompany = await dbContext.Companies.FirstOrDefaultAsync(x => x.Id == company.Id); + Assert.Equal(validationCompany.Name, existingCompany.Name); + } + + [Fact] + public async Task Updates_existing_entity_across_context_instances() + { + var contextFactory = serviceProvider.GetService>(); + var companyRetrievalRepository = new ContextFactoryRepository(contextFactory); + var companySaveRepository = new ContextFactoryRepository(contextFactory); + + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company { Name = "Test update existing company name", CountryId = country.Id }; + await repository.AddAsync(company); + + var existingCompany = await companyRetrievalRepository.GetByIdAsync(company.Id); + existingCompany.Name = "Updated company name"; + await companySaveRepository.UpdateAsync(existingCompany); + + var validationCompany = await dbContext.Companies.FirstOrDefaultAsync(x => x.Id == company.Id); + Assert.Equal(validationCompany.Name, existingCompany.Name); + } + + [Fact] + public async Task Updates_graph() + { + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company { Name = "Test update graph", CountryId = country.Id }; + var store = new Store { Name = "Store Number 1" }; + company.Stores.Add(store); + + await repository.AddAsync(company); + + var spec = new GetCompanyWithStoresSpec(company.Id); + var existingCompany = await repository.FirstOrDefaultAsync(spec); + existingCompany.Name = "Updated company name"; + var existingStore = existingCompany.Stores.FirstOrDefault(); + existingStore.Name = "Updated Store Name"; + + await repository.UpdateAsync(existingCompany); + + var validationCompany = await dbContext.Companies.FirstOrDefaultAsync(x => x.Id == company.Id); + Assert.Equal(validationCompany.Name, existingCompany.Name); + + var validationStore = await dbContext.Stores.FirstOrDefaultAsync(x => x.CompanyId == company.Id); + Assert.Equal(validationStore.Name, existingStore.Name); + } + + [Fact] + public async Task Updates_graph_across_context_instances() + { + var contextFactory = serviceProvider.GetService>(); + var companyRetrievalRepository = new ContextFactoryRepository(contextFactory); + var companySaveRepository = new ContextFactoryRepository(contextFactory); + + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company { Name = "Test update graph", CountryId = country.Id }; + var store = new Store { Name = "Store Number 1" }; + company.Stores.Add(store); + + await repository.AddAsync(company); + + var spec = new GetCompanyWithStoresSpec(company.Id); + var existingCompany = await companyRetrievalRepository.FirstOrDefaultAsync(spec); + existingCompany.Name = "Updated company name"; + var existingStore = existingCompany.Stores.FirstOrDefault(); + existingStore.Name = "Updated Store Name"; + + await companySaveRepository.UpdateAsync(existingCompany); + + var validationCompany = await dbContext.Companies.FirstOrDefaultAsync(x => x.Id == company.Id); + Assert.Equal(validationCompany.Name, existingCompany.Name); + + var validationStore = await dbContext.Stores.FirstOrDefaultAsync(x => x.CompanyId == company.Id); + Assert.Equal(validationStore.Name, existingStore.Name); + } + + [Fact] + public async Task Deletes_entity() + { + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company { Name = "Test update graph", CountryId = country.Id }; + await repository.AddAsync(company); + + var companyId = company.Id; + await repository.DeleteAsync(company); + + var validationCompany = await repository.GetByIdAsync(companyId); + Assert.Null(validationCompany); + } + } +} diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/EFRepositoryFactoryTests.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/EFRepositoryFactoryTests.cs new file mode 100644 index 00000000..c30df303 --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/EFRepositoryFactoryTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; +using Ardalis.Specification.UnitTests.Fixture.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests +{ + public class EFRepositoryFactoryTests : IClassFixture + { + protected TestDbContext dbContext; + protected IServiceProvider serviceProvider; + protected IRepositoryFactory> repositoryFactory; + protected IDbContextFactory contextFactory; + + public EFRepositoryFactoryTests(SharedDatabaseFixture fixture) + { + dbContext = fixture.CreateContext(); + + serviceProvider = new ServiceCollection() + .AddDbContextFactory((builder => builder.UseSqlServer(fixture.Connection)), + ServiceLifetime.Transient).BuildServiceProvider(); + + contextFactory = serviceProvider.GetService>(); + repositoryFactory = + new EFRepositoryFactory, Repository, TestDbContext>(contextFactory); + } + + [Fact] + public async Task Saves_new_entity() + { + var repository = repositoryFactory.CreateRepository(); + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company(); + company.Name = "Test save new company name"; + company.CountryId = country.Id; + + await repository.AddAsync(company); + Assert.NotEqual(0, company.Id); + } + + [Fact] + public async Task Updates_existing_entity() + { + var repository = repositoryFactory.CreateRepository(); + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company { Name = "Test update existing company name", CountryId = country.Id }; + await repository.AddAsync(company); + + var existingCompany = await repository.GetByIdAsync(company.Id); + existingCompany.Name = "Updated company name"; + await repository.UpdateAsync(existingCompany); + + var validationCompany = await dbContext.Companies.FirstOrDefaultAsync(x => x.Id == company.Id); + Assert.Equal(validationCompany.Name, existingCompany.Name); + } + + [Fact] + public async Task Updates_graph() + { + var repository = repositoryFactory.CreateRepository(); + var country = await dbContext.Countries.FirstOrDefaultAsync(); + + var company = new Company { Name = "Test update graph", CountryId = country.Id }; + var store = new Store { Name = "Store Number 1" }; + company.Stores.Add(store); + + await repository.AddAsync(company); + + var spec = new GetCompanyWithStoresSpec(company.Id); + var existingCompany = await repository.FirstOrDefaultAsync(spec); + existingCompany.Name = "Updated company name"; + var existingStore = existingCompany.Stores.FirstOrDefault(); + existingStore.Name = "Updated Store Name"; + + await repository.UpdateAsync(existingCompany); + + var validationCompany = await dbContext.Companies.FirstOrDefaultAsync(x => x.Id == company.Id); + Assert.Equal(validationCompany.Name, existingCompany.Name); + + var validationStore = await dbContext.Stores.FirstOrDefaultAsync(x => x.CompanyId == company.Id); + Assert.Equal(validationStore.Name, existingStore.Name); + } + } +} diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/ContextFactoryRepositoryOfT.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/ContextFactoryRepositoryOfT.cs new file mode 100644 index 00000000..80a4fd19 --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/ContextFactoryRepositoryOfT.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture +{ + public class ContextFactoryRepository : ContextFactoryRepositoryBaseOfT + where T : class where TContext : DbContext + { + public ContextFactoryRepository(IDbContextFactory dbContextFactory) : base(dbContextFactory) + { + } + + public ContextFactoryRepository(IDbContextFactory dbContextFactory, + ISpecificationEvaluator specificationEvaluator) : base(dbContextFactory, specificationEvaluator) + { + } + } +} diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/GetCompanyWithStoresSpec.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/GetCompanyWithStoresSpec.cs new file mode 100644 index 00000000..e4560e12 --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/GetCompanyWithStoresSpec.cs @@ -0,0 +1,12 @@ +using Ardalis.Specification.UnitTests.Fixture.Entities; + +namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture +{ + public class GetCompanyWithStoresSpec : Specification, ISingleResultSpecification + { + public GetCompanyWithStoresSpec(int companyId) + { + this.Query.Where(x => x.Id == companyId).Include(x => x.Stores); + } + } +} diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs index 191ad62c..cc82da8a 100644 --- a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/RepositoryOfT.cs @@ -5,6 +5,10 @@ public class Repository : RepositoryBase where T : class { protected readonly TestDbContext dbContext; + public Repository(TestDbContext dbContext) : this(dbContext, SpecificationEvaluator.Default) + { + } + public Repository(TestDbContext dbContext, ISpecificationEvaluator specificationEvaluator) : base(dbContext, specificationEvaluator) { this.dbContext = dbContext; diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/Ardalis.Specification.EntityFrameworkCore.UnitTests.csproj b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/Ardalis.Specification.EntityFrameworkCore.UnitTests.csproj new file mode 100644 index 00000000..059f6c6c --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/Ardalis.Specification.EntityFrameworkCore.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/EFRepositoryFactoryTests.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/EFRepositoryFactoryTests.cs new file mode 100644 index 00000000..07d50d64 --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.UnitTests/EFRepositoryFactoryTests.cs @@ -0,0 +1,27 @@ +using Ardalis.SampleApp.Core.Entities.CustomerAggregate; +using Ardalis.SampleApp.Core.Interfaces; +using Ardalis.SampleApp.Infrastructure.Data; +using Ardalis.SampleApp.Infrastructure.DataAccess; +using Microsoft.EntityFrameworkCore; +using Moq; +using Xunit; + +namespace Ardalis.Specification.EntityFrameworkCore.UnitTests; + +public class EFRepositoryFactoryTests +{ + [Fact] + public void CorrectlyInstantiatesRepository() + { + var mockContextFactory = new Mock>(); + mockContextFactory.Setup(x => x.CreateDbContext()) + .Returns(() => new SampleDbContext(new DbContextOptions())); + + var repositoryFactory = + new EFRepositoryFactory, MyRepository, SampleDbContext>(mockContextFactory + .Object); + + var repository = repositoryFactory.CreateRepository(); + Assert.IsType>(repository); + } +}