ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EF Core核心原理与高频面试题深度解析:从DbContext到性能优化

EF Core核心原理与高频面试题深度解析:从DbContext到性能优化 1. 为什么我劝你认真对待EF这门技术先说句实话很多人在简历上写着“熟练使用Entity Framework”但对着一道“为什么不要在每个请求中都new一个DbContext”的面试题就开始支支吾吾。这不是个别现象而是国内.NET开发者一个普遍的老毛病——会用但不理解原理。而EF最怕的就是“不理解原理”一旦你理解错了它的工作机制生产环境报错、性能崩盘、数据错乱都是迟早的事。EFEntity Framework是.NET生态里最主流的ORM对象关系映射框架它让你用操作普通C#对象的方式去操作数据库。简单说以前你写SqlConnection、SqlCommand、DataReader现在你直接用context.Users.Where(u u.IsActive)就完成了一次查询。EF负责把Lambda表达式翻译成SQL把查询结果物化成对象跟踪对象状态并在你调用SaveChanges()时生成对应的Insert/Update/Delete语句。这套东西能做什么省掉了大量手写SQL和DataReader取值赋值的繁琐代码让数据层的开发效率提升一个量级。它适合谁从刚入门.NET的初学者到带团队的技术负责人只要你写业务代码、访问数据库EF就是你绕不开的核心技能。尤其是EF Core作为微软官方主推的数据访问层方案在.NET 6时代几乎成了默认选择——新项目不选EF Core反而是需要解释的事。我写这篇内容的目的很直接把你必须知道的EF核心知识和常见的坑一次性讲透并且把网上反复出现的“EF Core面试题”背后的原理掰开揉碎讲清楚。因为你会发现面试题从来不是靠背能过的它考的就是你对ORM本质的理解深度。2. 先搞清楚EF Core到底替你做了什么2.1 ORM的核心价值与EF的工作方式我们不妨退回一步想为什么需要ORM因为数据库的世界和C#的世界是两套结构。数据库讲究表、行、列、主键外键、事务C#讲究类、属性、对象引用、集合。手写ADO.NET时你得在两层世界之间手动搭桥把DataTable里的每一列取出来塞进对象的属性反过来把对象的属性拆成一堆参数填进SQL命令。这种桥接代码写多了之后有一个致命问题一旦表结构变化所有取值赋值的代码都要跟着改动而且编译器不帮你查错。EF的价值就是把这座桥自动化了。你定义实体类Entity配置映射关系Fluent API/Data AnnotationEF在运行时通过表达式树分析生成SQL通过反射和动态代理实现状态跟踪。举个例子var users db.Users.Where(u u.Email.StartsWith(admin)).ToList();这一行代码背后发生的事情是EF先把u u.Email.StartsWith(admin)这个表达式树拆解开识别出你要查询Users表过滤条件是Email LIKE admin%然后生成参数化SQL发给数据库。返回数据后EF检查当前DbContext的跟踪器中是否存在相同主键的对象实体如果存在就复用现有实例如果不存在就创建一个新实例并加入跟踪器中。理解这个流程你才能理解后面所有坑的根源EF不是简单的SQL执行器它是一套带状态管理、身份解析、变更追踪的完整框架。2.2 EF6与EF Core到底怎么选别再用老眼光看新项目很多老程序员对EF6有很多年的感情因为它在.NET Framework时代确实稳定可靠EDMX实体数据模型可视化设计器当年也算一大亮点。但从.NET Core 3.0之后EF Core已经成了微软的主推方向EF6虽然可以做兼容性维护但新功能完全不再投入了。两者几个关键区别你可以记一下维度EF6EF CoreEDMX可视化设计器支持不支持官方推荐Code First懒加载默认支持默认不开启要配置UseLazyLoadingProxies异步查询支持有限全面支持async/await批量更新删除只能先查出来再逐条改支持ExecuteUpdate/ExecuteDeleteEF Core 7平台支持主要为.NET Framework.NET Core/.NET 5跨平台迁移机制有有但命令不同dotnet ef migrations add我的建议很清楚新项目一律EF Core除非你有老系统强依赖EDMX或者第三方插件必须EF6。还有一点很多同学问.NET Framework的项目能用EF Core吗答案是能但需要EF Core 2.x这类老版本新版本不支持.NET Framework了所以如果你是维护老系统别硬上EF Core 3.0老老实实EF6或者干脆ADO.NET都更稳妥。2.3 DbContext最容易被误解的“重量级”对象DbContext是EF Core的工作单元和对象跟踪器的综合体。很多人把它理解成数据库连接这是大错特错的。数据库连接只是DbContext内部管理的一个资源DbContext还负责更多它维护缓存、变更跟踪器、状态管理器、模型配置等。所以DbContext的创建和销毁是很有讲究的操作。它是轻量创建、重量状态。说轻量创建是因为new DbContext(options)本身开销很小但一旦开始跟踪实体它的内存占用和内部复杂度会随跟踪数量快速上升。我强烈建议你记住两个原则一是DbContext不能跨线程共享对Async方法尤其要小心二是DbContext生命周期应当和工作单元保持一致一般来说请求来的时候创建请求结束后释放。如果你把DbContext做成静态单例恭喜你你会遇到并发冲突、连接池爆掉、内存泄漏等各种惊喜如果你在每个请求里new了十几个DbContext虽然不会出大问题但会失去跨实体的统一缓存和事务协调性能也会白白浪费。3. 必须掌握的EF Core配置与迁移实战3.1 从零开始配置一个规范的DbContext假设我们要做一个简单的博客系统包含Blog和Post两个实体典型的配置过程如下首先定义实体public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public ListPost Posts { get; set; } new ListPost(); } public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }然后定义DbContextpublic class BloggingContext : DbContext { public DbSetBlog Blogs { get; set; } public DbSetPost Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(Server.;DatabaseBlogDb;Trusted_ConnectionTrue;TrustServerCertificateTrue;); } }这就跑起来了。但请注意生产环境千万不要在OnConfiguring里硬编码连接字符串应该用AddDbContext注册到DI容器里从配置中心读取这样方便后续做数据库切换、连接字符串加密、覆盖测试等。项目里比较推荐的做法是在Program.cs中builder.Services.AddDbContextBloggingContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(Default)));这里有三个细节值得注意第一UseSqlServer不仅是切换数据库驱动它还会影响某些SQL生成规则所以如果你从SQL Server切换成PostgreSQL不能只改连接字符串还要替换UseNpgsql()第二AddDbContext默认注册的是Scoped生命周期正好契合一个请求一个上下文的推荐模式不要改成Singleton第三如果你用了Identity或第三方库要留意它们是否已经注册了其他DbContext避免重复注册导致异常。3.2 迁移把你的实体模型变成数据库结构的正确姿势很多人习惯在数据库里直接建表然后用数据库反向生成实体这对于小项目来说可行但对协作团队或者需要持续集成、多环境发布的系统来说就是个灾难。因为数据库Schema的变化没有版本控制你根本不知道哪个环境是哪个版本加了字段也说不清是谁加的。EF Core提供了Migration机制来解决这个痛点。每次模型变更后运行dotnet ef migrations add AddPostTitleLengthEF会对比当前模型和上一次迁移记录的快照自动生成一个包含Up和Down方法的C#文件。然后dotnet ef database update就会按顺序执行所有未执行的迁移更新数据库Schema。这里有几个莫名其妙的坑我要帮你踩平迁移文件不要删不要改已发布过的文件。你可以做新的迁移但不要试图修改已经应用过的Up/Down因为一旦有同事已经在本地或测试环境应用过旧迁移你改掉它会导致两边“迁移验证失败”的异常。真需要改就新加一个迁移。使用dotnet ef命令之前先确认文件路径。命令默认从当前目录找Program.cs和Startup.cs如果你的启动项目在别的目录要指定--project和--startup-project否则会报Unable to create a DbContext...这种让人一头雾水的错。生产环境建议把dotnet ef database update换成SQL脚本方式因为生产数据库通常没有权限直接执行EF命令也没人愿意在生产上临时装SDK。正确做法是用dotnet ef migrations script -o upgrade.sql生成SQL脚本再通过数据库发布工具执行。3.3 种子数据怎么写才能避免“重复插入”问题EF Core的HasData可以用来给迁移加种子数据。它本质上是让EF把这些数据作为迁移的一部分插入而不是在运行时插入。好处是数据随数据库版本走坏处是每次新增迁移都会对比已有数据容易因为主键变化引发重复冲突。我遇到过的最诡异问题修改一个已有实体的主键值后运行迁移EF生成的HasData试图插入旧主键的数据结果数据库已经存在该键就报了约束冲突。解决办法是在新的迁移中先删除旧种子再重新插入或者干脆不用HasData改在Program.cs里通过检查if (!context.Blogs.Any())再AddRangeSaveChanges这样逻辑更可控。4. 查询与跟踪决定EF性能高低的胜负手4.1 延迟执行不是Bug是被用错了场景EF Core的查询是延迟执行的var query context.Blogs.Where(b b.IsActive);这一段代码只是构建了一个IQueryable不会真正访问数据库。只有当你执行ToList()、FirstOrDefault()、Count()、foreach等操作时SQL才会被发送到数据库。这个特性的好处是你可以一步步拼接查询条件而不产生额外数据库访问。典型例子IQueryableBlog query context.Blogs; if (!string.IsNullOrEmpty(name)) query query.Where(b b.Name.Contains(name)); if (pageSize 0) query query.Take(pageSize); var result await query.ToListAsync();但很多新手把它误解成“EF会在后台自动把数据加载到内存”于是出现这样的写法var blogs context.Blogs.Where(b b.IsActive); foreach (var blog in blogs) { Console.WriteLine(blog.Posts.Count); // 这里可能触发N1 }这段代码对Blogs只查了一次但访问blog.Posts.Count时如果没配置Include且懒加载未开启就会为每个Blog触发一次额外的SQL查询。N1问题就来了。4.2 Include、ThenInclude和AsSplitQuery怎么组合才不会翻车处理关联数据最常见的是用Includevar blogs await context.Blogs .Include(b b.Posts) .ThenInclude(p p.Comments) .ToListAsync();默认情况下EF Core对多层级Include会生成JOIN语句问题在于多个集合Include时会产生笛卡尔积——假设一张Blog有20个Post每个Post有30个Comment你一次查出来的行数就是2030600行而实际数据量可能只有202030620个实体行。为什么说“可能”因为EF会把每一行的所有字段重复返回网络传输和内存开销都被放大了。EF Core 5开始提供AsSplitQuery()var blogs await context.Blogs .Include(b b.Posts) .ThenInclude(p p.Comments) .AsSplitQuery() .ToListAsync();它会将查询拆成多个单独的SQL查询每个查询只加载一个层级的集合然后在内存中将它们关联起来。这能避免笛卡尔积但代价是多次数据库往返。如果层级少、数据量小用默认JOIN更合适如果层级多、数据量大用AsSplitQuery几乎总能显著降低数据传输量。我自己的经验是不要盲目全用AsSplitQuery先看查询日志中的SQL行数和耗时。如果你有SQL Server Profiler或者EF Core的日志输出打开看看每个查询实际返回的行数最直观。4.3 IQueryable与IEnumerable一字的区别性能的鸿沟这是面试里出现频率极高但错误率也极高的题。IQueryable在DbContext上返回时它背后有表达式树EF可以把它翻译成SQL所以Where/Select会下推到数据库执行。而如果你对DbSet先调用ToList()得到ListT这个对象就是IEnumerable后续的Where/Select都是在内存里执行。举个例子var activeBlogsFromDb await context.Blogs.Where(b b.IsActive).ToListAsync(); // 正确SQL过滤 var allBlogs await context.Blogs.ToListAsync(); var activeBlogsInMemory allBlogs.Where(b b.IsActive).ToList(); // 错误把全表数据拉到内存再过滤如果你表里有10万条数据前者只返回10条后者先把10万条全部捞到内存性能差距是巨大的。所以在写EF查询时凡是条件、排序、分页、投影都应该尽量在IQueryable阶段完成再用ToListAsync物化。4.4 AsNoTracking到底什么时候用EF Core默认对查询返回的实体进行跟踪Tracking。也就是说实体对象被放进DbContext的ChangeTracker后续如果修改属性再调用SaveChanges()时会自动生成Update语句。但很多时候我们只需要读取数据展示根本不会修改跟踪只会带来额外的内存开销和性能损耗。这时候就可以用AsNoTracking()var list await context.Blogs .AsNoTracking() .Where(b b.IsActive) .ToListAsync();使用AsNoTracking()之后查询出来的实体不会进入状态管理修改属性也不会影响到数据库。对纯查询场景性能提升非常明显尤其是大批量只读查询。不过要小心一个陷阱如果之后你需要修改这些无跟踪实体并保存不能直接调用SaveChanges()因为EF不认为它被跟踪。你需要先Attach或重新查询后才能修改保存。所以我一般是这样用的用于展示的列表数据全部AsNoTracking用于编辑保存的数据使用跟踪。4.5 懒加载能不开就别开开了要付“越用越多”的债懒加载是在访问导航属性时才加载相关数据示例配置如下builder.Services.AddDbContextBloggingContext(options options.UseLazyLoadingProxies().UseSqlServer(...));实体类需要满足两个条件导航属性必须是virtual类不能是sealed。EF Core利用动态代理在运行时创建子类重写virtual属性实现懒加载。懒加载初看很方便因为它避免了手动写Include。但它最大的代价是让SQL查询变得不可控因为你永远不知道一次访问会触发多少条SQL。尤其在循环里访问导航属性N1问题会极难排查因为每一条额外SQL都嵌套在业务逻辑深处。而且懒加载的代理还会产生额外类型、额外初始化开销在序列化时也容易引发循环引用的栈溢出错误。我的态度很明确新项目默认不开懒加载或者仅在一个非常明确的小范围内使用。把导航属性加载方式显式写在查询里才能掌控每一条SQL。5. 跟踪、状态与SaveChanges的运行内幕5.1 实体状态机EF如何知道你改了哪些数据调用SaveChanges()时EF并不是对比数据库中的旧数据而是靠实体当前状态来判断该怎么生成SQL。实体有四种状态Detached未被跟踪、Unchanged被跟踪但未修改、Added已标记为新增、Modified已修改、Deleted已标记删除。当你执行var blog await context.Blogs.FirstAsync(b b.Id 1);后这个实体是Unchanged。然后你修改blog.Name new name;ChangeTracker在属性赋值时检测到值变化自动把状态变成Modified。调用SaveChanges()时EF检查所有状态为Modified的实体和它们修改过的属性只为这些属性生成SET子句。这就是为什么EF能感知“你改了哪些字段”——它靠的是快照比较。在实体从数据库加载时EF为每个属性保存一份原始值之后供状态检测使用。如果实体属性特别多快照机制会占用一定内存这是合理代价。当然你也可以用Property(x x.Name).CurrentValue手动获取修改前后值。5.2 控制状态Attach、Entry与Remove的使用场景有时候你会遇到需要更新一只“游离”的实体从前端收到的DTO转成的实体对象。直接把这个实体传给DbContext并调用SaveChanges()不会起作用因为DbContext不认识这个实体也没有它的状态。典型操作是var existingBlog new Blog { Id 1, Name updated }; db.Attach(existingBlog); // 状态变为Unchanged db.Entry(existingBlog).Property(b b.Name).IsModified true; // 把Name标记为Modified await db.SaveChangesAsync();或者直接db.Blogs.Update(existingBlog); // 会把所有属性都标记为Modified await db.SaveChangesAsync();Update好用但有毒它会把所有列都更新一遍包括你根本没改的字段这样会带来不必要的并发冲突和浪费。如果你只想更新名称这一个字段用Entry方式会生成更精确的Update SQL只更新需要的列。这个细节面试官很喜欢问“为什么Update整实体不好”你要能说出这个本质。5.3 SaveChanges失败怎么办事务与异常后的状态恢复SaveChanges内部是开启了一个数据库事务的它会按顺序执行所有挂起的Insert/Update/Delete操作任何一个操作失败整个事务回滚数据库不会落入半更新状态。但你还要明白EF的ChangeTracker状态不会因为失败而自动回滚。比如你Add了一个实体然后SaveChanges抛异常这个实体仍然处于Added状态你下次调用SaveChanges时还会再次尝试插入。这就让很多人困惑为什么异常之后重新SaveChanges竟然又报同样的错解决办法一般是在异常捕获后将原有DbContext丢弃掉重新new一个DbContext或者使用依赖注入提供的Scoped实例重新进入新请求。多数项目里一个请求一个DbContext所以异常后整个请求失败上下文被销毁问题不明显但如果在一个后台任务里用同一个长生命周期DbContext做多次SaveChanges这种残留状态会让你抓狂。我的经验是在后台批处理任务里要么每处理一条数据就new一个轻量DbContext要么捕获异常后重置DbContext通过清除ChangeTracker的方式。db.ChangeTracker.Clear(); // 清空跟踪状态不推荐的取而代之重新new一个更简单但Clear()只能清空状态DbContext内部其他缓存未必干净所以更稳妥还是短生命周期。6. 并发控制不能不知道的乐观并发6.1 为什么你的更新覆盖了别人的修改假设两个人同时编辑同一篇博客A把标题改成“EF最佳实践”B把内容改成“全文完”。A先保存成功B随之保存如果没有任何并发控制B会用自己读取时的那份数据整体覆盖A的修改造成丢失更新。这个问题面试必问“EF Core怎么处理并发冲突”EF Core提供的方案是乐观并发默认是“最后写入者胜”要让EF知道某列值变化了就算冲突你必须给该列配置并发令牌ConcurrencyToken。public class Blog { public int Id { get; set; } public string Name { get; set; } [ConcurrencyCheck] public int Version { get; set; } }更常见的做法是使用rowversionSQL Server的时间戳public class Blog { public int Id { get; set; } public string Name { get; set; } [Timestamp] public byte[] RowVersion { get; set; } }在更新时EF会把RowVersion作为Where条件的一部分如果数据库中的RowVersion与实体加载时的RowVersion不同说明数据已经被其他事务修改EF会抛出DbUpdateConcurrencyException。6.2 捕获并发冲突后的正确处理流程发生并发冲突后你不能只是默默吞掉异常要做的是重新加载数据库中的最新值让用户决定如何处理catch (DbUpdateConcurrencyException ex) { var entry ex.Entries.Single(); var databaseValues await entry.GetDatabaseValuesAsync(); var clientValues entry.CurrentValues; // 你可以将数据库值合并到客户端或者提示用户覆盖 entry.OriginalValues.SetValues(databaseValues); // 刷新原始值 }这里有几个经验点OriginalValues.SetValues(databaseValues)会把数据库最新值填充为原始值此时你再调用SaveChangesAsync就相当于用自己的新值覆盖数据库的新值实现“用户强制覆盖”的语义。如果你不想覆盖直接丢弃客户端本次修改即可。6.3 高并发场景下对并发令牌的补充建议RowVersion不是银弹。在高频更新的表上每个字段的并发冲突风险不同如果整行RowVersion一冲突就全部拒绝用户体验会非常差。更精细的做法是使用IsConcurrencyToken只针对个别字段modelBuilder.EntityBlog() .Property(b b.Name) .IsConcurrencyToken();但这样EF只会检查Name列在更新时是否被改过。还有一个实际经验分布式系统里如果数据库层面使用UPDATE ... WHERE Version oldVersion这种模式效果和EF的乐观并发是一样的。在分库分表场景下行版本号往往不足以覆盖跨库数据一致性这时候就需要引入分布式锁或者业务层的乐观锁方案面试时如果能提到这一点会明显加分。7. EF Core性能调优的几条硬经验7.1 查询只用需要的字段Select投影是免费的午餐很多人习惯把整实体查出来再取字段比如列表页只需要显示博客名称和创建时间却把整个Content内容都查了出来。数据量大时这会浪费大量网络流量和内存。正确做法是使用Select投影var list await context.Blogs .Where(b b.IsActive) .Select(b new BlogSummaryDto { Id b.Id, Name b.Name, CreatedAt b.CreatedAt }) .ToListAsync();因为EF会把你选择的字段生成到SQL中数据库只返回这些列。而且投影出来的DTO默认不跟踪减少跟踪器开销。这可以说是性价比最高的一项优化但很多老手都懒得改宁可把所有列拿到前端再过滤非常不推荐。7.2 分页写不好产品上线就被举报“接口好慢”分页最经典的问题不是不用Skip/Take而是忘记了“排序后再分页”。在SQL Server里ORDER BY在OFFSET/FETCH之前必须存在否则EF会按数据库默认顺序取数据结果导致分页数据重复或缺失。推荐写法var page await context.Blogs .AsNoTracking() .OrderBy(b b.CreatedAt) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToListAsync();同时要避免的坑是如果你使用Skip/Take前没有OrderByEF Core会直接在SQL中发出ORDER BY (SELECT 1)数据库靠物理顺序返回数据一旦数据插入顺序变化分页结果就不稳定。所以无论你的需求怎么描述分页查询必须带stable sort key通常就加一个Id也可以。再补充一个EF Core 6之后的内置分页方法Paginate不过它属于Microsoft.EntityFrameworkCore扩展用起来和Skip/Take差不多有兴趣可以了解。7.3 不要忽略数据库索引对EF查询的影响EF不会自动为你创建索引你要在迁移中手动指定modelBuilder.EntityBlog() .HasIndex(b b.Url) .IsUnique();设计索引有一个原则索引必须和查询条件、排序规则匹配。EF Core 8甚至引入了Complex类型的复合索引支持但基础原则不变。很多性能问题的根因不是EF写出低效SQL而是数据库没有匹配的索引导致每次查询都是全表扫描。我见过一个典型案例一个表500万行查询条件是WHERE Type1 AND CreatedAt xxx建了(Type, CreatedAt)复合索引后查询时间从3秒降到30毫秒。所以别忙着用各种高级查询技巧先打开SQL日志看看执行计划索引才是第一生产力。7.4 EF Core日志与SQL监听你不看它它出问题你都不知道EF Core的日志是排查问题最直接的入口。简单配置一下builder.Logging.AddFilter(Microsoft.EntityFrameworkCore.Database.Command, LogLevel.Warning);这样就不会被大量日志刷屏只在SQL命令异常或慢命令时输出。如果你想让EF在日志中输出执行的SQL语句把过滤级别改成LogLevel.Information然后看控制台。注意信息级别的日志包含SQL参数值和语句发布生产前要确认是否可能泄露敏感数据或者直接禁止输出到日志文件。另一个工具是ToQueryString()可以输出当前IQueryable对应的SQL字符串var sql query.ToQueryString(); Console.WriteLine(sql);调试时相当好用。8. EF Core高频面试题精讲从原理到话术8.1 “EF Core的IQueryable和IEnumerable有什么区别”——高频中的高频既然面试题热词包含了“EF Core面试题”我把这几年我面试别人和被别人面试时遇到的经典题目整理一下。第一个就是IQueryable和IEnumerable的区别。标准回答思路两者都表示序列但IQueryable是可组合查询表达式树的入口EF Core能够通过表达式树将查询翻译成SQLIEnumerable则是在内存中迭代数据对应LINQ to Objects操作。所以你在DbSet上调用Where时Where不会被立即执行而是构建表达式树一旦ToList()之后后续再调Where就是纯内存过滤。正确的优化是尽可能把过滤、排序、分页写在IQueryable层面让SQL帮我们干活。注意面试官还喜欢追问“如果非要让EF生成两次查询条件怎么办”这就需要你回答AsQueryable()只能用在已经翻译过的查询之上并不是万能开关核心还是理解表达式树。8.2 “为什么DbContext不能是全局单例”——考的是生命周期认知很多人答“因为线程安全问题”这算对一半。真正的原因更严重DbContext内部有ChangeTracker它维护着一组实体的状态当你用单例DbContext时所有请求共享同一个ChangeTracker不同用户改同一个实体就会互相干扰同时DbContext内还有第二级缓存和连接管理长时间不释放会导致连接池耗尽、内存膨胀。EF官方明确说DbContext是“轻量级对象设计为Scoped生命周期”。所以最安全的答案要包含线程不安全、状态跟踪共享会导致脏数据、资源泄露。如果有余力还可以提一下“查询结果在同一个DbContext内因为Identity Resolution会返回同一个实例即使数据库里的值已经变了”。这知识点相当能体现深度。8.3 “SaveChanges为什么不生成Update语句”——追踪机制的细节这是一道很多人会答歪的题。代码可能是这样的var blog await db.Blogs.AsNoTracking().FirstAsync(b b.Id 1); blog.Name Hello; await db.SaveChangesAsync();你发现SaveChanges()没生效。原因是AsNoTracking()查询出的实体没有进入ChangeTrackerEF根本不知道这个实体的Name变了自然不生成Update。解决办法要么去掉AsNoTracking要么手动Attach后标记Property修改。这个题考的就是状态跟踪机制。8.4 “你如何排查EF Core性能问题” ——开放性大题的加分答法面试官如果问这种题重点不是让你背工具名而是想看你有没有实际线上排查经验。我个人建议按以下顺序回答打开EF Core日志记录所有SQL命令执行时间和参数化情况用ToQueryString()快速查看单条查询的SQL确认有没有多余的列、多余的JOIN、缺失的WHERE条件用SQL Server Profiler或数据库执行计划查看索引扫描 vs 索引查找分析是否有N1查询即一个主查询结束后又循环执行了大量子查询常见于未使用Include或懒加载使用AsNoTracking优化纯读场景使用投影减少字段返回使用AsSplitQuery避免笛卡尔积对于复杂统计考虑能不能用数据库端聚合替代内存聚合比如Count()、Sum()放到查询表达式里。8.5 “有哪些EF Core开发中的良好实践”——技术深水区这类题的通用答法是把日常开发原则总结下来约定式命名规范主键Id、外键导航属性等、DbContext在DI中注册为Scoped、所有异步API都要用Async版本、查询结果只返回必要字段、避免懒加载在循环中触发、迁移纳入版本控制、上线前用SQL脚本先审阅等。最后千万加上一句“具体项目中有很多取舍比如小项目可以简化分页方案大型系统可能要用读写分离或分布式缓存EF Core本身并不万能”。这会让面试官觉得你不是只背条款而是有真实权衡能力。9. 我在真实项目里最常用的一套EF Core落地模板最后分享一个我实际使用很久的项目模板骨架也算是我个人的经验总结。首先我会定义一个泛型仓储接口吗不一定。很多团队习惯给每个实体写一个Repository但EF Core的DbSet本身就是仓储DbContext本身就是工作单元。再包一层泛型仓储很多时候只是为了把接口和数据访问解耦但带来的抽象成本也不少你要写paging、condition、sort的泛型包装又要处理Include的传递最后经常出现“写Utility比写业务还累”的尴尬。我现在的做法是核心业务代码直接使用DbContext仅在对数据访问做隔离测试时才考虑轻量仓储而且仓储接口只暴露业务需要的具体查询方法不做万能泛型。这样最简洁也好维护。其次我会把连接字符串、数据库Provider类型放到配置文件中便于不同环境切换。迁移脚本定期用dotnet ef migrations script生成提交到Git中供DBA审阅。最后我建议所有团队都建立一条性能红线单个请求内的SQL次数不得超过某个值比如10次超过必须走专门Review单个SQL返回行数不得超过多少行比如5000行超过必须分页或加筛选。EF Core是强大的工具但正因为强大所以容易滥用红线的存在是保护自己和同事少熬夜。我在实际项目中被坑得最惨的一次是某个报表功能用了三层Include默认JOIN查询数据量一大SQL Server返回了超过一亿行的笛卡尔积。那次我盯着数据库监控傻了很久最后才意识到是Include组合的问题。十几次调试后换了AsSplitQuery再把报表查询改成只Select必要的DTO性能直接提升了20倍。从那以后我对每个EF查询都会多问一句“你真的需要这些导航属性吗真的需要加载所有列吗”——这就是我写这篇内容最大的动力。如果你正在准备面试或者正在团队里推广EF Core规范建议把这里的章节对照自己的代码过一遍尤其是查询跟踪、分页和并发控制这三个点是线上问题高发区。而理解了EF的工作原理之后你会发现面试题其实没有那么多“神秘套路”它考的就是你是否认真了解过你天天在用的框架。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进