mirror of https://github.com/abpframework/abp.git
committed by
GitHub
9 changed files with 319 additions and 4 deletions
@ -0,0 +1,18 @@ |
|||||
|
using Volo.Abp.EntityFrameworkCore.GlobalFilters; |
||||
|
using Volo.Abp.Modularity; |
||||
|
|
||||
|
namespace Volo.Abp.EntityFrameworkCore.DataFiltering; |
||||
|
|
||||
|
// Disables UseDbFunction so the EF.Property global filter path is exercised
|
||||
|
// (covers both ISoftDelete and IMultiTenant filters).
|
||||
|
[DependsOn(typeof(AbpEntityFrameworkCoreTestModule))] |
||||
|
public class AbpEntityFrameworkCoreTestModuleWithoutDbFunction : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpEfCoreGlobalFilterOptions>(options => |
||||
|
{ |
||||
|
options.UseDbFunction = false; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using Microsoft.EntityFrameworkCore.Metadata.Internal; |
||||
|
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<AbpEntityFrameworkCoreTestModuleWithoutDbFunction> |
||||
|
{ |
||||
|
private readonly IRepository<EntityWithCustomTenantIdColumn, Guid> _repository; |
||||
|
private readonly IDataFilter<IMultiTenant> _multiTenantFilter; |
||||
|
|
||||
|
public MultiTenant_With_Custom_Column_Name_Tests() |
||||
|
{ |
||||
|
_repository = GetRequiredService<IRepository<EntityWithCustomTenantIdColumn, Guid>>(); |
||||
|
_multiTenantFilter = GetRequiredService<IDataFilter<IMultiTenant>>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task MultiTenant_Filter_Should_Work_When_TenantId_Has_Custom_Column_Name() |
||||
|
{ |
||||
|
var ctx = ServiceProvider.GetRequiredService<TestAppDbContext>(); |
||||
|
var tenantIdProperty = ctx.Model.FindEntityType(typeof(EntityWithCustomTenantIdColumn))! |
||||
|
.FindProperty(nameof(IMultiTenant.TenantId))!; |
||||
|
tenantIdProperty.GetColumnName().ShouldBe(EntityWithCustomTenantIdColumn.TenantIdColumnName); |
||||
|
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"); |
||||
|
} |
||||
|
|
||||
|
[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)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using Microsoft.EntityFrameworkCore.Metadata.Internal; |
||||
|
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<AbpEntityFrameworkCoreTestModuleWithoutDbFunction> |
||||
|
{ |
||||
|
private readonly IRepository<EntityWithCustomSoftDeleteColumn, Guid> _repository; |
||||
|
private readonly IDataFilter<ISoftDelete> _softDeleteFilter; |
||||
|
|
||||
|
public SoftDelete_With_Custom_Column_Name_Tests() |
||||
|
{ |
||||
|
_repository = GetRequiredService<IRepository<EntityWithCustomSoftDeleteColumn, Guid>>(); |
||||
|
_softDeleteFilter = GetRequiredService<IDataFilter<ISoftDelete>>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SoftDelete_Filter_Should_Work_When_IsDeleted_Has_Custom_Column_Name() |
||||
|
{ |
||||
|
var ctx = ServiceProvider.GetRequiredService<TestAppDbContext>(); |
||||
|
var isDeletedProperty = ctx.Model.FindEntityType(typeof(EntityWithCustomSoftDeleteColumn))! |
||||
|
.FindProperty(nameof(ISoftDelete.IsDeleted))!; |
||||
|
isDeletedProperty.GetColumnName().ShouldBe(EntityWithCustomSoftDeleteColumn.IsDeletedColumnName); |
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Domain.Entities; |
||||
|
|
||||
|
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; } |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Domain.Entities; |
||||
|
using Volo.Abp.MultiTenancy; |
||||
|
|
||||
|
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…
Reference in new issue