做.NET开发这几年,几乎每个项目开场都是同一个问题:这次用哪个ORM?这个问题能吵一整天。EF Core、Dapper、SqlSugar、NHibernate、RepoDb……每一个都有人站队,每一个都有人踩坑。写这篇.NET主流ORM功能介绍大全,不是想告诉你“某某天下第一”,而是把我实际用下来的功能拆解、性能数据和迁移教训整理出来。看这篇东西的人,可能是刚入行的开发者,也可能是被存量项目逼着迁移的老手。不管你属于哪一类,读完至少能对“哪个ORM解决什么问题”有清晰判断,不至于被PPT式的性能对比带偏。
1. 先说清楚:.NET生态为什么需要这么多种ORM
1.1 从ADO.NET到全功能ORM:一个演化故事
很多人刚接触.NET时,第一课是ADO.NET,写SqlConnection、SqlCommand、DataReader,然后手动把数据塞进实体类。那会儿感觉还行,但项目一复杂就崩溃:几十张表、几百个字段,光是写映射代码就能写到怀疑人生。
于是ORM出现了。它的核心价值其实就一句话:让开发者用面向对象的方式操作数据库,把“关系模型”和“对象模型”之间的转换交给框架去干。按这个标准,.NET生态里的ORM可以分成两个阵营:
- 全功能ORM:EF Core、NHibernate、SqlSugar,自带实体映射、查询构建、状态跟踪、迁移工具,适合业务逻辑密集的CRUD系统。
- 微ORM:Dapper、RepoDb,只做一件事——把SQL查询结果快速映射成对象,其余全部交给你手写。
这就像一个是全自动挡汽车,一个是手动挡性能车。自动挡省心但操控感弱,手动挡累但提速直接。没有谁取代谁,只有你用在哪条路上。这也是为什么“选型之争”永远存在——因为大家的业务场景根本不一样。
1.2 选型先想清楚三个问题
踩过几次坑之后,我现在选ORM只问三个问题:
- 业务核心是CRUD还是复杂查询?如果90%的代码都是增删改查,全功能ORM能省一半代码;如果是报表、财务对账这种复杂SQL,手写反而更靠谱。
- 用的什么数据库?有些ORM对国产库(达梦、人大金仓、GaussDB)支持不到位,这在实际项目里是致命的。
- 团队水平怎么样?EF Core的延迟加载、状态跟踪,用好了是效率神器,用不好就是线上事故,Dapper直白,新手不容易写出“玄学代码”但更累。
这三个问题没有标准答案,但能在十分钟内筛掉一半候选方案。
1.3 一张表看清六大ORM的定位
我整理了一份速查表,基本代表目前的主流选择:
| ORM | 类型 | 关键优势 | 主要短板 | 最适配场景 |
|---|---|---|---|---|
| EF Core | 全功能 | 微软官方、功能最全、迁移好用 | 复杂查询性能弱、状态机坑多 | CRUD业务系统、原型到落地 |
| Dapper | 微ORM | 性能天花板、SQL自由度最高 | 无状态跟踪、CRUD全靠手写模板 | 高并发读写、复杂报表 |
| SqlSugar | 全功能 | 国产库适配好、API直观、更新快 | 国际社区小、生态依赖国内 | 信创项目、中小团队快速交付 |
| NHibernate | 全功能 | 老牌稳定、缓存机制成熟 | 学习曲线陡、配置繁琐 | 存量金融/政务项目维护 |
| RepoDb | 微ORM | 动态合并、批量操作性能好 | 社区小、资料少 | 批量导入导出场景 |
| LinqToDB | 中间型 | LINQ体验好、支持原生SQL混写 | 名气不大、坑得自己踩 | 需要LINQ但受不了EF重的团队 |
这张表没有对错,只有合不合适。接下来我把每个框架的核心功能和真实感受展开讲。
2. 逐个拆解:主流ORM的核心功能与实战表现
2.1 EF Core:功能最全,但不等于“最好用”
EF Core是微软亲儿子,跟着.NET版本迭代,功能覆盖面是六款里最全的。核心功能包括:LINQ查询、变更跟踪、迁移、全局过滤、延迟加载/显式加载、乐观并发控制,还有8.0引入的Complex Types和JSON列映射。
我最常用的其实是几个“生态级”能力:
- 迁移工具:
dotnet ef migrations add一条命令生成SQL脚本,配合CI/CD自动执行,环境差异问题大幅减少。 - 全局过滤:软删场景下,
HasQueryFilter一条配置,所有查询自动拼接IsDeleted = 0,不用在每个查询里重复写。 - LINQ的强类型体验:写代码时编译器就帮你查字段拼写错误,重构时也安全。
但千万别迷信“EF Core = 省心”。变更跟踪(ChangeTracker)是双刃剑:一个实体被查询出来后,只要DbContext还活着,任何属性修改都会被跟踪,最后SaveChanges时全量更新。如果你在一个长生命周期的作用域里查询了一堆实体,然后只改其中一个,EF Core会生成一条包含所有字段的UPDATE语句——字段多的时候性能直接崩。
实际项目中,我遇到过一次极端案例:某报表页面调用了一个复杂查询,返回几百个实体,然后循环里改了每个实体的一个状态字段,再SaveChanges。生成的SQL巨大,数据库CPU直接打满。排查半天才发现是“全字段更新”导致的。解决办法很粗暴:查询时加.AsNoTracking(),改完后单独创建更新实体。
提示:EF Core 8.0/9.0对JSON列的支持越来越强,但在复杂JSON查询上还远不如用原生数据库函数。
2.2 Dapper:SQL在手,天下我有
Dapper是Stack Overflow团队开源出来的,定位极其纯粹——一个高效的对象映射器,不碰任何查询构建。它最大的优势就是“透明”:你写什么SQL,就执行什么SQL,没有中间商赚差价,性能自然拉满。
核心API就几个:
Query<T>:查询并映射为强类型对象Execute:执行增删改QueryFirst/QuerySingle:取单条QueryMultiple:一次查询返回多个结果集ExecuteScalar:取标量值
举个典型例子:
var sql = @" SELECT o.Id, o.OrderNo, c.Name AS CustomerName FROM Orders o INNER JOIN Customers c ON o.CustomerId = c.Id WHERE o.CreateTime >= @start AND o.Status = @status ORDER BY o.CreateTime DESC OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY"; var list = connection.Query<OrderVm>(sql, new { start = DateTime.Now.AddDays(-7), status = 1, offset = 0, pageSize = 20 }).ToList();看到了吗?SQL是裸的,参数是匿名对象,结果直接映射为OrderVm。没有表达式树、没有状态跟踪、没有奇怪的SQL生成,这在复杂查询场景下就是性能保障。
但Dapper的短板也很明显:CRUD要自己写。我都习惯建一个“仓储基类”把通用增删改查封装起来,不然每个实体手写一遍INSERT/UPDATE,心情极度暴躁。
有一个坑必须提醒:Dapper的查询结果是“瞬时的”,实体和数据库之间没有任何关联。更新时必须把实体重新传给UPDATE语句,或者用一个包含主键和待更新字段的专用实体。很多人从EF转过来,会下意识改Dapper查出来的对象然后期待它自动同步——然后发现什么都没发生。
2.3 SqlSugar:国产ORM里的“全能选手”
SqlSugar在国内开发者里口碑一直不错,尤其这几年信创趋势下,它对国产数据库的适配是一大亮点。内置功能极其丰富:CodeFirst、DbFirst、分表分库、多租户、逻辑删除、乐观锁、批量操作、读写分离,基本你能想到的它都有。
它的API风格非常“中国式直觉”,上手速度快。比如分页查询:
var pageResult = db.Queryable<Order>() .Where(o => o.Status == StatusEnum.Paid) .Where(o => o.CreateTime >= startTime) .OrderBy(o => o.Id, OrderByType.Desc) .ToPageList(pageIndex, pageSize);这段代码用起来非常直观,语义清晰,不需要理解EF Core那套Skip/Take和表达式树里的坑。尤其是批量操作,SqlSugar的Storageable和BulkCopy性能出色,插入几万条数据时比EF Core快十倍以上。
它的核心优势还在于“省心”:
- 自动处理数据库差异:同样的代码,切到MySQL、SQLServer、达梦、人大金仓,SQL方言由框架翻译,项目迁移数据库时能少改很多代码。
- 内置逻辑删除:实体上有标记就自动过滤,不用像EF那样手动配置全局过滤器。
- 多表查询:内置
Join方法,比手写SQL动态拼接简单不少。
我一般在快速交付的项目、信创项目、中小团队协作场景首选SqlSugar。但也要说实话:它在国际社区的知名度不高,英文文档很少,遇到冷门bug时搜不到现成解决方案的概率比EF Core高。
注意:SqlSugar的分表分库功能,在真实项目里建议只在确定的场景使用(比如按订单号分表),全局启用会遇到跨分片查询、事务一致性等一系列复杂度。
2.4 NHibernate:老而弥坚,但学习曲线陡峭
NHibernate是Java Hibernate的.NET移植版,出道早,巅峰期在.NET Framework时代,现在用的人少了,但存量项目不少,尤其是一些金融、政务系统的核心模块还跑在它上面。
它的强项是成熟的二级缓存、持久化生命周期管理和复杂的继承映射,概念非常多:Session、SessionFactory、持久态、游离态……我当年第一次接触时直接懵了,感觉不是在写ORM,是在学习一套完整的状态机理论。
说实话,新项目我基本不推荐NHibernate。理由很简单:团队招人难,资料老,坑多。但如果你要维护老系统,必须掌握几个关键点:
- 查询用
ISession.Query<T>()走LINQ,少用HQL,代码可读性好一些。 - 二级缓存配置要在多个节点环境下特别小心,一旦缓存与数据库不一致,出现脏读的排查难度极大。
- NHibernate的“延迟加载(Lazy Loading)”比EF更激进,Session关闭后再访问导航属性,会直接抛异常,而且错误信息还经常不直观。
2.5 RepoDb与LinqToDB:小众但不能忽视
RepoDb是混合型ORM的典型代表,核心卖点是动态合并和批量操作性能出色。它的Merge<T>()方法能根据主键自动判断插入还是更新,在同步数据场景超级好用;批量删除和批量插入也做了深度优化。
LinqToDB则介于EF和Dapper之间:有LINQ查询体验,又不会像EF那样生成失控的SQL,支持SQL混写。可能更适合“想用LINQ但不想碰EF状态机”的团队。
这两个框架的社区规模都比较小,遇到问题时能查到的资料有限。我的建议是:新技术用它们没问题,但团队里至少要有人能吃透源码,不然生产环境出了奇怪的问题会很被动。
2.6 各ORM在典型业务场景中的适配度
从真实业务数据维度来看:
- 高并发读场景(比如商品详情页):Dapper > RepoDb > LinqToDB > SqlSugar > EF Core。
- 复杂CRUD管理后台:SqlSugar ≈ EF Core > NHibernate > Dapper(这里Dapper垫底是因为写CRUD太累)。
- 报表/财务对账(大量复杂SQL):Dapper > LinqToDB > 其他,手写SQL的掌控力是无价的。
- 国产数据库强制要求:SqlSugar > EF Core(部分版本需要第三方驱动)> 其他。
- 老系统维护:NHibernate(如果项目本来就是)或Dapper重写(如果是烂到没法看)。
3. 性能真相:为什么跑分差距那么大
3.1 用BenchmarkDotNet实测的一组数据
我经常被问“到底哪个ORM快”。这个问题得分场景。下面是我在一台普通开发机上、数据量50万条环境下跑过的一组基准测试结果(相对耗时,非绝对耗时):
| 操作类型 | EF Core | Dapper | SqlSugar | RepoDb |
|---|---|---|---|---|
| 主键单条查询 | 基准(最慢) | 快约2.5倍 | 快约2倍 | 快约3倍 |
| 六表Join聚合查询 | 基准 | 快约4倍 | 快约2.5倍 | 快约5倍 |
| 批量插入1万条 | 基准 | 快约8倍(配合SqlBulkCopy) | 快约6倍(BulkCopy内置) | 快约9倍 |
| 复杂条件分页 | 基准 | 快约3倍 | 快约2倍 | 快约3倍 |
这个结果很多人看到后第一反应是“EF Core太垃圾了”,但我想说的是:跑分快慢,不代表你的业务价值。Dapper快是因为它几乎不做额外工作,把SQL直接给ADO.NET执行,然后映射一下就完事;EF Core慢是它要在背后帮你做表达式树翻译、状态跟踪、缓存的差异比对。
3.2 慢的根源到底在哪
EF Core慢的核心原因有三个:
- 表达式树翻译开销:C#的LINQ表达式要翻译成SQL,这是CPU密集工作,而且翻译质量不稳定——同一个查询换个写法,SQL天差地别。
- 状态跟踪开销:默认查询都会进入ChangeTracker,EF要维护每个实体的快照,内存和CPU成本都不低。
- SQL生成质量:复杂查询下EF生成的SQL常有冗余子查询、不合理的JOIN顺序,数据库优化器再神也救不回来。
Dapper几乎没有这些成本:SQL是自己写的,参数是直接传给ADO.NET的,映射走的是IL动态生成,代价只在第一次调用时。所以Dapper快不是在“优化”上下功夫,而是“什么都不做”。
3.3 不同场景下的选型建议
基于以上数据,我个人比较“功利”的选型原则如下:
- 读多写少、高吞吐:Dapper是底线选项。这不是说比谁快,而是能让你看清每一条SQL,避免“慢查询”出现在你不注意的角落里。
- 管理后台、内部系统、CRUD密集:EF Core或SqlSugar。开发效率的提升远远覆盖那点查询性能损失。
- 报表系统:Dapper + 手写优化SQL。给数据库加索引、调整执行计划,这些在EF Core里非常难做到。
- 混合数据规模:读用Dapper,写用全功能ORM。这也不是什么高深做法,商业项目里非常常见。
很多团队盲目追求“极致性能”,结果业务还没跑起来就被Dapper的CRUD代码拖垮了开发效率;反过来,有些团队闭眼EF Core一把梭,线上一个复杂查询就超时。性能是衡量的维度之一,但不是唯一维度。你自己项目的复杂度、团队效率和可维护性,远比单次查询快几毫秒重要。
4. 落地实操:混合使用与迁移的完整记录
4.1 混合架构:读写分离的具体做法
先分享一个我在某电商库存项目里的真实方案。这个项目查询量巨大,但写操作相对简单,于是我们采用了“Dapper + EF Core”混合架构:
- 查询走Dapper:所有列表页、详情页、统计接口,用Dapper手写SQL,以最优执行计划为准。
- 写操作走EF Core:订单创建、状态流转等写操作,利用EF Core的乐观并发和事务能力,避免手写一堆冲突判断。
- 两者共用同一个连接字符串和事务:创建
IDbConnection时同时拿到DbContext,用Database.UseTransaction把两者置于同一个事务中。
关键代码大致是这样:
using var connection = new SqlConnection(connectionString); connection.Open(); using var transaction = connection.BeginTransaction(); // 写操作:EF Core var order = new Order { UserId = userId, Total = total, Status = OrderStatus.Created }; await dbContext.Orders.AddAsync(order); await dbContext.Database.UseTransaction(transaction); await dbContext.SaveChangesAsync(); // 读操作:Dapper,同一个事务 var detail = connection.QueryFirstOrDefault<OrderDetail>( "SELECT * FROM Orders WHERE Id = @id", new { id = order.Id }, transaction);这种做法的最大好处是:每个数据访问都选择了最合适的工具,而不是让一个工具干所有事。坏处也明显:团队要做好约定,否则很容易出现“这个查询为什么不用Dapper写”“这个更新为什么不用EF”的争论。
4.2 从EF Core迁移到Dapper的四个关键步骤
如果你的项目决定从EF Core迁移到Dapper,别指望一夜完成,下面是四个实操步骤,踩过的坑都写在里面:
- 先审计所有EF查询,划分优先级。把复杂查询、高并发查询列为第一批迁移对象,简单的CRUD可以继续用EF,不必追求一步到位。
- 建立参数化SQL的规范。EF Core用参数是自动的,Dapper虽然也自动,但要求你写SQL时使用
@name占位符。注意不要字符串拼接,防SQL注入不只是安全问题,也是性能问题——拼接SQL会破坏执行计划缓存。 - 处理Include调用。EF的
.Include()在Dapper里不存在,你需要手动写JOIN或多次查询后在内存中组装实体。推荐后者,复杂场景更可控。 - 迁移后必做回归测试。特别注意分页逻辑、软删条件、乐观并发字段的显式处理。
有一个最容易被忽略的地方:EF Core查询默认会自动拼接软软删除过滤条件(如果在模型里配置了全局过滤),但Dapper不会。迁移时手动补上WHERE IsDeleted = 0,漏了就是线上数据泄露事故。
4.3 从SqlSugar迁移到EF Core的注意事项
SqlSugar转到EF Core,方向反过来,也有不少“水土不服”的点:
- 分页写法差异:SqlSugar的
ToPageList内部帮你做了分页数和偏移量的计算,EF里要写Skip/Take,而且Skip/Take必须跟在OrderBy之后,否则某些数据库会报错。 - 批量更新差异:SqlSugar的
UpdateColumns可以指定只更新部分列,EF Core的批量更新在8.0之前都是逐条执行的,8.0引入了ExecuteUpdate才补上这块能力。 - 实体特性配置:SqlSugar用特性直接标注表名列名,EF Core虽然也支持但推荐在
OnModelCreating里用Fluent API配置,信息更集中、便于审查。
我见过一个团队迁移后性能反而变差,排查原因发现是EF默认的查询跟踪导致内存暴涨。迁移EF后第一件事,就是检查所有只读查询是否需要.AsNoTracking()。很多时候不是EF慢,是你在错误地使用它。
5. 常见问题与排查技巧实录
5.1 EF Core的N+1查询与连接池耗尽
N+1是EF Core最经典的坑,没有之一。它长这样:你查了一个订单列表,然后在循环里访问每个订单的客户信息,EF为每个客户信息单独执行了一条查询。列表里有几百条数据,数据库就要执行几百条查询,性能直接垮掉。
排查技巧很简单:在ConfigureLogging里加上EnableSensitiveDataLogging,然后打开控制台输出,所有SQL都会打出来。一旦发现循环查询模式,改用Include或者Select投影。如果数据关联非常复杂,可以拆成多个查询,然后用内存组装。
连接池耗尽的问题通常出现在:长生命周期DbContext + 慢事务。解决办法:
- 用
AddDbContextFactory,每次请求创建新的DbContext实例。 - 确保所有异步查询都用了
await,没被丢弃的Task会占用连接。 - 事务控制范围要小,不要用事务包住一个需要长耗时计算的循环。
5.2 Dapper的参数化与结果映射
Dapper的自定义映射有一个大坑:默认是按列名匹配属性名,且区分大小写时容易踩坑。如果数据库返回的列名是customer_id,你的实体属性是CustomerId,就直接映射不上——解决办法是SQL里AS CustomerId,或者用Dapper.Contrib的属性配置。
还有一个常见的运行时错误:DynamicParameters使用同名字典时,如果SQL里同一个参数出现两次,DynamicParameters在旧版本可能不生效。我的习惯是:不要依赖同一个DynamicParameters实例在复杂SQL里多次复用,要么写多次参数添加,要么直接改成匿名对象。匿名对象的新鲜度反而更高,因为它每次都是新实例。
在处理结果映射时,建议给每条查询建一个专用的ViewModel或Record,不要直接用实体类。这样既能避免映射到不必要字段,也为SQL的列清单提供了明确约定。用实体类接收未知列,Dapper不会报错,但你会得到一堆null值,而且查不出来为什么。
5.3 SqlSugar的批量更新与自增主键
SqlSugar的Storageable是批量更新的好帮手,它内部会自动判断INSERT还是UPDATE。但有个坑:如果你在循环里反复调用Storageable,性能最好也不如一次性传List。批量操作一定要在内存中先组装好集合,一次调用框架API,数据库往返次数要最小化。
自增主键方面,SqlSugar默认实体主键如果是int类型,插入后会把自增值回填到实体属性上。但如果你用了long类型并且数据库列不是自增而是序列(Oracle、达梦),需要在实体特性或全局配置里声明,否则插入后主键还是0,后续关联逻辑全崩。
注意:SqlSugar的批量插入在数据量超过1万条时,建议直接走
BulkCopy接口而不是普通Insertable,前者走的是原生数据库批量协议,后者还是逐条拼接。
5.4 迁移脚本与多ORM混用的事务一致性
项目上线前,最容易被忽略的是“迁移脚本生成”和“事务一致性”两个环节。
迁移脚本我在EF Core里遇到过几次“生成的迁移与实际数据库不一致”,原因大多是开发机和生产环境装了不同版本的数据库引擎。在提交迁移前,务必在目标数据库版本上跑一遍dotnet ef database update做验证,不要只在本地开发库跑通就完事。
多ORM混用时,事务的一致性靠的是“同一个连接 + 同一个事务对象”。但很多团队会在服务层创建两个仓库对象,各自new了一个连接,事务就失效了。解决方法是引入工作单元模式(UnitOfWork),同时在IoC容器里注册同一个IDbConnection,确保一次请求一个连接。
我实测下来,混用ORM最稳定的组合,是“一个全功能ORM负责写与事务,一个微ORM负责读与查”,而不是两个全功能ORM拼在一起,那样只会让事务和迁移的管理复杂度翻倍。
最后分享一点个人体会:ORM选型没有标准答案,但有一点是共通的——用的人必须清楚手里这个工具在每个环节到底在做什么。EF Core会在你不知道的地方生成SQL,Dapper会把SQL执行和映射的掌控权全部交给你,SqlSugar则在两者之间提供了一条“好上手”的路线。我在实际项目里吃过不少亏,最大的那次就是从EF Core一把梭到线上复杂查询超时,最后靠Dapper重写才救回来。也是从那之后,我才开始认真做功能拆解和性能测试,不再迷信某个框架的“万能”风评。
如果你正在纠结选型,我的建议是:拿真实业务中最复杂的一个查询,在候选ORM里各写一遍,再跑一次简单基准测试。哪一款让你写起来舒服、查起来可控、维护起来不慌,就选哪一款。