.NET 业务系统自定义扩展字段方案(EF Core / 通用)

核心需求:业务表不改动结构,前端可以动态新增字段、录入、查询、导出,常见 3 种方案,按落地复杂度、性能、查询需求选型:

适用:客户要自定义属性、商品扩展属性、档案自定义字段、表单自定义字段等场景

方案对比

表格

方案实现方式优点缺点适用场景
EAV 实体 – 属性 – 值主表 + 扩展字段定义表 + 扩展值表标准范式,结构干净,支持字段类型、校验、枚举;易权限控制联表多,复杂查询慢;聚合统计麻烦企业 CRM、档案、客户、订单,字段数量不多,查询不算极端复杂
JSON 字段(推荐大多数 .NET 项目)主表加一个 ExtensionData JSONB/JSON 字段,存键值对象开发最快,单表查询,EF Core 原生支持;查询简单无法数据库层面加索引(部分数据库支持 JSON 索引);字段元数据要单独存;约束弱中小系统、自定义字段数量不稳定、快速迭代,最常用
动态建表(不推荐)运行时 ALTER TABLE 新增列查询性能最好,原生字段DDL 锁表、迁移麻烦、版本控制灾难、多租户冲突极少用,不建议业务系统首选

工业级项目优先:JSON 字段 + 独立元数据表(兼顾开发效率和字段管理:类型、标签、是否必填、下拉选项、排序)


方案一:JSON 扩展字段(EF Core 最简落地)

1. 两张核心表

  1. 元数据表:CustomField(定义字段) 用来管理这个业务类型有哪些扩展字段:字段名、显示名、类型(文本 / 数字 / 日期 / 下拉)、是否必填、下拉选项、归属业务类型(比如 Customer、Order)
public class CustomField
{
    public long Id { get; set; }
    /// <summary>归属业务对象:Customer / Order / Product</summary>
    public string BizType { get; set; }
    /// <summary>前端key,英文唯一标识</summary>
    public string FieldKey { get; set; }
    /// <summary>显示名称</summary>
    public string Label { get; set; }
    /// <summary>类型:Text/Number/Date/Select/Bool</summary>
    public string FieldType { get; set; }
    public bool Required { get; set; }
    /// <summary>下拉选项JSON</summary>
    public string? Options { get; set; }
    public int Sort { get; set; }
}
  1. 业务主表,增加 JSON 扩展字段
public class Customer
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Phone { get; set; }

    // 扩展字段,EF Core 映射为JSON
    public Dictionary<string, object> ExtensionData { get; set; } = new();
}

2. EF Core 配置(不同数据库)

PostgreSQL(JSONB,支持索引,首选)

modelBuilder.Entity<Customer>()
    .Property(p => p.ExtensionData)
    .HasColumnType("jsonb");

PG 可以给 jsonb 建索引,支持 @>、? 等查询。

MySQL

modelBuilder.Entity<Customer>()
    .Property(p => p.ExtensionData)
    .HasColumnType("json");

SQL Server

modelBuilder.Entity<Customer>()
    .Property(p => p.ExtensionData)
    .HasColumnType("nvarchar(max)");

SQL Server 2016+ 支持 JSON_VALUE 函数查询。

3. 写入示例

var customer = new Customer
{
    Name = "张三",
    ExtensionData = new Dictionary<string, object>
    {
        ["factory"] = "昆明加工厂",
        ["contractNo"] = "HT20261001",
        ["deliveryDate"] = DateTime.Now
    }
};
db.Customers.Add(customer);
await db.SaveChangesAsync();

4. 查询扩展字段(重点)

方式 A:内存过滤(简单场景,数据量大不推荐)

var list = db.Customers.AsEnumerable()
    .Where(x => x.ExtensionData.TryGetValue("factory", out var v) && v.ToString() == "昆明加工厂")
    .ToList();

方式 B:数据库原生 JSON 函数(推荐,可走索引)

SQL Server:

SELECT * FROM Customer WHERE JSON_VALUE(ExtensionData, '$.factory') = '昆明加工厂'

PG:

