Skip to content

定义实体类与 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 → Categories

3. 配置架构(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 进行高级配置
  • ✅ 初始化集合属性避免空引用

下一步:

  1. 学习 基础 CRUD 操作
  2. 深入理解 关系映射
  3. 掌握 实体配置方式

基于 MIT 许可发布