1. 一首打油诗背后的.NET开发生态
最近在技术社群里看到有人贴了一首《.NET开发赋》的打油诗,开头一句就是“.NET开发真神奇,安全漏洞数第一”,底下评论区瞬间炸开了锅。有人说这是黑,有人说是自嘲,还有人直接搬出OWASP Top 10开始争论。我做了十多年.NET方向,从.NET Framework一路跟到现在的.NET 8,看到这种争论其实挺有感触的——这首诗能火,恰恰说明.NET开发者的处境正在发生深刻变化。
先说我的结论:这诗写的是开发者的自嘲,但自嘲背后藏着一个真实命题——.NET开发这个圈子,正在从过去“微软全家桶一键搞定”的舒适区,走向一个需要自己动手处理安全、性能、架构、部署的复杂时代。诗里那几句“安全漏洞数第一、需求天天改、性能调优难上难”,几乎就是每个一线.NET开发者都经历过的痛点。
这篇文章不打算替微软辩护,也不打算唱衰,就是以一个过来人的身份,把这首诗里提到的几个点逐一拆开,讲讲我在实际项目中遇到的情况、踩过的坑,以及现在回头看,哪些问题其实有解,哪些问题依然无解。你会发现,真正值钱的经验,往往都藏在这些“看起来像段子”的日常里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. .NET开发者的真实生存状态:从“只会拖控件”到“全栈杂工”
2.1 过去十年,.NET开发者经历了什么
如果有人问我现在入行.NET还有没有前途,我会先讲一段历史。2016年之前,.NET开发者的典型画像是什么?打开Visual Studio,新建一个ASP.NET Web Forms项目,从工具箱里拖几个按钮、文本框,双击按钮写一段事件代码,一个网站就“开发”完了。那时候我接触过很多传统企业的信息部门,他们口中的“.NET开发”基本等于“用微软的工具做微软的东西”,数据库用SQL Server,服务器用Windows Server,部署用IIS,整个链路闭源但闭环,新手也能很快上手。
这个阶段的安全漏洞问题,表面看是“数第一”,实际是“没人认真数过”。Web Forms的ViewState、事件验证、Session状态这些机制虽然被后来的人诟病“过度封装”,但起码框架帮你挡住了一部分低级攻击。那时候更常见的翻车现场是:开发者把数据库连接字符串直接写在web.config里,然后整个文件被提交到Git;或者SQL语句用字符串拼接,参数化查询是什么,根本没人教。我在2015年帮一家制造企业做代码审计,一个进销存系统里找出三十多处SQL注入点,负责人还很惊讶,说“这不是内部系统吗,谁没事黑我们”。
转折点出现在.NET Core发布之后。2016年微软宣布.NET Core开源跨平台,2018年发布.NET Core 2.0,2020年直接改名.NET 5,走“一个.NET”路线。这一系列动作把. NET开发者从“Windows专属程序员”变成了“基建工程师”——你得懂Linux、懂Docker、懂K8s、懂微服务、懂CI/CD,因为公司开始用.NET开发云原生应用了。但问题是,很多人以前没被要求懂这些,转身的速度跟不上时代,于是出现了大量“半吊子”状态:代码写得像Framework时代的风格,部署却要上K8s,中间全是事故。
2.2 为什么“安全漏洞数第一”这句吐槽值得认真听
“安全漏洞数第一”这个说法,如果只看攻击数据总量,多少有点冤枉.NET ——毕竟GitHub上百分之七八十的漏洞报告集中在JavaScript系和Java系,.NET的漏洞绝对数量并不算高。但为什么给开发者的体感是“第一”?我分析有三个原因。
第一,.NET的漏洞一旦出现,往往影响面极广,因为企业级应用比例高。一个反序列化漏洞可能导致一堆政府、金融、制造系统同时中招,新闻一报,体感就拉满。2017年爆出的Json.NET反序列化RCE漏洞,很多老项目用到JsonConvert.DeserializeObject传TypeNameHandling.All,直接能把服务器打穿,当时圈子里气氛确实紧张。
第二,历史包袱重。老Framework项目的代码质量参差不齐,加上很多系统已经运行十年以上,维护者自己对代码都不熟悉,出了漏洞只能盲目打补丁。更麻烦的是,有些老系统用的第三方控件库已经停止维护,漏洞根本没有官方修复包。
第三,Docker和云原生兴起后,.NET开发者被迫接触更多安全面。以前只需要考虑IIS配置、Windows补丁、SQL Server权限;现在要面对容器镜像漏洞、K8s RBAC配置、API网关鉴权、云服务密钥管理,任何一个环节出问题都是大事故。安全技能没有系统补上,漏洞自然看起来多。
但我想说一个观点:.NET的安全漏洞不是.NET的问题,是“开发者有没有把安全当成基本功”的问题。框架能替你挡住一部分,但挡不住你代码里的业务逻辑漏洞、鉴权缺失、敏感信息泄露。这些在任何语言里都会出,只是在我们这个生态里,问题更集中在“用的框架太老了”“没按新feature正确使用”这两个点上。
2.3 行业对.NET开发者的要求,已经完全不同
现在你打开招聘软件搜“.NET开发工程师”,就会发现要求已经变得五花八门:要熟悉Docker、K8s、Redis、RabbitMQ、MySQL/PostgreSQL、前端Vue/React、云服务、DevOps……看起来像在招一个全栈架构师。这种要求背后是行业的真实需求:.NET现在最常被用在核心业务系统、微服务、云原生应用、高并发平台,而不是过去的内部信息网站。
我在几个项目里的体验是,现在的.NET开发,已经不是“写代码”这么简单。你写的接口要能扛住瞬时并发,要能适配多环境部署,要能在内存占用和性能之间找到平衡,还要在等保测评时拿出完整的安全设计文档。技术栈变宽了,个人能力模型必须跟着变。所以那首诗里写的“需求天天改”,改的其实不只是业务需求,还有技术需求——今天让你接个第三方API,明天让你优化几百毫秒的接口响应,后天让你排查线上OOM,全是杂活,但全是真实能力。
3. 安全,真的是.NET的阿喀琉斯之踵吗
3.1 常见高危场景:我真正见过的问题
说到.NET安全,我优先想聊的是实际项目里最容易踩、也经常被忽略的几个坑。每一个都是我在代码评审或者线上事故中真实遇到的。
反序列化漏洞是我认为最典型的“历史问题”。很多开发者在存缓存、传对象、写消息队列时,为了方便,直接把对象序列化成二进制或JSON后传输或存储,但没控制类型。.NET里BinaryFormatter在反序列化时会自动实例化传入类型,攻击者只要控制输入,就能构造一串Payload,最终在服务器上执行任意命令。我在2019年对一个老运维平台做加固时,发现系统用BinaryFormatter接收客户端上传的“配置对象”——这几乎等于把服务器钥匙挂在门口。解决方案是:新代码一律禁用BinaryFormatter(.NET 8里它已经默认被标记为过时),改用序列化白名单,或者直接改用JSON + 类型校验。
SQL注入听起来是上个世纪的漏洞,但在.NET业务系统里依旧活跃。典型场景是“高级搜索”功能:用户输入多个筛选条件,开发者图省事把字段名、排序方式都拼接进SQL。还有走ADO.NET的老代码,大量用字符串拼接。我在一次审计里还见过从存储过程里继续拼动态SQL、然后EXEC起来的,调试狂魔见了都头疼。让这类问题彻底退出,只能靠规范:所有用户输入必须走参数化查询或ORM,动态条件用表达式树(Expression)、查询构造器(Query Builder)或专门库(如SqlKata)生成,绝不能拼字符串。
硬编码密钥与连接串是很多团队的“常识坑”。我以前帮朋友看一个上线两年的系统,GitHub私有仓库里居然躺着appsettings.json文件,里面写了数据库账号、SMTP密码、加密证书私钥。不是他们故意泄,而是根本没意识到这些文件不该进版本库。后来我习惯在项目里加一条铁律:任何机密不能出现在源码目录,本地开发用User Secrets或环境变量,部署用Azure Key Vault/云厂商的秘密管理服务,CI/CD里用Secret变量注入。
.NET Remoting / WCF的滥用也值得单独提。老项目里WCF、.NET Remoting服务的鉴权经常就是“设了一个用户名密码,客户端拿着就行”,完全没有证书校验、传输加密。如果有人能接触到内网流量,基本等于裸奔。现在的方向一致是把这类服务用gRPC或WebAPI替换掉,在网关层统一做鉴权、限流、审计,链路清晰还能做全链路追踪。
3.2 等保测评和SDL,我踩过的那些流程坑
安全工作不只在代码里,还在一堆流程文档里。国内做政企项目的.NET开发,大多绕不开等保测评。我第一次接触等保时,天真地以为“等保就是装个杀毒软件,开个防火墙,设个密码”,后来被测评机构打回了好几轮。印象最深的一次,他们要求“所有登录接口必须支持防暴力破解”,我们当时只做了验证码和多次失败锁定,测评老师直接问:“你这里有没有账号锁定策略?超过多少次锁定多久?锁定的原因是‘用户不存在’还是‘密码错误’?这两者不能一样。”追问到最后我才意识到,等保考察的是“你有没有一座能防住攻击的墙”,细节标准能精确到“错误提示的口径”。
另一个流程坑是SDL(安全开发生命周期):需求评审就要评估威胁,设计阶段就要定安全方案,编码阶段要有安全编码规范,测试阶段要有安全用例。很多团队嫌流程重就省略,但后来一旦出事,背锅和整改的成本远大于当时“多写的几页文档”。我现在在项目里会尽量保证,高危模块(支付、登录、权限、文件上传)的安全设计文档不能缺,代码评审的时候必须有安全视角,上线前做一次依赖库漏洞扫描和SAST(静态代码扫描)。
3.3 从框架到代码,安全其实有章可循
说了这么多问题,也想给大家一个相对可落地的安全度量框架。我把.NET安全分为四个层面:
- 框架和依赖层:这是地基。框架版本要尽量新(.NET 6/7/8,别还在.NET Framework 4.x的坑里),NuGet包要定期做漏洞扫描。目前主流方案是用GitHub Dependabot、Sonatype Nexus Lifecycle,或者微软的Defender for Cloud来跟踪CVE。
- 配置和部署层:检查有无硬编码密钥、是否关闭危险HTTP方法(WebDAV、PUT、DELETE)、是否启用HTTPS、是否开启CORS(注意别配成
*)、Cookie有没有设置HttpOnly和Secure、认证会话有没有设超时。这些属于“基础卫生”,但能挡住很多自动化攻击。 - 应用逻辑层:接口层面最小化权限、请求参数校验(服务端为主)、越权访问的控制(防止水平越权和垂直越权)、文件上传的类型和大小限制(还要做压缩和病毒扫描)、敏感数据脱敏(在日志中隐藏身份证、手机号、卡号)。
- 自建防御和监控层:给关键接口加限流,对异常登录、异常调用链做告警,定期做数据备份与恢复演练,还有最容易被忽略的——攻击日志的可审计性(谁在什么时间通过什么IP访问了什么接口,不能只记“请求成功”和“请求失败”)。
每次做安全加固,我都会先在纸上按这四个层面过一遍,再决定优先级。优先级排序很简单:能直接导致RCE(远程代码执行)、敏感数据泄露、核心业务瘫痪的最高优先,其次是大范围可被自动化利用的问题,再次是低危、需特定条件才能触发的“锦上添花”项。
4. 需求变更与MVC:为什么说“不能只写Controller”
4.1 打油诗里的“需求天天改”,其实是架构问题
“需求天天改”是所有业务开发的痛,但我要把它拆开来看。有些需求是真的变化快,比如市场政策、运营活动、合作方接口,这类变化是业务本质决定的,谁也拦不住。另一类“天天改”,其实是架构设计不灵活导致的:业务逻辑全堆在Controller里、数据库表结构硬编码、状态流转写死、规则散落在好几个项目里,每次改动都要处处小心、连环爆炸,开发周期自然被拉长,矛盾自然就爆发在“需求变更”上。
MVC(Model-View-Controller)的出现,本意就是把界面、数据、业务逻辑分开,让改动互不干扰。这个设计在桌面端、传统Web时代是有效的,但在现在复杂的业务系统里,只靠MVC三个字母应付不了所有场景。我个人的体会是,真正让业务代码好改,靠的不是“MVC”这个名词,而是“分层架构+领域边界+依赖倒置”的组合拳。换句话说,Controller应该只负责接收请求、编排参数、返回响应,业务规则放到Service层,数据访问放到Repository层,领域模型和UI模型分离,再用依赖注入把这些东西粘起来。
举一个我实际经历过的例子。某个功能原来只有一个状态字段,后来业务方说“我们需要支持审核撤回”“状态要加一个‘待运营复核’”。如果代码里到处都是if (status == 1)这种硬编码,改起来就是灾难。我当时建议把状态管理改成一个状态机模型,每个状态定义允许的流转事件和回调动作,新的状态和流转只需扩展一个枚举和一张配置表,不需要改动太多业务代码。这就是“面向未来的改动”和“面向当下的改动”的区别。
4.2 MVC与MVVM、前后端分离的选型逻辑
现在新项目默认走前后端分离:后端提供Web API,前端用Vue/React/Angular,MVC老项目很多还在用Razor视图。这时候就有一个选型问题:什么时候用MVC+Razor,什么时候用Web API+SPA?
我的经验是看项目类型和团队能力。如果是一个内部管理后台、数据录入系统、表单流程类应用,Razor视图的MVC反而开发效率很高,因为服务端渲染天然好做权限控制、表单回显、多页面状态维护,而且不依赖Node.js构建链、不用额外处理跨域。如果是一个对交互体验要求很高、有大量实时交互或面向公众的Web应用,比如商城、社交、数据可视化平台,SPA前端体验明显更好,后端就专心做API。
不要为了“前后端分离”而前后端分离。我见过有些项目为了看起来“先进”,硬把纯后台管理页面拆成前端工程+Vue,结果团队不熟前端,前后端联调的时间比直接写Razor页面还长。选型的第一原则永远是人效比和可维护性。
4.3 架构演进:从三层到DDD、微服务,别盲目追新
.NET项目里有三种常见架构层级,我简单归类一下:
- 单机/单体+分层:最经典,适合中小型项目。库表、服务、页面全都部署在一套代码里,按Controller、Service、Repository分层。优点是开发快、部署简单;缺点是代码多了以后边界模糊,改一个功能容易碰坏另一个。
- 模块化单体(Modular Monolith):在一个部署单元内按业务模块拆分,模块之间通过明确接口通信。适合中型项目,既能保留单体部署的简单性,又能让团队按模块并行开发。这个形态我强烈推荐给那些“规模不大但要求一定要能扛住三年内演进”的项目。
- 微服务/分布式:适合大团队、高并发、多团队独立交付的场景。.NET生态里的ABP框架、MassTransit、Orleans(虚拟Actor模型)、gRPC、K8s,都是这个层面的常用工具。
我在选择架构时有个原则:先问“问题的复杂度在哪儿”。如果瓶颈是团队多而杂,那就拆分微服务;如果瓶颈是单体代码太乱,先试试模块化单体;如果只是流量大了但业务简单,优先考虑缓存、异步、读写分离,而不是直接上K8s。架构不是越复杂越厉害,而是越匹配越有效。
4.4 如何养成“能扛需求变化”的MVC代码习惯
哪怕不换架构,在MVC框架下也有一些让代码好改的习惯:
- Controller瘦身:Controller里不要写业务逻辑,超过十几行的操作就抽到Service里。
- 用DTO/ViewModel做输入输出:不要直接把Entity对象抛给前端,尤其是带导航属性和审计字段的表实体,很容易暴露数据且改动导致序列化异常。
- 依赖注入只暴露接口:Controller依赖的是Service接口而不是具体类,这样以后替换实现(比如从EF换成Dapper、加缓存层)不会牵连Controller。
- 日志、异常、校验、鉴权尽量用中间件/过滤器(Filter/Attribute)统一处理,而不是在每个Action里复制粘贴。
- 引入MediatR这类中介者模式(可选):请求、处理逻辑、响应独立成类,代码跳转清楚,应对需求变化时也可以更集中修改。
我自己最深的体会是:MVC框架原本只是一个分层思想,真正让它有生命力的,是开发者在每个类、每个方法的边界上有没有克制住“懒”和“贪”——懒是懒得拆分、懒得抽象,贪是明知不该写在Controller里的判断,为了少改文件就顺手写进去了。这两点一犯,需求一改就炸。
5. 性能调优,依然是一门硬功夫
5.1 先从“慢”开始定位:性能问题排查流程
“性能调优难上难”,这句也是很多.NET团队的共鸣。但在我看来,性能调优难在“找不到瓶颈”,而不是“不会优化”。我习惯用一套排查流程,避免被表象带跑。
第一步,从监控和日志入手,而不是从代码入手。你是否能看到接口的P95/P99耗时、CPU/内存占用、GC频率、数据库慢查询?如果没有完善的APM(如SkyWalking、Jaeger、OpenTelemetry + Prometheus + Grafana),就先补监控再谈优化。
第二步,复制问题并做剖析(Profiling)。用dotnet-counters、dotnet-trace、PerfView、JetBrains dotMemory/dotPeek,跑一遍出问题场景,拿到最底层的代码热点。很多时候是大对象的分配导致GC频繁,而不是某个算法本身慢。
第三步,区分“代码慢”和“环境慢”。数据库连接池满、Redis超时、外部API响应慢,这些都可能让接口耗时长,但不一定是业务代码的锅。我遇到过一个“慢查询”案例,最后发现是某个服务启动时没有预热连接池,流量高峰来了全在排队建连接。从三项订单数据排查到第五天才定位到连接池参数配置,印象极深。
5.2 缓存、异步、数据库优化,这是最有效的三板斧
优化动作我一般不追求太多,有几个路径几乎每次都能见效。
缓存是提升读性能最直接的手段。 从内存缓存(IMemoryCache)到分布式缓存(Redis),按数据特征分级缓存。但缓存有个最大的坑:缓存一致性。改数据时怎么同时更新缓存?是先删缓存再更新库,还是先更新库再删缓存?我推荐的是“先更新数据库,再删除缓存”的Cache-Aside模式,并且给缓存设一个较短的过期时间作为兜底。如果你用Redis,还要注意缓存穿透(查一个不存在的数据导致每次打到数据库)和缓存雪崩(大量key同一时间失效)的问题。穿透的解法是空值缓存或布隆过滤器;雪崩的解法是过期时间加随机值,上线时错峰发布。
异步化和队列化是稳定性和性能兼顾的常用招。 秒杀扣库存、注册发邮件、订单确认短信、日志上报,这些不需要同步返回的场景都该用消息队列或后台任务。.NET里BackgroundService就是很好用的后台调度基座,结合Channel(进程内内存队列)或RabbitMQ/Kafka(跨进程队列)都能实现削峰填谷。用了队列之后,用户的请求响应时间显著下降,系统在流量高峰时也更稳定了。
数据库永远是最终瓶颈。 SQL Server/PostgreSQL/MySQL各有特点,但核心优化套路相通:加索引、避免回表、临时表变顺利、分页优化、读写分离、归档历史数据。.NET里EF Core要特别注意“N+1查询问题”——先查了列表,再循环逐条查关联数据,代码写起来很顺手,但性能烂到家。正确的做法是用Include或Projection一次性捞数据,或者拆分接口。
5.3 关于GC、JIT和异步IO,你需要知道的几件事
.NET的性能话题绕不开GC(垃圾回收)。GC并不是瓶颈本身,而是“被频繁分配的对象”导致的。要想让GC少干活,就要减少不必要的分配(allocation),尤其是在循环和热路径里。比如用StringBuilder替代字符串拼接,用ArrayPool复用字节数组,避免在循环里捕获循环变量导致匿名类分配,还可以用ValueTask减少异步方法的堆分配。这些优化看似不起眼,但叠加在高并发接口上,效果立竿见影。
JIT(即时编译)方面,.NET 7之后引入了Profile Guided Optimization(PGO),能根据运行时热点信息动态优化,比如把接口调用去虚拟化、内联热方法,官方称能带来约5%-10%的提升。.NET 8又进一步做了优化和AOT(Native AOT)编译支持,我们发布极致追求启动速度和内存占用的工具型服务时,可以选择AOT发布成原生可执行文件。但AOT也不是银弹,一些动态反射特性会受限,需要真机多平台测试后再定。
异步IO方面,还是有很多老代码用.Result、.Wait(),把async链路打断,造成线程池线程被阻塞,一旦并发量上来线程池线程耗尽,整个应用就像一个被堵死的路口。这类代码一旦出现几乎都是事故级的,所以我特别强调代码评审时注意“async一路到底”:从Controller到Service到Repository,所有方法都用async/await,不阻塞线程。
5.4 高并发压测:调优以后你到底能不能扛住
性能调优不能靠“感觉”。压测才是验证方案是否有效的唯一标准。.NET下常用工具是JMeter,也可以用k6、NBomber(NBomber就是.NET原生的压测库),还可以基于C#写一个脚本化的小压测工具,自动把TPS、响应时间、错误率、CPU、内存记录下来。
我自己做压测的习惯是:先确定压测的目标指标(比如核心接口P99 < 200ms、TPS > 1000、错误率 < 0.1%),然后从小并发开始逐步加压,同时监控服务器指标。当吞吐量不再线性增长时,就到瓶颈点,逐层定位:是线程池?数据库连接数?GC?还是外部服务?每调完一个参数,再重新压测对比。压测不是一锤子买卖,上线前、大促前、版本更新后,都要重复。
压测里最容易忽略的是“数据规模”和“业务数据分布”。我见过有人用几万条数据压测结果很好,上线后面对上千万条历史数据直接卡死——索引没建好,执行计划完全不同。所以压测前一定要准备和生产环境数量级一致的数据样本。
6. 从打油诗到实战:我的.NET 8项目是这么落地的
6.1 直接给你一套可复用的团队风格代码骨架
前面聊了很多理念,这里给出一套可以直接抄作业的.NET Web API项目骨架,适合中小型团队搭建新项目。
项目分层建议:
src/YourApp.Application:用例层(Use Cases),定义接口、DTO、映射(Mapster/AutoMapper)、命令/查询(如果用MediatR)。src/YourApp.Domain:领域层,实体、值对象、领域服务、领域事件。src/YourApp.Infrastructure:基础设施层,EF Core DbContext、仓储实现、各种第三方服务(Redis、Mongo、SMTP、云SDK)。src/YourApp.Api:宿主层,Controller/Middleware/Filter/认证配置/端点路由/模型校验。tests/:单元测试(xUnit+NSubstitute+FluentAssertions)、集成测试(WebApplicationFactory + Testcontainers)。
核心包版本:.NET 8 SDK,ASP.NET Core 8。ORM用EF Core 8,缓存用StackExchange.Redis,认证用JWT Bearer,序列化用System.Text.Json,API文档用Swashbuckle(Swagger)。
关键配置片段(appsettings.Production.json类):
json复制{
"ConnectionStrings": {
"Default": "Server=...;Database=...;User Id=...;Password=...;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},
"Redis": {
"ConnectionString": "192.168.1.10:6379,password=...,defaultDatabase=0",
"InstanceName": "MyApp:"
},
"Jwt": {
"Issuer": "YourApp",
"Audience": "YourAppClients",
"SigningKey": "ENV__Jwt_SigningKey",
"ExpiresMinutes": 60
},
"Serilog": {
"MinimumLevel": "Information",
"WriteTo": ["Console", { "Name": "File", "Args": { "path": "logs/log-.txt", "rollingInterval": "Day" } }]
}
}
配置里的重点有二:一是密码键不要写死,用用户机密(Development环境)和平台环境变量/密钥服务(Production环境);二是数据库连接串开启Encrypt=True和TrustServerCertificate=False,强制走加密连接。
中间件管线顺序建议(Program.cs):
csharp复制var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
.AddJsonOptions(o =>
{
o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
o.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 认证与授权
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* config */ });
builder.Services.AddAuthorization();
// 数据层
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// 业务层
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ApplicationMarker).Assembly));
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
// 缓存
builder.Services.AddStackExchangeRedisCache(opt =>
{
opt.Configuration = builder.Configuration["Redis:ConnectionString"];
opt.InstanceName = builder.Configuration["Redis:InstanceName"];
});
// 后台任务、健康检查、Flurl/LongHttpClient
builder.Services.AddHostedService<SomeBackgroundService>();
builder.Services.AddHealthChecks();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseSerilogRequestLogging();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();
注意:中间件顺序里,UseAuthentication和UseAuthorization必须在MapControllers之前,且一般放在UseHttpsRedirection之后。顺序错了,认证就失效。
6.2 把安全和性能优化写进CI,让问题在上线前被发现
我特别想强调“左移”这个理念:安全问题和性能问题不要在测试甚至生产环境才爆出来,要在开发阶段、构建阶段就发现。落地方式就是靠CI流水线。
一个不算复杂但有效的流水线大概包含以下阶段:
- 构建:
dotnet restore->dotnet build -c Release。构建时打开WarningAsError,让自己和团队认真对待警告。 - 单元测试:
dotnet test,跑完出覆盖率报告(coverlet)。 - 代码扫描:SonarQube/SonarCloud(或改用免费的模式扫描),支持C#、VB.NET,能发现代码异味、反模式和安全风险项。
- SAST:微软的Microsoft.CodeAnalysis.NetAnalyzers配合
dotnet format analyzers,以及在CI里执行dotnet list package --vulnerable --include-transitive来检查NuGet依赖库漏洞。 - 镜像安全扫描:如果走Docker发布,构建完镜像后用Trivy或Grype扫描镜像,抓出操作系统层和.NET基础镜像里的高危CVE。
- 部署:先到一个类生产环境(Staging),跑一轮冒烟测试和轻量压测,再进入生产。
我自己的习惯是让CI流水线在任何一次PR合并时都跑起来,主分支更新后自动触发构建和测试。别小看这些“浪费时间”的环节,真正的成本是线上出问题后的排查和修复,那才叫昂贵。
6.3 部署与可观测性:不能只管“上线”
很多团队把交付的边界画在“代码部署完成”,这其实是半个工程。真正要保证系统稳定,运维和可观测性建设必须从第一天就参与进去。
部署方式上,.NET 8应用发布Docker镜像已经是常态。我会用一个多阶段Dockerfile,第一步用mcr.microsoft.com/dotnet/sdk:8.0构建发布,第二步用mcr.microsoft.com/dotnet/aspnet:8.0作为运行镜像,这样产物小、攻击面小、启动也快。
dockerfile复制FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "YourApp.Api.dll"]
镜像里注意非root运行、不装bash、不留不必要的包,减少被入侵后的横向移动面。
可观测性方面,至少三件事要做:
- 日志:用Serilog结构化日志,统一写JSON格式,方便日志平台全文检索。
- 指标:通过OpenTelemetry + Prometheus暴露HTTP请求数、P99耗时、GC次数、线程池使用量、数据库查询耗时等关键指标。
- 链路追踪:集成OpenTelemetry——如果用了gRPC、HTTP调用多个服务,没有Trace根本没法看一次请求到底卡在哪个环节。
最后加一句实在话:可观测性完备之后,你才会真正有底气去调优和重构。没有数据支撑的架构调整,都是拆东墙补西墙。
7. 聊聊那些非技术层面的“难”
7.1 “谈.NET色变”的求职现状,是事实还是偏见
写到最后这部分,想聊点非技术但同样重要的事。这几年求职市场上,.NET方向的岗位数量在二线城市确实没有Java多,一线城市也有不少企业转Go、转Java。但如果你真正去看优质岗位(高并发平台、云原生基础设施、金融核心系统、工业制造平台、SaaS产品),.NET的占比并不低,而且很多是大厂的核心部门。微软云、Azure、Dynamics、Power Platform,加上国内外大量制造、零售、物流企业的核心系统,都在用.NET。更关键的是,.NET 6/7/8这几代的性能和开发体验提升非常明显,新项目选择.NET的比例在回升。
我接触过一些年轻开发者,学了半年Java发现竞争红海,转过来学.NET,反而在就业市场上找到了差异化。面试官对你的期望值是“你能不能用C#和.NET解决复杂问题”,而不是“你会不会背八股文”。当然,前提是你真的把一个项目的坑踩过、把性能优化真实做了一遍、把安全漏洞真的修过,而不是只停留在教程demo。
7.2 在团队中做技术决策,怎么说服别人用.NET
每个.NET开发者几乎都遇到过“为什么不用XX语言”的挑战。我总结了几条说服思路,核心都是“用事实说话”:
- 性能对比数据:TechEmpower Web Framework Benchmarks里,ASP.NET Core常年位居综合性能排名前列,远超某些动态语言框架,接近甚至倒挂部分Go框架。把具体榜单截图和场景摆出来,最有说服力。
- 生态与生产力:C#语言特性(模式匹配、记录类型、可空引用类型、异步流)在开发效率上很有优势;NuGet包数量和质量在云原生和基础库领域已经足够丰富;EF Core的LINQ体验无出其右。
- 跨平台与云原生:.NET 8已支持所有主流云平台、容器生态成熟。如果一个团队以前对“.NET只能跑Windows”的认知停在2015年,那就演示一遍Linux下部署和Docker发布。
- 人才供给与长期维护:国内.NET人才储备虽不如Java大,但在微软生态内(Power Platform、Azure)需求稳定增长;而且.NET代码规范性强,新人上手路径清晰。
当然,也要承认:如果公司已经有了成熟的Java技术栈和大量Java工程师,为了纯技术偏好去改造技术栈是不理性的。技术在业务目标面前是手段,不是目的。合适就好。
7.3 学习路线建议:从框架到架构师,我还差几步
很多读者可能会问:我从.NET Framework时代过来,没学过.NET Core?会不会掉队?我的建议是——不会,但必须转。具体路径可以参考这个顺序:
- 打牢C#语言基础:泛型、委托、事件、LINQ、异步编程、内存管理(引用类型/值类型)、性能敏感写法。这是根。
- 吃透ASP.NET Core核心机制:中间件管线、依赖注入、配置系统、选项模式、过滤器、日志。不要只看demo,要亲手实现一个模块,处处跟源码。
- 深入一个ORM和一个缓存:EF Core和Redis基本是标配。同一场景多用SQL Server/PostgreSQL/MySQL各练一次,理解差异。
- 掌握微服务和分布式常用件:gRPC、RabbitMQ/Kafka、Consul/Nacos/etcd、K8s/Docker Compose,至少做到“知道什么时候该用、怎么搭起来、怎么排查”。
- 补齐架构与工程化:DDD战略/战术设计、MediatR、CAP(事件总线框架)、单元测试/集成测试、CI/CD、监控告警。
- 在真实项目里练手:学习永远替代不了实操。找几个开源项目(如eShopOnContainers、AspNetCore.Docs里有大量示例)读源码,做一个自己的小型中台系统,把前面知识串起来。
如果未来目标是架构师,还要额外关注成本估算、容量规划、技术选型决策、团队指导、风险管理——这些不在代码里,而在业务理解和管理能力里。
7.4 为什么我劝你别只盯着“技术热点”
写这篇长文的起点,是那首《.NET开发赋》。诗里在调侃开发者的日常,但我想说的是:开发者的职业安全感和成就感,从来不取决于你用的语言是不是最热门,而取决于你能不能真正解决业务问题。
这些年我见过太多人追热点:前几年追过微服务,后来又追过Serverless、低代码、大模型。追得上、追得对的是少数,更多人是折腾一圈,最后发现最核心的能力还是“设计出简单可靠的系统”。.NET这个生态的好处是,它足够成熟、足够均衡——既有底层控制力(指针、SIMD、Native AOT),又有高级抽象(LINQ、async/await),它不会给你太多花里胡哨的选择,但能把你需要的东西稳定给到位。这不是平庸,而是稳定。
如果让我评估未来五年.NET的趋势,我会说:.NET 8 LTS会持续稳定很多年,AOT、云原生、AI(Semantic Kernel、ML.NET)会是新的增长点;同时,老业务系统的现代化迁移(.NET Framework -> .NET 8+)会释放很多机会。这些都需要真正懂底层、懂性能、懂安全的开发者,而不是只会搬砖的“Engineer”。所以,回到标题里的那首打油诗——安全漏洞、需求变更、性能调优,这就是我们这行最真实的三座大山。翻过它们,你就不是被吐槽的“难上难”,而是那个“原来还能这样优化”的破局者。
最后分享我这些年最实用的一条经验:不管外界怎么吐槽. NET,真正决定你上限的是你有没有把每一个“调优”做到位——代码的调优、内存的调优、接口的调优、架构的调优,还有心态的调优。后面的路长着呢,保持好奇,保持动手,保持对线上系统的敬畏,就够了。
