From d373e15612f2db63f6c0c49dcbedd564fe3edbb1 Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 6 Jun 2026 15:46:08 +0800 Subject: [PATCH 1/2] Use CLR property name in soft-delete and multi-tenant filters --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 8 +-- ...rameworkCoreTestModuleWithoutDbFunction.cs | 17 +++++++ ...ltiTenant_With_Custom_Column_Name_Tests.cs | 49 +++++++++++++++++++ ...oftDelete_With_Custom_Column_Name_Tests.cs | 49 +++++++++++++++++++ .../TestMigrationsDbContext.cs | 15 ++++++ .../EntityFrameworkCore/TestAppDbContext.cs | 32 ++++++++++++ .../EntityWithCustomSoftDeleteColumn.cs | 11 +++++ .../Domain/EntityWithCustomTenantIdColumn.cs | 12 +++++ 8 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/AbpEntityFrameworkCoreTestModuleWithoutDbFunction.cs create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/MultiTenant_With_Custom_Column_Name_Tests.cs create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_With_Custom_Column_Name_Tests.cs create mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs create mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs 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 553f90f859..da1a47d779 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -957,8 +957,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) { - var softDeleteColumnName = entityTypeBuilder.Metadata.FindProperty(nameof(ISoftDelete.IsDeleted))?.GetColumnName() ?? "IsDeleted"; - expression = e => !IsSoftDeleteFilterEnabled || !EF.Property(e, softDeleteColumnName); + var softDeletePropertyName = entityTypeBuilder.Metadata.FindProperty(nameof(ISoftDelete.IsDeleted))?.Name ?? nameof(ISoftDelete.IsDeleted); + expression = e => !IsSoftDeleteFilterEnabled || !EF.Property(e, softDeletePropertyName); if (UseDbFunction()) { expression = e => AbpEfCoreDataFilterDbFunctionMethods.SoftDeleteFilter(((ISoftDelete)e).IsDeleted, true); @@ -971,8 +971,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) { - var multiTenantColumnName = entityTypeBuilder.Metadata.FindProperty(nameof(IMultiTenant.TenantId))?.GetColumnName() ?? "TenantId"; - Expression> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property(e, multiTenantColumnName) == CurrentTenantId; + var multiTenantPropertyName = entityTypeBuilder.Metadata.FindProperty(nameof(IMultiTenant.TenantId))?.Name ?? nameof(IMultiTenant.TenantId); + Expression> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property(e, multiTenantPropertyName) == CurrentTenantId; if (UseDbFunction()) { multiTenantFilter = e => AbpEfCoreDataFilterDbFunctionMethods.MultiTenantFilter(((IMultiTenant)e).TenantId, CurrentTenantId, true); 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 new file mode 100644 index 0000000000..e4a0d14e28 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/AbpEntityFrameworkCoreTestModuleWithoutDbFunction.cs @@ -0,0 +1,17 @@ +using Volo.Abp.EntityFrameworkCore.GlobalFilters; +using Volo.Abp.Modularity; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering; + +// Disables UseDbFunction so the EF.Property soft-delete filter path is exercised. +[DependsOn(typeof(AbpEntityFrameworkCoreTestModule))] +public class AbpEntityFrameworkCoreTestModuleWithoutDbFunction : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.UseDbFunction = false; + }); + } +} 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 new file mode 100644 index 0000000000..d5afbfcced --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/MultiTenant_With_Custom_Column_Name_Tests.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering; + +public class MultiTenant_With_Custom_Column_Name_Tests : TestAppTestBase +{ + private readonly IRepository _repository; + private readonly IDataFilter _multiTenantFilter; + + public MultiTenant_With_Custom_Column_Name_Tests() + { + _repository = GetRequiredService>(); + _multiTenantFilter = GetRequiredService>(); + } + + [Fact] + public async Task MultiTenant_Filter_Should_Work_When_TenantId_Has_Custom_Column_Name() + { + var ctx = ServiceProvider.GetRequiredService(); + var tenantIdProperty = ctx.Model.FindEntityType(typeof(EntityWithCustomTenantIdColumn))! + .FindProperty(nameof(IMultiTenant.TenantId))!; + tenantIdProperty.GetColumnName().ShouldBe("custom_tenant_id_column"); + tenantIdProperty.Name.ShouldBe(nameof(IMultiTenant.TenantId)); + + using (_multiTenantFilter.Disable()) + { + await _repository.InsertAsync(new EntityWithCustomTenantIdColumn { Name = "host", TenantId = null }); + await _repository.InsertAsync(new EntityWithCustomTenantIdColumn { Name = "tenant", TenantId = Guid.NewGuid() }); + + var all = await _repository.GetListAsync(); + all.Count.ShouldBe(2); + } + + var hostScoped = await _repository.GetListAsync(); + hostScoped.Count.ShouldBe(1); + hostScoped[0].Name.ShouldBe("host"); + } +} 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 new file mode 100644 index 0000000000..0154eda6f3 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_With_Custom_Column_Name_Tests.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering; + +public class SoftDelete_With_Custom_Column_Name_Tests : TestAppTestBase +{ + private readonly IRepository _repository; + private readonly IDataFilter _softDeleteFilter; + + public SoftDelete_With_Custom_Column_Name_Tests() + { + _repository = GetRequiredService>(); + _softDeleteFilter = GetRequiredService>(); + } + + [Fact] + public async Task SoftDelete_Filter_Should_Work_When_IsDeleted_Has_Custom_Column_Name() + { + var ctx = ServiceProvider.GetRequiredService(); + var isDeletedProperty = ctx.Model.FindEntityType(typeof(EntityWithCustomSoftDeleteColumn))! + .FindProperty(nameof(ISoftDelete.IsDeleted))!; + isDeletedProperty.GetColumnName().ShouldBe("custom_is_deleted_column"); + isDeletedProperty.Name.ShouldBe(nameof(ISoftDelete.IsDeleted)); + + await _repository.InsertAsync(new EntityWithCustomSoftDeleteColumn { Name = "kept" }); + await _repository.InsertAsync(new EntityWithCustomSoftDeleteColumn { Name = "removed", IsDeleted = true }); + + var visible = await _repository.GetListAsync(); + visible.Count.ShouldBe(1); + visible[0].Name.ShouldBe("kept"); + + using (_softDeleteFilter.Disable()) + { + var all = await _repository.GetListAsync(); + all.Count.ShouldBe(2); + all.ShouldContain(x => x.Name == "removed" && x.IsDeleted); + } + } +} 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 96f4b1a2e1..3aba18a42f 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 @@ -27,6 +27,10 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet Categories { get; set; } + public DbSet EntityWithCustomSoftDeleteColumns { get; set; } + + public DbSet EntityWithCustomTenantIdColumns { get; set; } + public DbSet AppEntityWithNavigations { get; set; } public DbSet AppEntityWithNavigationChildOneToMany { get; set; } @@ -67,6 +71,17 @@ public class TestMigrationsDbContext : AbpDbContext base.OnModelCreating(modelBuilder); + // 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"); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.TenantId).HasColumnName("custom_tenant_id_column"); + }); + modelBuilder.Entity(b => { b.HasKey(p => new { p.PersonId, p.Number }); 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 2b95877bed..fb52b0f164 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 @@ -1,12 +1,14 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Volo.Abp.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.TestApp.FourthContext; using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext; +using Volo.Abp.MultiTenancy; using Volo.Abp.TestApp.Domain; using Volo.Abp.TestApp.Testing; @@ -33,6 +35,10 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Categories { get; set; } + public DbSet EntityWithCustomSoftDeleteColumns { get; set; } + + public DbSet EntityWithCustomTenantIdColumns { get; set; } + public DbSet AppEntityWithNavigations { get; set; } public DbSet AppEntityWithNavigationChildOneToMany { get; set; } @@ -176,4 +182,30 @@ 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. + protected override void ConfigureBaseProperties(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) + { + base.ConfigureBaseProperties(modelBuilder, mutableEntityType); + + if (typeof(EntityWithCustomSoftDeleteColumn).IsAssignableFrom(typeof(TEntity))) + { + modelBuilder.Entity() + .Property(nameof(ISoftDelete.IsDeleted)) + .HasColumnName("custom_is_deleted_column"); + + ConfigureGlobalFilters(modelBuilder, mutableEntityType, modelBuilder.Entity()); + } + + if (typeof(EntityWithCustomTenantIdColumn).IsAssignableFrom(typeof(TEntity))) + { + modelBuilder.Entity() + .Property(nameof(IMultiTenant.TenantId)) + .HasColumnName("custom_tenant_id_column"); + + 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 new file mode 100644 index 0000000000..f352726dfa --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs @@ -0,0 +1,11 @@ +using System; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.TestApp.Domain; + +public class EntityWithCustomSoftDeleteColumn : AggregateRoot, ISoftDelete +{ + 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 new file mode 100644 index 0000000000..484fd7e192 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs @@ -0,0 +1,12 @@ +using System; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.TestApp.Domain; + +public class EntityWithCustomTenantIdColumn : AggregateRoot, IMultiTenant +{ + public string Name { get; set; } + + public Guid? TenantId { get; set; } +} From f277b72cb52c3d5309d80b4a4117029218fe10cd Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 6 Jun 2026 16:29:16 +0800 Subject: [PATCH 2/2] Cover IMultiTenant filter and assert property name in expression --- ...rameworkCoreTestModuleWithoutDbFunction.cs | 3 +- .../FilterExpressionPropertyNameInspector.cs | 83 +++++++++++++++++++ ...ltiTenant_With_Custom_Column_Name_Tests.cs | 22 ++++- ...oftDelete_With_Custom_Column_Name_Tests.cs | 22 ++++- .../TestMigrationsDbContext.cs | 4 +- .../EntityFrameworkCore/TestAppDbContext.cs | 12 +-- .../EntityWithCustomSoftDeleteColumn.cs | 2 + .../Domain/EntityWithCustomTenantIdColumn.cs | 2 + 8 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/FilterExpressionPropertyNameInspector.cs 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; }