diff --git a/docs/en/Data-Filtering.md b/docs/en/Data-Filtering.md index 830979e710..4706912fe7 100644 --- a/docs/en/Data-Filtering.md +++ b/docs/en/Data-Filtering.md @@ -217,17 +217,14 @@ protected override bool ShouldFilterEntity(IMutableEntityType entityTyp return base.ShouldFilterEntity(entityType); } -protected override Expression> CreateFilterExpression() +protected override Expression> CreateFilterExpression(ModelBuilder modelBuilder) { - var expression = base.CreateFilterExpression(); + var expression = base.CreateFilterExpression(modelBuilder); if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) { - Expression> isActiveFilter = - e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); - expression = expression == null - ? isActiveFilter - : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); + Expression> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); + expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); } return expression; @@ -251,6 +248,78 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } ```` +#### Using User-defined function mapping for global filters + +Using [User-defined function mapping](https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping) for global filters will gain performance improvements. + +To use this feature, you need to change your DbContext like below: + +````csharp +protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled() ?? false; + +protected override bool ShouldFilterEntity(IMutableEntityType entityType) +{ + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + return true; + } + + return base.ShouldFilterEntity(entityType); +} + +protected override Expression> CreateFilterExpression(ModelBuilder modelBuilder) +{ + var expression = base.CreateFilterExpression(modelBuilder); + + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + Expression> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); + + if (UseDbFunction()) + { + isActiveFilter = e => IsActiveFilter(((IIsActive)e).IsActive, true); + + var abpEfCoreCurrentDbContext = this.GetService(); + modelBuilder.HasDbFunction(typeof(MyProjectNameDbContext).GetMethod(nameof(IsActiveFilter))!) + .HasTranslation(args => + { + // (bool isActive, bool boolParam) + var isActive = args[0]; + var boolParam = args[1]; + + if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled() == true) + { + // isActive == true + return new SqlBinaryExpression( + ExpressionType.Equal, + isActive, + new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping), + boolParam.Type, + boolParam.TypeMapping); + } + + // empty where sql + return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); + }); + } + + expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); + } + + return expression; +} + +public static bool IsActiveFilter(bool isActive, bool boolParam) +{ + throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage); +} + +public override string GetCompiledQueryCacheKey() +{ + return $"{base.GetCompiledQueryCacheKey()}:{IsActiveFilterEnabled}"; +} +```` + ### 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/docs/en/UI/AspNetCore/Static-JavaScript-Proxies.md b/docs/en/UI/AspNetCore/Static-JavaScript-Proxies.md index fffc53cf83..2ad46a2f9d 100644 --- a/docs/en/UI/AspNetCore/Static-JavaScript-Proxies.md +++ b/docs/en/UI/AspNetCore/Static-JavaScript-Proxies.md @@ -66,7 +66,7 @@ acme.bookStore.authors.author.get = function(id, ajaxParams) { }; ```` -> `generate-proxy` command generates proxies for only the APIs you've defined in your application (assumes `app` as the module name). If you are developing a modular application, you can specify the `-m` (or `--module`) parameter to specify the module you want to generate proxies. See the *generate-proxy* section in the [ABP CLI](../CLI.md) documentation for other options. +> `generate-proxy` command generates proxies for only the APIs you've defined in your application (assumes `app` as the module name). If you are developing a modular application, you can specify the `-m` (or `--module`) parameter to specify the module you want to generate proxies. See the *generate-proxy* section in the [ABP CLI](../../CLI.md) documentation for other options. ### Using the Proxy Functions @@ -154,4 +154,4 @@ See the [jQuery.ajax](https://api.jquery.com/jQuery.ajax/) documentation for all * [Dynamic JavaScript API Client Proxies](Dynamic-JavaScript-Proxies.md) * [Auto API Controllers](../../API/Auto-API-Controllers.md) -* [Web Application Development Tutorial](../../Tutorials/Part-1.md) \ No newline at end of file +* [Web Application Development Tutorial](../../Tutorials/Part-1.md) diff --git a/docs/zh-Hans/Data-Filtering.md b/docs/zh-Hans/Data-Filtering.md index 8a43ad93b1..01c4590761 100644 --- a/docs/zh-Hans/Data-Filtering.md +++ b/docs/zh-Hans/Data-Filtering.md @@ -166,17 +166,14 @@ protected override bool ShouldFilterEntity(IMutableEntityType entityTyp return base.ShouldFilterEntity(entityType); } -protected override Expression> CreateFilterExpression() +protected override Expression> CreateFilterExpression(ModelBuilder modelBuilder) { - var expression = base.CreateFilterExpression(); + var expression = base.CreateFilterExpression(modelBuilder); if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) { - Expression> isActiveFilter = - e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); - expression = expression == null - ? isActiveFilter - : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); + Expression> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); + expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); } return expression; @@ -186,6 +183,78 @@ protected override Expression> CreateFilterExpression DataFilter?.IsEnabled() ?? false; + +protected override bool ShouldFilterEntity(IMutableEntityType entityType) +{ + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + return true; + } + + return base.ShouldFilterEntity(entityType); +} + +protected override Expression> CreateFilterExpression(ModelBuilder modelBuilder) +{ + var expression = base.CreateFilterExpression(modelBuilder); + + if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))) + { + Expression> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property(e, "IsActive"); + + if (UseDbFunction()) + { + isActiveFilter = e => IsActiveFilter(((IIsActive)e).IsActive, true); + + var abpEfCoreCurrentDbContext = this.GetService(); + modelBuilder.HasDbFunction(typeof(MyProjectNameDbContext).GetMethod(nameof(IsActiveFilter))!) + .HasTranslation(args => + { + // (bool isActive, bool boolParam) + var isActive = args[0]; + var boolParam = args[1]; + + if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled() == true) + { + // isActive == true + return new SqlBinaryExpression( + ExpressionType.Equal, + isActive, + new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping), + boolParam.Type, + boolParam.TypeMapping); + } + + // empty where sql + return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); + }); + } + + expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter); + } + + return expression; +} + +public static bool IsActiveFilter(bool isActive, bool boolParam) +{ + throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage); +} + +public override string GetCompiledQueryCacheKey() +{ + return $"{base.GetCompiledQueryCacheKey()}:{IsActiveFilterEnabled}"; +} +```` + ### MongoDB ABP抽象了 `IMongoDbRepositoryFilterer` 接口为[MongoDB 集成](MongoDB.md)实现数据过滤, 只有正确的使用仓储,它才会工作. 否则你需要手动过滤数据. diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs index 0e6820c913..d50a19401e 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Check.cs @@ -10,7 +10,7 @@ public static class Check { [ContractAnnotation("value:null => halt")] public static T NotNull( - T? value, + [System.Diagnostics.CodeAnalysis.NotNull] T? value, [InvokerParameterName][NotNull] string parameterName) { if (value == null) @@ -23,7 +23,7 @@ public static class Check [ContractAnnotation("value:null => halt")] public static T NotNull( - T? value, + [System.Diagnostics.CodeAnalysis.NotNull] T? value, [InvokerParameterName][NotNull] string parameterName, string message) { @@ -37,7 +37,7 @@ public static class Check [ContractAnnotation("value:null => halt")] public static string NotNull( - string? value, + [System.Diagnostics.CodeAnalysis.NotNull] string? value, [InvokerParameterName][NotNull] string parameterName, int maxLength = int.MaxValue, int minLength = 0) @@ -62,7 +62,7 @@ public static class Check [ContractAnnotation("value:null => halt")] public static string NotNullOrWhiteSpace( - string? value, + [System.Diagnostics.CodeAnalysis.NotNull] string? value, [InvokerParameterName][NotNull] string parameterName, int maxLength = int.MaxValue, int minLength = 0) @@ -87,7 +87,7 @@ public static class Check [ContractAnnotation("value:null => halt")] public static string NotNullOrEmpty( - string? value, + [System.Diagnostics.CodeAnalysis.NotNull] string? value, [InvokerParameterName][NotNull] string parameterName, int maxLength = int.MaxValue, int minLength = 0) @@ -111,7 +111,9 @@ public static class Check } [ContractAnnotation("value:null => halt")] - public static ICollection NotNullOrEmpty(ICollection? value, [InvokerParameterName][NotNull] string parameterName) + public static ICollection NotNullOrEmpty( + [System.Diagnostics.CodeAnalysis.NotNull] ICollection? value, + [InvokerParameterName][NotNull] string parameterName) { if (value == null || value.Count <= 0) { @@ -339,7 +341,7 @@ public static class Check } public static T NotDefaultOrNull( - T? value, + [System.Diagnostics.CodeAnalysis.NotNull] T? value, [InvokerParameterName][NotNull] string parameterName) where T : struct { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpEntityFrameworkCoreMySQLModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpEntityFrameworkCoreMySQLModule.cs index 6431590230..0b493f5058 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpEntityFrameworkCoreMySQLModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpEntityFrameworkCoreMySQLModule.cs @@ -1,4 +1,5 @@ -using Volo.Abp.Guids; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; +using Volo.Abp.Guids; using Volo.Abp.Modularity; namespace Volo.Abp.EntityFrameworkCore.MySQL; @@ -17,5 +18,10 @@ public class AbpEntityFrameworkCoreMySQLModule : AbpModule options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString; } }); + + Configure(options => + { + options.UseDbFunction = true; + }); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs index 3b4129b272..317f664b04 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo/Abp/EntityFrameworkCore/Oracle/Devart/AbpEntityFrameworkCoreOracleDevartModule.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; using Volo.Abp.Guids; using Volo.Abp.Modularity; @@ -18,5 +19,10 @@ public class AbpEntityFrameworkCoreOracleDevartModule : AbpModule options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; } }); + + Configure(options => + { + options.UseDbFunction = true; + }); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs index 54627a90c0..76ffe32f7a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle/Volo/Abp/EntityFrameworkCore/Oracle/AbpEntityFrameworkCoreOracleModule.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; using Volo.Abp.Guids; using Volo.Abp.Modularity; @@ -16,5 +17,10 @@ public class AbpEntityFrameworkCoreOracleModule : AbpModule options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsBinary; } }); + + Configure(options => + { + options.UseDbFunction = true; + }); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/PostgreSql/AbpEntityFrameworkCorePostgreSqlModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/PostgreSql/AbpEntityFrameworkCorePostgreSqlModule.cs index 2b679cccef..b82b128b3b 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/PostgreSql/AbpEntityFrameworkCorePostgreSqlModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.PostgreSql/Volo/Abp/EntityFrameworkCore/PostgreSql/AbpEntityFrameworkCorePostgreSqlModule.cs @@ -1,4 +1,5 @@ -using Volo.Abp.Guids; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; +using Volo.Abp.Guids; using Volo.Abp.Modularity; namespace Volo.Abp.EntityFrameworkCore.PostgreSql; @@ -17,5 +18,10 @@ public class AbpEntityFrameworkCorePostgreSqlModule : AbpModule options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString; } }); + + Configure(options => + { + options.UseDbFunction = true; + }); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo/Abp/EntityFrameworkCore/SqlServer/AbpEntityFrameworkCoreSqlServerModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo/Abp/EntityFrameworkCore/SqlServer/AbpEntityFrameworkCoreSqlServerModule.cs index 8186809846..acebc98d86 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo/Abp/EntityFrameworkCore/SqlServer/AbpEntityFrameworkCoreSqlServerModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo/Abp/EntityFrameworkCore/SqlServer/AbpEntityFrameworkCoreSqlServerModule.cs @@ -1,4 +1,5 @@ -using Volo.Abp.Guids; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; +using Volo.Abp.Guids; using Volo.Abp.Modularity; namespace Volo.Abp.EntityFrameworkCore.SqlServer; @@ -17,5 +18,10 @@ public class AbpEntityFrameworkCoreSqlServerModule : AbpModule options.DefaultSequentialGuidType = SequentialGuidType.SequentialAtEnd; } }); + + Configure(options => + { + options.UseDbFunction = true; + }); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs index e772546154..52415d73b1 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo/Abp/EntityFrameworkCore/Sqlite/AbpEntityFrameworkCoreSqliteModule.cs @@ -1,3 +1,4 @@ +using Volo.Abp.EntityFrameworkCore.GlobalFilters; using Volo.Abp.Modularity; namespace Volo.Abp.EntityFrameworkCore.Sqlite; @@ -7,5 +8,11 @@ namespace Volo.Abp.EntityFrameworkCore.Sqlite; )] public class AbpEntityFrameworkCoreSqliteModule : AbpModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.UseDbFunction = true; + }); + } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreDbContextOptionsBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreDbContextOptionsBuilderExtensions.cs new file mode 100644 index 0000000000..8c4b636d9e --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Volo.Abp.EntityFrameworkCore; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class AbpEfCoreDbContextOptionsBuilderExtensions +{ + public static DbContextOptionsBuilder AddAbpDbContextOptionsExtension(this DbContextOptionsBuilder optionsBuilder) + { + ((IDbContextOptionsBuilderInfrastructure) optionsBuilder).AddOrUpdateExtension(new AbpDbContextOptionsExtension()); + return optionsBuilder; + } + + public static DbContextOptionsBuilder AddAbpDbContextOptionsExtension(this DbContextOptionsBuilder optionsBuilder) + where TContext : DbContext + { + ((IDbContextOptionsBuilderInfrastructure) optionsBuilder).AddOrUpdateExtension(new AbpDbContextOptionsExtension()); + return optionsBuilder; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs new file mode 100644 index 0000000000..19ad9e0267 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs @@ -0,0 +1,68 @@ +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; +using Volo.Abp; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; +using Volo.Abp.MultiTenancy; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class AbpEfCoreModelBuilderExtensions +{ + public static ModelBuilder ConfigureSoftDeleteDbFunction(this ModelBuilder modelBuilder, MethodInfo methodInfo, AbpEfCoreCurrentDbContext abpEfCoreCurrentDbContext) + { + modelBuilder.HasDbFunction(methodInfo) + .HasTranslation(args => + { + // (bool isDeleted, bool boolParam) + var isDeleted = args[0]; + var boolParam = args[1]; + + if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled() == true) + { + // IsDeleted == false + return new SqlBinaryExpression( + ExpressionType.Equal, + isDeleted, + new SqlConstantExpression(Expression.Constant(false), boolParam.TypeMapping), + boolParam.Type, + boolParam.TypeMapping); + } + + // empty where sql + return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); + }); + + return modelBuilder; + } + + public static ModelBuilder ConfigureMultiTenantDbFunction(this ModelBuilder modelBuilder, MethodInfo methodInfo, AbpEfCoreCurrentDbContext abpEfCoreCurrentDbContext) + { + modelBuilder.HasDbFunction(methodInfo) + .HasTranslation(args => + { + // (Guid? tenantId, int? currentTenantId) + var tenantId = args[0]; + var currentTenantId = args[1]; + var boolParam = args[2]; + + if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled() == true) + { + // TenantId == CurrentTenantId + return new SqlBinaryExpression( + ExpressionType.Equal, + tenantId, + currentTenantId, + boolParam.Type, + boolParam.TypeMapping); + } + + // empty where sql + return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping); + }); + + return modelBuilder; + } +} 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 138bf82ec4..794058ad6e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -11,11 +11,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; @@ -24,6 +25,7 @@ using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore.ChangeTrackers; using Volo.Abp.EntityFrameworkCore.EntityHistory; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.ValueConverters; using Volo.Abp.EventBus.Distributed; @@ -37,7 +39,7 @@ using Volo.Abp.Uow; namespace Volo.Abp.EntityFrameworkCore; -public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, ITransientDependency +public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, IAbpEfCoreDbFunctionContext, ITransientDependency where TDbContext : DbContext { public IAbpLazyServiceProvider LazyServiceProvider { get; set; } = default!; @@ -78,6 +80,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, public IOptions Options => LazyServiceProvider.LazyGetRequiredService>(); + public IOptions GlobalFilterOptions => LazyServiceProvider.LazyGetRequiredService>(); + private static readonly MethodInfo ConfigureBasePropertiesMethodInfo = typeof(AbpDbContext) .GetMethod( @@ -99,9 +103,12 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, BindingFlags.Instance | BindingFlags.NonPublic )!; + protected readonly DbContextOptions DbContextOptions; + protected AbpDbContext(DbContextOptions options) : base(options) { + DbContextOptions = options; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -713,7 +720,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, { if (mutableEntityType.BaseType == null && ShouldFilterEntity(mutableEntityType)) { - var filterExpression = CreateFilterExpression(); + var filterExpression = CreateFilterExpression(modelBuilder); if (filterExpression != null) { modelBuilder.Entity().HasAbpQueryFilter(filterExpression); @@ -782,7 +789,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, return false; } - protected virtual Expression>? CreateFilterExpression() + protected virtual Expression>? CreateFilterExpression(ModelBuilder modelBuilder) where TEntity : class { Expression>? expression = null; @@ -790,14 +797,37 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) { expression = e => !IsSoftDeleteFilterEnabled || !EF.Property(e, "IsDeleted"); + + if (UseDbFunction()) + { + expression = e => AbpEfCoreDataFilterDbFunctionMethods.SoftDeleteFilter(((ISoftDelete)e).IsDeleted, true); + modelBuilder.ConfigureSoftDeleteDbFunction(AbpEfCoreDataFilterDbFunctionMethods.SoftDeleteFilterMethodInfo, this.GetService()); + } } if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) { Expression> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property(e, "TenantId") == CurrentTenantId; + + if (UseDbFunction()) + { + multiTenantFilter = e => AbpEfCoreDataFilterDbFunctionMethods.MultiTenantFilter(((IMultiTenant)e).TenantId, CurrentTenantId, true); + modelBuilder.ConfigureMultiTenantDbFunction(AbpEfCoreDataFilterDbFunctionMethods.MultiTenantFilterMethodInfo, this.GetService()); + } + expression = expression == null ? multiTenantFilter : QueryFilterExpressionHelper.CombineExpressions(expression, multiTenantFilter); } return expression; } + + protected virtual bool UseDbFunction() + { + return LazyServiceProvider != null && GlobalFilterOptions.Value.UseDbFunction && DbContextOptions.FindExtension() != null; + } + + public virtual string GetCompiledQueryCacheKey() + { + return $"{CurrentTenantId?.ToString() ?? "Null"}:{IsSoftDeleteFilterEnabled}:{IsMultiTenantFilterEnabled}"; + } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsExtension.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsExtension.cs new file mode 100644 index 0000000000..d9dc4cef9f --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContextOptionsExtension.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; + +namespace Volo.Abp.EntityFrameworkCore; + +public class AbpDbContextOptionsExtension : IDbContextOptionsExtension +{ + public void ApplyServices(IServiceCollection services) + { + var serviceDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(ICompiledQueryCacheKeyGenerator)); + if (serviceDescriptor != null && serviceDescriptor.ImplementationType != null) + { + services.Remove(serviceDescriptor); + services.AddScoped(serviceDescriptor.ImplementationType); + services.Add(ServiceDescriptor.Scoped(provider => + ActivatorUtilities.CreateInstance(provider, + provider.GetRequiredService(serviceDescriptor.ImplementationType) + .As()))); + } + + services.Replace(ServiceDescriptor.Scoped()); + services.AddSingleton(typeof(AbpEfCoreCurrentDbContext)); + } + + public void Validate(IDbContextOptions options) + { + } + + public DbContextOptionsExtensionInfo Info => new AbpOptionsExtensionInfo(this); + + private class AbpOptionsExtensionInfo : DbContextOptionsExtensionInfo + { + public AbpOptionsExtensionInfo(IDbContextOptionsExtension extension) + : base(extension) + { + } + + public override bool IsDatabaseProvider => false; + + public override int GetServiceProviderHashCode() + { + return 0; + } + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + { + return other is AbpOptionsExtensionInfo; + } + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + } + + public override string LogFragment => "AbpOptionsExtension"; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEntityQueryProvider.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEntityQueryProvider.cs new file mode 100644 index 0000000000..665757e17b --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEntityQueryProvider.cs @@ -0,0 +1,49 @@ +using System.Linq.Expressions; +using System.Threading; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query.Internal; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; + +namespace Volo.Abp.EntityFrameworkCore; + +#pragma warning disable EF1001 +public class AbpEntityQueryProvider : EntityQueryProvider +{ + protected AbpEfCoreCurrentDbContext AbpEfCoreCurrentDbContext { get; } + protected ICurrentDbContext CurrentDbContext { get; } + + public AbpEntityQueryProvider( + IQueryCompiler queryCompiler, + AbpEfCoreCurrentDbContext abpEfCoreCurrentDbContext, + ICurrentDbContext currentDbContext) + : base(queryCompiler) + { + AbpEfCoreCurrentDbContext = abpEfCoreCurrentDbContext; + CurrentDbContext = currentDbContext; + } + + public override object Execute(Expression expression) + { + using (AbpEfCoreCurrentDbContext.Use(CurrentDbContext.Context as IAbpEfCoreDbFunctionContext)) + { + return base.Execute(expression); + } + } + + public override TResult Execute(Expression expression) + { + using (AbpEfCoreCurrentDbContext.Use(CurrentDbContext.Context as IAbpEfCoreDbFunctionContext)) + { + return base.Execute(expression); + } + } + + public override TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = new CancellationToken()) + { + using (AbpEfCoreCurrentDbContext.Use(CurrentDbContext.Context as IAbpEfCoreDbFunctionContext)) + { + return base.ExecuteAsync(expression, cancellationToken); + } + } +} +#pragma warning restore EF1001 diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs index 1b3a6f48b5..113a4aa86c 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore.GlobalFilters; using Volo.Abp.MultiTenancy; namespace Volo.Abp.EntityFrameworkCore.DependencyInjection; @@ -27,6 +28,11 @@ public static class DbContextOptionsFactory PreConfigure(options, context); Configure(options, context); + if (serviceProvider.GetRequiredService>().Value.UseDbFunction) + { + context.DbContextOptions.AddAbpDbContextOptionsExtension(); + } + return context.DbContextOptions.Options; } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpCompiledQueryCacheKeyGenerator.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpCompiledQueryCacheKeyGenerator.cs new file mode 100644 index 0000000000..e43ae1e04d --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpCompiledQueryCacheKeyGenerator.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query; + +namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; + +public class AbpCompiledQueryCacheKeyGenerator : ICompiledQueryCacheKeyGenerator +{ + protected ICompiledQueryCacheKeyGenerator InnerCompiledQueryCacheKeyGenerator { get; } + protected ICurrentDbContext CurrentContext { get; } + + public AbpCompiledQueryCacheKeyGenerator( + ICompiledQueryCacheKeyGenerator innerCompiledQueryCacheKeyGenerator, + ICurrentDbContext currentContext) + { + InnerCompiledQueryCacheKeyGenerator = innerCompiledQueryCacheKeyGenerator; + CurrentContext = currentContext; + } + + public virtual object GenerateCacheKey(Expression query, bool async) + { + var cacheKey = InnerCompiledQueryCacheKeyGenerator.GenerateCacheKey(query, async); + if (CurrentContext.Context is IAbpEfCoreDbFunctionContext abpEfCoreDbFunctionContext) + { + return new AbpCompiledQueryCacheKey(cacheKey, abpEfCoreDbFunctionContext.GetCompiledQueryCacheKey()); + } + + return cacheKey; + } + + private readonly struct AbpCompiledQueryCacheKey : IEquatable + { + private readonly object _compiledQueryCacheKey; + private readonly string _currentFilterCacheKey; + + public AbpCompiledQueryCacheKey(object compiledQueryCacheKey, string currentFilterCacheKey) + { + _compiledQueryCacheKey = compiledQueryCacheKey; + _currentFilterCacheKey = currentFilterCacheKey; + } + + public override bool Equals(object? obj) + { + return obj is AbpCompiledQueryCacheKey key && Equals(key); + } + + public bool Equals(AbpCompiledQueryCacheKey other) + { + return _compiledQueryCacheKey.Equals(other._compiledQueryCacheKey) && + _currentFilterCacheKey == other._currentFilterCacheKey; + } + + public override int GetHashCode() + { + return HashCode.Combine(_compiledQueryCacheKey, _currentFilterCacheKey); + } + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreCurrentDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreCurrentDbContext.cs new file mode 100644 index 0000000000..c3a29b6d70 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreCurrentDbContext.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; + +namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; + +public class AbpEfCoreCurrentDbContext +{ + private readonly AsyncLocal _current = new AsyncLocal(); + + public IAbpEfCoreDbFunctionContext? Context => _current.Value; + + public IDisposable Use(IAbpEfCoreDbFunctionContext? context) + { + var previousValue = Context; + _current.Value = context; + return new DisposeAction(() => + { + _current.Value = previousValue; + }); + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreDataFilterDbFunctionMethods.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreDataFilterDbFunctionMethods.cs new file mode 100644 index 0000000000..10cb6be280 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreDataFilterDbFunctionMethods.cs @@ -0,0 +1,25 @@ +using System; +using System.Reflection; + +namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; + +public static class AbpEfCoreDataFilterDbFunctionMethods +{ + public const string NotSupportedExceptionMessage = "Your EF Core database provider does not support 'User-defined function mapping'." + + "Please set 'UseDbFunction' of 'AbpEfCoreGlobalFilterOptions' to false to disable it." + + "See https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping for more information." ; + + public static bool SoftDeleteFilter(bool isDeleted, bool boolParam) + { + throw new NotSupportedException(NotSupportedExceptionMessage); + } + + public static MethodInfo SoftDeleteFilterMethodInfo => typeof(AbpEfCoreDataFilterDbFunctionMethods).GetMethod(nameof(SoftDeleteFilter))!; + + public static bool MultiTenantFilter(Guid? tenantId, Guid? currentTenantId, bool boolParam) + { + throw new NotSupportedException(NotSupportedExceptionMessage); + } + + public static MethodInfo MultiTenantFilterMethodInfo => typeof(AbpEfCoreDataFilterDbFunctionMethods).GetMethod(nameof(MultiTenantFilter))!; +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreGlobalFilterOptions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreGlobalFilterOptions.cs new file mode 100644 index 0000000000..f97031ff13 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/AbpEfCoreGlobalFilterOptions.cs @@ -0,0 +1,10 @@ +namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; + +public class AbpEfCoreGlobalFilterOptions +{ + /// + /// Use User-defined function mapping to filter data. + /// https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping + /// + public bool UseDbFunction { get; set; } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/IAbpEfCoreDbFunctionContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/IAbpEfCoreDbFunctionContext.cs new file mode 100644 index 0000000000..85096e9e0a --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/GlobalFilters/IAbpEfCoreDbFunctionContext.cs @@ -0,0 +1,16 @@ +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.EntityFrameworkCore.GlobalFilters; + +public interface IAbpEfCoreDbFunctionContext +{ + IAbpLazyServiceProvider LazyServiceProvider { get; set; } + + ICurrentTenant CurrentTenant { get; } + + IDataFilter DataFilter { get; } + + string GetCompiledQueryCacheKey(); +} diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs index 4fe3ae9c16..4e14dbc262 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs @@ -70,7 +70,7 @@ public class AbpAuditingTestModule : AbpModule connection.Open(); using (var context = new AbpAuditingTestDbContext(new DbContextOptionsBuilder() - .UseSqlite(connection).Options)) + .UseSqlite(connection).AddAbpDbContextOptionsExtension().Options)) { context.GetService().CreateTables(); } 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 5e81aa1a7d..a13aea2945 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 @@ -72,7 +72,7 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule { options.Configure(abpDbContextConfigurationContext => { - abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection); + abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection).AddAbpDbContextOptionsExtension(); }); }); } @@ -101,7 +101,7 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule var connection = new AbpUnitTestSqliteConnection("Data Source=:memory:"); connection.Open(); - using (var context = new TestMigrationsDbContext(new DbContextOptionsBuilder().UseSqlite(connection).Options)) + using (var context = new TestMigrationsDbContext(new DbContextOptionsBuilder().UseSqlite(connection).AddAbpDbContextOptionsExtension().Options)) { context.GetService().CreateTables(); context.Database.ExecuteSqlRaw( diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Comments/Approve/index.js b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Comments/Approve/index.js index 23a84dee34..f8aa2ba80d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Comments/Approve/index.js +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Comments/Approve/index.js @@ -57,8 +57,8 @@ orderable: false, visible: abp.auth.isGranted('CmsKit.Comments.Update'), render: function (data, type, row) { - let approveButton = $(``); - let rejectButton = $(``); + let approveButton = $(``); + let rejectButton = $(``); let buttons = []; buttons.push(approveButton); buttons.push(rejectButton); @@ -99,6 +99,16 @@ return ""; } }, + { + title: l("URL"), + data: "url", + render: function (data, type, row) { + if (data !== null) { + return ''; + } + return ""; + } + }, { title: l("Text"), data: "text",