Skip to content

实体配置方式 ​

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 + 配置类
团队协作 → 必须使用配置类

下一步:

  1. 学习 关系映射
  2. 掌握 高级特性
  3. 理解 变更跟踪

基于 MIT 许可发布