导读:本期聚焦于小伙伴创作的《Dapper如何映射到C# 9的record类型?Record与实体类映射区别详解》,敬请观看详情。把数据库查询结果直接绑定到C# 9的record类型时,不少项目会遇到属性赋值失败或只读字段无法写入的问题。record作为不可变引用类型,其构造方式与传统class不同,Dapper默认基于反射和动态代理的映射机制需要适配主构造函数参数。本文说明Dapper从2.0.30版本开始对record的支持原理,对比传统实体类与record在映射性能、可变性、值相等语义上的差异,并给出使用init属性、主构造函数、自定义TypeHandler三种落地方案,帮助开发者在只读模型与ORM查询之间平滑衔接。

在C# 9引入record类型之后,很多团队希望用不可变的数据模型来接收数据库查询结果,Dapper作为轻量级ORM,从较新版本开始已经能够直接把行数据映射到record。理解它的映射机制,有助于在保持代码简洁的同时避免常见的赋值错误。

Dapper如何映射到C# 9的record类型?Record与实体类映射区别详解

一、Dapper对record类型的映射原理

Dapper在执行查询时,会先通过反射获取目标类型的构造函数和属性信息。对于record类型,编译器会生成一个包含全部主键属性的主构造函数,同时生成对应的只读属性。Dapper在内部使用System.Reflection获取构造函数参数名称,并将其与数据库字段名进行不区分大小写的匹配,从而通过构造函数完成对象创建。

当record仅包含get;的自动属性时,Dapper会优先寻找带有全部参数的构造函数;如果找不到合适的构造函数,就会尝试通过属性setter赋值,但record的默认属性是只读的,这时会抛出异常。因此,使用record接收Dapper结果时,要么保留编译器生成的主构造函数,要么显式定义带参数的构造函数。

1.1 基础映射示例

下面定义一个简单的record,并用Dapper查询数据。注意属性名称需要与数据库字段一致,或者使用列别名。

public record UserRecord(int Id, string Name, string Email);

using var conn = new SqlConnection("Server=127.0.0.1;Database=test;User Id=sa;Password=pwd;");
var list = conn.Query<UserRecord>("SELECT Id, Name, Email FROM Users").ToList();
foreach (var u in list)
{
    Console.WriteLine($"{u.Id}-{u.Name}-{u.Email}");
}

上述代码可以正常运行,因为UserRecord的主构造函数参数与查询字段完全对应。Dapper通过构造函数注入完成了映射,没有依赖属性setter。

如果数据库字段是user_name而record参数是Name,则需要在SQL中写user_name AS Name,或者配置自定义的映射规则,否则参数匹配会失败。

二、record与传统class实体的区别

传统class实体通常带有公共的get; set;属性,Dapper可以通过无参构造函数创建对象后再逐个赋值。record则倾向于不可变,默认提供值相等比较,并且在模式匹配、深拷贝时有语法优势。两者在映射时的核心差异体现在可变性和生命周期管理上。

对比维度传统class实体record类型
可变性属性可随时修改默认只读,需init或构造函数传参
相等语义引用相等值相等,按属性内容比较
Dapper映射方式无参构造+属性setter主构造函数参数匹配
适用场景需要跟踪状态变更的富领域模型只读查询模型、DTO传输对象

从性能角度看,record通过构造函数一次性赋值,减少了属性setter的调用开销,在高频只读查询中略有优势。但它的不可变性也意味着更新操作必须创建新对象,不适合需要原地修改的业务实体。

另一个容易忽略的点是,record生成的ToString方法会输出所有属性值,在日志调试时非常方便,而class默认只输出类型名。对于排查Dapper映射结果是否符合预期,record自带的诊断信息能节省不少时间。

三、使用init属性兼容Dapper

如果希望record既保持不可变语义,又能在某些场景下用属性赋值,可以使用init访问器。init属性允许在对象初始化表达式中被赋值,但在初始化完成后变为只读。Dapper较新版本在映射时会识别init属性并尝试赋值。

public record Product
{
    public int Id { get; init; }
    public string ProductName { get; init; }
    public decimal Price { get; init; }
}

using var conn = new SqlConnection("Server=192.168.0.1;Database=shop;User Id=root;Password=123;");
var products = conn.Query<Product>("SELECT Id, ProductName, Price FROM Products").ToList();

这段代码中Product没有显式构造函数,但所有属性都是init,Dapper会利用无参构造函数创建实例,然后通过init setter填充数据。这种方式比主构造函数更灵活,也更容易和EF Core等其它框架混用。

需要注意的是,如果同时定义了主构造函数和init属性,Dapper仍以主构造函数为准。当数据库字段比构造函数参数少时,未传参的属性会保持默认值,这可能引发隐蔽的数据缺失问题。

四、自定义TypeHandler处理复杂映射

当record中包含Dapper无法直接转换的类型,例如JSON字符串转对象,或者枚举使用自定义存储格式时,可以通过实现SqlMapper.TypeHandler<T>来扩展。下面示例将数据库中的逗号分隔字符串映射到record的List字段。

public record TaggedUser(int Id, List<string> Tags);

public class TagListHandler : SqlMapper.TypeHandler<List<string>>
{
    public override List<string> Parse(object value)
    {
        return value?.ToString().Split(',').ToList() ?? new List<string>();
    }
    public override void SetValue(IDbDataParameter parameter, List<string> value)
    {
        parameter.Value = string.Join(",", value);
    }
}

SqlMapper.AddTypeHandler(new TagListHandler());

using var conn = new SqlConnection("Server=127.0.0.1;Database=test;User Id=sa;Password=pwd;");
var users = conn.Query<TaggedUser>("SELECT Id, Tags FROM UserTags").ToList();

通过自定义Handler,record的不可变特性不会受到破坏,因为转换发生在构造函数参数赋值之前。这样即使底层存储格式特殊,上层模型依然保持干净的值对象形态。

在微服务架构中,这种写法尤其有用:数据库里的冗余字段可以在映射层被收敛成强类型的record,避免把字符串解析逻辑散落在业务代码里。

五、常见错误与排查建议

开发者常遇到的错误是“无法找到合适的构造函数”或“只读属性赋值失败”。这通常是因为record被手动改成了只有无参构造函数,却忘了给属性加init,或者SQL字段别名和参数名不一致。建议在写查询时用AS显式对齐名称。

排查时可以先把record临时改成class并加上setter,若此时映射成功,说明问题出在构造函数或只读属性上,而不是SQL本身。

另外,Dapper版本低于2.0.30时对record支持不完善,升级NuGet包通常能解决大部分兼容问题。若项目受限于旧框架无法升级,可以用匿名类型接收后再用LINQ投影成record,作为折中方案。

var anon = conn.Query("SELECT Id, Name FROM Users").Select(r => new UserRecord(r.Id, r.Name)).ToList();

这种手动投影方式不依赖Dapper的反射适配,在老版本中也能稳定工作,只是多了一步转换代码。对于小型查询影响不大,但在批量场景下要留意额外的枚举开销。

Dapperrecord_typeCSharp9修改时间:2026-08-01 17:21:18

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。