将 EF Core 全局筛选器应用于租户集合而不是单个 TenantId

问题描述 投票:0回答:1

我正在使用 Entity Framework Core 在 ASP.NET Core 应用程序中实现多租户。我已经有了一个将全局查询过滤器应用于具有单个

TenantId
的实体的解决方案,如下所示:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Company>(builder =>
    {
        builder.HasIndex(c => c.TenantId);

        builder.HasQueryFilter(c => c.TenantId == _tenantId);
    });

    modelBuilder.Entity<Sale>(builder =>
    {
        builder.HasIndex(s => s.TenantId);

        builder.HasQueryFilter(s => s.TenantId == _tenantId);
    });
}

然后可以使用以下扩展方法来简化:

modelBuilder.SetQueryFilterOnAllEntities<ITenantEntity>(x => x.TenantId == _tenantId);

但是,我有一些属于多个租户的实体(如

Deal
FinanceAgreement
),表示为
Tenants
的集合。以下是我手动处理它们的方法:

modelBuilder.Entity<Deal>(builder =>
{
    builder.HasQueryFilter(x => x.Tenants.Any(le => le.Id == _tenantId));
});

modelBuilder.Entity<FinanceAgreement>(builder =>
{
    builder.HasQueryFilter(x => x.Tenants.Any(le => le.Id == _tenantId));
});

有没有办法让我调整下面的扩展方法来支持上面的这种场景,我们没有一个

TenantId
而是多个?

扩展

public static class ModelBuilderExtensions
{
    private static readonly MethodInfo SetQueryFilterMethod = typeof(ModelBuilderExtensions)
        .GetMethods(BindingFlags.NonPublic | BindingFlags.Static)
        .Single(t => t is { IsGenericMethod: true, Name: nameof(SetQueryFilter) });

    public static void SetQueryFilterOnAllEntities<TEntityInterface>(
        this ModelBuilder builder,
        Expression<Func<TEntityInterface, bool>> filterExpression)
    {
        var entityTypes = builder.Model.GetEntityTypes()
            .Where(t => t.BaseType == null)
            .Select(t => t.ClrType)
            .Where(t => typeof(TEntityInterface).IsAssignableFrom(t));

        foreach (var type in entityTypes)
        {
            builder.SetEntityQueryFilter(type, filterExpression);
        }
    }

    private static void SetEntityQueryFilter<TEntityInterface>(
        this ModelBuilder builder,
        Type entityType,
        Expression<Func<TEntityInterface, bool>> filterExpression) =>
        SetQueryFilterMethod
            .MakeGenericMethod(entityType, typeof(TEntityInterface))
            .Invoke(null, [builder, filterExpression]);

    private static void SetQueryFilter<TEntity, TEntityInterface>(
        this ModelBuilder builder,
        Expression<Func<TEntityInterface, bool>> filterExpression)
        where TEntityInterface : class
        where TEntity : class, TEntityInterface
    {
        var concreteExpression = filterExpression
            .Convert<TEntityInterface, TEntity>();
        builder.Entity<TEntity>()
            .AppendQueryFilter(concreteExpression);
    }

    // CREDIT: This comment by magiak on GitHub https://github.com/dotnet/efcore/issues/10275#issuecomment-785916356
    private static void AppendQueryFilter<T>(this EntityTypeBuilder entityTypeBuilder, Expression<Func<T, bool>> expression)
        where T : class
    {
        var parameterType = Expression.Parameter(entityTypeBuilder.Metadata.ClrType);

        var expressionFilter = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), parameterType, expression.Body);

        var currentQueryFilter = entityTypeBuilder.Metadata.GetQueryFilter();
        if (currentQueryFilter is not null)
        {
            var currentExpressionFilter = ReplacingExpressionVisitor.Replace(currentQueryFilter.Parameters.Single(), parameterType, currentQueryFilter.Body);
            expressionFilter = Expression.AndAlso(currentExpressionFilter, expressionFilter);
        }

        var lambdaExpression = Expression.Lambda(expressionFilter, parameterType);
        entityTypeBuilder.HasQueryFilter(lambdaExpression);
    }
}

public static class ExpressionExtensions
{
    // This magic is courtesy of this StackOverflow post.
    // https://stackoverflow.com/questions/38316519/replace-parameter-type-in-lambda-expression
    // I made some tweaks to adapt it to our needs - @haacked
    public static Expression<Func<TTarget, bool>> Convert<TSource, TTarget>(this Expression<Func<TSource, bool>> root)
    {
        var visitor = new ParameterTypeVisitor<TSource, TTarget>();
        return (Expression<Func<TTarget, bool>>)visitor.Visit(root);
    }

    private class ParameterTypeVisitor<TSource, TTarget> : ExpressionVisitor
    {
        private ReadOnlyCollection<ParameterExpression>? _parameters;

        protected override Expression VisitParameter(ParameterExpression node) =>
            _parameters?.FirstOrDefault(p => p.Name == node.Name) ?? (node.Type == typeof(TSource) ? Expression.Parameter(typeof(TTarget), node.Name) : node);

        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            _parameters = VisitAndConvert(node.Parameters, "VisitLambda");
            return Expression.Lambda(Visit(node.Body), _parameters);
        }
    }
}
.net asp.net-core entity-framework-core multi-tenant global-query-filter
1个回答
0
投票

您可以为所有此类实体添加另一个接口

IMultiTenantEntity

public interface IMultiTenantEntity
{
    ICollection<Tenant> Tenants { get; set; }
}

然后

modelBuilder.SetQueryFilterOnAllEntities<ITenantEntity>(x =>
    x.Tenants.Any(t =>
        t.Id == _tenantId
    )
);
© www.soinside.com 2019 - 2024. All rights reserved.