去年有个做电商后台的读者找我吐槽,说入职时接手的.NET项目像"千层饼"——业务逻辑、数据访问、API控制器全揉在一个文件夹里,改个订单状态要翻7层目录。他花了两周才理清调用链,而原作者早已离职。

这不是个例。微软官方调研显示,68%的.NET项目在技术债评估中被标记为"维护困难",而根因往往追溯到第一天建的文件夹结构。

文件夹即架构,命名即契约。新手常犯的错,是把"能跑"当成"能活"。

第一层陷阱:按"技术类型"分层的幻觉

第一层陷阱:按"技术类型"分层的幻觉

打开Visual Studio新建项目,默认模板会给你Controllers、Models、Views三个文件夹。很多教程告诉你:MVC三层架构,清晰明了。

问题是,项目长到10个业务模块时,你的Controllers文件夹会有40个文件,横跨用户、订单、支付、库存四个领域。改支付接口时,你得和库存的代码挤在同一个目录里"抢地盘"。

这叫"技术分层"的暴政——它服务于框架的整洁,而非人的认知。

更隐蔽的伤害在协作端。两个开发同时改Controllers,Git冲突概率指数级上升。我见过一个团队为此制定了"Controller排班表",上午归A组,下午归B组,荒诞得像餐厅后厨的灶台轮值。

破解方法很简单:按业务领域切分,而非技术角色。

把Orders、Payments、Inventory做成顶层文件夹,每个里面再藏自己的Controllers、Services、Repositories。改支付?只打开Payments目录,世界瞬间安静。

这叫"垂直切片",微软在.NET 6的官方模板里悄悄推荐过,但没写在显眼位置。

第二层陷阱:Services文件夹的"黑洞效应"

第二层陷阱:Services文件夹的"黑洞效应"

几乎每个.NET项目都有个Services或BusinessLogic文件夹,它像黑洞一样吞噬所有说不清归属的代码

起步时只有UserService、OrderService,很干净。三个月后,里面会冒出PaymentHelper、NotificationManager、ExcelExportUtils、ThirdPartyAdapter……最后变成60个文件的垃圾场,没人敢删任何一个,因为"可能有人在用"。

更糟的是命名。Service后缀被滥用:OrderService既处理业务逻辑,又调数据库,还发消息队列。一个类干了三层的事,单元测试时你得mock整个宇宙。

正确的切割方式是按"变更原因"聚类——一起变的代码放一起。

支付流程变了?只改Payments领域的内部。通知模板变了?只动Notifications模块。这种结构下,代码审查的上下文被压缩到一个目录,Reviewer不用在七个文件夹之间跳来跳去。

有个细节很多人忽略:解决方案文件(.sln)里的项目引用顺序,应该反映业务依赖关系,而非字母顺序。Payments项目引用Orders,是因为支付依赖订单状态,这个箭头方向要一眼能看懂。

第三层陷阱:DTO和ViewModel的"复制粘贴文明"

第三层陷阱:DTO和ViewModel的"复制粘贴文明"

.NET项目里常见三个模型类:数据库实体(Entity)、API入参出参(DTO)、页面展示(ViewModel)。新手教程通常说:三层各建一个Models文件夹,安全隔离。

结果是同一个"用户地址"字段,在UserEntity、UserDto、UserViewModel里写了三遍,连属性名都不统一——有的是Address,有的是FullAddress,有的是Location。

映射代码像胶水一样糊在Controller里:_mapper.Map(entity),后面跟20行手工补字段。某天数据库加了个字段,开发改了Entity和Dto,忘了ViewModel,前端拿到null,调试两小时才发现是命名不一致。

更优雅的方案是共享内核模式:把真正稳定的概念(如Address值对象)抽到共享项目,三层都引用。只有边界处(如敏感字段脱敏)才做显式转换。

AutoMapper不是必需品。我见过太多项目为了"省几行代码"引入它,结果调试时像隔着毛玻璃看数据——你知道映射发生了,但不知道在哪一层、按什么规则变的。

显式的工厂方法或构造函数映射,代码量多20%,但可读性高一个数量级。这是.NET社区里Jimmy Bogard(MediatR作者)的观点,他在2022年的技术博客里专门批过过度使用AutoMapper的风气。

一个被低估的工具:解决方案过滤器

一个被低估的工具:解决方案过滤器

Visual Studio的.slnf文件(解决方案过滤器)是大型项目的救命稻草。

当你只想看Payments模块时,加载Payments.slnf,其他10个领域从Solution Explorer里消失,编译时间从3分钟降到20秒。这对新成员熟悉代码库极其友好——第一次打开项目,面对的是500个文件还是50个文件,认知负荷完全不同。

更进阶的玩法是把.slnf和CI/CD管道绑定:改Payments代码,只跑Payments相关的测试套件。一个被拆成8个垂直切片的.NET项目,全量测试要40分钟,切片测试只要6分钟。

反馈速度决定重构勇气。测试跑得慢,人就会倾向于"凑活用",技术债就这么滚起来了。

最后说个反直觉的判断:好的项目结构,应该让"删除"比"添加"更容易。

当你想砍掉一个功能时,理想情况是删除一个文件夹,编译器立刻告诉你还有哪些残留引用。坏的结构里,代码像霉菌一样蔓延——你知道它在那,但永远清不干净。

那位电商后台的读者后来怎么做的?他花了三个周末,把项目按领域重构成7个垂直切片,每个切片内部再用Clean Architecture分层。上线那天,他在团队群里发了张截图:删除旧分支时,Git显示-12,000行代码。

"删代码比写代码爽多了。"他说。

你的第一个.NET项目,文件夹结构现在长什么样?如果明天要删掉其中一块业务,你能确定不会扯出隐藏的依赖线吗?