尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
四层平台五层MVC:ASP.NET Core业务系统架构实践
1. 四层平台的五层MVC从概念到落地做.NET开发这些年我最怕听到的不是“需求又变了”而是“你先搭个框架”。因为框架本身没有标准答案不同团队对同样一句话的理解可能差出十万八千里。前几天有个朋友问我想用VS里的C# ASP.NET MVC模板做一个能模拟真实人物关系、人员档案、组织架构的系统到底该怎么分层我听完他的描述脑子里冒出来八个字四层平台五层MVC。这不是教科书里的标准名词而是我这几年在做业务系统时总结出的一套实践思路。所谓“四层平台”是指把整个系统拆成表现层、应用服务层、领域层、基础设施层四层底座所谓“五层MVC”是指在真正处理Http请求的这层里把View、Controller、ViewModel、Service、Repository这五个环节串成一条清晰的链路。这套架构想要解决的就是标题里说的那件事让代码里的“思维语言世界”尽量贴近现实世界中的人物对象和业务规则。先说一个直觉。传统ASP.NET MVC项目模板默认只有Models、Views、Controllers三个文件夹我非常喜欢它的干净但真正跑起业务来又总觉得缺口气。Controller要负责接收请求、做参数校验、调用业务逻辑、拼装Model、返回视图Model还要同时承载数据库结构和页面表单数据。当系统只有十几个页面的时候一切都很美好当系统出现部门管理、岗位管理、考勤规则、薪酬科目这些乱七八糟的业务时Controller和Model会迅速膨胀代码开始绕成一团浆糊。而“四层平台的五层MVC”能解决的核心问题就是让每一个对象都有明确的归属地让每一段代码都能回答“我到底该住在哪一层”。1.1 为什么是“四层平台”而不是“三层架构”很多文章都在讲三层架构UI层、业务层、数据访问层。这套东西没错但它太粗了。业务层往往会被塞进一大堆与业务无关的东西比如缓存逻辑、消息推送、导入导出、审计日志。数据访问层又容易被数据库技术绑架今天用EF Core明天换Dapper后天又可能想接一个第三方API作为数据源于是业务层被迫跟着改。我习惯把“业务层”再拆成应用服务层和领域层。应用服务层负责编排用例也就是“创建员工”“调整部门”“计算考勤”这些业务入口领域层负责沉淀核心业务规则比如“工号不能重复”“离职员工不能再安排培训”。把这些规则放进独立的领域层后不管Web框架怎么换、数据库怎么换核心业务规则都稳如磐石。这时候你会得到一个四层结构表现层WebMVC的View和Controller只关心HTTP请求和响应。应用服务层Application编排业务用例不包含具体业务规则。领域层Domain业务实体、规则、仓储接口。基础设施层Infrastructure数据库访问、文件存储、邮件发送等。这四层合在一起才是完整的系统平台。MVC只是表现层里的一个开发模式不是整个架构的全部。1.2 五层MVC到底拆了哪五层传统MVC里M是ModelV是ViewC是Controller。在“五层MVC”里我并没有推翻这三个核心概念而是把Model拆成了更细的两块ViewModel和Entity领域实体。再加上Service和Repository就变成五层层次原名职责常见位置1View展示页面、收集用户输入Views文件夹下的cshtml2Controller接收请求、调用服务、返回结果Controllers文件夹3ViewModel面向视图的数据结构ViewModels文件夹4Entity / Domain Model面向业务规则的对象Domain项目5Repository含Service数据访问与业务编排Application / Infrastructure项目严格说第五层包含了Service和Repository两个角色但在请求回路上它们是一个整体Controller调用ServiceService调用RepositoryRepository操作数据库。如果非要抠字眼把Service和Repository也算作两个独立层那已经是“六层”了。我为了强调“MVC内部仍然有五个关键节点”所以统称为五层MVC。这种拆分最大的意义在于页面上的数据结构和数据库里的数据模型不再是同一个东西。你可以在页面上看到一个“人员列表ViewModel”包含姓名、部门、入职日期而领域实体里面还会有工号、身份证号、状态、内部备注。两者通过映射互相转换谁都不需要迁就谁。1.3 这套架构和普通MVC模板的本质区别如果只是新建一个ASP.NET MVC项目直接开写三天后Controller里就会出现类似这样的代码从数据库查出结果判断某个字段再更新另一个表同时发一封邮件。这些代码在没有分层时写起来很爽但后续维护会非常痛苦。因为它把“HTTP请求处理的逻辑”和“业务规则”揉在了一起。四层平台加五层MVC后的请求链路是这样 浏览器发起请求 - 路由命中Controller Action - Controller解析参数并校验ViewModel - 调用ApplicationService - Service调用Domain层接口 - Repository实现真正读写数据 - 返回结果给Controller - Controller返回View或Json。这条链路的每一步职责单一遇到问题时你不需要翻遍全部代码才能定位。普通MVC模板更像是给你一张白纸而四层平台是给你一套“带隔断的收纳箱”。白纸灵活但时间一长就乱收纳箱一开始需要花点心思安装但后面放东西、找东西都明确。我个人理解标题里的“人智数智的思维语言世界”其实是在说人的业务认知和系统的代码实现之间需要一种共同语言。四层平台的五层MVC就是帮助建立这种共同语言的脚手架领域层用业务语言表现层用页面语言基础设施层用技术语言层与层之间通过接口和映射翻译。这样一来人脑中的“真实人物对象世界”和被代码模拟出来的对象世界才能逐步对齐。2. 在Visual Studio里创建C# ASP.NET MVC解决方案2.1 开发环境准备与版本选择如果你要复现这套架构我建议直接用Visual Studio 2022社区版选择“ASP.NET Core Web应用模型-视图-控制器”模板。.NET 8目前是LTS版本稳定性和生态都挺成熟比.NET Framework 4.8更适合新项目。需要安装的工作负载包括ASP.NET和Web开发以及.NET SDK。有些朋友会纠结VS和VS Code怎么选。VS Code轻量适合跨平台开发、写脚本、调整配置但做完整的ASP.NET MVC项目我更推荐Visual Studio因为它对项目模板、调试器、数据库工具、发布流程的支持都更完整。热搜里经常会看到“无法与某个地址建立连接未能下载VS Code服务器”这类报错那多半是远程开发场景下的下载问题和本文的架构没有直接关系但也能看出VS Code在某些环节并不像本地VS那么省心。创建项目时先选好一个解决方案名称例如MyCompany.PersonSystem。解决方案下建议按src和tests两个目录组织src放源码tests放单元测试项目。在src里创建四个项目src/MyCompany.PersonSystem.Web src/MyCompany.PersonSystem.Application src/MyCompany.PersonSystem.Domain src/MyCompany.PersonSystem.Infrastructure2.2 解决方案目录结构与项目引用关系四个项目之间必须遵守单向依赖这是整个架构的底线Web引用Application、Domain、Infrastructure。Application引用Domain。Infrastructure引用Domain。Domain不引用任何项目。为什么允许Web直接引用Infrastructure因为在启动项目里我们需要注册DbContext和仓储实现。但这不代表Web里的Controller可以越过分层直接调用DbContext。Controller只能面向Application层的接口编程。依赖关系可以看这张引用矩阵项目引用方向说明WebApplication、Domain、Infrastructure表现层只知道接口不知道实现细节ApplicationDomain服务层编排领域对象InfrastructureDomain仓储实现、数据库上下文Domain无定义实体和仓储接口在实际代码里Application层不会出现Microsoft.EntityFrameworkCore的引用不会出现SqlConnection也不会出现HttpContext因为它天生就不依赖这些技术。这个约束写进代码Review清单里比口头约定靠谱得多。2.3 命名空间约定与项目模板选择命名空间我建议按业务模块拆而不是按技术类型硬堆。比如Domain项目里并不是只有Entities文件夹而是按模块分Domain/ Employees/ Employee.cs IEmployeeRepository.cs Departments/ Department.cs IDepartmentRepository.cs Common/ EntityBase.csApplication项目也按模块组织每个模块放一个接口和一个实现接口放在Application.Contracts命名空间下实现放在Application.Services命名空间下。有人会用类库项目单独隔离Contracts但如果项目规模不大接口和实现放同一个项目里的不同文件夹已经足够清晰。Web层则保留ASP.NET MVC的传统结构Controllers、Views、ViewModels。ViewModels按Controller的Action对应关系命名例如EmployeeCreateViewModel、EmployeeListViewModel避免出现一个三百字段的全局Model。3. 五层MVC各层职责与代码设计3.1 View层Razor视图只负责“说话”Razor视图应该是整套系统里最简单、最“笨”的一层。它做的事情只有两件从ViewModel里读取数据把HTML渲染给浏览器接收用户提交的表单数据映射到ViewModel上。不要在cshtml里写数据库查询不要在cshtml里判断用户角色后执行复杂逻辑更不要让视图直接去操作领域的实体对象。我见过很多视图被写成了“万能展示器”页面里又是if又是switch还要调用静态类方法去格式化日期。这种代码不是不能跑而是每次改样式都可能引发功能回归。正确做法是尽量把逻辑放到Controller或Service里视图只保留简单的遍历和展示。如果确实有通用展示逻辑封装成HTML Helper或自定义TagHelper。视图里还应坚持使用强类型模型model EmployeeListViewModel table tr th工号/th th姓名/th th部门/th th入职日期/th /tr foreach (var item in Model.Employees) { tr tditem.EmployeeNo/td tditem.Name/td tditem.DepartmentName/td tditem.HireDate.ToShortDateString()/td /tr } /table写一个页面之前先问自己一个问题这个页面要展示什么ViewModel这个ViewModel需要哪些字段确定了这些再去思考Controller返回什么Service怎么拼装。3.2 Controller层让Action瘦成“皮包骨”理想的Controller Action应该非常薄接收参数、调用服务、返回结果。如果一个Action超过二十行基本可以判定有逻辑漏到表现层了。Controller还要处理好ModelState的校验及时返回错误信息但不要自己在Controller里实现“统一社会信用代码校验”这种业务规则。下面这段代码是我在人员管理系统中常用的Controller写法public class EmployeeController : Controller { private readonly IEmployeeAppService _employeeAppService; public EmployeeController(IEmployeeAppService employeeAppService) { _employeeAppService employeeAppService; } public IActionResult Index() { var employees _employeeAppService.GetEmployees(); var viewModel new EmployeeListViewModel { Employees employees }; return View(viewModel); } [HttpPost] public IActionResult Create(EmployeeCreateViewModel viewModel) { if (!ModelState.IsValid) { return View(viewModel); } _employeeAppService.CreateEmployee(viewModel); return RedirectToAction(nameof(Index)); } }这里看不到任何一行SQL操作看不到任何业务规则判断。Controller存在的意义是理解HTTP语义比如Get、Post、Redirect而不是处理“人员对象世界”里的业务问题。如果你发现某个Action里需要new一个DbContext那一定是架构抽象失效了。3.3 ViewModel与Entity两个“世界”的翻译官在五层MVC里Entity是真实人物对象的代码替身ViewModel是页面需要的数据容器。这种分离非常关键因为业务领域里的员工对象可能包含身份证、薪资、绩效等敏感字段但列表页只需要显示姓名、工号、部门详情页才需要展示更多字段。我通常会把领域实体设计成私有set所有修改都通过方法来完成。例如员工离职不是直接让页面提交Status字段而是调用Employee.Deactivate()方法。这样业务规则被封装在领域对象内部而不是散落在服务里。ViewModel则完全是普通public属性用于数据双向绑定不需要包含方法。例如public class EmployeeCreateViewModel { [Required] [Display(Name 工号)] public string EmployeeNo { get; set; } [Required] [Display(Name 姓名)] public string Name { get; set; } [Required] [Display(Name 部门)] public int DepartmentId { get; set; } [Display(Name 入职日期)] public DateTime HireDate { get; set; } [EmailAddress] [Display(Name 邮箱)] public string Email { get; set; } }从Entity到ViewModel的映射字段少的时候手写就好字段多的时候用AutoMapper也能省不少事。但要注意不能因为使用了自动映射就忽略字段差异。比如创建员工时页面传来的DepartmentId是int而部门是否存在需要Service去查领域模型验证不能直接信任输入值。3.4 Service层业务用例的编排者Service是五层MVC里最容易写歪的一层。有人会把所有方法都堆在一个大服务里比如IEmployeeService里放着GetEmployees、CreateEmployee、UpdateEmployee、ExportExcel、SendWelcomeEmail、CalculateSalary……这样Service又变成了“大杂烩”。我更推荐按用例划分服务至少按业务模块拆分。IEmployeeAppService只围绕员工这个聚合根提供方法GetEmployees、GetEmployeeDetail、CreateEmployee、UpdateEmployee、DeactivateEmployee。每个方法内部先做参数校验然后加载领域实体调用领域规则最后通过仓储持久化。一个创建员工的服务方法大致是public void CreateEmployee(EmployeeCreateViewModel viewModel) { var department _departmentRepository.GetById(viewModel.DepartmentId); if (department null) { throw new BusinessException(部门不存在); } var exists _employeeRepository.ExistsByEmployeeNo(viewModel.EmployeeNo); if (exists) { throw new BusinessException(工号已存在); } var employee new Employee( viewModel.EmployeeNo, viewModel.Name, department.Name, viewModel.HireDate, viewModel.Email); _employeeRepository.Add(employee); _unitOfWork.SaveChanges(); }注意这里没有直接操作DbContext也没有将Http请求对象传入。Service永远面向业务语言才能做到跨技术栈复用。3.5 Repository层领域模型与数据库的隔离带仓储接口定义在Domain层实现放在Infrastructure层。接口的语义要贴近领域而不是贴数据库。比如IEmployeeRepository.ExistsByEmployeeNo这个方法比IRepository .Any(x x.EmployeeNo no)更清晰也更安全因为不会让调用方写出乱七八糟的Lambda表达式。我的Repository层通常采用泛型基类加专用接口结合的方式public interface IRepositoryT where T : EntityBase { T GetById(int id); IReadOnlyListT GetAll(); void Add(T entity); void Update(T entity); void Remove(T entity); }但要注意不要把所有复杂查询都塞进这个泛型接口。如果某个查询只属于员工模块就放到IEmployeeRepository里如果某个报表统计需要多表聚合就单独建一个IEmployeeReportsRepository或者使用专用的查询服务。仓储不是“万能数据工具箱”它要控制数据访问的粒度防止IQueryable泄漏到上层。4. 实操记录开发一个“人员档案管理系统”4.1 领域模型用C#类描述真实人物对象下面我用一个具体例子把整个开发流程串起来。假设我们正在做一个人员档案管理系统第一个核心模块是员工管理。在Domain项目下创建Employee实体让它成为“真实人物对象世界”在代码里的投影。public enum EmployeeStatus { Active 1, Inactive 2 } public class Employee { public int Id { get; private set; } public string EmployeeNo { get; private set; } public string Name { get; private set; } public int DepartmentId { get; private set; } public DateTime HireDate { get; private set; } public string Email { get; private set; } public EmployeeStatus Status { get; private set; } protected Employee() { } public Employee(string employeeNo, string name, int departmentId, DateTime hireDate, string email) { SetCoreInfo(employeeNo, name, departmentId, hireDate, email); Status EmployeeStatus.Active; } public void UpdateBaseInfo(string name, int departmentId, DateTime hireDate, string email) { SetCoreInfo(Name, name, departmentId, hireDate, email); } public void Deactivate() { if (Status EmployeeStatus.Inactive) { throw new BusinessException(该员工已是离职状态); } Status EmployeeStatus.Inactive; } private void SetCoreInfo(string currentName, string name, int departmentId, DateTime hireDate, string email) { if (string.IsNullOrWhiteSpace(name)) { throw new BusinessException(员工姓名不能为空); } Name name; DepartmentId departmentId; HireDate hireDate; Email email; } }注意属性是private set外部代码不能随意把员工状态改成离职。所有关于员工状态的变化必须通过Deactivate方法而方法里内部包含了对状态的校验。这就是领域规则的内聚性。4.2 数据访问与实体映射Infrastructure项目里创建ApplicationDbContext并配置Employee的映射。使用EF Core的Fluent API把Domain模型和数据库表之间的细微差异放在这一层处理。public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { } public DbSetEmployee Employees SetEmployee(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityEmployee(e { e.HasKey(x x.Id); e.Property(x x.EmployeeNo).HasMaxLength(20).IsRequired(); e.Property(x x.Name).HasMaxLength(50).IsRequired(); e.Property(x x.Email).HasMaxLength(100); e.Property(x x.Status).HasConversionint(); }); base.OnModelCreating(modelBuilder); } }生成数据库迁移时需要在Package Manager Console里执行Add-Migration InitialCreate -Project MyCompany.PersonSystem.Infrastructure -StartupProject MyCompany.PersonSystem.Web Update-Database -Project MyCompany.PersonSystem.Infrastructure -StartupProject MyCompany.PersonSystem.Web为什么要指定Project和StartupProject因为DbContext在Infrastructure项目里而启动项目是Web。如果不指定EF Core可能找不到连接字符串或者把迁移文件生成到Web项目下导致项目引用混乱。4.3 仓储实现与依赖注入在Domain项目里定义IEmployeeRepositorypublic interface IEmployeeRepository : IRepositoryEmployee { bool ExistsByEmployeeNo(string employeeNo); IReadOnlyListEmployee GetActiveEmployees(); Employee GetByEmployeeNo(string employeeNo); }在Infrastructure项目里实现它public class EmployeeRepository : RepositoryBaseEmployee, IEmployeeRepository { private readonly ApplicationDbContext _context; public EmployeeRepository(ApplicationDbContext context) : base(context) { _context context; } public bool ExistsByEmployeeNo(string employeeNo) { return _context.Employees.Any(x x.EmployeeNo employeeNo); } public IReadOnlyListEmployee GetActiveEmployees() { return _context.Employees .Where(x x.Status EmployeeStatus.Active) .ToList(); } }在Program.cs里注册服务builder.Services.AddDbContextApplicationDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection))); builder.Services.AddScopedIEmployeeRepository, EmployeeRepository(); builder.Services.AddScopedIEmployeeAppService, EmployeeAppService(); builder.Services.AddControllersWithViews();这里使用AddScoped而不是AddSingleton或AddTransient是为了保证在一次请求内所有Service和Repository共用同一个DbContext实例避免多个实例同时跟踪同一实体造成冲突。4.4 服务与控制器串联从URL到View的完整链路Application项目中的服务接口public interface IEmployeeAppService { IReadOnlyListEmployeeListDto GetEmployees(); EmployeeDetailDto GetEmployeeDetail(int id); void CreateEmployee(EmployeeCreateViewModel viewModel); void UpdateEmployee(EmployeeUpdateViewModel viewModel); void DeactivateEmployee(int id); }实现中的核心是CreateEmployee。前面已经列过代码这里再补一个查询详情的例子public EmployeeDetailDto GetEmployeeDetail(int id) { var employee _employeeRepository.GetById(id); if (employee null) { throw new BusinessException(员工不存在); } return new EmployeeDetailDto { Id employee.Id, EmployeeNo employee.EmployeeNo, Name employee.Name, DepartmentId employee.DepartmentId, HireDate employee.HireDate, Email employee.Email, Status employee.Status }; }Controller拿到这个DTO后可以直接塞进View Model并返回给视图。视图只是把数据展示出来不需要关心这些数据是从哪个数据库表里查出来的。这样一个完整的“人-机-数”链路就打通了真实世界里的人事数据被采集进数据库经过领域层翻译成业务概念再由应用服务组织成页面模型最终呈现给操作人员。5. 常见问题排查与优化实录5.1 控制器越来越胖先看Service是不是变成了“披萨饼”很多团队从三层架构迁移到五层MVC时都会经历一个错觉控制器瘦下来了Service胖起来了。如果一个Service方法里有大量分支判断、状态转换、数据拼接说明它没有把领域规则下沉到实体里而是用“过程化脚本”在操作领域对象。我排查这类问题时有个笨办法看Service方法里有没有大量if嵌套有没有直接new出另一个实体的内部状态有没有调用其他Service但其他Service又在调用当前Service。一旦出现循环依赖就要考虑把公共领域逻辑抽到Domain层。合理的Service方法应该是先防御性判断然后加载实体调用实体方法最后调用仓储保存。如果你在Service里写了很多线性的步骤一定要反思那些步骤是属于调度还是属于业务规则。调度留在Service规则放Domain。5.2 EF Core跟踪与多层传递的冲突五层MVC里Entity从Repository返回后会被Service使用Service再返回DTOEntity生命周期可能跨越多个层。如果同一个Id的实体被查询两次或者一个被追踪、一个不被追踪在调用SaveChanges时就会报错System.InvalidOperationException: The instance of entity type Employee cannot be tracked because another instance with the same key value is already being tracked.解决思路有三种。第一在仓储查询时统一使用AsNoTracking()除非明确知道要更新实体再调用Update。第二每次Service方法只在方法内部保存一次不要把一个Entity传递给另一个Service并让另一个Service去保存。第三如果业务确实复杂使用UnitOfWork模式把多个仓储的SaveChanges统一到一个事务里避免每调用一个仓储方法就SaveChanges一次。我自己的习惯是写操作开始前用GetById获取实体并要求该仓储返回可追踪实体读操作全部AsNoTracking。这样能最大程度减少冲突也避免多一次无谓的数据库往返。5.3 ViewModel映射坑位手动映射ViewModel和Entity时最常见的坑是把不该暴露的字段暴露出去。比如Employee实体里有Status而更新员工基本信息的页面上并不需要让用户修改状态结果因为用了AutoMapper自动把Status也映射过来了于是页面提交一个无效状态把员工状态改坏了。使用AutoMapper时一定要为每个映射单独指定Ignore规则而不是无脑CreateMap。我建议在配置里增加一行统一声明MapperConfiguration config new MapperConfiguration(cfg { cfg.CreateMapEmployeeCreateViewModel, Employee() .ForMember(dest dest.Status, opt opt.Ignore()); });更稳妥的做法是让更新操作只通过Controller接收页面允许修改的字段Service内部再根据业务规则改变真实实体的状态。页面权限和领域规则是两回事不能混为一谈。5.4 数据库迁移与部署环境冲突我常在试验阶段遇到这个问题本地使用LocalDB数据都在发布到服务器后忘记执行迁移打开系统就报表不存在。或者在服务器上执行Update-Database但服务器没装Visual Studio也没有NuGet环境。推荐的做法是在发布流程中自动执行迁移但要有并发保护。例如在Program.cs里用临时环境变量控制或者单独做一个数据库迁移脚本工具。最保险的方式是生成SQL脚本在数据库发布阶段统一执行Script-Migration -Project MyCompany.PersonSystem.Infrastructure -StartupProject MyCompany.PersonSystem.Web -OutputFile migrate.sql然后把migrate.sql交给数据库管理员执行避免一个Web应用运行时自动建表引发权限问题。这个习惯在做生产级别系统时尤其重要。5.5 常见问题速查表问题现象可能原因处理建议Controller Action超过20行业务逻辑未下沉Service抽取Service方法Controller只做调度页面出现意外的敏感字段ViewModel映射到了实体全部字段按页面定制ViewModel映射时Ignore无关字段EF Core报同名实体重复跟踪同一个Id被多次查询查询统一AsNoTracking更新时再追踪数据库表未创建忘记执行迁移使用Update-Database或发布SQL脚本部署后连接不上数据库连接字符串仍指向LocalDB替换为生产数据库连接并检查防火墙权限修改实体保存无效private set导致值未被赋值调用实体行为方法不要直接给属性赋值6. 一些我个人偏爱的小技巧和后续可以扩展的方向6.1 用依赖注入容器管理生命周期很多人只把依赖注入当成“在构造函数里拿对象”的工具但生命周期才是最关键的部分。对于DbContext必须使用Scoped对于无状态的Service可以用Scoped或者Transient对于缓存类服务可以用Singleton。如果误用Singleton注入DbContext就会导致多请求共享同一个数据库上下文出现数据错乱和并发问题。我通常在项目里放一个CompositionRoot文件夹集中管理依赖注册。Application和Infrastructure各自提供扩展方法Web层在Program.cs里统一调用例如builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration);这样Web项目非常干净其它项目也可以在单元测试中灵活替换实现。6.2 从人员档案延伸到组织、权限、考勤本文里的“人员档案管理系统”只是最小可行示例。当你理解了四层平台加五层MVC就可以很自然地把组织架构、岗位管理、权限管理、考勤规则这些业务域一个个加进来。每个域内都使用独立的实体、仓储、服务、Controller和View域与域之间通过应用服务协作而不是直接操作对方的仓储。例如员工和部门之间员工实体保存一个DepartmentId但在查询员工列表时需要展示部门名称。这个场景不必让Employee实体去依赖Department而是在Application服务里同时加载两个仓储把数据拼装成EmployeeListDto。领域模型之间尽量保持低耦合应用服务负责“跨域翻译”。另外当系统复杂度进一步上升你可以考虑引入CQRS命令查询职责分离或者事件驱动的消息通知。但不要一开始就上这些架构先把基础的“四层平台、五层MVC”走稳等发现确实存在并发、扩展、响应性能等问题时再演进。6.3 最后分享一个真实踩坑别把仓储当成万能数据工具箱早期我实现IRepository 时把分页、排序、多表查询、统计报表全塞进泛型接口结果这个接口的方法数量是数据库表的十倍。每换一张表就要为它实现一堆八竿子打不着的查询方法代码复用变成了灾难。后来我把仓储拆成了两类面向聚合根的常规仓储以及面向查询场景的专用查询服务。聚合根仓储只负责实体的加载与持久化查询服务可以使用Dapper或EF Core的FromSqlRaw返回业务需要的DTO。这样一来核心领域模型不被报表查询污染五层MVC的层次感才真正体现出来。如果你也想实践这套架构我的建议是从一个不超过三个模块的小项目开始先把Controller、Service、Repository之间的依赖方向理顺再逐步加入新的业务域。架构不是越复杂越好而是要让每一段代码都能在“四层平台”上找到属于自己的位置让“思维语言世界”和“真实人物对象世界”之间的映射变得自然、稳定、可维护。
RELATED

