资讯详情

资讯详情

EF6与EF Core实战:从版本选型到Code First迁移的完整指南

1. 动手之前先想清楚EF框架的版本与项目匹配问题1.1 同样是EFEF6和EF Core是两套东西第一次在Visual Studio里接触EF框架的人打开NuGet包管理器时大概率会愣一下搜EntityFramework出来一大排EntityFramework 6.x、Microsoft.EntityFrameworkCore、Microsoft.EntityFrameworkCore.SqlServer到底装哪个我在各种项目里来回切换过结论其实很直接如果项目是.NET Framework体系常见于传统WinForms、WPF、老Web项目、很多工控上位机老老实实用EF6如果项目是.NET Core或.NET 5以上的现代体系用EF Core。这两者的区别不只是版本号整个底层设计都换了。EF6是.NET Framework时代成长起来的重量级ORM功能高度集成配置文件多对老项目支持好尤其是像WinForms上位机这类需要快速对接本地数据库、内网部署的场景EF6非常稳。EF Core是微软后来针对跨平台和现代架构重写的产物模块化、体积轻、支持依赖注入、原生异步性能也优化了不少EF Core 8/9已经做到了很多以前EF6做不到的事。判断方式极简单在Visual Studio里新建项目时目标框架那栏如果是.NET 6/7/8/9就选EF Core如果是.NET Framework 4.6.1/4.7.2/4.8选EF6。用错版本最典型的后果是你明明在EF Core项目里装了EF6的包运行时报一堆奇怪的程序集加载失败。这不是代码问题是选型问题越早理解越省事。1.2 Visual Studio版本与EF框架的兼容关系Visual Studio本身只是个IDE它对EF框架的使用影响主要在两点一是新建项目时决定你能选什么目标框架二是NuGet包管理器帮你拉依赖。VS2019、VS2022、VS2026这些版本对EF6和EF Core都能正常支持没有说哪个VS版本必须配哪个EF版本。真正会产生影响的是你的项目目标框架。比如EF Core 8官方要求net8.0如果你的VS里装的是老版.NET SDK新建项目时只能选net6.0那强行装EF Core 8包就会报NU1202不兼容的包依赖。解决方案也直接要么降低EF Core版本比如装6.x或7.x要么升级项目目标框架到net8.0。我个人的建议是新机器新项目直接上VS2022以上的版本并安装标准的工作负载。在Visual Studio Installer里勾选ASP.NET和Web开发和.NET桌面开发前者覆盖Web API、MVC项目后者覆盖WinForms、WPF上位机项目。安装完成后在项目属性里确认目标框架保证和要用的EF版本匹配后面基本不会在环境层面出幺蛾子。2. 从建项目到装好EF包NuGet环节的几个关键细节2.1 项目骨架怎么搭最省心很多初学者一上来就想着分层架构、仓储模式、依赖注入其实在Visual Studio里第一步尝试EF建一个简单的控制台项目或ASP.NET Core Web API项目就够用了。控制台项目方便你打断点调试Web API项目更容易模拟真实场景。我个人推荐第一次练手用ASP.NET Core Web API加EF Core因为后续写接口、注入DbContext都顺路。创建项目时注意命名空间规范比如解决方案名EFDemo项目名EFDemo.Api。类库要不要单独拆出来初期不用拆把实体类和DbContext直接放在主项目下等运行通了一个完整CRUD流程再考虑拆分。为什么因为EF框架的调试难度主要集中在数据库连接和模型映射上项目结构越简单你越容易定位问题。2.2 NuGet安装EF6和EF Core各自的正确姿势在Visual Studio里装EF包有两条路一条是图形界面右键项目选管理NuGet程序包浏览页搜索包名选对应版本安装另一条是包管理器控制台工具- NuGet包管理器 - 程序包管理器控制台输命令。EF6只需要一个包Install-Package EntityFramework这个包会把EF6的运行时、设计时工具全部带上不用再装别的。如果你用的是SQL Server家族数据库这个包就够。EF Core则稍有讲究通常不需要单独装Microsoft.EntityFrameworkCore而是直接装对应的数据库提供程序包它会通过依赖把核心包带进来Install-Package Microsoft.EntityFrameworkCore.SqlServer如果你用的是SQLite、PostgreSQL、MySQL则分别装Microsoft.EntityFrameworkCore.Sqlite、Npgsql.EntityFrameworkCore.PostgreSQL、Pomelo.EntityFrameworkCore.MySql。很多人装了主包忘了提供程序包结果运行时报没有找到数据库提供程序的错误根源就在这里。EF Core的设计哲学就是你用哪个库就装哪个包其余的都当传递依赖自动处理。2.3 安装中常年见到的三类异常第一类是版本冲突。表现是安装进度条走了半天最后报NU1107或NU1608之类依赖冲突错误。处理思路不是瞎试版本而是看项目目标框架和包版本是否匹配比如net6.0项目强行装EF Core 8就是典型冲突这种情况要么升目标框架要么降EF版本到6.0.x。第二类是下载卡在0B或进度条一直不动。这在Visual Studio里相当常见原因是NuGet默认源访问慢或网络被限制。处理方法是在NuGet包管理器右上角的源设置里把包源切换到国内可用的镜像源。第三类是Packages.config和PackageReference混用造成的重复引用。老项目用packages.config新项目用PackageReference如果同一个项目里两种引用方式都存在编译时会看到大量重复引用警告甚至导致程序集版本错乱。建议在项目文件里把packages.config删掉统一改成PackageReference或者通过VS的迁移功能一键转换。3. Code First建模从实体类到数据库诞生的完整链路3.1 实体类定义别只写属性主键和外键约定要看懂EF框架的三种建模方式——Code First、Database First、Model First——我强烈建议新项目用Code First也就是先写C#类再由EF帮你创建数据库表。这种方式的可读性和维护性都是最好的代码即模型配合迁移机制数据库结构的演进就是代码变更的历史记录。下面是一个典型的订单场景包含商品和分类两个表外加订单明细public class Category { public int Id { get; set; } public string Name { get; set; } string.Empty; public ICollectionProduct Products { get; set; } new ListProduct(); } public class Product { public int Id { get; set; } public string Name { get; set; } string.Empty; public decimal Price { get; set; } public int CategoryId { get; set; } public Category? Category { get; set; } }这里有几个约定值得解释。主键属性取名Id或以类名Id结尾比如CategoryIdEF会自动识别为主键不需要额外标注。外键Product里的CategoryId会被识别为外键同时定义了一个Category导航属性EF就能自动建立两个表的关系。字符串类型默认映射为nvarchar(max)这在实际项目里通常不够严谨需要用Data Annotation特性或Fluent API限定长度。public class Product { public int Id { get; set; } [MaxLength(100)] public string Name { get; set; } string.Empty; [Column(TypeName decimal(18,2))] public decimal Price { get; set; } }3.2 DbContextEF框架的交通枢纽DbContext是EF框架的使用核心。它负责管理实体对象、查询数据库、保存变更有点像数据库连接和实体集合之间的总调度室。一个简洁的DbContext这样写public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetProduct Products SetProduct(); public DbSetCategory Categories SetCategory(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.EntityProduct(entity { entity.Property(p p.Price).HasPrecision(18, 2); entity.HasOne(p p.Category) .WithMany(c c.Products) .HasForeignKey(p p.CategoryId); }); } }我建议表结构复杂时优先使用Fluent API就是OnModelCreating里的写法因为Data Annotation把规则散落在各个实体属性上表关系一多看起来非常乱而Fluent API集中管理别人接手时一眼能看到所有映射规则。下面这几种配置在真实项目中出现频率极高限制字符串长度、设置decimal精度、建立唯一索引。modelBuilder.EntityProduct(entity { entity.HasIndex(p p.Name).IsUnique(); });3.3 连接字符串的两种配置位置连接字符串告诉EF去哪里找数据库。EF6通常写在App.config或Web.config的 节connectionStrings add nameAppDbContext connectionStringServer.;DatabaseEFDemoDb;Trusted_ConnectionTrue;TrustServerCertificateTrue; providerNameSystem.Data.SqlClient / /connectionStringsEF Core则写appsettings.json{ ConnectionStrings: { DefaultConnection: Server.;DatabaseEFDemoDb;Trusted_ConnectionTrue;TrustServerCertificateTrue; } }注意EF6的配置里多了providerNameSystem.Data.SqlClient而EF Core不需要。很多人在EF Core项目里照抄EF6的配置结果报关键字不受支持的错就是这个原因。另外本地开发建议用默认实例加Windows身份验证真到部署环境再切换成SQL账号连接串。3.4 数据库生成执行迁移命令的那几步写好实体和DbContext后要让Visual Studio帮我们生成数据库步骤如下首先在包管理器控制台里把默认项目选成DbContext所在的项目。然后输入Enable-MigrationsEF6的旧命令EF Core则不用这一步直接从Add-Migration开始。依次执行Add-Migration Init Update-Database -VerboseAdd-Migration会扫描模型变化并生成一个迁移类里面写了Up和Down方法Update-Database把迁移应用到数据库-Verbose可以打印实际执行的SQL我强烈建议带上这个参数能直观看到EF是怎么建表的。如果数据库不存在EF会自行创建。第一次跑通这套流程后你会发现自己建库的速度比在SSMS里手写SQL快得多。而且因为有了迁移脚本后续无论换机器还是换同事一条Update-Database就能把库结构还原出来。4. 日常CRUD之外DbContext真正值得花时间研究的几个点4.1 SaveChanges是分水岭增删改都要靠它落库EF框架的增删改查看着简单实际用起来有几个重要的心理预期。先说增删改一个完整的插入流程如下using var db new AppDbContext(GetOptions()); var category new Category { Name 饮料 }; db.Categories.Add(category); await db.SaveChangesAsync();关键在于后面这行SaveChangesAsync。Add方法只是把实体标记为Added状态对象图还停留在内存里只有调了SaveChangesEF才会真正生成INSERT语句发往数据库。即使你Add了十个、一百个实体都是同一行SaveChanges统一提交。删除和更新也是同一个套路先查出实体要么Remove标记删除要么改属性等SaveChanges统一Update。var product await db.Products.FindAsync(id); if (product ! null) { product.Price 9.9m; await db.SaveChangesAsync(); }很多新手在这里犯迷糊改完属性以为立刻生效切到数据库一看没变化来来回回排查半天其实只是少了一句SaveChanges。4.2 IQueryable的延迟执行原来查询并没有真的查询EF框架的查询有个反直觉的地方。你用db.Products.Where(p p.Price 10)并不会立刻执行SQL它只是构建了一个IQueryable表达式树。真正打开数据库连接、跑SQL的地方是遍历结果的那一刻比如ToListAsync、FirstOrDefaultAsync。这个特性带来的好处是你可以在分页、过滤条件不确定的场景下逐步叠加查询条件最后再一次性执行。但副作用也明显如果你在循环里遍历某个IQueryable结果每遍历一次就执行一次SQL性能立马崩盘。导航属性也有类似陷阱。只查询Product列表时Category是空的因为EF默认不加载关联对象。要一次性把关联数据取出来用Includevar products await db.Products .Include(p p.Category) .Where(p p.CategoryId 1) .ToListAsync();这样生成的SQL会带JOIN一次取回关联数据。如果不加Include循环里又访问p.Category.Name就会造成所谓的N1查询问题——主表查一次每个子对象再查一次数据量大时性能极其难看。判断这个问题的办法很简单把EF生成的SQL打印出来看LOG里的SELECT条数几条就是几个查询。4.3 只读场景加AsNoTracking批量插入关掉自动检测EF框架默认会跟踪查询出来的每一个实体。这意味着当你查询1万条数据时DbContext内部的状态管理器就咬着这1万条实体的快照不放内存占用和变更检测的开销都不小。对于纯展示、不修改数据的查询加一个AsNoTracking就能绕开跟踪机制var categories await db.Categories .AsNoTracking() .ToListAsync();注意用了AsNoTracking之后查询出来的实体就脱离DbContext管理了对它做修改再SaveChanges是无效的。所以这个优化只对只读场景使用。批量插入的另一个隐藏性能杀手是AutoDetectChangesEnabled。EF每次SaveChanges之前都会自动探测所有实体有没有变化如果你的循环里逐条Add实际开销会随实体数量指数级上升。大量导入时先关掉检测db.ChangeTracker.AutoDetectChangesEnabled false; for (int i 0; i 10000; i) { db.Products.Add(new Product { Name 商品 i }); } await db.SaveChangesAsync(); db.ChangeTracker.AutoDetectChangesEnabled true;真实项目中我见过有人循环Add一万条数据足足跑了30多秒关掉自动检测后压到2秒左右效果非常明显。4.4 事务和并发不是高端技巧是保命技能多个表要么同时成功要么同时失败这种场景必须用事务。EF6里用Database.BeginTransactionEF Core里同样支持await using var transaction await db.Database.BeginTransactionAsync(); try { db.Categories.Add(new Category { Name 日用 }); await db.SaveChangesAsync(); db.Products.Add(new Product { Name 纸巾, Price 3.5m }); await db.SaveChangesAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }并发控制方面最简单可靠的方式是给表加rowversion并发字段。SQL Server里定义一列byte[]类型的Version或TimestampEF自动将其映射为rowversion。当两个用户同时修改同一条记录时后提交的一方会抛出DbUpdateConcurrencyException你就知道这条数据被别人动过了可以提示用户重新加载数据。5. 数据库结构变更以后迁移机制的正确打开方式5.1 别肝数据库表迁移才是正路项目迭代到中期实体类必然会加字段、改长度、加索引。如果你直接去SQL Server Management Studio里手改表结构代码倒是能跑一阵但EF框架内部维护着一个模型快照ModelSnapshot只要你改了实体EF就知道模型和数据库对不上运行时直接给你抛模型已更改的异常。正确做法永远走迁移。每改一次模型执行一次Add-Migration再Update-Database。比如给Product加一个Stock字段public int Stock { get; set; }然后Add-Migration AddStockToProduct Update-Database这样EF会生成一个包含ALTER TABLE语句的迁移文件数据库结构就跟着代码走了。5.2 Add-Migration生成的文件里藏着什么迁移文件分三部分Up方法写的是正向迁移的变更Down方法写的是回退。比如加了Stock字段Down方法里就会删掉这一列。这意味着你的数据库结构是可以向前向后任意迁移的团队里任何一个人拉代码后执行Update-Database就能得到和开发环境一致的数据库结构。不过有一点要注意迁移文件的顺序很重要EF按时间顺序应用迁移不要随便删改已经应用过的迁移文件。如果迁移还没上线你想改模型重新生成可以直接删掉该迁移和数据库里对应的版本记录再重来如果已经部署到生产了那就只能继续追加新迁移不能回头改旧的这是原则。5.3 生产环境更新用脚本而不是直接连库执行生产环境的数据库一般不会允许你直接从Visual Studio连上去Update-Database正规一点的操作是导出SQL脚本交给DBA审核执行Update-Database -Script -SourceMigration Init -TargetMigration Latest这个命令会把从Init迁移到最新版本的所有SQL脚本生成出来不连接数据库。也可以简写成Update-Database -Script在有迁移历史记录的库上它会输出增量变更脚本。线下系统我还会配合使用EnsureCreated和Migrate的区别说明EnsureCreated适合一次性创建数据库的极简场景比如本地demo它完全不记录迁移历史后面加字段也不会自动更新而Migrate则是正式项目应该使用的初始化方式它会应用所有迁移并维护版本表。生产环境务必用Migrate或脚本更新而不是EnsureCreated。6. 实战里我和EF框架交过手的几个经典问题6.1 自数据库创建后已更改这个报错的真相这是EF6里最著名的报错之一自数据库创建后模型是否有更改。触发原因基本就是数据库表结构和模型不一致而EF6默认用一张EdmMetadata表记录模型哈希值一旦哈希对不上就直接拒绝干活。解决方案不是禁用检查而是重新走一次迁移让模型和库对齐。如果是在EF Core里同样的问题表现稍有不同但排查思路一致先对比实体类和数据库表找出新增的字段或移除的列再补一条迁移。6.2 未能加载文件或程序集EntityFramework怎么查这类报错大多出现在EF6项目里项目能编译但一运行就挂。大概率是NuGet包没装全或者配置文件缺少entityFramework节。EF6默认会在App.config/Web.config里写入这样一段entityFramework defaultConnectionFactory typeSystem.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework / /entityFramework如果没有这段并且你的项目中也没显式指定连接工厂运行时就可能找不到程序集。最稳妥的修复方式卸载EntityFramework包重新安装让系统自动生成配置。如果是EF Core项目更有可能是数据库提供程序包没装比如你只装了Microsoft.EntityFrameworkCore却用了SqlServer连接运行时报Unable to find provider。6.3 连接字符串没生效数据库连到了奇怪的地方我见过很多次这种情况明明在appsettings.json里写了连到生产库的字符串程序却连到了本地LocalDB。排查思路很直接先看Program.cs里UseSqlServer是否显式传入了连接字符串名称比如builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection)));注意CreateDbContext或Program.cs里注入时用的字符串名称必须要和配置文件里的Key完全一致大小写也得对。另外EF Core在设计时迁移比如Add-Migration时会尝试读取连接串如果读取不到或名称不对它会默认选择一个错误的数据源。最简单粗暴的验证方法是在DbContext的OnConfiguring里临时写一个固定的连接串跑通流程确认问题出在配置读取后再改回依赖注入的写法。6.4 导航属性明明写了virtual懒加载还是不生效EF6里使用懒加载有两个条件导航属性必须标记为virtual并且DbContext配置了LazyLoadingEnabled。EF Core从3.0开始默认就关闭了懒加载需要额外安装Microsoft.EntityFrameworkCore.Proxies包并显式开启UseLazyLoadingProxies。optionsBuilder.UseLazyLoadingProxies() .UseSqlServer(connectionString);但我的实际建议是新项目不要依赖懒加载。懒加载虽然在访问属性时自动加载数据很爽但实际上是在隐藏查询行为稍不留神就产生N1查询问题。与其等坑出现再优化不如从一开始就用Include或Select显式加载你真正需要的数据。关闭懒加载之后代码逻辑反而清晰因为每次数据库访问都在你能看见的地方。6.5 批量插入几千条数据越插越慢除了前面提过的AutoDetectChangesEnabled执行批量插入时还有一个隐藏开销每次Add后DbContext会实时计算关系修复操作逐条Add上几千条数据这部分消耗同样不小。除了关掉自动检测更彻底的办法是改用EF Core 7及以上版本引入的ExecuteDelete和ExecuteUpdate这类批处理API。比如批量更新价格await db.Products .Where(p p.CategoryId 1) .ExecuteUpdateAsync(setters setters.SetProperty(p p.Price, 9.9m));这种操作绕过DbContext的跟踪机制直接生成一条UPDATE语句发到数据库不加载实体也不逐条SaveChanges几十万条数据的更新也只要几秒钟。6.6 迁移历史表不一致导致Update-Database失败团队协作时迁移版本和数据库里实际已应用的版本对不上是Update-Database报错的高发区。检查对策分两步第一步在SQL Server里看__EFMigrationsHistory表里面记录了所有已执行的迁移ID第二步在项目迁移文件夹里对比迁移文件找出本地有但数据库没记录的那个版本确认是别人提交的新迁移就继续执行如果是自己本地生成的重复迁移就要考虑删除或者手动补一条History表记录。这里有个容易误操作的点手动往__EFMigrationsHistory表插记录来欺骗系统认为某个迁移已经执行过。偶尔遇到紧急情况可以临时用但事后必须补上对应的表结构变更否则生产环境迟早暴雷。我个人的原则是迁移历史必须和数据库结构完全一致宁可重来也绝不滥改历史表。最后想提醒一句EF框架用到后面会发现真正决定你用得顺不顺手的其实不是ORM本身而是你对数据库的尊重程度。该建的索引要建该收敛的查询一定要收敛该写迁移就写迁移别拿手改数据库当捷径。把这套习惯养成了无论项目换到EF6还是EF Core你都能比绝大多数人少踩一半的坑。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →