EF Core全局查询过滤器适用于SaveChanges(更新,删除)

3

简而言之;

当您调用SaveChanges时,是否有办法在EF Core中将查询过滤器应用于生成的UPDATEDELETE语句的WHERE子句?(与仅适用于SELECT场景的全局查询过滤器非常相似。)


全局查询过滤器对于SELECT场景非常有效,通过向生成的SELECT语句的WHERE子句添加附加条件来实现,但是当您调用DbContext的SaveChanges方法时,它们不会对UPDATEDELETE语句执行相同的操作,我想知道在调用SaveChanges时是否可能使用EF Core将查询过滤器应用于生成的UPDATEDELETE语句的WHERE子句? 就像HasQueryFilter对于SELECT语句一样。

我将在小型应用程序的多租户场景中使用此功能,并且我能够在其他层中处理此要求,或通过覆盖SaveChanges并检查已删除或修改的实体是否属于正确的租户来处理此要求; 但是,由于性能和安全考虑,我更喜欢查询过滤器方法。

更多上下文

查询过滤器的常见应用场景之一是多租户。一个租户的数据应该受到其他租户读取和修改的保护:

  • 被其他租户读取:使用HasQueryFilter注册全局查询过滤器可以很好地解决。
  • 被其他租户修改:如何在EF DbContext中实现?

您可以查看Microsoft Docs上的查询过滤器或者他们在GitHub上的示例代码

为什么我要寻找UPDATE和DELETE的查询过滤器?

主要原因如下:

  • 性能考虑
  • 安全考虑

毕竟,我们有SELECT语句的查询过滤器,为什么不为UPDATE和DELETE提供呢?

性能考虑

我能够处理这个需求在其他层,比如API、DAL、BLL或是通过重写SaveChanges并检查被删除或修改的实体是否属于正确租户来完成;我不想在BLL或SaveChanges中做这个是因为性能问题。一个WHERE子句将在数据库服务器端进行评估,这是安全和高性能的,相当类似于全局查询过滤器。
例如,在删除场景下,一个明显的答案可能是像这样的:
var entity = db.BlogPosts.Find(id);
if(entity!=null)
    db.Entry(entity).State = EntityState.Deleted;
db.SaveChanges();

基本上是执行一个 SELECT 然后再执行一个 DELETE 语句。

但是我更倾向于像这样直接执行一个 DELETE 语句:

db.Entry(new BlogPost() { Id = id }).State = EntityState.Deleted;
db.SaveChanges();

安全考虑

首先,这些实体在 DbContext.Entries 中不一定具有TenantId。另一方面,即使它们为其TenantId指定了一个值,该值也是不可靠的,因为它可能已更改。例如 BlogPost.Id = 2 可能属于租户 2,而租户 1 正试图对其进行修改或删除。


1
我认为EntityFrameworkCore.Triggered可以满足你的需求。还有许多其他的EF Core工具和扩展。根据描述,其中一些也可能很有用。 - Alexander Petrov
1
使用每个租户的架构。更加安全。所有 EF 命令都将进入一个架构,永远不会跨越租户边界。更不用说它还能大大简化数据模型了。 - Gert Arnold
@GertArnold 我同意。但我仍然想知道是否有类似于 HasQueryFilter 的方法可用(或者可以轻松地创建)用于 DbContext,在 UPDATE 和 DELETE 语句中添加条件来应用过滤器到 SaveChanges - Reza Aghaei
我重写了savechanges函数,以便在允许保存之前重新验证所有添加/修改的对象。那么...如何使用验证属性来确认此用户的租户是否有效? - Jeremy Lakeman
属性也可以做到这一点,只要该属性是某种远程属性(使用数据库进行验证)。但就性能而言,“WHERE”子句要好得多。与“SELECT”的“HasQueryFilter”工作方式相同。我已经分享了一个快速而简单的示例,演示如何使用命令拦截器来实现它,但我也乐于接受更好的建议。 - Reza Aghaei
显示剩余6条评论
2个回答

3
更新:目前没有内置支持,但我已经在EF Core存储库中预定了一个问题/特性建议。它可能会作为批量操作方案的一部分或作为SaveChanges的改进而在未来实现。您可以在那里分享有关该功能的想法。
我找不到任何像全局查询过滤器那样适用于更新和删除语句的内置功能。
作为一种选择,您可以考虑命令拦截器来应用更新和删除语句的全局查询过滤器。它们允许您在执行命令之前修改命令,例如向命令文本添加查询过滤器。
例如,以下代码显示了如何修改UPDATE或DETELE语句并添加类似WHERE TenantId = @__TenantId__ AND 的子句。这样,您可以确保正在更新或删除的数据属于租户,租户不能修改或删除另一个租户的数据。
为此,创建一个DbCommandInterceptor来拦截更新和删除命令:
public class BlogPostsCommandInterceptor : DbCommandInterceptor
{
    ITenantProvider tenantProvider;
    public BlogPostsCommandInterceptor(ITenantProvider tenantProvider)
    {
        this.tenantProvider = tenantProvider;
    }
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, 
        InterceptionResult<DbDataReader> result) {
        ModifyCommand(command);
        return base.ReaderExecuting(command, eventData, result);
    }
    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command, CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default) {
        ModifyCommand(command);
        return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
    }
    private void ModifyCommand(DbCommand command) {
        if (command.CommandText.StartsWith("UPDATE \"BlogPosts\"") ||
            command.CommandText.StartsWith("DELETE FROM \"BlogPosts\""))
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = "@__TenantId__";
            parameter.Value = tenantProvider.GetTenantId();
            command.Parameters.Add(parameter);
            command.CommandText = command.CommandText.Replace("WHERE",
                $"WHERE (\"TenantId\" = {parameter.ParameterName}) AND ");
        }
    }
}

注册拦截器:

protected override void OnConfiguring(
    DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.AddInterceptors(
        new BlogPostsCommandInterceptor(tenantProvider));
}

这不完全是我要找的内容,但无论如何,它可能会激励其他人;如果有更好的选项来回答原始问题,我会很高兴接收。

0
另一个租户的修改:如何在EF DbContext中实现?
您可以重写SaveChanges()并验证所有实体是否属于正确的租户。

谢谢David,我相信这是一个显而易见的选择,然而就像我在问题中解释的那样,我正在寻找像HasQueryFilter一样的东西来向Update和Delete查询的where子句添加条件 - Reza Aghaei
您可以将TenantID作为ConcurrencyToken添加,这将使其添加到UPDATE和DELETE的WHERE子句中,并阻止您更改TenantID。但对于INSERT没有帮助。https://learn.microsoft.com/en-us/ef/core/modeling/concurrency?tabs=data-annotations - David Browne - Microsoft
我对插入没有问题;它被处理得像这样 - Reza Aghaei
我会看一下ConcurrencyToken。在这里,更改TenantId不是一个问题,我将从更新中排除它。但修改属于另一个租户的博客文章是一个问题。想象一下,一个BlogPost(具有属于另一个租户的id)已经附加到DbContext并将保存在DB上。将为此生成更新命令,如UPDATE BlogPost SET Content=@Content WHERE Id = @Id;我想在BlogPost表上的所有更新语句中添加AND Tenant=tenant1。这基本上就是HasQueryFilter对于SELECT语句所做的事情。 - Reza Aghaei
如果你让TenantID成为每个主键的前导列,那么这个问题也会被解决。 - David Browne - Microsoft
我正在考虑使用拦截器 - Reza Aghaei

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接