ASP.NET Core五层MVC架构详解:从领域模型到依赖注入的工程实践

发布时间:2026/11/2 3:21:14
ASP.NET Core五层MVC架构详解:从领域模型到依赖注入的工程实践 第一次看到这个标题时我愣了一下——“人智数智的思维语言世界”“人机统一地隐喻真实的人物对象世界”这些词堆在一起第一反应是哲学论文而不是技术方案。但在这个圈子里待久了你会明白越是绕口的表述落到工程上往往越简单。标题想做的事情说白了就是用VS里的C#和ASP.Net MVC架构模板搭出一套分层清晰、数据模型与真实业务对象一一映射的系统。让“思维逻辑”和“表达语言”像人的大脑分工一样各司其职让“对象本体”和“数据存档”也有各自清晰的家。这篇文章就把这个抽象命题落成可以照着敲的解决方案适合正在做.NET项目、想搞明白MVC分层到底怎么分以及为什么分层比不分层更好的开发者——尤其是那些被“架构”两个字吓住的新手。1. 拆解标题MVC为什么能隐喻“思维语言世界”与“对象世界”1.1 三层架构与MVC的本质差异很多初学者一提起MVC就想到“分层”但三层架构UI、BLL、DAL和MVC不是一回事。三层架构是按技术职责横着切界面层管界面、业务层管业务、数据层管数据MVC则是按协作关系纵着切View是“这个东西看起来什么样”Controller是“接收到外界的刺激后如何决策”Model是“这个东西本身是什么”。这个分工方式非常接近人的认知模式。一个人的行动链路大致是眼睛耳朵接收到信息 → 大脑做判断 → 身体做出动作 → 对象世界产生结果。对应到代码里View就是那个接收信息、呈现结果的感官和嘴巴Controller是接收请求、转交判断的神经中枢Service层是大脑里真正做推理的灰质Model和仓储层是记忆和对象本体。我第一次给团队讲这个类比时大家看分层架构的眼神都不一样了页面渲染慢的时候你会下意识想“是不是嘴皮子太笨”业务规则出错时会想“是不是大脑逻辑没有约束好”。这就是标题里“思维语言世界”和“人物对象世界”对应的技术落点。1.2 把“四层平台”和“五层MVC”先对齐成工程定义“四层平台的五层MVC”不是.NET里的官方术语我理解它是两个维度叠加四层平台说的是运行时平台从下往上分四层——数据存储平台数据库、缓存、应用服务平台业务代码运行、事务管理、Web宿主平台Kestrel/IIS、中间件管道、客户端展现平台浏览器/前端五层MVC说的是在经典MVC模型基础上细化的代码分层——View层、Controller层、Application Service层、Domain层、Infrastructure层。需要确认一下^MVC^在传统ASP.Net和ASP.Net Core里都成立标题写的“VS中C#.ASP.Net MVC架构模板”指的就是基于Visual Studio创建的MVC项目模板。我这里用的是目前主流的ASP.Net Core MVC在VS 2022中可以直接创建模板会生成Views、Controllers、Models三个目录正好是我们五层结构的起点。下面表格把这套映射关系固定下来后面所有代码都按照这个表来放运行平台代码分层隐喻角色职责客户端展现平台View层表达语言界面呈现、输入校验展示、处理结果反馈Web宿主平台Controller层感知与决策接口接收请求、调用服务、控制页面跳转应用服务平台Application Service层思维判断业务规则、用例编排、事务边界应用服务平台与数据存储平台之间Domain层对象本体实体、值对象、领域约束数据存储平台Infrastructure层记忆存档仓储实现、EF Core、文件存储、第三方访问1.3 一张映射表谁在模拟思维谁在模拟对象把标题中的两个“世界”直接映射到五层代码上是这篇文章最关键的理解工具。我画过一张非常顺手的对照表项目评审时贴在白板旁边比讲十页PPT都管用人类认知软件隐喻典型代码位置我想表达一个意图View表单、Razor视图Person/Create.cshtml外界把话递给了我路由与ModelBindingProgram.cs路由配置大脑判断这件事该不该做Application Service业务规则PersonService.CreateAsync我想起对象是什么模样Domain实体属性Person.Name、Person.BirthDate记忆被调取和存储Repository与DbContextPersonRepository、AppDbContext我说出处理结果ActionResult与View渲染RedirectToAction、Html.Raw这张表我建议你保存下来。标题里的“人机统一地隐喻真实的人物对象世界”在工程上完全可以落地为界面的表单就是你想对这个世界说的话Controller是你的嘴Service是你的脑Domain实体是客观世界里的那个人Repository是那个人留在社会系统里的档案。理解到这个程度下面搭建工程时的每一个代码文件你都能说出它属于哪个“世界”。2. 从VS空白解决方案开始五层工程的划分与依赖约束2.1 五个项目的命名与创建过程我推荐直接用VS 2022的CLI创建解决方案比在界面上一步步点快很多而且项目结构一目了然。在终端里执行下面这组命令dotnet new sln -n HumanMachineUnified dotnet new mvc -n Hmu.Web --use-local-db dotnet new classlib -n Hmu.Domain dotnet new classlib -n Hmu.Application dotnet new classlib -n Hmu.Infrastructure dotnet new classlib -n Hmu.Shared dotnet sln add Hmu.Web Hmu.Domain Hmu.Application Hmu.Infrastructure Hmu.Shared项目命名建议加上Hmu前缀Human Machine Unified一眼就能看出属于这套架构。创建完后的解决方案里Hmu.Web是启动项目包含Controllers、Views、Models目录Hmu.Domain放实体和领域接口Hmu.Application放服务实现Hmu.Infrastructure放EF Core的DbContext和仓储实现Hmu.Shared放枚举、常量、通用扩展方法这类被多项目共享的东西。为什么不把Domain和Application并成一个项目因为很多团队失败的第一原因就是实体和服务耦合结果Domain里引了一堆IQueryable和DTO最后重构时牵一发动全身。Domain独立出来的价值在于它是整个系统里最稳定、最不该被技术框架污染的一层。2.2 引用方向为什么只能向下依赖五层结构的引用关系有一个铁律依赖方向严格向下每一层只能引用自己正下方和更下层的项目。具体如下项目可以引用不允许引用Hmu.WebApplication、Domain、Shared、Infrastructure仅限DI注册无顶层Hmu.ApplicationDomain、SharedWeb、InfrastructureHmu.InfrastructureDomain、SharedWeb、ApplicationHmu.DomainSharedWeb、Application、InfrastructureHmu.Shared无任何上层项目WeB项目允许引用Infrastructure只是为了让DI容器能注册具体实现业务代码中不能直接用Infrastructure的类型。Application不能引用Infrastructure意味着服务层只认接口不认EF Core或某个第三方库这样以后把SQL Server换成PostgreSQL或者把EF Core换成DapperApplication层代码一行都不用动。2.3 四个运行时平台与代码分层的对应关系四层平台中的每一层在实际部署时会落在不同进程甚至不同机器上。Hmu.Web运行在Web宿主平台上也就是Kestrel或IIS里Application和Domain运行在应用服务平台它们跟着Web应用同一个进程走Infrastructure里的DbContext最终操作的数据存储平台是SQL Server或者Redis。这种物理平台的划分决定了日志、异常处理、配置中心都按平台切分Web层管HTTP状态码和请求日志Application层管业务异常和审计日志Infrastructure层管数据库连接和查询超时。提示四层平台不意味着必须部署四台服务器。在中小项目里五层代码完全可以在同一个进程里跑平台是逻辑边界不是物理边界。只有当某一层确实有独立部署需求比如把Application单独做成微服务才需要拆进程。3. 以“人员档案”为例逐层编码从对象实体到页面展示3.1 Domain层人物对象世界的最小建模单元Domain层是整个隐喻系统的“真实人物对象世界”先在这里定义Person实体。这个类不引用任何框架纯粹表达一个人员对象本身的属性和行为规则我把关键方法直接写在实体里避免贫血模型namespace Hmu.Domain.Persons; public class Person { public Guid Id { get; private set; } public string Name { get; private set; } public DateTime BirthDate { get; private set; } public Guid DepartMentId { get; private set; } public DepartMent DepartMent { get; private set; } private Person() { } public Person(string name, DateTime birthDate, Guid departMentId) { Id Guid.NewGuid(); SetName(name); SetBirthDate(birthDate); DepartMentId departMentId; } public void SetName(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException(姓名不能为空, nameof(name)); Name name.Trim(); } public void SetBirthDate(DateTime birthDate) { if (birthDate DateTime.Today) throw new ArgumentException(出生日期不能晚于今天, nameof(birthDate)); BirthDate birthDate; } }你会注意到属性setter是私有的修改必须走方法。这就是“对象世界”的精髓——真实世界的人不会允许别人随意篡改他的出生日期代码里的对象也应该一样。私有构造函数是给EF Core做材料化用的这是EF Core的常见做法不是缺陷。3.2 Infrastructure层用EF Core保存对象状态Infrastructure层负责把Domain对象“存档”到数据库。先定义AppDbContext再把Person实体映射到Person表namespace Hmu.Infrastructure.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetPerson Persons SetPerson(); public DbSetDepartMent DepartMents SetDepartMent(); protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.EntityPerson(e { e.ToTable(Persons); e.HasKey(x x.Id); e.Property(x x.Name).HasMaxLength(50).IsRequired(); e.HasOne(x x.DepartMent) .WithMany(d d.Persons) .HasForeignKey(x x.DepartMentId); }); } }然后是PersonRepository。仓储的职责是封装所有对Person表的查询和写入调用方拿到的永远是Domain实体或领域类型看不到IQueryable的痕迹using Hmu.Domain.Persons; using Microsoft.EntityFrameworkCore; namespace Hmu.Infrastructure.Repositories; public sealed class PersonRepository : IPersonRepository { private readonly AppDbContext _context; public PersonRepository(AppDbContext context) { _context context; } public async TaskPerson? GetByIdAsync(Guid id, CancellationToken ct default) { return await _context.Persons .FirstOrDefaultAsync(p p.Id id, ct); } public async Taskint CountByDepartMentAsync(Guid departMentId, CancellationToken ct default) { return await _context.Persons .CountAsync(p p.DepartMentId departMentId, ct); } public async Task AddAsync(Person person, CancellationToken ct default) { await _context.Persons.AddAsync(person, ct); await _context.SaveChangesAsync(ct); } }一些团队喜欢直接用DbContext不包仓储。小项目确实可以但一旦Domain层定义了自己的仓储接口Infrastructure去实现项目整体的依赖方向就稳住了。这就是依赖倒置原则在五层结构里的具体体现。3.3 Application服务层思维语言世界的判断逻辑服务层是“思维判断”发生的地方。控制器把用户的请求递进来服务层负责判断“这件事能不能做”“做了之后要改哪些对象”然后指挥仓储执行。比如“向某部门添加新人员”这个用例业务规则是部门存在、且当前人数没满员using Hmu.Domain.DepartMents; using Hmu.Domain.Persons; namespace Hmu.Application.Persons; public sealed class PersonService : IPersonService { private readonly IPersonRepository _personRepository; private readonly IDepartMentRepository _departMentRepository; public PersonService( IPersonRepository personRepository, IDepartMentRepository departMentRepository) { _personRepository personRepository; _departMentRepository departMentRepository; } public async Task(bool Success, string? ErrorMessage, Guid? PersonId) CreateAsync( string name, DateTime birthDate, Guid departMentId, CancellationToken ct default) { var departMent await _departMentRepository.GetByIdAsync(departMentId, ct); if (departMent is null) return (false, 所选部门不存在, null); var currentCount await _personRepository.CountByDepartMentAsync(departMentId, ct); if (currentCount departMent.MaxMemberCount) return (false, $部门人数已满上限{currentCount}人, null); var person new Person(name, birthDate, departMentId); await _personRepository.AddAsync(person, ct); return (true, null, person.Id); } }注意返回值我用了元组简单用例不需要创建Request和Response两个类过度设计也是分层架构常见的坑。真正的关键是所有业务判断都在这里完成Controller不知道满员这种规则存在仓储不知道业务规则存在每一层只做自己的事。3.4 Web层Controller、View与ViewModel如何协作Web层是“表达语言”的出口。Controller负责接收HTTP请求、调用服务、决定返回哪个视图。ViewModel是五层里容易被忽略但实际上极其重要的一环——它充当对象世界和语言世界之间的翻译官。以列表页为例using Hmu.Application.Persons; using Hmu.Web.Models; namespace Hmu.Web.Controllers; public class PersonController : Controller { private readonly IPersonService _personService; public PersonController(IPersonService personService) { _personService personService; } public async TaskIActionResult Index(CancellationToken ct) { var persons await _personService.GetAllAsync(ct); var vm new PersonListViewModel { Items persons.Select(p new PersonItemViewModel { Id p.Id, Name p.Name, BirthDate p.BirthDate, DepartMentName p.DepartMent?.Name ?? 未分配 }).ToList() }; return View(vm); } }视图接收强类型ViewModel而不是直接接收Domain实体这也是一个容易踩的线。ViewModel只包含页面需要展示的字段既不暴露审计字段也不会因为导航属性被前端意外触发而发生性能问题。Razor视图里直接写ViewModel属性页面职责非常纯粹model PersonListViewModel table thead tr th姓名/th th出生日期/th th所属部门/th /tr /thead tbody foreach (var item in Model.Items) { tr tditem.Name/td tditem.BirthDate.ToShortDateString()/td tditem.DepartMentName/td /tr } /tbody /table4. 一次请求的完整旅程两个世界的对话如何闭环4.1 从URL到数据库再到HTML的链路拆解把五层代码写出来后真正考验理解的是请求链路。我建议画一条线从浏览器发起请求开始在纸上走一遍浏览器输入/person/index这个URL先被路由解析ASP.Net Core会把路径映射到PersonController的Index方法Controller被DI容器实例化注入了IPersonServiceAction调Service方法Service调用仓储接口仓储实现里EF Core生成SQL语句发送到数据库数据库返回行集EF Core将其物化为Person实体Service把实体返回给ControllerController组装ViewModel交给Razor引擎渲染成HTML最后浏览器把这个HTML“说”给用户听。这条链路有一个很容易忽略的点ModelBinding。表单提交时浏览器传来的字符串要转成Action参数比如Create页面里用户填的BirthDate是一个字符串ModelBinder会自动做类型转换和验证。这个过程就是标题里“思维语言世界”的入口翻译——外界说的话HTTP表单经过翻译变成系统听得懂的参数翻译得过关验证通过才能进到大脑判断阶段。4.2 ViewModel与DTO对象世界与语言世界之间的翻译官很多项目把Domain实体直接当ViewModel用短期觉得省事后期代价很大。我给你列一下直接暴露Domain实体的后果第一页面可能会显示不该显示的内容比如Person.InternalRemark这种内部备注被渲染出来第二Update方法如果直接接受Person实体用户可以通过表单域注入修改任意属性这就是经典的Mass Assignment漏洞第三延迟加载不可控Razor视图里一旦访问了某个导航属性EF Core会额外发查询经常拖慢页面。所以我在第五章里把ViewModel独立成一个目录并且坚持每个页面一个ViewModel。例如namespace Hmu.Web.Models; public class PersonCreateViewModel { public string Name { get; set; } string.Empty; [DataType(DataType.Date)] public DateTime BirthDate { get; set; } DateTime.Today; public Guid DepartMentId { get; set; } public ListDepartMentOptionViewModel DepartMents { get; set; } new(); }Controller的Create Post方法里先判断ModelState.IsValid再调用Service返回成功就Redirect失败就重新绑好DepartMents下拉列表返回视图。这套流程看似多写了几个类但实际上每多一个类都是在为对象世界与语言世界之间建立一道清晰的翻译边界。4.3 依赖注入让五层架构在四个平台上流畅运行五层之间不是靠new来连接的而是靠DI容器注入。在Program.cs里完成所有注册这是Web层引用Infrastructure的唯一合法理由using Hmu.Application.Persons; using Hmu.Domain.Persons; using Hmu.Infrastructure.Data; using Hmu.Infrastructure.Repositories; using Microsoft.EntityFrameworkCore; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddScopedIPersonRepository, PersonRepository(); builder.Services.AddScopedIDepartMentRepository, DepartMentRepository(); builder.Services.AddScopedIPersonService, PersonService(); builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(Default))); var app builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.MapControllerRoute( name: default, pattern: {controllerPerson}/{actionIndex}/{id?}); app.Run();DI的生命周期是一个值得展开的点AddScoped表示一次HTTP请求内只有一个实例这对应“一次会话中的一次思考过程”请求结束实例销毁AddTransient每次解析都是一个新实例适合无状态的小工具类AddSingleton整个应用生命周期只创建一次适合缓存和配置对象。如果理解不了就把大脑里的神经元比作Scoped——一个请求就是一次思考思考过程中所有环节共享同一个上下文。提示千万别把DbContext注册成Singleton。EF Core的DbContext本身不是线程安全的Singleton意味着多线程并发操作同一个实例你会看到一堆诡异的数据库连接错误。5. 实操经验这套架构模板的边界、取舍与常见坑5.1 循环引用最典型的架构污染我做技术评审时最常遇到的问题是项目里出现了循环引用。典型场景Application层想用某个Infrastructure层的帮助类直接加了引用过了两天Infrastructure层的另一个文件又用了Application的某个接口于是VS报出循环引用错误被迫merge项目分层彻底失效。循环引用的解决方案只有一个找到公共的那个概念往下移。如果两个上层项目都需要某个功能这个功能应该放在Domain或Shared里。拿AOP日志举例Application需要一个ILogger但日志实现放在InfrastructureApplication不能引用Infrastructure那就在Domain或Shared里定义ILogger接口Infrastructure去实现它注册时通过DI把实现注入到Application。依赖倒置在这里的体现就是上层定义抽象下层实现抽象但依赖方向永远向下。5.2 空转分层五层模板沦为摆设的三个信号五层架构也有致命伤就是为分层而分层。我见过一个项目Service层每个方法都是三行代码调用仓储、返回结果、没有任何业务逻辑。Controller又把Service的结果原封不动交给View。整个项目代码量翻了一倍但功能没有变多。这属于典型的空转分层判断标准有三个第一Service层有没有实际业务规则如果每个Service方法只是往Repository传参数没有if判断、没有对象之间的协调Service层就是摆设第二ViewModel是否真的和Domain实体字段不同如果页面用的字段和实体完全一致先不要急着建ViewModel直接重构实体本身第三检查是否出现了“A调用B、B调用A”的透传链条比如Controller调ServiceService调RepositoryRepository调一个Extension方法——三层都在做同一件事。遇到这种信号我的建议是先从最小的业务模块开始简化把Service撤掉Controller直接使用仓储接口等业务复杂度真正起来了再重新引入Service。架构模板是给项目一个共同的骨架认知不是让每个项目都要长成标准的五层。5.3 隐喻应该停留在理解层不要污染代码命名最后一个经验是关于标题本身的。用“思维语言世界”和“人物对象世界”来理解MVC分层非常有效但不要把隐喻带进代码。我见过有人把服务层类名起成BrainService把仓储类名起成MemoryRepository这种命名对团队是灾难。代码命名应该使用领域业务语言PersonService、OrderRepository、DepartMentController隐喻只存在于架构讨论和设计文档中帮助理解职责分配真正进入代码时还是要遵循领域术语。这就回到了标题里那句“人智数智的思维语言世界”。我的理解是人类智能和数字智能统一之后计算机程序不再是冷冰冰的CRUD而是像一个有思考、有语言、有记忆、有对象认知能力的个体。MVC的分层结构恰好提供了一副骨架让这种统一能落地到具体的工程代码里。但骨架是骨架血肉还是要由具体的业务规则和领域模型来填充。最后再分享一个我从多个项目里磨出来的体会这套架构模板最适合用在业务规则复杂、团队规模超过三个人、需求会持续演进的项目上。如果你只是写个内部小工具三个人不到老老实实用一个Web项目加简单的Repository就够了。真正熟练的架构师最擅长的不是搭出五层而是在项目成长的过程中按需把某一两层加回来或者把某一两层拆出去。我现在做代码评审时看的从来不是分层数量而是每个层是否真的承担了不可替代的职责。用这个方法去验证你正在写的项目比死记模板有用得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询