1. 项目概述:为什么一个文件夹结构能拿三个奖?
“.Net项目分层与文件夹结构大全(最佳架子奖、吐槽奖、阴沟翻船奖揭晓)”——这标题乍看像技术圈的脱口秀预告,但实打实是每个C#开发者入职前三天必经历的“灵魂拷问现场”。我带过二十多个.Net团队,从金融核心系统到社区团购后台,几乎每场代码评审都会卡在同一个地方: 某个Service类为什么跑在Web层?DTO和Entity混在一个Models文件夹里?Migration脚本藏在Controllers目录下? 这些不是风格偏好,而是架构债务的起点。所谓“最佳架子”,不是指最炫酷的命名,而是 上线后半年不改结构就能自然支撑新模块接入、测试覆盖率能轻松拉到75%以上、新人三天内能准确定位订单超时逻辑在哪一层 。而“吐槽奖”往往颁给那些表面遵循DDD却把Domain Events塞进Infrastructure、用AutoMapper做跨层强耦合映射的项目;“阴沟翻船奖”则属于那种在Startup.cs里写满AddScoped ,结果压测时发现所有服务共享同一数据库连接池,凌晨三点排查Connection Timeout的兄弟。核心关键词—— .Net分层、文件夹结构、领域驱动设计、依赖注入、可测试性、团队协作成本 ——全部指向一个现实问题: 结构不是画在PPT里的UML图,而是每天敲代码时手指最先触达的物理路径 。这篇文章不讲抽象理论,只复盘我亲手踩过的27个结构陷阱、3次因文件夹命名引发的线上事故,以及如何用一套可验证的检查清单,在新建项目时10分钟内锁定90%的架构风险点。
2. 分层设计底层逻辑:为什么不是“表现层-业务层-数据层”三段论?
2.1 真实世界的分层动因从来不是教科书
很多团队建项目第一件事就是照搬“Presentation-Business-Data”三层结构,结果三个月后Controller里出现1200行代码,Repository直接调用HttpClient发HTTP请求。问题出在混淆了 分层目的 和 分层形式 。分层的本质是 控制变更影响范围 ,而非机械切割代码位置。举个具体例子:某电商项目需要将支付网关从支付宝切换为微信支付。如果PaymentService直接new AlipayClient(),那修改就得扫描全项目找所有new语句;但如果PaymentService只依赖IPaymentGateway接口,而AlipayClient和WechatPayClient都实现该接口,变更就锁死在Infrastructure.Payment.WeChat目录下。这个“锁死”能力,才是分层的核心价值。所以真正的分层依据是 变化频率 和 抽象程度 :UI层天天改样式,Domain层三年才动一次核心规则,Infrastructure层可能因云厂商SDK升级而批量重构。我见过最反直觉但最有效的实践——把Entity和ValueObject放在Domain层,但 数据库表结构定义(如EF Core的Fluent API配置)放在Persistence层 。有人质疑:“Entity不就是数据结构吗?”错。Entity代表业务概念(比如OrderStatus必须是枚举且含状态流转校验),而Table Schema是存储实现细节(比如OrderStatus字段用tinyint还是varchar)。混在一起,等于把业务规则和MySQL版本强绑定。
2.2 .Net生态特有的分层陷阱:依赖注入容器成了“万能胶水”
.NET Core的DI容器让分层变得“太容易”——只要把类注册进去,Controller就能直接用Service,Service又能用Repository。这种便利性恰恰埋下最大隐患: 依赖方向被物理路径掩盖 。典型场景:Web层引用Application层,Application层引用Domain层,Domain层却意外引用了Microsoft.EntityFrameworkCore(因为某个ValueObject用了EF的OwnedAttribute)。编译通过,但Domain层从此无法脱离EF运行,单元测试得启动整个DbContext。解决方案不是删掉Attribute,而是 用领域事件解耦 :Domain层只定义OrderPlacedDomainEvent,Application层监听该事件并触发EF操作。这里的关键检查点是: Domain层的.csproj文件中,PackageReference必须为零 。我强制要求团队用CI脚本扫描Domain层项目文件,发现任何PackageReference立即阻断构建。另一个高频陷阱是“跨层调用幻觉”:Application层Service调用Infrastructure层的EmailSender,但EmailSender内部又调用Web层的IHttpContextAccessor获取当前用户ID。这实际形成了Web→Application→Infrastructure→Web的循环依赖,运行时靠DI容器“巧妙”解决,但调试时栈跟踪会变成迷宫。破局点在于 明确每一层的“能力边界” :Domain层只处理业务规则,Application层协调事务和跨系统交互,Infrastructure层只负责“把东西存进去/取出来/发出去”,绝不参与业务决策。
2.3 领域驱动设计(DDD)在.Net中的落地变形记
DDD常被当作银弹,但.NET项目里最常见的误用是 把分层当领域 。比如建一个“User”文件夹,里面放UserEntity、UserDto、UserViewModel、UserRepository、UserService——这根本不是DDD,这是“按名词分层”。真正的DDD分层围绕 限界上下文(Bounded Context) 展开。以电商为例,“用户”在订单上下文里只是OrderId+Email的轻量引用,在会员上下文里却是包含积分、等级、优惠券的复杂聚合根。因此结构上应是:
src/
├── OrderContext/ # 限界上下文1
│ ├── Domain/ # 订单规则:OrderAggregate, OrderStatus
│ ├── Application/ # 订单用例:CreateOrderCommandHandler
│ └── Infrastructure/ # 订单存储:OrderDbContext
└── MembershipContext/ # 限界上下文2
├── Domain/ # 会员规则:MemberAggregate, PointRule
└── Application/ # 会员用例:RedeemCouponCommandHandler
关键差异在于: OrderContext.Domain不能引用MembershipContext.Domain ,哪怕它们都有“User”概念。跨上下文通信必须通过发布领域事件(如OrderPlaced)或API调用。我曾重构一个单体项目,将原“User”大模块拆成4个限界上下文,结果发现30%的“用户相关”代码根本不需要存在——因为订单上下文只需要用户邮箱,根本不需要加载会员等级。这种减法,才是DDD带来的真实收益。
3. 文件夹结构实战指南:从命名到物理隔离的硬核细节
3.1 命名规范:为什么“Models”是技术债的温床?
“Models”文件夹是.NET项目中最危险的命名。它像一个黑洞,把Entity、DTO、ViewModel、Request/Response对象全吸进去。新人看到UserModel.cs,根本分不清这是数据库实体还是API返回对象。更糟的是,当需要为同一业务对象生成不同DTO时(如AdminUserDto vs CustomerUserDto),开发者往往复制粘贴再改名,导致字段变更时漏改某个DTO。我的解决方案是 用前缀强制语义分离 :
-
Domain.Entities.User:仅含业务属性和领域方法(如User.ChangeEmail()) -
Application.Dtos.UserCreateRequest:API入参,含[Required]等验证特性 -
Application.Dtos.UserSummaryResponse:API出参,只含前端需要的字段 -
Infrastructure.Persistence.Entities.UserEntity:EF Core实体,含[Column]、[Index]等映射特性
注意: Domain.Entities 和 Infrastructure.Persistence.Entities 是两个独立命名空间,即使属性名相同,也绝不复用类。这样做的好处是:当数据库表结构调整(如User表拆分成User和UserProfile),只需修改Infrastructure层,Domain层完全不受影响。曾有个项目因未分离,DBA执行 ALTER TABLE User DROP COLUMN Address 后,Domain层的User实体编译报错,导致整个CI流水线中断2小时。
3.2 物理隔离:如何用项目文件(.csproj)筑起防火墙?
文件夹结构只是视觉分层,真正的隔离靠项目文件。一个健康.NET解决方案至少包含5个项目:
MyApp.sln
├── MyApp.Domain.csproj # 无外部NuGet依赖,仅System.*
├── MyApp.Application.csproj # 引用Domain,可引用MediatR等框架
├── MyApp.Infrastructure.csproj # 引用Domain+Application,可引用EF Core/Redis
├── MyApp.WebApi.csproj # 引用Application+Infrastructure,含Controllers
└── MyApp.Tests.csproj # 引用Domain+Application,禁用Infrastructure
关键约束:
- Domain项目禁止引用任何NuGet包 (包括System.Text.Json)。若需JSON序列化,定义
IDomainSerializer<


432

被折叠的 条评论
为什么被折叠?