相关推荐

CTF实战解题思路大全:高频考点与非常规解法

CTF实战解题思路大全:高频考点与非常规解法

1. CTF解题思路宝典的价值与定位 作为一名打了七年CTF的老兵,我深刻理解选手们在赛前那种"总感觉缺了点什么"的不安感。这份《100个实战解题思路大全》正是为了解决这种焦虑而生——它不是普通的技巧合集,而是将各类赛事高频考点、非常规解法和…

📅 2026/11/2 16:32:10
远程连接管理工具1Remote:统一RDP/SSH/VNC协议与凭据加密实践

远程连接管理工具1Remote:统一RDP/SSH/VNC协议与凭据加密实践

远程连接这件事,几乎每个做运维、开发或者技术支持的人都绕不开。手头一堆机器,有的走RDP连Windows,有的用SSH登Linux,还有VNC、Telnet、SFTP各种协议混着来,时间一长,桌面上的快捷方式、记事本里的IP清单、…

📅 2026/11/2 16:32:10
n8n工作流安全数据传输与加密配置实战

n8n工作流安全数据传输与加密配置实战

1. 为什么n8n工作流需要安全数据交换在自动化工作流中处理敏感数据时,传统的HTTP明文传输就像用明信片邮寄银行密码。我曾亲眼见过一个电商公司的客户数据因为工作流中的API调用未加密,导致2万条用户信息在传输过程中被截获。n8n作为开源自动化工具&…

