一、什么是全局过滤器

想象一下,你正在开发一个SaaS系统,需要为不同租户隔离数据。如果每次查询都手动添加Where条件,不仅容易遗漏,还会让代码变得臃肿。这时候,全局过滤器(Global Query Filters)就像个贴心的管家,自动帮你搞定这些重复劳动。

在Entity Framework Core中,全局过滤器是定义在DbContext级别的查询条件,它会自动应用到所有相关查询中。比如多租户场景下,你可以强制每个查询都带上TenantId = currentTenantId的条件。

二、如何实现多租户过滤

我们以ASP.NET Core + EF Core的技术栈为例,先定义一个简单的租户模型:

// 租户实体
public class Tenant
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// 带有租户ID的基础实体
public interface ITenantEntity
{
    int TenantId { get; set; }
}

// 产品实体实现多租户接口
public class Product : ITenantEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int TenantId { get; set; } // 租户标识字段
}

接着在DbContext中配置过滤器:

public class AppDbContext : DbContext
{
    private readonly int _currentTenantId; // 从DI或Claims中获取

    public AppDbContext(DbContextOptions options, ITenantProvider tenantProvider) 
        : base(options)
    {
        _currentTenantId = tenantProvider.GetTenantId();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 关键代码:为所有实现ITenantEntity的实体添加过滤器
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
            {
                modelBuilder.Entity(entityType.ClrType)
                    .HasQueryFilter(e => EF.Property<int>(e, "TenantId") == _currentTenantId);
            }
        }
    }

    public DbSet<Product> Products { get; set; }
    public DbSet<Tenant> Tenants { get; set; }
}

三、高级用法与注意事项

1. 动态禁用过滤器

有时候你需要临时绕过过滤器,比如管理员查看全量数据:

var products = context.Products
    .IgnoreQueryFilters() // 禁用所有过滤器
    .ToList();

2. 多条件组合过滤

过滤器支持复杂逻辑,比如同时按租户和软删除状态过滤:

modelBuilder.Entity<Product>()
    .HasQueryFilter(p => 
        p.TenantId == _currentTenantId && 
        !p.IsDeleted);

3. 性能陷阱

注意:过滤条件中使用变量(如_currentTenantId)会导致查询计划无法重用。对于高频查询,建议改用参数化:

var tenantIdParam = Expression.Parameter(typeof(int));
var filter = Expression.Lambda<Func<Product, bool>>(
    Expression.Equal(
        Expression.Property(entityParameter, "TenantId"),
        tenantIdParam
    ), entityParameter);

四、技术对比与选型建议

与手动过滤相比,全局过滤器有三大优势:

  1. 代码简洁:避免重复的Where条件
  2. 防遗漏:自动保护所有查询
  3. 易维护:修改过滤逻辑只需调整一处

但也要注意局限性:

  • 不适合需要动态切换条件的复杂场景
  • 对原生SQL查询无效
  • 可能影响迁移和种子数据

五、完整示例:ASP.NET Core集成

最后来个实战示例,展示如何在控制器中使用:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _context;

    public ProductsController(AppDbContext context)
    {
        _context = context;
    }

    [HttpGet]
    public IActionResult GetAll()
    {
        // 自动过滤非本租户的数据
        var products = _context.Products.ToList();
        return Ok(products);
    }

    [HttpGet("admin")]
    public IActionResult GetAllForAdmin()
    {
        // 管理员查看全量数据
        var products = _context.Products
            .IgnoreQueryFilters()
            .ToList();
        return Ok(products);
    }
}

配套的租户解析中间件:

public class TenantMiddleware
{
    private readonly RequestDelegate _next;

    public TenantMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, ITenantProvider tenantProvider)
    {
        // 从JWT或子域名解析租户ID
        var tenantId = context.User.FindFirst("tenantId")?.Value;
        tenantProvider.SetTenantId(int.Parse(tenantId));
        
        await _next(context);
    }
}

六、总结

全局过滤器就像给数据库查询装了个智能水龙头,能精准控制数据的流出。虽然实现简单,但需要注意性能影响和边界情况。对于多租户系统来说,这绝对是提升代码安全性的利器——毕竟谁也不想因为忘记加Where条件而泄露数据吧?

下次当你发现自己在重复编写相同的查询条件时,不妨试试这个特性,让你的代码既干净又安全!