做.NET开发这几年几乎每个项目开场都是同一个问题这次用哪个ORM这个问题能吵一整天。EF Core、Dapper、SqlSugar、NHibernate、RepoDb……每一个都有人站队每一个都有人踩坑。写这篇.NET主流ORM功能介绍大全不是想告诉你“某某天下第一”而是把我实际用下来的功能拆解、性能数据和迁移教训整理出来。看这篇东西的人可能是刚入行的开发者也可能是被存量项目逼着迁移的老手。不管你属于哪一类读完至少能对“哪个ORM解决什么问题”有清晰判断不至于被PPT式的性能对比带偏。1. 先说清楚.NET生态为什么需要这么多种ORM1.1 从ADO.NET到全功能ORM一个演化故事很多人刚接触.NET时第一课是ADO.NET写SqlConnection、SqlCommand、DataReader然后手动把数据塞进实体类。那会儿感觉还行但项目一复杂就崩溃几十张表、几百个字段光是写映射代码就能写到怀疑人生。于是ORM出现了。它的核心价值其实就一句话让开发者用面向对象的方式操作数据库把“关系模型”和“对象模型”之间的转换交给框架去干。按这个标准.NET生态里的ORM可以分成两个阵营全功能ORMEF Core、NHibernate、SqlSugar自带实体映射、查询构建、状态跟踪、迁移工具适合业务逻辑密集的CRUD系统。微ORMDapper、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 DapperSQL在手天下我有Dapper是Stack Overflow团队开源出来的定位极其纯粹——一个高效的对象映射器不碰任何查询构建。它最大的优势就是“透明”你写什么SQL就执行什么SQL没有中间商赚差价性能自然拉满。核心API就几个QueryT查询并映射为强类型对象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.QueryOrderVm(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.QueryableOrder() .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.QueryT()走LINQ少用HQL代码可读性好一些。二级缓存配置要在多个节点环境下特别小心一旦缓存与数据库不一致出现脏读的排查难度极大。NHibernate的“延迟加载Lazy Loading”比EF更激进Session关闭后再访问导航属性会直接抛异常而且错误信息还经常不直观。2.5 RepoDb与LinqToDB小众但不能忽视RepoDb是混合型ORM的典型代表核心卖点是动态合并和批量操作性能出色。它的MergeT()方法能根据主键自动判断插入还是更新在同步数据场景超级好用批量删除和批量插入也做了深度优化。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太累。报表/财务对账大量复杂SQLDapper LinqToDB 其他手写SQL的掌控力是无价的。国产数据库强制要求SqlSugar EF Core部分版本需要第三方驱动 其他。老系统维护NHibernate如果项目本来就是或Dapper重写如果是烂到没法看。3. 性能真相为什么跑分差距那么大3.1 用BenchmarkDotNet实测的一组数据我经常被问“到底哪个ORM快”。这个问题得分场景。下面是我在一台普通开发机上、数据量50万条环境下跑过的一组基准测试结果相对耗时非绝对耗时操作类型EF CoreDapperSqlSugarRepoDb主键单条查询基准最慢快约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天差地别。状态跟踪开销默认查询都会进入ChangeTrackerEF要维护每个实体的快照内存和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.QueryFirstOrDefaultOrderDetail( 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的N1查询与连接池耗尽N1是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会在你不知道的地方生成SQLDapper会把SQL执行和映射的掌控权全部交给你SqlSugar则在两者之间提供了一条“好上手”的路线。我在实际项目里吃过不少亏最大的那次就是从EF Core一把梭到线上复杂查询超时最后靠Dapper重写才救回来。也是从那之后我才开始认真做功能拆解和性能测试不再迷信某个框架的“万能”风评。如果你正在纠结选型我的建议是拿真实业务中最复杂的一个查询在候选ORM里各写一遍再跑一次简单基准测试。哪一款让你写起来舒服、查起来可控、维护起来不慌就选哪一款。