diff --git a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/RepositoryBaseOfT.cs b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/RepositoryBaseOfT.cs index 2c9493a1..e9140913 100644 --- a/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/RepositoryBaseOfT.cs +++ b/Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/RepositoryBaseOfT.cs @@ -77,6 +77,15 @@ public virtual async Task DeleteRangeAsync(IEnumerable entities, Cancellation { _dbContext.Set().RemoveRange(entities); + await SaveChangesAsync(cancellationToken); + } + + /// + public virtual async Task DeleteRangeAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + var query = ApplySpecification(specification); + _dbContext.Set().RemoveRange(query); + await SaveChangesAsync(cancellationToken); } diff --git a/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/Fixture/Collections/WriteCollection.cs b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/Fixture/Collections/WriteCollection.cs new file mode 100644 index 00000000..9b46bf83 --- /dev/null +++ b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/Fixture/Collections/WriteCollection.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace Ardalis.Specification.EntityFramework6.IntegrationTests.Fixture.Collections; + +[CollectionDefinition("WriteCollection")] +public class WriteCollection : ICollectionFixture +{ + public WriteCollection() + { + + } +} diff --git a/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/Fixture/RepositoryOfT.cs b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/Fixture/RepositoryOfT.cs index c2b3516a..2c7e8c91 100644 --- a/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/Fixture/RepositoryOfT.cs +++ b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/Fixture/RepositoryOfT.cs @@ -5,8 +5,12 @@ public class Repository : RepositoryBase where T : class { protected readonly TestDbContext dbContext; - public Repository(TestDbContext dbContext) : base(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.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_DeleteRangeAsync.cs b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_DeleteRangeAsync.cs new file mode 100644 index 00000000..a805fb73 --- /dev/null +++ b/Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_DeleteRangeAsync.cs @@ -0,0 +1,71 @@ +using Ardalis.Specification.EntityFramework6.IntegrationTests.Fixture; +using Ardalis.Specification.UnitTests.Fixture.Entities; +using Ardalis.Specification.UnitTests.Fixture.Specs; +using FluentAssertions; +using System.Threading.Tasks; +using Xunit; + +namespace Ardalis.Specification.EntityFramework6.IntegrationTests; + +[Collection("WriteCollection")] +public class RepositoryOfT_DeleteRangeAsync +{ + private readonly string _connectionString; + + public RepositoryOfT_DeleteRangeAsync(DatabaseFixture fixture) + { + _connectionString = fixture.ConnectionString; + } + + [Fact] + public virtual async Task DeletesProductWithStoreIdOne_GivenProductByStoreIdSpec() + { + using var dbContext = new TestDbContext(_connectionString); + var repo = new Repository(dbContext, SpecificationEvaluator.Default); + + await repo.DeleteRangeAsync(new ProductByStoreIdSpec(1)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.StoreId == 1); + } + + [Fact] + public virtual async Task DeletesProductWithIdOne_GivenProductByIdSpec() + { + using var dbContext = new TestDbContext(_connectionString); + var repo = new Repository(dbContext, SpecificationEvaluator.Default); + + await repo.DeleteRangeAsync(new ProductByIdSpec(2)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.Id == 2); + } + + [Fact] + public virtual async Task DeletesProductWithIdOne_GivenProductByIdAsUntrackedWithIdentityResolutionSpec() + { + using var dbContext = new TestDbContext(_connectionString); + var repo = new Repository(dbContext, SpecificationEvaluator.Default); + + await repo.DeleteRangeAsync(new ProductByIdAsUntrackedWithIdentityResolutionSpec(3)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.Id == 3); + } + + [Fact] + public virtual async Task DeletesProductWithIdOne_GivenProductByIdAsTrackedSpec() + { + using var dbContext = new TestDbContext(_connectionString); + var repo = new Repository(dbContext, SpecificationEvaluator.Default); + + await repo.DeleteRangeAsync(new ProductByIdAsTrackedSpec(4)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.Id == 4); + } +} diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs index 7be55c15..0c493603 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/ContextFactoryRepositoryBaseOfT.cs @@ -187,6 +187,16 @@ public async Task DeleteRangeAsync(IEnumerable entities, CancellationTo dbContext.Set().RemoveRange(entities); await SaveChangesAsync(dbContext, cancellationToken); + } + + /// + public async Task DeleteRangeAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + await using var dbContext = _dbContextFactory.CreateDbContext(); + var query = ApplySpecification(specification, dbContext); + dbContext.Set().RemoveRange(query); + + await SaveChangesAsync(cancellationToken); } /// diff --git a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs index 36ce58c9..88448aab 100644 --- a/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs +++ b/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs @@ -62,13 +62,22 @@ public virtual async Task DeleteAsync(T entity, CancellationToken cancellationTo _dbContext.Set().Remove(entity); await SaveChangesAsync(cancellationToken); - } - + } + /// public virtual async Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) { _dbContext.Set().RemoveRange(entities); + await SaveChangesAsync(cancellationToken); + } + + /// + public virtual async Task DeleteRangeAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + var query = ApplySpecification(specification); + _dbContext.Set().RemoveRange(query); + await SaveChangesAsync(cancellationToken); } diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/Collections/WriteCollection.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/Collections/WriteCollection.cs new file mode 100644 index 00000000..781c034e --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/Fixture/Collections/WriteCollection.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture.Collections; + +[CollectionDefinition("WriteCollection")] +public class WriteCollection : ICollectionFixture +{ + public WriteCollection() + { + + } +} diff --git a/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_DeleteRangeAsync.cs b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_DeleteRangeAsync.cs new file mode 100644 index 00000000..bfc37391 --- /dev/null +++ b/Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_DeleteRangeAsync.cs @@ -0,0 +1,80 @@ +using Ardalis.Specification.EntityFrameworkCore.IntegrationTests.Fixture; +using Ardalis.Specification.UnitTests.Fixture.Entities; +using Ardalis.Specification.UnitTests.Fixture.Specs; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Xunit; + +namespace Ardalis.Specification.EntityFrameworkCore.IntegrationTests; + +[Collection("WriteCollection")] +public class RepositoryOfT_DeleteRangeAsync : RepositoryOfT_DeleteRangeAsync_TestKit +{ + public RepositoryOfT_DeleteRangeAsync(DatabaseFixture fixture) : base(fixture, SpecificationEvaluator.Default) + { + } +} + +public abstract class RepositoryOfT_DeleteRangeAsync_TestKit +{ + private readonly DbContextOptions _dbContextOptions; + private readonly ISpecificationEvaluator _specificationEvaluator; + + protected RepositoryOfT_DeleteRangeAsync_TestKit(DatabaseFixture fixture, ISpecificationEvaluator specificationEvaluator) + { + _dbContextOptions = fixture.DbContextOptions; + _specificationEvaluator = specificationEvaluator; + } + + [Fact] + public virtual async Task DeletesProductWithStoreIdOne_GivenProductByStoreIdSpec() + { + using var dbContext = new TestDbContext(_dbContextOptions); + var repo = new Repository(dbContext, _specificationEvaluator); + + await repo.DeleteRangeAsync(new ProductByStoreIdSpec(1)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.StoreId == 1); + } + + [Fact] + public virtual async Task DeletesProductWithIdOne_GivenProductByIdSpec() + { + using var dbContext = new TestDbContext(_dbContextOptions); + var repo = new Repository(dbContext, _specificationEvaluator); + + await repo.DeleteRangeAsync(new ProductByIdSpec(2)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.Id == 2); + } + + [Fact] + public virtual async Task DeletesProductWithIdOne_GivenProductByIdAsUntrackedWithIdentityResolutionSpec() + { + using var dbContext = new TestDbContext(_dbContextOptions); + var repo = new Repository(dbContext, _specificationEvaluator); + + await repo.DeleteRangeAsync(new ProductByIdAsUntrackedWithIdentityResolutionSpec(3)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.Id == 3); + } + + [Fact] + public virtual async Task DeletesProductWithIdOne_GivenProductByIdAsTrackedSpec() + { + using var dbContext = new TestDbContext(_dbContextOptions); + var repo = new Repository(dbContext, _specificationEvaluator); + + await repo.DeleteRangeAsync(new ProductByIdAsTrackedSpec(4)); + + var products = await repo.ListAsync(); + products.Should().NotBeNullOrEmpty(); + products.Should().NotContain(e => e.Id == 4); + } +} diff --git a/Specification/src/Ardalis.Specification/IRepositoryBase.cs b/Specification/src/Ardalis.Specification/IRepositoryBase.cs index 896d854b..1cd737f6 100644 --- a/Specification/src/Ardalis.Specification/IRepositoryBase.cs +++ b/Specification/src/Ardalis.Specification/IRepositoryBase.cs @@ -62,7 +62,15 @@ public interface IRepositoryBase : IReadRepositoryBase where T : class /// /// The entities to remove. /// A task that represents the asynchronous operation. - Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default); + Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default); + + /// + /// Removes the all entities of , that matches the encapsulated query logic of the + /// , from the database. + /// + /// The encapsulated query logic. + /// A task that represents the asynchronous operation. + Task DeleteRangeAsync(ISpecification specification, CancellationToken cancellationToken = default); /// /// Persists changes to the database. diff --git a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdAsTrackedSpec.cs b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdAsTrackedSpec.cs new file mode 100644 index 00000000..00991906 --- /dev/null +++ b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdAsTrackedSpec.cs @@ -0,0 +1,9 @@ +namespace Ardalis.Specification.UnitTests.Fixture.Specs; + +public class ProductByIdAsTrackedSpec : Specification, ISingleResultSpecification +{ + public ProductByIdAsTrackedSpec(int id) + { + Query.Where(product => product.Id == id).AsTracking(); + } +} diff --git a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdAsUntrackedWithIdentityResolutionSpec.cs b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdAsUntrackedWithIdentityResolutionSpec.cs new file mode 100644 index 00000000..690422ad --- /dev/null +++ b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdAsUntrackedWithIdentityResolutionSpec.cs @@ -0,0 +1,9 @@ +namespace Ardalis.Specification.UnitTests.Fixture.Specs; + +public class ProductByIdAsUntrackedWithIdentityResolutionSpec : Specification, ISingleResultSpecification +{ + public ProductByIdAsUntrackedWithIdentityResolutionSpec(int id) + { + Query.Where(product => product.Id == id).AsNoTrackingWithIdentityResolution(); + } +} diff --git a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdSpec.cs b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdSpec.cs new file mode 100644 index 00000000..667c234d --- /dev/null +++ b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByIdSpec.cs @@ -0,0 +1,9 @@ +namespace Ardalis.Specification.UnitTests.Fixture.Specs; + +public class ProductByIdSpec : Specification +{ + public ProductByIdSpec(int id) + { + Query.Where(x => x.Id == id); + } +} diff --git a/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByStoreIdSpec.cs b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByStoreIdSpec.cs new file mode 100644 index 00000000..339aeb3e --- /dev/null +++ b/Specification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/ProductByStoreIdSpec.cs @@ -0,0 +1,9 @@ +namespace Ardalis.Specification.UnitTests.Fixture.Specs; + +public class ProductByStoreIdSpec : Specification +{ + public ProductByStoreIdSpec(int storeId) + { + Query.Where(x => x.StoreId == storeId); + } +} diff --git a/sample/Ardalis.Sample.App3/IRepository.cs b/sample/Ardalis.Sample.App3/IRepository.cs index 4f857ba4..d385d462 100644 --- a/sample/Ardalis.Sample.App3/IRepository.cs +++ b/sample/Ardalis.Sample.App3/IRepository.cs @@ -10,6 +10,7 @@ public interface IRepository where T : class, IAggregateRoot Task UpdateAsync(T entity, CancellationToken cancellationToken = default); Task DeleteAsync(T entity, CancellationToken cancellationToken = default); Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default); + Task DeleteRangeAsync(ISpecification specification, CancellationToken cancellationToken = default); Task SaveChangesAsync(CancellationToken cancellationToken = default); Task FindAsync(TId id, CancellationToken cancellationToken = default) where TId : notnull; diff --git a/sample/Ardalis.Sample.App3/RepositoryBase.cs b/sample/Ardalis.Sample.App3/RepositoryBase.cs index bb679bb3..79a70079 100644 --- a/sample/Ardalis.Sample.App3/RepositoryBase.cs +++ b/sample/Ardalis.Sample.App3/RepositoryBase.cs @@ -58,6 +58,13 @@ public virtual async Task DeleteRangeAsync(IEnumerable entities, Cancellation { _dbContext.Set().RemoveRange(entities); + await SaveChangesAsync(cancellationToken); + } + public virtual async Task DeleteRangeAsync(ISpecification specification, CancellationToken cancellationToken = default) + { + var query = ApplySpecification(specification); + _dbContext.Set().RemoveRange(query); + await SaveChangesAsync(cancellationToken); } public virtual async Task SaveChangesAsync(CancellationToken cancellationToken = default)