diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md index dc237f6151..7dfac258eb 100644 --- a/docs/en/Data-Filtering.md +++ b/docs/en/Data-Filtering.md @@ -237,6 +237,20 @@ protected override Expression> CreateFilterExpression(b => + { + b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); + }); +} +```` + ### MongoDB ABP abstracts the `IMongoDbRepositoryFilterer` interface to implement data filtering for the [MongoDB Integration](MongoDB.md), it works only if you use the repositories properly. Otherwise, you should manually filter the data. diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 1803e4acfe..bc93ca97b8 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -626,7 +626,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, var filterExpression = CreateFilterExpression(); if (filterExpression != null) { - modelBuilder.Entity().HasQueryFilter(filterExpression); + modelBuilder.Entity().HasAbpQueryFilter(filterExpression); } } } @@ -705,44 +705,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) { Expression> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property(e, "TenantId") == CurrentTenantId; - expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter); + expression = expression == null ? multiTenantFilter : QueryFilterExpressionHelper.CombineExpressions(expression, multiTenantFilter); } return expression; } - - protected virtual Expression> CombineExpressions(Expression> expression1, Expression> expression2) - { - var parameter = Expression.Parameter(typeof(T)); - - var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter); - var left = leftVisitor.Visit(expression1.Body); - - var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter); - var right = rightVisitor.Visit(expression2.Body); - - return Expression.Lambda>(Expression.AndAlso(left, right), parameter); - } - - class ReplaceExpressionVisitor : ExpressionVisitor - { - private readonly Expression _oldValue; - private readonly Expression _newValue; - - public ReplaceExpressionVisitor(Expression oldValue, Expression newValue) - { - _oldValue = oldValue; - _newValue = newValue; - } - - public override Expression Visit(Expression node) - { - if (node == _oldValue) - { - return _newValue; - } - - return base.Visit(node); - } - } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityTypeBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityTypeBuilderExtensions.cs new file mode 100644 index 0000000000..2cf90ba5ac --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityTypeBuilderExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Volo.Abp.EntityFrameworkCore; + +public static class EntityTypeBuilderExtensions +{ + /// + /// This method is used to add a query filter to this entity which combine with ABP EF Core builtin query filters. + /// + /// + public static EntityTypeBuilder HasAbpQueryFilter(this EntityTypeBuilder builder, Expression> filter) + where TEntity : class + { +#pragma warning disable EF1001 + var queryFilterAnnotation = builder.Metadata.FindAnnotation(CoreAnnotationNames.QueryFilter); +#pragma warning restore EF1001 + if (queryFilterAnnotation != null && queryFilterAnnotation.Value != null && queryFilterAnnotation.Value is Expression> existingFilter) + { + filter = QueryFilterExpressionHelper.CombineExpressions(filter, existingFilter); + } + + return builder.HasQueryFilter(filter); + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/QueryFilterExpressionHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/QueryFilterExpressionHelper.cs new file mode 100644 index 0000000000..dfd5edf38f --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/QueryFilterExpressionHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq.Expressions; + +namespace Volo.Abp.EntityFrameworkCore; + +public static class QueryFilterExpressionHelper +{ + public static Expression> CombineExpressions(Expression> expression1, Expression> expression2) + { + var parameter = Expression.Parameter(typeof(T)); + + var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter); + var left = leftVisitor.Visit(expression1.Body); + + var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter); + var right = rightVisitor.Visit(expression2.Body); + + return Expression.Lambda>(Expression.AndAlso(left!, right!), parameter); + } + + private class ReplaceExpressionVisitor : ExpressionVisitor + { + private readonly Expression _oldValue; + private readonly Expression _newValue; + + public ReplaceExpressionVisitor(Expression oldValue, Expression newValue) + { + _oldValue = oldValue; + _newValue = newValue; + } + + public override Expression Visit(Expression node) + { + return node == _oldValue ? _newValue : base.Visit(node); + } + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index 181690ed9f..f23ac14fda 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -1,9 +1,12 @@ +using System; +using System.Collections.Generic; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Autofac; +using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore.Domain; using Volo.Abp.EntityFrameworkCore.Sqlite; using Volo.Abp.EntityFrameworkCore.TestApp.FifthContext; @@ -14,6 +17,7 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.TestApp; using Volo.Abp.TestApp.Domain; using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.Threading; namespace Volo.Abp.EntityFrameworkCore; @@ -71,6 +75,20 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule public override void OnPreApplicationInitialization(ApplicationInitializationContext context) { context.ServiceProvider.GetRequiredService().Database.Migrate(); + + using (var scope = context.ServiceProvider.CreateScope()) + { + var categoryRepository = scope.ServiceProvider.GetRequiredService>(); + AsyncHelper.RunSync(async () => + { + await categoryRepository.InsertManyAsync(new List + { + new Category { Name = "volo.abp" }, + new Category { Name = "abp.cli" }, + new Category { Name = "abp.core", IsDeleted = true } + }); + }); + } } private static SqliteConnection CreateDatabaseAndGetConnection() diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/EfCore_Custom_Filter_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/EfCore_Custom_Filter_Tests.cs new file mode 100644 index 0000000000..a8f0bdbeb8 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/EfCore_Custom_Filter_Tests.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering; + +public class EfCore_Custom_Filter_Tests : TestAppTestBase +{ + private readonly IBasicRepository _categoryRepository; + + public EfCore_Custom_Filter_Tests() + { + _categoryRepository = GetRequiredService>(); + } + + [Fact] + public async Task Should_Combine_Abp_And_Custom_QueryFilter_Test() + { + var categories = await _categoryRepository.GetListAsync(); + categories.Count.ShouldBe(1); + categories[0].Name.ShouldBe("abp.cli"); + + using (GetRequiredService>().Disable()) + { + categories = await _categoryRepository.GetListAsync(); + categories.Count.ShouldBe(2); + categories.ShouldContain(x => x.Name == "abp.cli" && x.IsDeleted == false); + categories.ShouldContain(x => x.Name == "abp.core" && x.IsDeleted == true); + } + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index 99e689bdd4..2185bce2d4 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -1,5 +1,6 @@ using System; using Microsoft.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.TestApp.SecondContext; using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext; using Volo.Abp.TestApp.Domain; @@ -23,6 +24,8 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet Products { get; set; } + public DbSet Categories { get; set; } + public TestMigrationsDbContext(DbContextOptions options) : base(options) { @@ -40,7 +43,6 @@ public class TestMigrationsDbContext : AbpDbContext b.HasKey(p => new { p.PersonId, p.Number }); }); - modelBuilder.Entity(b => { b.Property(x => x.LastActiveTime).ValueGeneratedOnAddOrUpdate().HasDefaultValue(DateTime.Now); @@ -57,5 +59,10 @@ public class TestMigrationsDbContext : AbpDbContext }); modelBuilder.Entity(); + + modelBuilder.Entity(b => + { + b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); + }); } } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/Domain/Category.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/Domain/Category.cs new file mode 100644 index 0000000000..06efad2a12 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/Domain/Category.cs @@ -0,0 +1,11 @@ +using System; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.TestApp.Domain; + +public class Category : AggregateRoot, ISoftDelete +{ + public string Name { get; set; } + + public bool IsDeleted { get; set; } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index 66772343af..245867776b 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -32,6 +32,8 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Products { get; set; } + public DbSet Categories { get; set; } + public TestAppDbContext(DbContextOptions options) : base(options) { @@ -85,6 +87,11 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, modelBuilder.Entity(); + modelBuilder.Entity(b => + { + b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); + }); + modelBuilder.TryConfigureObjectExtensions(); } }