Appearance
实体配置方式
Fluent API vs 数据注解,完整配置指南
📖 目录
三种配置方式对比
1. 约定(Convention)
EF Core 自动应用的默认规则:
csharp
public class Product
{
public int Id { get; set; } // ✅ 自动识别为主键
public string Name { get; set; } // ✅ 允许 NULL
public decimal Price { get; set; } // ✅ decimal(18,2)
public int CategoryId { get; set; } // ✅ 自动识别为外键
public Category Category { get; set; }
}优点:
- ✅ 零配置
- ✅ 简洁
缺点:
- ❌ 灵活性差
- ❌ 无法满足复杂需求
2. 数据注解(Data Annotations)
使用特性(Attributes)配置:
csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public Category Category { get; set; }
}优点:
- ✅ 直观,靠近属性定义
- ✅ 简单易用
缺点:
- ❌ 功能不完整(部分配置不支持)
- ❌ 污染实体类
- ❌ 难以测试
3. Fluent API(推荐)
在 OnModelCreating 中配置:
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100);
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
entity.HasOne(e => e.Category)
.WithMany(c => c.Products)
.HasForeignKey(e => e.CategoryId);
});
}优点:
- ✅ 功能完整(100% 覆盖)
- ✅ 代码分离,不污染实体
- ✅ 易于测试和维护
- ✅ 支持复杂配置
缺点:
- ⚠️ 配置分散(可通过配置类解决)
对比总结
| 特性 | 约定 | 数据注解 | Fluent API |
|---|---|---|---|
| 功能完整性 | 30% | 70% | 100% |
| 代码分离 | ✅ | ❌ | ✅ |
| 可读性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 灵活性 | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 可测试性 | ✅ | ❌ | ✅ |
| 推荐度 | 简单场景 | 中等场景 | 所有场景 |
建议: 优先使用 Fluent API,简单配置可用数据注解。
Fluent API 完整指南
1. 主键配置
csharp
modelBuilder.Entity<Product>(entity =>
{
// 单列主键
entity.HasKey(e => e.Id);
// 复合主键
entity.HasKey(e => new { e.OrderId, e.ProductId });
// 自定义主键名称
entity.HasKey(e => e.Id)
.HasName("PK_Products");
});2. 属性配置
字符串属性
csharp
entity.Property(e => e.Name)
.IsRequired() // NOT NULL
.HasMaxLength(100); // nvarchar(100)
entity.Property(e => e.Description)
.HasMaxLength(500)
.IsUnicode(false); // varchar(500)
entity.Property(e => e.Code)
.IsFixedLength() // char/nchar
.HasMaxLength(10);数值属性
csharp
// 精度和小数位
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
// EF Core 8+ 简化写法
entity.Property(e => e.Weight)
.HasPrecision(10, 4);
// 默认值
entity.Property(e => e.Quantity)
.HasDefaultValue(0);
// 默认值 SQL
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");计算列
csharp
entity.Property(e => e.TotalPrice)
.HasComputedColumnSql("[Quantity] * [UnitPrice]");
// 持久化计算列(SQL Server)
entity.Property(e => e.TotalPrice)
.HasComputedColumnSql("[Quantity] * [UnitPrice]", stored: true);3. 表配置
csharp
entity.ToTable("Products", "catalog"); // 表名和架构
// 注释
entity.ToTable(tb => tb.HasComment("产品表"));
// 视图映射
entity.ToView("ProductViews");
// 查询类型(无键实体)
entity.HasNoKey();
entity.ToView("ProductStatistics");4. 索引配置
csharp
// 单列索引
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();
// 筛选索引
entity.HasIndex(e => e.IsActive)
.HasFilter("[IsActive] = 1");
// 自定义索引名称
entity.HasIndex(e => e.Name)
.HasDatabaseName("IX_Products_Name");5. 关系配置
一对多
csharp
entity.HasOne(e => e.Category)
.WithMany(c => c.Products)
.HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_Products_Categories_CategoryId");一对一
csharp
entity.HasOne(e => e.Address)
.WithOne(a => a.Customer)
.HasForeignKey<Address>(a => a.CustomerId);多对多
csharp
entity.HasMany(e => e.Tags)
.WithMany(t => t.Products)
.UsingEntity<Dictionary<string, object>>(
"ProductTag",
j => j.HasOne<Tag>().WithMany().HasForeignKey("TagId"),
j => j.HasOne<Product>().WithMany().HasForeignKey("ProductId")
);6. 全局查询过滤器
csharp
// 软删除
entity.HasQueryFilter(e => !e.IsDeleted);
// 多租户
entity.HasQueryFilter(e => e.TenantId == currentTenantId);
// 组合过滤
entity.HasQueryFilter(e => !e.IsDeleted && e.IsActive);数据注解完整指南
1. 主键
csharp
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ExternalId { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime CreatedAt { get; set; }2. 字符串配置
csharp
[Required]
[MaxLength(100)]
public string Name { get; set; }
[MinLength(5)]
[MaxLength(100)]
public string Code { get; set; }
[StringLength(100, MinimumLength = 5)]
public string Description { get; set; }3. 列配置
csharp
[Column("product_name")]
public string Name { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[Column(Order = 1)] // 复合主键顺序
public int OrderId { get; set; }4. 外键和导航
csharp
[ForeignKey("Category")]
public int CategoryId { get; set; }
public Category Category { get; set; }
[InverseProperty("Products")]
public Category Category { get; set; }5. 索引
csharp
[Index(nameof(Name))]
[Index(nameof(Sku), IsUnique = true)]
[Index(nameof(CategoryId), nameof(Price))]
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Sku { get; set; }
public int CategoryId { get; set; }
public decimal Price { get; set; }
}6. 不映射
csharp
[NotMapped]
public string TempData { get; set; }
[NotMapped]
public decimal CalculatedValue => Price * Quantity;配置类分离模式
1. 定义配置类
csharp
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
// 表配置
builder.ToTable("Products");
// 主键
builder.HasKey(e => e.Id);
// 属性
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100);
builder.Property(e => e.Price)
.HasColumnType("decimal(18,2)");
// 索引
builder.HasIndex(e => e.Name);
builder.HasIndex(e => e.Sku).IsUnique();
// 关系
builder.HasOne(e => e.Category)
.WithMany(c => c.Products)
.HasForeignKey(e => e.CategoryId);
// 全局过滤器
builder.HasQueryFilter(e => !e.IsDeleted);
}
}2. 应用配置类
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 自动应用所有配置类
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
base.OnModelCreating(modelBuilder);
}3. 多个配置类示例
csharp
// CategoryConfiguration.cs
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.ToTable("Categories");
builder.HasKey(e => e.Id);
builder.Property(e => e.Name).IsRequired().HasMaxLength(50);
builder.HasIndex(e => e.Name).IsUnique();
}
}
// OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(e => e.Id);
builder.Property(e => e.OrderDate).HasDefaultValueSql("GETUTCDATE()");
builder.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId);
}
}优点:
- ✅ 职责分离
- ✅ 易于维护
- ✅ 支持团队并行开发
- ✅ 便于单元测试
高级配置技巧
1. 值转换器
csharp
// 枚举转字符串
entity.Property(e => e.Status)
.HasConversion<string>();
// 自定义转换
entity.Property(e => e.PhoneNumber)
.HasConversion(
v => v.ToString(),
v => PhoneNumber.Parse(v));
// JSON 序列化
entity.Property(e => e.Settings)
.HasConversion(
v => JsonSerializer.Serialize(v, null),
v => JsonSerializer.Deserialize<Settings>(v, null));2. 影子属性
csharp
entity.Property<DateTime>("CreatedAt")
.HasDefaultValueSql("GETUTCDATE()");
entity.Property<string>("LastModifiedBy")
.HasMaxLength(50);
// 使用
var product = new Product();
context.Entry(product).Property("CreatedAt").CurrentValue = DateTime.UtcNow;3. 备用键(Alternate Key)
csharp
entity.HasAlternateKey(e => e.Sku);
// 作为外键引用
builder.HasOne(e => e.Product)
.WithMany()
.HasForeignKey(e => e.ProductSku)
.HasPrincipalKey(p => p.Sku);4. 继承映射配置
TPH(默认)
csharp
// 无需特殊配置,自动使用 Discriminator 列TPT(EF Core 7+)
csharp
[Table("Animals")]
public class Animal { ... }
[Table("Dogs")]
public class Dog : Animal { ... }
// Fluent API
modelBuilder.Entity<Dog>().ToTable("Dogs");
modelBuilder.Entity<Cat>().ToTable("Cats");TPC(EF Core 8+)
csharp
modelBuilder.Entity<Animal>()
.UseTpcMappingStrategy();5. 复杂类型配置
csharp
entity.OwnsOne(e => e.Address, address =>
{
address.Property(a => a.Street).HasMaxLength(100);
address.Property(a => a.City).HasMaxLength(50);
address.Property(a => a.ZipCode).HasMaxLength(10);
});
// 集合 owned
entity.OwnsMany(e => e.PhoneNumbers, phone =>
{
phone.Property(p => p.Number).HasMaxLength(20);
phone.Property(p => p.Type).HasMaxLength(10);
});最佳实践
1. 优先使用 Fluent API
csharp
// ✅ 推荐
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.Name).HasMaxLength(100);
});
// ❌ 避免
public class Product
{
[MaxLength(100)]
public string Name { get; set; }
}2. 使用配置类分离
csharp
// ✅ 推荐: 配置类
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
// 配置...
}
}
// ❌ 避免: 所有配置都在 OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 数百行配置...
}3. 提取通用配置
csharp
public static class ModelBuilderExtensions
{
public static void ConfigureAuditable<T>(this EntityTypeBuilder<T> builder)
where T : class, IAuditable
{
builder.Property(e => e.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
builder.Property(e => e.ModifiedAt);
builder.Property(e => e.CreatedBy)
.HasMaxLength(50);
builder.Property(e => e.ModifiedBy)
.HasMaxLength(50);
}
}
// 使用
builder.ConfigureAuditable();4. 环境区分配置
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
// 开发环境: 种子数据
if (Environment.IsDevelopment())
{
modelBuilder.Entity<Product>().HasData(
new Product { Id = 1, Name = "Sample" }
);
}
}5. 验证配置
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
// 验证模型
var model = modelBuilder.FinalizeModel();
// 检查未配置的实体
foreach (var entityType in model.GetEntityTypes())
{
if (!entityType.ClrType.IsClass)
continue;
// 自定义验证逻辑
}
}📚 延伸阅读
💡 小结
核心要点:
- ✅ 优先使用 Fluent API
- ✅ 使用配置类分离(IEntityTypeConfiguration)
- ✅ ApplyConfigurationsFromAssembly 自动应用
- ✅ 数据注解用于简单配置
- ✅ 提取通用配置扩展方法
- ✅ 验证模型配置完整性
配置选择:
简单项目 → 数据注解
中型项目 → Fluent API
大型项目 → Fluent API + 配置类
团队协作 → 必须使用配置类下一步: