diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/AbpEntityFrameworkCoreTestModuleWithoutDbFunction.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/AbpEntityFrameworkCoreTestModuleWithoutDbFunction.cs index e4a0d14e28..363d33727c 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/AbpEntityFrameworkCoreTestModuleWithoutDbFunction.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/AbpEntityFrameworkCoreTestModuleWithoutDbFunction.cs @@ -3,7 +3,8 @@ using Volo.Abp.Modularity; namespace Volo.Abp.EntityFrameworkCore.DataFiltering; -// Disables UseDbFunction so the EF.Property soft-delete filter path is exercised. +// Disables UseDbFunction so the EF.Property global filter path is exercised +// (covers both ISoftDelete and IMultiTenant filters). [DependsOn(typeof(AbpEntityFrameworkCoreTestModule))] public class AbpEntityFrameworkCoreTestModuleWithoutDbFunction : AbpModule { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/FilterExpressionPropertyNameInspector.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/FilterExpressionPropertyNameInspector.cs new file mode 100644 index 0000000000..318631c0da --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/FilterExpressionPropertyNameInspector.cs @@ -0,0 +1,83 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using Microsoft.EntityFrameworkCore; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering; + +internal static class FilterExpressionPropertyNameInspector +{ + // EF Core stores the registered query filter under the CoreAnnotationNames.QueryFilter annotation. + // Since EF Core 9 the value is a QueryFilterCollection (internal) whose elements expose an + // Expression property. Older versions stored a bare LambdaExpression. We handle both via duck typing + // so the test stays compatible across EF Core versions. + public static List GetEfPropertyStringArgs(object annotationValue) + { + return Inspect(annotationValue).Args; + } + + public static InspectionResult Inspect(object annotationValue) + { + var collector = new EfPropertyArgCollector(); + var dump = new StringBuilder(); + dump.Append("annotationType=").Append(annotationValue?.GetType().FullName ?? "").AppendLine(); + + switch (annotationValue) + { + case LambdaExpression lambda: + dump.Append("lambda: ").AppendLine(lambda.ToString()); + collector.Visit(lambda); + break; + case IEnumerable enumerable: + var index = 0; + foreach (var item in enumerable) + { + if (item == null) + { + continue; + } + + dump.Append("[").Append(index++).Append("] itemType=").Append(item.GetType().FullName).AppendLine(); + var expr = item.GetType().GetProperty("Expression")?.GetValue(item) as Expression + ?? item as Expression; + if (expr != null) + { + dump.Append(" expr: ").AppendLine(expr.ToString()); + collector.Visit(expr); + } + else + { + dump.AppendLine(" (no Expression property)"); + } + } + break; + } + + return new InspectionResult(collector.Args, dump.ToString()); + } + + public sealed record InspectionResult(List Args, string Dump); + + private sealed class EfPropertyArgCollector : ExpressionVisitor + { + public List Args { get; } = new(); + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(EF) + && node.Method.Name == nameof(EF.Property) + && node.Arguments.Count == 2) + { + // The 2nd arg comes from a closure-captured local at filter registration, so it + // appears as a MemberExpression rather than a ConstantExpression. Evaluate it. + if (Expression.Lambda(node.Arguments[1]).Compile().DynamicInvoke() is string name) + { + Args.Add(name); + } + } + + return base.VisitMethodCall(node); + } + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/MultiTenant_With_Custom_Column_Name_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/MultiTenant_With_Custom_Column_Name_Tests.cs index d5afbfcced..890b11e533 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/MultiTenant_With_Custom_Column_Name_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/MultiTenant_With_Custom_Column_Name_Tests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.Data; @@ -30,7 +31,7 @@ public class MultiTenant_With_Custom_Column_Name_Tests : TestAppTestBase(); var tenantIdProperty = ctx.Model.FindEntityType(typeof(EntityWithCustomTenantIdColumn))! .FindProperty(nameof(IMultiTenant.TenantId))!; - tenantIdProperty.GetColumnName().ShouldBe("custom_tenant_id_column"); + tenantIdProperty.GetColumnName().ShouldBe(EntityWithCustomTenantIdColumn.TenantIdColumnName); tenantIdProperty.Name.ShouldBe(nameof(IMultiTenant.TenantId)); using (_multiTenantFilter.Disable()) @@ -46,4 +47,23 @@ public class MultiTenant_With_Custom_Column_Name_Tests : TestAppTestBase(); + var entityType = ctx.Model.FindEntityType(typeof(EntityWithCustomTenantIdColumn))!; + +#pragma warning disable EF1001 + var annotation = entityType.FindAnnotation(CoreAnnotationNames.QueryFilter); +#pragma warning restore EF1001 + annotation.ShouldNotBeNull(); + + var args = FilterExpressionPropertyNameInspector.GetEfPropertyStringArgs(annotation.Value!); + args.ShouldNotBeEmpty(); + foreach (var arg in args) + { + arg.ShouldBe(nameof(IMultiTenant.TenantId)); + } + } } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_With_Custom_Column_Name_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_With_Custom_Column_Name_Tests.cs index 0154eda6f3..454e171c0c 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_With_Custom_Column_Name_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_With_Custom_Column_Name_Tests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.Data; @@ -29,7 +30,7 @@ public class SoftDelete_With_Custom_Column_Name_Tests : TestAppTestBase(); var isDeletedProperty = ctx.Model.FindEntityType(typeof(EntityWithCustomSoftDeleteColumn))! .FindProperty(nameof(ISoftDelete.IsDeleted))!; - isDeletedProperty.GetColumnName().ShouldBe("custom_is_deleted_column"); + isDeletedProperty.GetColumnName().ShouldBe(EntityWithCustomSoftDeleteColumn.IsDeletedColumnName); isDeletedProperty.Name.ShouldBe(nameof(ISoftDelete.IsDeleted)); await _repository.InsertAsync(new EntityWithCustomSoftDeleteColumn { Name = "kept" }); @@ -46,4 +47,23 @@ public class SoftDelete_With_Custom_Column_Name_Tests : TestAppTestBase x.Name == "removed" && x.IsDeleted); } } + + [Fact] + public void SoftDelete_Filter_Should_Reference_IsDeleted_By_Clr_Property_Name() + { + var ctx = ServiceProvider.GetRequiredService(); + var entityType = ctx.Model.FindEntityType(typeof(EntityWithCustomSoftDeleteColumn))!; + +#pragma warning disable EF1001 + var annotation = entityType.FindAnnotation(CoreAnnotationNames.QueryFilter); +#pragma warning restore EF1001 + annotation.ShouldNotBeNull(); + + var result = FilterExpressionPropertyNameInspector.Inspect(annotation.Value!); + result.Args.ShouldNotBeEmpty(customMessage: result.Dump); + foreach (var arg in result.Args) + { + arg.ShouldBe(nameof(ISoftDelete.IsDeleted), customMessage: result.Dump); + } + } } 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 3aba18a42f..dfdb1fee9c 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 @@ -74,12 +74,12 @@ public class TestMigrationsDbContext : AbpDbContext // Mirror the column renames in TestAppDbContext so the generated SQLite schema matches. modelBuilder.Entity(b => { - b.Property(x => x.IsDeleted).HasColumnName("custom_is_deleted_column"); + b.Property(x => x.IsDeleted).HasColumnName(EntityWithCustomSoftDeleteColumn.IsDeletedColumnName); }); modelBuilder.Entity(b => { - b.Property(x => x.TenantId).HasColumnName("custom_tenant_id_column"); + b.Property(x => x.TenantId).HasColumnName(EntityWithCustomTenantIdColumn.TenantIdColumnName); }); modelBuilder.Entity(b => 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 fb52b0f164..02c12f5882 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 @@ -183,9 +183,11 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, modelBuilder.TryConfigureObjectExtensions(); } - // Renames IsDeleted / TenantId to a custom column and re-registers the global filter so the - // EF.Property path captures the new column name — covered by SoftDelete_With_Custom_Column_Name_Tests - // and MultiTenant_With_Custom_Column_Name_Tests. + // Renames IsDeleted / TenantId to a non-default column name and re-registers the global filter + // afterwards. CreateFilterExpression then captures the renamed column name; before the fix it + // would feed that string to EF.Property(...), which expects a CLR property name and breaks + // translation. Covered by SoftDelete_With_Custom_Column_Name_Tests and + // MultiTenant_With_Custom_Column_Name_Tests. protected override void ConfigureBaseProperties(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) { base.ConfigureBaseProperties(modelBuilder, mutableEntityType); @@ -194,7 +196,7 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, { modelBuilder.Entity() .Property(nameof(ISoftDelete.IsDeleted)) - .HasColumnName("custom_is_deleted_column"); + .HasColumnName(EntityWithCustomSoftDeleteColumn.IsDeletedColumnName); ConfigureGlobalFilters(modelBuilder, mutableEntityType, modelBuilder.Entity()); } @@ -203,7 +205,7 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, { modelBuilder.Entity() .Property(nameof(IMultiTenant.TenantId)) - .HasColumnName("custom_tenant_id_column"); + .HasColumnName(EntityWithCustomTenantIdColumn.TenantIdColumnName); ConfigureGlobalFilters(modelBuilder, mutableEntityType, modelBuilder.Entity()); } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs index f352726dfa..e3ba3821a3 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs @@ -5,6 +5,8 @@ namespace Volo.Abp.TestApp.Domain; public class EntityWithCustomSoftDeleteColumn : AggregateRoot, ISoftDelete { + public const string IsDeletedColumnName = "custom_is_deleted_column"; + public string Name { get; set; } public bool IsDeleted { get; set; } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs index 484fd7e192..ac700bfe16 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs @@ -6,6 +6,8 @@ namespace Volo.Abp.TestApp.Domain; public class EntityWithCustomTenantIdColumn : AggregateRoot, IMultiTenant { + public const string TenantIdColumnName = "custom_tenant_id_column"; + public string Name { get; set; } public Guid? TenantId { get; set; }