2026 年的 .NET 面试长什么样
2026 年的 .NET 面试已经没人问”什么是依赖注入”了。现在考官问的是”你把 scoped 服务注入到 singleton 里,开发环境正常、生产环境数据随机错误,问题在哪?”
面试已经从定义考试变成了场景诊断。考官关注的是你能不能把一个问题的根因讲清楚、能不能说出取舍、能不能给出默认推荐并解释何时偏离。
这张页面是整个 .NET 面试准备的枢纽。下面先讲四轮面试结构,然后按专题提供 20 道跨领域真题——每道题都带 red flag 答案和考官最常接上的追问。每个专题我还有完整深度页面,目前 EF Core(30 题)和 Web API(45 题)已经上线,其余专题会陆续发布。
所有内容以 .NET 10 和 C# 14 为准。
.NET 面试的四轮结构
大多数产品公司的 .NET 面试跑四轮:
1. 筛选项 / 基础轮——C# 基础、值类型 vs 引用类型、async/await、LINQ。这轮是过滤不会写代码的人。定义在这轮还能接受,但好的筛选项已经是场景题了。
2. 技术深挖轮——ASP.NET Core 内部机制、EF Core、你实际会用的框架。这是场景题的主场,也是大多数候选人定胜负的轮次。
3. 系统设计轮——“设计一个限流器”、“设计一个通知服务”。考察你对规模、取舍和故障模式的推理能力。中高级岗位权重很高。
4. 行为 / 判断轮——“讲一次你处理过的线上事故”、“哪次你反对了一个技术决策”。考察 ownership 和沟通能力。
跨所有轮次,区分强候选人的特征只有一个:他们不背诵,他们推理。 先说默认做法,再说代价,再解释何时偏离。全文的题目都是围绕这个习惯设计的。
跨专题真题 20 道
C# 语言
Q1. Record 和 Class 有什么区别?什么时候用 Record?
record 是引用类型,和 class 一样,但编译器额外给了它基于值的相等比较、内置的 ToString、以及 with 表达式拷贝。两个属性值相同的 record 实例相等;两个 class 实例不相等(除非你自己 override Equals)。
当类型的定义由数据驱动且基本不可变时,用 record——DTO、API 请求/响应模型、值对象、事件。当类型有独立标识和可变行为时,用 class——EF Core 实体、服务类。
红牌回答: “Record 就是 class 的简写语法。“——完全没理解基于值的相等语义,而这才是 record 存在的全部意义。
Q2. 值类型装箱时发生了什么?为什么要在乎?
装箱把值类型(int、struct)包进堆上的一个对象里,让它能当引用类型用。它分配内存;拆箱再把值拷出来。在热循环里,反复装箱产生的 GC 压力会表现为延迟尖峰。
它发生的比人想象的多:把 struct 存进非泛型集合、把 struct 传给预期 object 或接口的参数、用字符串插值格式化值类型。在高吞吐 API 里这是隐形的分配源,泛型或 Span<T> 通常能避免。
红牌回答: “装箱无所谓,GC 会处理。“——等你给热路径做了 profiling,发现一半分配来自装箱,就知道有没有所谓了。
Q3. Code Review 里看到 async void,为什么你要标记它?
async void 是你不能 await 的 fire-and-forget。调用方没法知道它什么时候完成、是否失败了。更糟的是,async void 内部抛出的异常不会被捕到返回的 Task 上——它被抛到同步上下文,通常直接崩溃进程。
唯一合法的用途是事件处理器。其余所有地方都应该是 async Task。我在事件处理器以外看到 async void,就默认存在一个未被检测到的异常在等着干碎应用。
红牌回答: “没事,它还是能跑的。“——它能跑,直到它抛异常,而没有任何东西能抓住。
追问: “async void 在什么场景下反而是唯一正确的选择?“
异步与多线程
Q4. 调 Task 的 .Result 或 .Wait() 到底发生了什么?
你阻塞了当前线程,直到 Task 完成——这就把 async 的全部意义丢掉了。在请求处理路径里这比浪费更糟:高负载下你占用线程池线程来等待 I/O,线程池饥饿,即使 CPU 空闲吞吐量也崩了。
这就是线程池饥饿,它通常表现为 API 在轻度负载下很快、真实流量下直接倒下。
我的规则:async 一路到底。如果一个方法调了异步方法,它自己在链上也返回 Task 并被 await。不用 .Result 来桥接同步和异步。
红牌回答: “我不想把方法改成 async,就用
.Result。“——这就是你上线一个规模化就挂的 API 的方式。
Q5. Task 和 Thread 的区别是什么?
Thread 是 OS 级线程——重,~1MB 栈,创建昂贵。Task 是更高层的抽象,表示可能跑在线程池线程上的工作、可能在 I/O 上完全不占线程地异步完成、也可能内联执行。现代 .NET 几乎不需要直接创建 Thread,用 Task 加 async/await 让运行时调度。
面试官想听的区别:Task 不是”一个线程”。一个被 awaited 的 I/O 密集型 Task 在等待期间会把它的线程释放回池里,这正是 async 能横向扩展的原因。
红牌回答: “它们一样,Task 就是一个线程。“——那你就没法解释为什么 async I/O 比起线程扩展得更好。
LINQ
Q6. LINQ 查询到底什么时候执行?
LINQ 用延迟执行。构建一个带 Where、Select、OrderBy 的查询并不会跑任何东西——它只是在组合表达式。查询在枚举时才执行:ToList、ToArray、Count、First、或 foreach。在此之前,它只是一份工作描述。
这在两方面很重要。EF Core 里,延迟执行就是让 provider 把整条链翻译成一条 SQL 语句的基础。同时也是个陷阱:如果你枚举同一查询两次,它就执行两次,打两遍数据库。如果结果需要多次使用,先 ToList 物化。
红牌回答: “查询在我写
.Where的时候就执行了。“——那你就不理解为什么你的查询打了三次数据库。
ASP.NET Core
Q7. 中间件的正确顺序是什么?Routing、CORS、Authentication、Authorization?
顺序不是装饰——每个中间件依赖于它前面执行了什么。正确顺序是 UseRouting,然后是 UseCors,然后是 UseAuthentication,然后是 UseAuthorization,最后是端点。
认证必须在授权之前跑,因为你不知道用户是谁就没法判断他能做什么。CORS 必须在认证中间件之前,这样 preflight 请求才能拿到它们的头部。
经典 bug 是把 CORS 或认证注册错了位置,得到看起来像框架 bug 的静默 401 或缺失 CORS 头——实际上都是顺序错误。
红牌回答: “顺序无所谓,ASP.NET Core 自己能搞定。“——绝对有关系,这是面试里最喜欢的陷阱。
Q8. 把 Scoped 服务注入到 Singleton 里会发生什么?
这是捕获依赖。Singleton 创建一次、存活整个应用生命周期,所以它捕获 scoped 服务并永远持有同一个实例——尽管 scoped 服务本应每个请求一个。如果那是 DbContext,就是一个 context 被所有请求共享:线程安全违反、脏数据、变更追踪器无限增长。
在 .NET 里,DI 容器在 scope validation 打开时(开发环境默认)会在启动时检测到并抛异常。修复是给 singleton 注入 IServiceScopeFactory,每个工作单元自己创建 scope。
红牌回答: “不会怎样,它本来就能跑。“——demo 里能跑,并发下会搞坏数据。
追问: “内置容器怎么检测这个?什么时候它检测不到?“
EF Core
Q9. 端点返回了 50 条数据,日志显示跑了 51 条查询。怎么搞的?
这是 N+1 查询问题:一条查询加载 50 条父记录,然后 EF Core 每条记录又跑一次查询来加载关联导航属性——50 次额外往返。通常是懒加载导致的,或者你忘了 Include 就迭代了导航属性。
修复用饥加载 Include,或者更好:用 Select 投影只取需要的字段,单次查询、不多取。我也在开发环境打开 EF Core 查询日志,让一波相同的参数化查询立刻可见。
红牌回答: “把连接池大小调大。“——这是放大症状,不是解决原因。
Q10. AsNoTracking 什么时候用,有什么坑?
AsNoTracking 跳过创建变更追踪快照,只读查询省内存省 CPU——所以我对所有不修改结果的查询都用它。面试官要的坑:没有追踪,EF Core 默认不做标识解析,所以同一行在 join 里出现两次会物化为两个独立对象。而且你显然不能对没追踪的实体调 SaveChanges。
红牌回答: “我到处加
AsNoTracking,因为更快。“——那你的更新是怎么工作的?
系统设计
Q11. 为一个多租户 API 设计限流器
先澄清维度:按租户限、按用户限、还是按 API key 限?档次是什么?然后选算法——token bucket 适合平滑突发,fixed/sliding window 适合简单配额——并按租户标识符分区。
面试官听的关键点:内存限流器跨多实例不生效。每个副本独立执行限额,三个实例意味着大约三倍于预期的速率。分布式部署下计数器必须存在共享存储(比如 Redis)。另外返回 429 带 Retry-After 头,让客户端能正确退避。
红牌回答: “我用内置的限流中间件就行了。“——单实例没问题;扩容那一刻限流就静默失效了。
追问: “Redis 挂了,你的限流器怎么处理——fail open 还是 fail closed?“
Q12. 后台任务处理器落后了 200 万条消息,你怎么办?
先区隔紧急和根因。紧急:消费者规模跟得上工作量吗?横向扩消费者,检查处理是 CPU 密集型、I/O 密集型、还是被一个锁或单个分区串行化了。积压往往意味着一个慢的下游依赖,而不是算力不够。
然后是结构性修复:让消息处理幂等,这样能安全重放;下游允许之处做批处理;加死信队列让毒消息不堵住整条线。推理过程比任何一个单一修复更重要——展示你能诊断分布式瓶颈,而不只是”加服务器”。
红牌回答: “多起几个消费者就行。“——可能,但如果瓶颈在下游数据库,更多消费者只会雪上加霜。
Q13. 怎么让一个支付 API 端点幂等?
幂等意味着同一个请求应用两次,效果和一次一样——对支付至关重要,客户端重试不能重复扣款。让客户端在请求里带一个幂等键(GUID)。服务端把键和结果一起存储;如果同一个键再次到达,直接返回已存储的结果,不再处理。
区分强答案的细节:键必须和扣款操作原子持久化,放同一个事务里。如果在扣款和记录键之间崩溃了,重复扣款的窗口就还在。
红牌回答: “插入之前先查有没有已有支付记录。“——这个 check-then-act 在并发重试下有竞态。
架构与判断
Q14. 什么时候你不会用微服务?
大多数团队。微服务解决的是组织和规模化问题——大团队的独立部署和独立扩缩——不是代码质量问题。如果你团队小、领域还在快速变化、没有真正的独立扩缩压力、DevOps 成熟度有限,微服务增加的是分布式系统成本(网络故障、最终一致性、可观测性开销),而收益你暂时不需要。
我的默认做法是模块化单体:一个可部署单元里有清晰的模块边界。你获得大部分分离的好处,只在某个具体、命名的压力迫使时才提取一个服务出去。
红牌回答: “微服务是现代开发的正确方式,永远该用。“——这是简历驱动设计,关于分布式事务的追问通常会直接终结对话。
Q15. CQRS 值不值?什么时候引入?
CQRS——把读模型和写模型分离——当读和写有真正不同的形态或扩缩需求时才值回票价:一个复杂的有业务不变量的写操作领域,加上你想从反范式化或缓存的投影来服务的高容量读操作。它不是免费的,增加了活动部件,而且两端之间往往是最终一致性。
我不会在简单 CRUD 服务上引入它。当读和写路径开始互相冲突——查询模型被扭曲去适应命令模型,或者反过来——我才考虑。
红牌回答: “我永远配合 mediator 用 CQRS 实现整洁架构。“——给 CRUD 加间接层不是整洁,是多余。
性能与缓存
Q16. 200 个请求同时命中了一个过期的缓存键,全打到数据库了。这是什么?怎么防?
这是缓存雪崩(thundering herd):一个热门键过期,所有并发请求同时缓存未命中,每个都独立去数据库算出同一个值,正好在缓存本该保护数据库的时刻把负载打上去了。
在 .NET 10 我用 HybridCache,它有内置的防雪崩保护——对同一缺失键的并发请求等同一个计算完成,而不是全部冲向源。在 HybridCache 之前需要自己在缓存缺失路径上做 per-key 锁。
红牌回答: “把缓存过期时间设长一点就行。“——这让雪崩更罕见,但不是不可能,而且服务的数据更旧。
测试与生产
Q17. 怎么对访问真实数据库的 API 做集成测试?
用 WebApplicationFactory 把应用在内存里启动、带真实的请求管线,并通过 Testcontainers 把数据库指向跑在容器里的真实实例——而不是 InMemory provider。InMemory provider 不强制真实的 SQL 行为(约束、事务、并发令牌),所以测试通过、生产挂掉。
这个组合给你的是测试跑在实际请求管线、实际数据库引擎上的效果,每次测试运行启动、结束时销毁。这是能抓到单元测试漏掉那些 bug 的置信度。
红牌回答: “我用 EF Core InMemory provider 做集成测试。“——它的行为和真数据库不一样,所以测试证明的比你以为的少。
Q18. Kubernetes 里 .NET API 的优雅关闭怎么处理?
编排器发 SIGTERM 时,应用应该停止接收新请求、在期限内完成进行中的请求、释放资源。ASP.NET Core 通过 IHostApplicationLifetime 接入这个流程,并遵循关闭超时。后台工作方面,传给 BackgroundService 的 stoppingToken 就是干净收尾的信号。
展示生产经验的细节:readiness probe 应该在关闭前就切为 unhealthy,让负载均衡器提前停止路由流量;关闭超时需要比你最长合理请求更长,否则进行中的工作就会被中途干掉。
红牌回答: “Kubernetes 直接杀 pod 重启,无所谓。“——这会丢掉进行中的请求,破坏未做 checkpoint 的工作。
Web API
Q19. POST 创建了一个资源,同时触发了异步后台任务。你返回什么状态码?
取决于你响应时到底完成了什么。如果资源是同步创建、后台任务是副作用,返回 201 Created 带 Location 头指向新资源。如果整个操作是排队状态、尚未完成,返回 202 Accepted 带一个客户端可以轮询状态的 URL。唯一错误的回答是 200 OK——它没告诉客户端任何关于发生了什么的信息。
红牌回答: “200,因为成功了。“——对创建端点来说,这丢弃了状态码存在的全部语义信息。
Q20. PUT 和 PATCH 有什么区别?你默认用哪个?
PUT 替换整个资源——客户端发送完整表示。PATCH 做部分更新——只改变化了的字段。PATCH 听起来更好但加了实际的复杂度:你得区分”字段设为 null”和”字段被省略了”,而且 JSON Patch 对客户端不友好。
我对大多数内部 API 默认用 PUT 带完整对象。更简单、更少出错,也符合 REST 最佳实践。只在实体很大、带宽敏感、或部分更新是真实需求时才用 PATCH。
红牌回答: “它们可以互换。“——它们语义不同,混用会产生行为不可预测的 API。
5 个导致 .NET 开发者被拒的面试错误
面试了足够多 .NET 开发者之后,这些模式会毁掉本来能力不差的候选人——跨每一轮、每个专题:
1. 背诵定义而不是推理。 “中间件是坐落在请求和响应之间的软件”等于什么都没告诉我。“我有一次把 CORS 注册在认证后面,花了三小时排查幽灵 401”告诉我一切。
2. 说”看情况”然后停下。 每个 senior 回答都可以从”看情况”开始——但好的回答会立刻跟上”这是我的默认选择,以及我什么时候会偏离”。要有一个立场。
3. 卡在两个大版本之前。 如果你在 2026 年的面试里提到 Swashbuckle、IDistributedCache、EF6 的懒加载默认行为、或 .NET 6 的模式,面试官会假设你已经停止学习了。要知道什么东西变了。
4. 没有生产信号。 面试官在听”我见过这个挂掉是在……”、“我们有次事故……”、“我 benchmark 过这个”。如果你的每个回答都是纯理论的,你会输给那个有现场经验的候选人。
5. 跳过”为什么”。 “我会用 output caching”是 C 级回答。“我用 output caching 是因为它支持基于标签的失效,所以能在写入时淘汰而不是等一个 TTL 过期”是 A 级回答。
面试准备专题索引
这个页面是枢纽。下面是各专题的深度页面——已上线的可以直接跳过去,其余会陆续发布:
| 专题 | 题量 | 状态 |
|---|---|---|
| .NET Web API | 45 题 | 已上线 |
| EF Core | 30 题 | 已上线 |
| C# 语言 | 40 题 | 即将上线 |
| ASP.NET Core 内部机制 | 30 题 | 即将上线 |
| 系统设计 | 25 题 | 即将上线 |
| 异步与多线程 | 25 题 | 即将上线 |
| LINQ | 25 题 | 即将上线 |
| 微服务 | 25 题 | 即将上线 |
| Senior / 资深 | 30 题 | 即将上线 |
| SQL Server | 20 题 | 即将上线 |
| Docker & DevOps | 20 题 | 即将上线 |
如果你关注 AI 助手、开发工具和软件工程实践,可以关注 Aide Hub。这里会继续分享能落地的工具教程、技术观察和项目经验。
参考
- .NET Interview Questions: The 2026 Guide — codewithmukesh
- 45 .NET Web API Interview Questions
- 30 EF Core Interview Questions
- Middleware in ASP.NET Core — Microsoft Docs
- Async Programming in C# — Microsoft Docs
- HybridCache in ASP.NET Core
- Rate Limiting in ASP.NET Core
- RESTful API Best Practices for .NET Developers