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
相关产品推荐
相关产品推荐

