全局查询筛选器与拥有实体
使用全局查询筛选器实现租户隔离和软删除,并使用拥有实体类型映射值对象。
全局查询筛选器与拥有实体 是 CoddyKit 上的免费 C# Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
什么是全局查询筛选器
全局查询筛选器是自动应用于某种实体类型的每个 LINQ 查询的 WHERE 谓词。它们非常适合实现软删除、多租户和行级安全,无需在各处重复编写条件。
使用全局筛选器实现软删除
添加 IsDeleted 标志并配置全局筛选器,这样除非显式忽略筛选器,否则不会返回已删除的记录。
// Entity with soft-delete flag
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool IsDeleted { get; set; }
}
// Configure in OnModelCreating
modelBuilder.Entity<Post>()
.HasQueryFilter(p => !p.IsDeleted);
// All queries now implicitly add WHERE IsDeleted = 0:
var posts = await _db.Posts.ToListAsync();使用全局筛选器实现多租户
使用从具有作用域的服务中读取当前租户的筛选器。每个查询都会自动限定为当前租户的数据。
public class AppDbContext : DbContext
{
private readonly ICurrentUserService _user;
public AppDbContext(DbContextOptions<AppDbContext> opt, ICurrentUserService user)
: base(opt) => _user = user;
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Order>().HasQueryFilter(
o => o.TenantId == _user.TenantId);
}
}
// Every Order query automatically filters by TenantId忽略全局筛选器
有时您需要绕过筛选器,例如执行查询所有租户的管理员操作,或恢复软删除的记录。请仅对该查询使用 IgnoreQueryFilters()。
// Admin: see ALL posts including deleted
var allPosts = await _db.Posts
.IgnoreQueryFilters()
.ToListAsync();
// Restore a specific deleted post
var deleted = await _db.Posts
.IgnoreQueryFilters()
.FirstOrDefaultAsync(p => p.Id == 42);
if (deleted is not null)
{
deleted.IsDeleted = false;
await _db.SaveChangesAsync();
}导航属性上的筛选器
全局筛选器也会参与 JOIN。如果 Post 设置了筛选器,那么通过 Include 加载的相关 Post 也会自动进行筛选。
// Blog with soft-delete filter on Posts
var blog = await _db.Blogs
.Include(b => b.Posts) // only non-deleted posts are included
.FirstOrDefaultAsync(b => b.Id == 1);
// blog.Posts contains only active posts — the filter applied in the JOIN拥有实体类型:概念
拥有实体类型是专属于单个所属实体的类。它们没有主键,默认存储在同一张表中。它们非常适合在 DDD 中表示值对象。
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Address BillingAddress { get; set; } = new();
public Address ShippingAddress { get; set; } = new();
}
public class Address
{
public string Street { get; set; } = "";
public string City { get; set; } = "";
public string ZipCode { get; set; } = "";
public string Country { get; set; } = "";
}配置拥有实体
使用 OwnsOne 配置单个拥有实体。默认情况下,EF Core 会使用导航属性名称作为列名前缀。
modelBuilder.Entity<Customer>(entity =>
{
entity.OwnsOne(c => c.BillingAddress, addr =>
{
addr.Property(a => a.Street).HasMaxLength(300);
addr.Property(a => a.Country).HasMaxLength(2);
});
entity.OwnsOne(c => c.ShippingAddress);
});
// Table: Customers with columns:
// BillingAddress_Street, BillingAddress_City, ...
// ShippingAddress_Street, ShippingAddress_City, ...将拥有实体存储在单独的表中
在 OwnsOne 中使用 ToTable(),将拥有实体存储在自己的表中;当数据量较大或为可选数据时,这种方式很有用。
modelBuilder.Entity<Customer>().OwnsOne(
c => c.BillingAddress,
addr => addr.ToTable("CustomerBillingAddresses"));
// Now BillingAddress columns are in a separate table
// with a FK back to CustomerId使用 OwnsMany 配置值对象集合
OwnsMany 用于配置拥有实体集合,例如存储在子表中的电子邮件地址列表或电话号码列表。
public class Customer
{
public int Id { get; set; }
public IList<PhoneNumber> PhoneNumbers { get; set; } = new List<PhoneNumber>();
}
public class PhoneNumber
{
public string Number { get; set; } = "";
public string Type { get; set; } = "";
}
modelBuilder.Entity<Customer>()
.OwnsMany(c => c.PhoneNumbers,
phone => phone.ToTable("CustomerPhones"));每层级一个表与拥有实体的比较
全局筛选器也适用于 TPH(每层级一个表)继承,其中鉴别器列用于区分子类型。您可以根据鉴别器应用筛选器,以执行特定类型的查询。
// Base entity
public abstract class Payment { public int Id { get; set; } }
public class CardPayment : Payment { public string CardLast4 { get; set; } = ""; }
public class BankPayment : Payment { public string IBAN { get; set; } = ""; }
// All Payment subtypes in one table with a Discriminator column
// No extra config needed — EF Core handles TPH by default
var cards = await _db.Set<CardPayment>().ToListAsync();
// SQL: SELECT * FROM Payments WHERE Discriminator = 'CardPayment'实战:SaaS 租户隔离
完整的 SaaS 多租户方案会将全局查询筛选器与具有作用域的租户上下文结合起来,确保每个查询都能自动限定范围,无需为每个查询编写代码。
// Scoped tenant service
public class TenantContext
{
public Guid TenantId { get; set; }
}
// DbContext uses it
modelBuilder.Entity<Invoice>().HasQueryFilter(
inv => inv.TenantId == _tenantCtx.TenantId);
modelBuilder.Entity<Customer>().HasQueryFilter(
c => c.TenantId == _tenantCtx.TenantId);
// Queries are automatically scoped:
var invoices = await _db.Invoices.ToListAsync();
// WHERE TenantId = 'current-tenant-guid'快速检查
要暂时绕过特定查询的全局查询筛选器,应调用哪个方法?
回顾:全局筛选器与拥有实体
要点:
- 全局查询筛选器会自动为实体的每个查询添加 WHERE 谓词
- 非常适合软删除、多租户和行级安全
- 对于管理员操作或恢复操作,可使用
IgnoreQueryFilters()绕过筛选器 - 拥有实体用于表示值对象——没有 PK,存储在所属实体的表中
OwnsOne/OwnsMany用于配置拥有关系;使用ToTable()将其拆分到单独的表中
常见问题解答
「全局查询筛选器与拥有实体」课时是免费的吗?
是的 — 「全局查询筛选器与拥有实体」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「全局查询筛选器与拥有实体」这节课中我会学到什么?
使用全局查询筛选器实现租户隔离和软删除,并使用拥有实体类型映射值对象。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「全局查询筛选器与拥有实体」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 预先加载、延迟加载与显式加载
- 原始 SQL、存储过程与插值
- 编译查询与性能
- 全局查询筛选器与拥有实体