Browse Source

Cover IMultiTenant filter and assert property name in expression

pull/25568/head
maliming 4 months ago
parent
commit
f277b72cb5
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 3
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/AbpEntityFrameworkCoreTestModuleWithoutDbFunction.cs
  2. 83
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/FilterExpressionPropertyNameInspector.cs
  3. 22
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/MultiTenant_With_Custom_Column_Name_Tests.cs
  4. 22
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_With_Custom_Column_Name_Tests.cs
  5. 4
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs
  6. 12
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs
  7. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs
  8. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs

3
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
{

83
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<string> 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 ?? "<null>").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<string> Args, string Dump);
private sealed class EfPropertyArgCollector : ExpressionVisitor
{
public List<string> 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);
}
}
}

22
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<AbpEnti
var ctx = ServiceProvider.GetRequiredService<TestAppDbContext>();
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<AbpEnti
hostScoped.Count.ShouldBe(1);
hostScoped[0].Name.ShouldBe("host");
}
[Fact]
public void MultiTenant_Filter_Should_Reference_TenantId_By_Clr_Property_Name()
{
var ctx = ServiceProvider.GetRequiredService<TestAppDbContext>();
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));
}
}
}

22
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<AbpEntit
var ctx = ServiceProvider.GetRequiredService<TestAppDbContext>();
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<AbpEntit
all.ShouldContain(x => x.Name == "removed" && x.IsDeleted);
}
}
[Fact]
public void SoftDelete_Filter_Should_Reference_IsDeleted_By_Clr_Property_Name()
{
var ctx = ServiceProvider.GetRequiredService<TestAppDbContext>();
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);
}
}
}

4
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs

@ -74,12 +74,12 @@ public class TestMigrationsDbContext : AbpDbContext<TestMigrationsDbContext>
// Mirror the column renames in TestAppDbContext so the generated SQLite schema matches.
modelBuilder.Entity<EntityWithCustomSoftDeleteColumn>(b =>
{
b.Property(x => x.IsDeleted).HasColumnName("custom_is_deleted_column");
b.Property(x => x.IsDeleted).HasColumnName(EntityWithCustomSoftDeleteColumn.IsDeletedColumnName);
});
modelBuilder.Entity<EntityWithCustomTenantIdColumn>(b =>
{
b.Property(x => x.TenantId).HasColumnName("custom_tenant_id_column");
b.Property(x => x.TenantId).HasColumnName(EntityWithCustomTenantIdColumn.TenantIdColumnName);
});
modelBuilder.Entity<Phone>(b =>

12
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs

@ -183,9 +183,11 @@ public class TestAppDbContext : AbpDbContext<TestAppDbContext>, IThirdDbContext,
modelBuilder.TryConfigureObjectExtensions<TestAppDbContext>();
}
// 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<T>(...), 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<TEntity>(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
{
base.ConfigureBaseProperties<TEntity>(modelBuilder, mutableEntityType);
@ -194,7 +196,7 @@ public class TestAppDbContext : AbpDbContext<TestAppDbContext>, IThirdDbContext,
{
modelBuilder.Entity<TEntity>()
.Property(nameof(ISoftDelete.IsDeleted))
.HasColumnName("custom_is_deleted_column");
.HasColumnName(EntityWithCustomSoftDeleteColumn.IsDeletedColumnName);
ConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType, modelBuilder.Entity<TEntity>());
}
@ -203,7 +205,7 @@ public class TestAppDbContext : AbpDbContext<TestAppDbContext>, IThirdDbContext,
{
modelBuilder.Entity<TEntity>()
.Property(nameof(IMultiTenant.TenantId))
.HasColumnName("custom_tenant_id_column");
.HasColumnName(EntityWithCustomTenantIdColumn.TenantIdColumnName);
ConfigureGlobalFilters<TEntity>(modelBuilder, mutableEntityType, modelBuilder.Entity<TEntity>());
}

2
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomSoftDeleteColumn.cs

@ -5,6 +5,8 @@ namespace Volo.Abp.TestApp.Domain;
public class EntityWithCustomSoftDeleteColumn : AggregateRoot<Guid>, ISoftDelete
{
public const string IsDeletedColumnName = "custom_is_deleted_column";
public string Name { get; set; }
public bool IsDeleted { get; set; }

2
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithCustomTenantIdColumn.cs

@ -6,6 +6,8 @@ namespace Volo.Abp.TestApp.Domain;
public class EntityWithCustomTenantIdColumn : AggregateRoot<Guid>, IMultiTenant
{
public const string TenantIdColumnName = "custom_tenant_id_column";
public string Name { get; set; }
public Guid? TenantId { get; set; }

Loading…
Cancel
Save