一、什么是全局过滤器
想象一下,你正在开发一个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);
四、技术对比与选型建议
与手动过滤相比,全局过滤器有三大优势:
- 代码简洁:避免重复的
Where条件 - 防遗漏:自动保护所有查询
- 易维护:修改过滤逻辑只需调整一处
但也要注意局限性:
- 不适合需要动态切换条件的复杂场景
- 对原生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条件而泄露数据吧?
下次当你发现自己在重复编写相同的查询条件时,不妨试试这个特性,让你的代码既干净又安全!
评论