📅 2026/11/2 16:32:10
MORE NEWS

更多资讯

📰

老旧货梯智能化改造:协议转换与声明式梯控架构设计

1. 老旧货梯智能化改造的行业痛点 在工业物联网领域,设备改造最棘手的往往不是全新系统的搭建,而是对存量老旧设备的智能化升级。以货梯为例,全国制造业园区中超过60%的货梯设备使用年限超过15年,这些设备普遍存在三大典型问题&am…

📰

四边形不等式优化DP:原理与实现详解

1. 四边形不等式优化DP概述 动态规划(DP)是算法竞赛中的核心技巧,而四边形不等式优化则是提升DP效率的重要方法。这种优化技术利用决策单调性,将原本O(n)的复杂度降低到O(nlogn),在处理大规模数据时优势显著。 四边形不等式优化的核心思想是…

📰

Codex + Jev 组合实战:类型安全与 Skill 机制配置指南

1. 这套组合到底在解决什么问题 先把话说在前头:Codex 本身是个能力很强的代码智能体,但它的默认配置和默认模型,在真实项目里经常会出现“能跑但不够顺手”的情况。尤其是当你需要它处理复杂类型推导、跨文件重构、或者需要它理解一套自定义…

📰

Vue Router滚动行为控制与优化实践

1. 为什么需要路由跳转后的滚动控制?在单页应用(SPA)开发中,Vue Router 的路由跳转不会像传统页面跳转那样自动重置滚动位置。这会导致两个常见问题:从长页面跳转到新页面时,新页面会继承前一个页面的滚动位…

📰

ABAP协变性(Covariance)解析与Java对比实践

1. 为什么ABAP开发者需要关注协变性(Covariance)在面向对象编程领域,协变性(Covariance)是一个经常被讨论但容易被忽视的概念。作为一名有十年ABAP开发经验的工程师,我发现很多同行对这个问题存在误解——要么完全忽视,要么简单套用Java等语言…

📰

ZXing扫码优化实战:从40%失败率降至5%的图像预处理全链路

简介:本资源是一套基于ZXing开源库实现二维码与条形码扫描功能的完整Android项目实践方案,面向Android开发初学者及中级工程师,解决移动应用中快速集成扫码能力的核心需求。压缩包共1874个文件,涵盖254个JSON配置与接口定义、158个…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