Appearance
定义实体类与 DbSet
学习如何设计 EF Core 实体类和配置 DbSet 属性
📖 核心概念
什么是实体(Entity)?
实体是映射到数据库表的 C# 类。每个实体实例对应表中的一行记录。
C# 类 (Entity) 数据库表 (Table)
───────────── ─────────────────
Product 类 ←→ Products 表
product 对象 ←→ 一行记录
Name 属性 ←→ Name 列什么是 DbSet<T>?
DbSet<T> 表示数据库中某个实体类型的集合,提供查询和操作数据的方法。
csharp
public class AppDbContext : DbContext
{
// DbSet 对应数据库中的表
public DbSet<Product> Products => Set<Product>();
// 使用方式:
// context.Products.ToListAsync() → SELECT * FROM Products
}🏗️ 实体类设计
1. 基础实体类
csharp
public class Product
{
// 主键(约定:Id 或 <ClassName>Id)
public int Id { get; set; }
// 标量属性
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}生成的数据库表:
sql
CREATE TABLE Products (
Id INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(MAX),
Price DECIMAL(18,2),
Description NVARCHAR(MAX),
IsActive BIT NOT NULL DEFAULT 1,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);2. 主键约定
EF Core 自动识别以下命名为主键:
csharp
// 方式 1:Id
public class Product
{
public int Id { get; set; } // ✅ 自动识别为主键
}
// 方式 2:<ClassName>Id
public class Product
{
public int ProductId { get; set; } // ✅ 自动识别为主键
}
// 方式 3:显式配置
public class Product
{
public int Key { get; set; } // ❌ 需要配置
}
// DbContext 中配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasKey(e => e.Key);
}复合主键:
csharp
public class OrderItem
{
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
}
// 必须显式配置复合主键
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderItem>()
.HasKey(e => new { e.OrderId, e.ProductId });
}3. 外键与导航属性
一对多关系
csharp
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性(集合)
public ICollection<Product> Products { get; set; } = new List<Product>();
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// 外键(约定:导航属性名 + Id)
public int CategoryId { get; set; }
// 导航属性(引用)
public Category Category { get; set; }
}生成的 SQL:
sql
CREATE TABLE Categories (
Id INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(MAX)
);
CREATE TABLE Products (
Id INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(MAX),
Price DECIMAL(18,2),
CategoryId INT NOT NULL,
FOREIGN KEY (CategoryId) REFERENCES Categories(Id)
);多对多关系(EF Core 5.0+)
csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性(集合)
public ICollection<Tag> Tags { get; set; } = new List<Tag>();
}
public class Tag
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性(集合)
public ICollection<Product> Products { get; set; } = new List<Product>();
}自动创建连接表:
sql
CREATE TABLE ProductTag (
ProductsId INT NOT NULL,
TagsId INT NOT NULL,
PRIMARY KEY (ProductsId, TagsId),
FOREIGN KEY (ProductsId) REFERENCES Products(Id),
FOREIGN KEY (TagsId) REFERENCES Tags(Id)
);一对一关系
csharp
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性
public Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public string Street { get; set; }
public string City { get; set; }
// 外键
public int CustomerId { get; set; }
// 导航属性
public Customer Customer { get; set; }
}🔧 属性配置
1. 字符串属性
csharp
public class Product
{
// 默认:nvarchar(max)
public string Description { get; set; }
// 限制长度:nvarchar(100)
[MaxLength(100)]
public string Name { get; set; }
// 固定长度:nchar(10)
[MaxLength(10)]
[MinLength(10)]
public string Code { get; set; }
// ASCII:varchar(50)
[MaxLength(50)]
public string Sku { get; set; }
}
// Fluent API 配置
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.Name)
.IsRequired() // NOT NULL
.HasMaxLength(100);
entity.Property(e => e.Sku)
.IsUnicode(false); // varchar 而非 nvarchar
});2. 数值属性
csharp
public class Product
{
// 默认:decimal(18,2)
public decimal Price { get; set; }
// 自定义精度
[Column(TypeName = "decimal(10,4)")]
public decimal Weight { get; set; }
// int, float, double 等
public int Quantity { get; set; }
public double Rating { get; set; }
}
// Fluent API
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
entity.Property(e => e.Weight)
.HasPrecision(10, 4); // EF Core 8+
});3. 日期时间属性
csharp
public class Order
{
// 默认值
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// 可为空
public DateTime? ShippedAt { get; set; }
// DateTimeOffset(推荐用于多时区应用)
public DateTimeOffset OrderDate { get; set; }
}
// Fluent API
modelBuilder.Entity<Order>(entity =>
{
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
});4. 布尔属性
csharp
public class Product
{
public bool IsActive { get; set; } = true;
public bool IsDeleted { get; set; }
}
// 生成的 SQL: BIT 类型 (SQL Server)5. 枚举属性
csharp
public enum ProductStatus
{
Draft = 0,
Published = 1,
Archived = 2
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// 存储为整数(默认)
public ProductStatus Status { get; set; }
}
// 或存储为字符串
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.Status)
.HasConversion<string>();
});数据库值:
- 整数:
0,1,2 - 字符串:
"Draft","Published","Archived"
🎯 高级特性
1. 影子属性(Shadow Properties)
不在实体类中定义,但在数据库中存在的属性。
csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// 没有 CreatedAt 属性
}
// DbContext 中配置影子属性
modelBuilder.Entity<Product>(entity =>
{
entity.Property<DateTime>("CreatedAt")
.HasDefaultValueSql("GETUTCDATE()");
});
// 使用
var product = new Product { Name = "Laptop" };
context.Entry(product).Property("CreatedAt").CurrentValue = DateTime.UtcNow;适用场景:
- 审计字段(CreatedBy、ModifiedAt)
- 租户 ID(多租户系统)
- 不想污染实体类的技术字段
2. 后备字段(Backing Fields)
csharp
public class Product
{
private string _name; // 后备字段
public int Id { get; set; }
// 属性
public string Name
{
get => _name;
set => _name = value?.Trim(); // 自定义逻辑
}
}
// Fluent API 配置
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.Name)
.UsePropertyAccessMode(PropertyAccessMode.Field);
});3. 计算列
csharp
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
// 计算列(数据库自动计算)
public decimal TotalPrice { get; set; }
}
modelBuilder.Entity<OrderItem>(entity =>
{
entity.Property(e => e.TotalPrice)
.HasComputedColumnSql("[Quantity] * [UnitPrice]");
});注意:计算列在数据库中自动生成,不能直接赋值。
4. 索引配置
csharp
modelBuilder.Entity<Product>(entity =>
{
// 单列索引
entity.HasIndex(e => e.Name);
// 唯一索引
entity.HasIndex(e => e.Sku)
.IsUnique();
// 复合索引
entity.HasIndex(e => new { e.CategoryId, e.Price });
// 包含列的索引(EF Core 8+)
entity.HasIndex(e => e.CategoryId)
.IncludeProperties(e => e.Name, e.Price);
// 降序索引
entity.HasIndex(e => e.CreatedAt)
.IsDescending();
});📋 DbSet 配置
1. 基础用法
csharp
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<Order> Orders => Set<Order>();
}2. 配置表名
csharp
// 方式 1:数据注解
[Table("MyProducts")]
public class Product { ... }
// 方式 2:Fluent API
modelBuilder.Entity<Product>(entity =>
{
entity.ToTable("MyProducts");
});
// 方式 3:复数化(需要 NuGet 包)
// Install-Package Humanizer
modelBuilder.UsePluralizer();
// Product → Products
// Category → Categories3. 配置架构(Schema)
csharp
modelBuilder.Entity<Product>(entity =>
{
entity.ToTable("Products", "catalog");
// 生成: catalog.Products
});4. 排除实体
csharp
// 方式 1:不添加 DbSet
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
// 没有 AuditLog 的 DbSet
}
// 方式 2:显式忽略
modelBuilder.Ignore<AuditLog>();
// 方式 3:条件排除
public class Product
{
public int Id { get; set; }
[NotMapped]
public string TempData { get; set; } // 不映射到数据库
}🏛️ 继承映射
TPH(Table-per-Hierarchy)- 默认策略
csharp
public abstract class Animal
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Dog : Animal
{
public string Breed { get; set; }
}
public class Cat : Animal
{
public bool IsIndoor { get; set; }
}
// 所有类型存储在一张表中
// 自动添加 Discriminator 列区分类型生成的表结构:
sql
CREATE TABLE Animals (
Id INT PRIMARY KEY,
Name NVARCHAR(MAX),
Breed NVARCHAR(MAX), -- Dog 专用
IsIndoor BIT, -- Cat 专用
Discriminator NVARCHAR(MAX) -- 'Dog' 或 'Cat'
);TPT(Table-per-Type)- EF Core 7+
csharp
[Table("Animals")]
public class Animal { ... }
[Table("Dogs")]
public class Dog : Animal { ... }
[Table("Cats")]
public class Cat : Animal { ... }
// 每个类型一张表,通过 JOIN 查询💡 最佳实践
1. 使用基类提取公共字段
csharp
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ModifiedAt { get; set; }
}
public class Product : BaseEntity
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class Category : BaseEntity
{
public string Name { get; set; }
}2. 初始化集合属性
csharp
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
// ✅ 初始化为空集合,避免 NullReferenceException
public ICollection<Product> Products { get; set; } = new List<Product>();
}3. 使用值对象封装复杂类型
csharp
// 值对象
public class Money
{
public decimal Amount { get; set; }
public string Currency { get; set; } = "USD";
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// 复杂类型
public Money Price { get; set; }
}
// 配置
modelBuilder.Entity<Product>(entity =>
{
entity.OwnsOne(p => p.Price, price =>
{
price.Property(p => p.Amount)
.HasColumnName("Price_Amount");
price.Property(p => p.Currency)
.HasColumnName("Price_Currency");
});
});4. 遵循单一职责原则
csharp
// ❌ 错误:实体类包含业务逻辑
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public void ApplyDiscount(decimal discount)
{
Price -= discount; // 业务逻辑不应在实体中
}
}
// ✅ 正确:实体只包含数据
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
// 业务逻辑放在服务层
public class ProductService
{
public void ApplyDiscount(Product product, decimal discount)
{
product.Price -= discount;
}
}⚠️ 常见陷阱
1. 忘记初始化集合
csharp
// ❌ 错误
public class Category
{
public ICollection<Product> Products { get; set; } // null
}
var category = new Category();
category.Products.Add(product); // NullReferenceException!
// ✅ 正确
public ICollection<Product> Products { get; set; } = new List<Product>();2. 循环引用导致序列化问题
csharp
public class Order
{
public Customer Customer { get; set; }
}
public class Customer
{
public ICollection<Order> Orders { get; set; }
}
// JSON 序列化时会循环引用
// 解决:使用 DTO 或配置序列化选项3. 过度使用虚拟属性(延迟加载)
csharp
// ❌ 不推荐:性能问题
public virtual Category Category { get; set; }
// ✅ 推荐:显式使用 Include
var product = await context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);📚 延伸阅读
💡 小结
关键点:
- ✅ 实体类映射到数据库表
- ✅
DbSet<T>提供数据访问接口 - ✅ 遵循命名约定简化配置
- ✅ 使用 Fluent API 进行高级配置
- ✅ 初始化集合属性避免空引用
下一步:
- 学习 基础 CRUD 操作
- 深入理解 关系映射
- 掌握 实体配置方式