You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ADO.NET环境下实体选型:Record还是Class?未来拟用EF/Dapper

Record vs Class选型及双维护的弊端分析

问题背景

我搭建了一个完全依赖存储过程处理数据操作的数据库,现在要编写一个类库供同事在网站开发中使用,该类库还需要配合BackgroundService(用于通过OPC采集数据),包含多个业务实体。

目前陷入选型困境:

  • 资料普遍说Record适合做DTO,用它来获取全量记录这类操作确实更便捷;
  • 但如果要给实体添加验证逻辑,且未来可能接入EF或Dapper,Class似乎更适配。

另外想了解:同时维护Class及其对应的Record,会存在哪些弊端?

现有代码实现

Department.cs

/* Department.cs */
namespace DB.Models;

public class Department
{
  private readonly short _id;

  public short ID { 
    get => _id; 
    init {
      if (value < 0) 
        throw new ArgumentOutOfRangeException($"{nameof(ID)} cannot be negative", nameof(ID));
      _id = value;
    }
  }

  private readonly short _itid;
  
  public short ITID { 
    get => _itid; 
    init {
      if (value < 0) 
        throw new ArgumentOutOfRangeException($"{nameof(ITID)} cannot be negative", nameof(ITID));
      _itid = value;
    }
  }
  
  private readonly string _name;
  
  public string Name { 
    get => _name; 
    init {
      if (value is null) 
        throw new ArgumentNullException(nameof(Name), $"{nameof(Name)} cannot be null");
      if (value.All(char.IsWhiteSpace))
        throw new ArgumentException($"{nameof(Name)} cannot be whitespace", nameof(Name));
      _name = value;
    } 
  }
  
  private readonly string? _description;

  public string? Description { 
    get => _description; 
    init {
      if (value is not null && value.All(char.IsWhiteSpace))
        throw new ArgumentException($"{nameof(Department)} cannot be whitespace", nameof(Department));
      _description = value;
    } 
  }
  
  public Department(short id, short itid, string name, string? description)
  {
    ID = id;
    ITID = itid;
    Name = name;
    Description = description;
  }
}

MZTP.cs

/* MZTP.cs */
namespace DB;

public class MZTP
{
// constructor, properties and other methods of the class
public async Task<List<Department>?> GetDepartments()
  {
      List<Department>? departments = new(); 
      try 
      {
          using (FbConnection con = new(ConfigConnectionString)) 
          {
              con.Open();

              using (FbCommand cmd = new() { CommandText = "GET_DEPARTMENTS", Connection = con, CommandType = CommandType.StoredProcedure }) 
              {
                  FbDataReader reader = await cmd.ExecuteReaderAsync();

                  if (!reader.HasRows) 
                      return departments;

                  while (await reader.ReadAsync()) 
                  {
                      var id = reader.GetInt16(reader.GetOrdinal("ID"));
                      var itid = reader.GetInt16(reader.GetOrdinal("ITID"));
                      var name = reader.GetString(reader.GetOrdinal("NAME"));
                      var descrOrdinal = reader.GetOrdinal("DESCRIPTION");
                      var description = (await reader.IsDBNullAsync(descrOrdinal))
                                                   ? null
                                                   : reader.GetString(descrOrdinal);

                      departments.Add(new Department(id, itid, name, description));
                  }
              }
          }
          return departments;
      } 
      catch (Exception err) 
      {
          ErrorMessage = err.Message;
          departments = null;
      }

      return departments;
  }

  public async Task<short?> UpdateDepartment(Department department)
  {
    try 
    {
      using (FbConnection con = new(ConfigConnectionString)) 
      {
        con.Open();
        using (FbCommand cmd = new() { CommandText = "UPDATE_DEPARTMENT", Connection = con, CommandType = CommandType.StoredProcedure }) 
        {
          cmd.Parameters.AddRange(new FbParameter[] {
            new FbParameter("@ID", department.ID),
            new FbParameter("@ITID", department.ITID),
            new FbParameter("@NAME", department.Name),
            new FbParameter("@DESCRIPTION", department.Description)
          });

          if (await cmd.ExecuteScalarAsync() is short id) 
          {
            return id;
          }
          else 
          {
            return null;
          } 
        }
      }
    } 
    catch (Exception err) 
    {
      ErrorMessage = err.Message;
      return null;
    }
  }
}

选型建议与双维护弊端分析

1. Record vs Class 选型判断

优先选Record的场景

如果你的类库当前核心是数据传递(DTO/数据载体),且主要和存储过程交互,Record是更优选择:

  • 语法简洁:可以用主构造函数直接定义属性,省去冗余的私有字段和构造函数赋值,比如你的Department可以改成如下Record写法,验证逻辑完全保留:
public record Department(
    short ID,
    short ITID,
    string Name,
    string? Description)
{
    public short ID { get; init; } = ID >= 0 ? ID : throw new ArgumentOutOfRangeException(nameof(ID), $"{nameof(ID)} cannot be negative");
    public short ITID { get; init; } = ITID >= 0 ? ITID : throw new ArgumentOutOfRangeException(nameof(ITID), $"{nameof(ITID)} cannot be negative");
    public string Name { get; init; } = !string.IsNullOrWhiteSpace(Name) ? Name : throw new ArgumentException($"{nameof(Name)} cannot be null or whitespace", nameof(Name));
    public string? Description { get; init; } = string.IsNullOrWhiteSpace(Description) ? null : Description;
}
  • 不可变性:天然的不可变特性适合数据传递场景,避免意外修改,配合BackgroundService采集数据时更安全。
  • 对Dapper友好:Dapper通过反射映射数据,Record和Class的映射效率几乎无差别,无需额外配置。

优先选Class的场景

如果未来确定要深度使用EF Core的变更追踪、懒加载、实体状态管理等特性,Class更灵活:

  • EF Core虽然支持Record,但对于需要频繁修改实体属性的场景,Class的可写属性(非init)更适配EF的变更追踪逻辑;
  • 若要给实体添加业务方法(不仅仅是数据载体),Class的扩展性更好,Record更偏向于纯数据结构。

2. 同时维护Class和Record的弊端

  • 冗余维护成本:每次实体结构变更(加字段、改验证规则)都要同步修改两个类型,容易出现遗漏或不一致,长期来看维护成本翻倍;
  • 使用混淆:同事在开发时需要区分什么时候用Class、什么时候用Record,增加理解成本,甚至可能出现类型混用导致的序列化/映射错误;
  • 额外映射开销:如果在不同层之间需要转换Class和Record,还要编写映射逻辑(比如AutoMapper),增加代码复杂度和运行时开销。

内容的提问来源于stack exchange,提问作者Anthony Voronkov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 01:11:05