SELECT * FROM Customer WHERE ExtensionData @> '{"factory":"昆明加工厂"}'

EF Core 可以封装扩展方法,拼接 JSON 查询表达式。

5. 前端联动逻辑

  1. 前端先根据 BizType 查询 CustomField 拿到字段定义,动态渲染表单
  2. 提交时把动态表单的值打包进 ExtensionData
  3. 回显:主表数据 + 字段定义,循环渲染

6. 缺点 & 坑

  • 数据库无法做外键、原生唯一约束;校验逻辑放 C# 代码层
  • 排序、复杂统计如果基于扩展字段,性能差
  • 字段删除后,历史数据 JSON 里脏 key 需要清理

方案二:EAV 实体属性值表(规范方案,适合强自定义、需要统计筛选)

三张表:

  1. CustomField 字段定义(同上)
  2. CustomFieldValue 字段值表
public class CustomFieldValue
{
    public long Id { get; set; }
    public long BizId { get; set; }       // 主业务Id,CustomerId
    public string BizType { get; set; }   // 业务类型
    public long FieldId { get; set; }     // 关联CustomField
    // 统一字符串存储,读取时按字段类型转换
    public string Value { get; set; }
}

✅ 优点:

  • 可以给 FieldId、BizId 建索引,筛选条件稳定
  • 可以做字段级权限、字段审计、单独字段历史变更日志
  • 报表、筛选条件可以统一封装查询组件

❌ 缺点:

  • 查一条主数据需要左联 N 行扩展值,数据量大时性能下滑
  • 分页、排序、多条件组合查询写起来繁琐

优化:查列表时,先查主表,再批量查扩展值,内存组装(减少多次循环查询)

EAV 查询示例思路:

// 1. 分页查客户主表
var customers = db.Customers.Skip(0).Take(20).ToList();
var ids = customers.Select(x=>x.Id).ToList();
// 2. 批量查该批客户的所有扩展字段值
var fieldValues = db.CustomFieldValues
    .Where(v => v.BizType == "Customer" && ids.Contains(v.BizId))
    .ToList();
// 3. 分组组装到客户对象

很多 CRM、低代码平台底层就是 EAV 模型。


方案三:混合方案(生产最稳,推荐商用项目)

元数据单独表 CustomField + 业务数据用 JSON 存储

  • 字段定义、类型、校验、下拉、权限走 CustomField
  • 数据值存在主表 JSON
  • 高频筛选的扩展字段:同步冗余到主表独立列(双写)

举例:客户有一个高频筛选字段 Source(客户来源),虽然是用户自定义的,但是经常用来筛选统计,就新增独立字符串字段,新增 / 修改时同步写 JSON + 独立列,兼顾灵活和性能。


通用配套能力(业务必做)

  1. 字段类型校验:根据 FieldType 对传入值做类型转换、必填校验、长度校验
  2. 字段版本 / 禁用:字段不要物理删除,加状态,历史数据保留
  3. 导出支持:导出时,读取 CustomField 定义,把扩展字段动态追加到 Excel 列
  4. 权限控制:不同角色是否可见、可编辑某个扩展字段
  5. 多租户场景:CustomField 增加 TenantId,租户之间字段隔离
  6. 查询封装:封装统一查询条件解析器,前端传 fieldKey,op,value,自动翻译成 JSON 查询或者 EAV 查询

架构选型建议

  1. 内部管理系统、CRM、档案,字段不多、筛选不极端 → JSON 方案(开发快)
  2. 专门低代码平台、大量自定义字段、高频按扩展字段筛选报表 → EAV
  3. 客户需求不稳定,同时部分字段要高频筛选 → 混合方案(JSON + 冗余高频列)

常见坑提醒

  • ❌ 不要直接用字符串拼接 SQL 做 JSON 查询,防注入
  • ❌ 不要让前端随便传入任意 key,必须校验 FieldKey 存在于 CustomField
  • ❌ EAV 不要不加索引,BizType + BizId 联合索引是必须的
  • ❌ 动态 ALTER TABLE 运行时加列,上线、迁移、多租户极易翻车