第03章 01学习笔记:模型绑定、验证及 EF Core 数据操作
板块①:本章地图
一句话定位
本章是 MVC 数据流转闭环的核心章节——连接 HTTP 请求 ↔ 模型 ↔ 数据库的桥梁。学完本章后,你将掌握"接收用户输入 → 验证 → 持久化"的完整数据管道。
前置依赖清单表
| 序号 | 前置知识点 | 来源章节 | 当前状态(自填) | 版本要求 |
|---|---|---|---|---|
| 1 | EF Core 实体模型(Category, Product, Supplier) | Ch1 | □ 已掌握 □ 需回顾 □ 全新 | EF Core 9+ |
| 2 | EF Core DbContext(NorthwindContext) | Ch1 | □ 已掌握 □ 需回顾 □ 全新 | EF Core 9+ |
| 3 | Docker / Azure SQL Edge 运行 | Ch1 | □ 已掌握 □ 需回顾 □ 全新 | Docker Desktop |
| 4 | MVC 控制器与 Action 方法 | Ch2 | □ 已掌握 □ 需回顾 □ 全新 | ASP.NET Core 9+ |
| 5 | ASP.NET Core 路由系统 | Ch2 | □ 已掌握 □ 需回顾 □ 全新 | ASP.NET Core 9+ |
| 6 | Razor 视图与 Tag Helpers | Ch2 | □ 已掌握 □ 需回顾 □ 全新 | ASP.NET Core 9+ |
| 7 | IActionResult 返回类型(View, Content, Json, RedirectToAction) | Ch2 | □ 已掌握 □ 需回顾 □ 全新 | ASP.NET Core 9+ |
| 8 | HomeSupplierViewModel 基础定义 | Ch2 | □ 已掌握 □ 需回顾 □ 全新 | — |
| 9 | Dependency Injection(DI)服务注册 | Ch1 | □ 已掌握 □ 需回顾 □ 全新 | — |
版本说明
本章涉及的核心 API(模型绑定器、数据注解、EF Core CRUD)自 .NET Core 3.1 以来保持高度稳定。唯一变化是:本书使用 .NET 9,当前最新为 .NET 10,但 零改动 可在 .NET 10 上直接运行。异步方法(ToListAsync、SingleOrDefaultAsync)是自 EF Core 3.0 起的标准 API,无需任何迁移。
运行环境
| 组件 | 版本要求 | 安装命令 |
|---|---|---|
| .NET SDK | ≥ 10.0.300 | winget install Microsoft.DotNet.SDK.10 |
| EF Core + SqlServer | 10.0.8 | 已包含于 Northwind 项目 |
| Azure SQL Edge(Docker) | latest | docker run --name azuresqledge -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=YourPassword123!' -p 1433:1433 -d mcr.microsoft.com/azure-sql-edge |
| 浏览器 | Chrome / Edge | 用于手动测试 |
⚠️ 与全书基准环境一致。
板块②:核心概念深度卡("剥洋葱"三层拆解)
概念卡片 1:模型绑定(Model Binding)
| 属性 | 说明 |
|---|---|
| 概念名 | 模型绑定 / Model Binding |
| 稳定性 | 稳定(自 MVC 2.0 以来核心机制不变) |
| 全书追踪 | Ch3(首次)→ Ch4(Tag Helpers 配合)→ Ch9(Web API 绑定)→ Ch10(OData 绑定)→ Ch11(FastEndpoints 请求映射) |
第一层(一句话直击本质):
ASP.NET Core 自动将 HTTP 请求的各部分数据(Route / Query String / Form / Body)映射到 C# 对象的过程,无需手动解析 Request.QueryString["key"]。
第二层(核心机制展开):
模型绑定器(DefaultModelBinder)按优先级顺序从四个来源提取值:
- 表单值(Form):HTTP POST 体中的
application/x-www-form-urlencoded数据 - 路由数据(Route):URL 路径模板中的占位符(如
/Home/Detail/{id}) - 查询字符串(Query String):URL
?之后的部分 - 上传文件:仅在参数为
IFormFile时触发
绑定目标可以是:
- 简单类型:
int、string、bool、DateTime等 - 复杂类型:递归地将子属性与请求键值匹配
- 集合类型:
List<T>或数组,通过[0]、[1]索引匹配
绑定流程:
HTTP Request│├─ 1. 解析路由 → 提取 {controller}/{action}/{id?}├─ 2. 解析 Query String → 提取 ?key1=val1&key2=val2├─ 3. 解析 Form Body → 提取 POST 体中的键值对│▼ModelBinder│ 按优先级:Form > Route > Query String│ 按名称匹配:请求键 ↔ 模型属性名│ 递归绑定:复杂类型逐层展开▼ModelState│ 存储绑定结果 + 验证错误▼Controller Action 参数(已完成映射的 C# 对象)
第三层(架构本质):
模型绑定体现了 ASP.NET Core 的 约定优于配置(Convention over Configuration) 设计哲学。核心思想:将重复的、机械的 HTTP 数据提取工作从开发者手中剥离,让开发者专注于业务逻辑。与 Ruby on Rails 的 Strong Parameters 和 Spring MVC 的 @ModelAttribute 类似,但 ASP.NET Core 的绑定器不强制显式声明绑定源——它自动按优先级探测,这带来了便利但也是 Over-posting 攻击的根源。DefaultModelBinder 本身是一个可替换的中间件组件,通过 IModelBinderProvider 和 IModelBinder 接口实现策略模式,允许自定义绑定逻辑。
概念卡片 2:模型验证 / Data Annotations
| 属性 | 说明 |
|---|---|
| 概念名 | 模型验证 / Data Annotations |
| 稳定性 | 稳定(自 .NET 3.5 引入 Data Annotations 以来核心属性不变) |
| 全书追踪 | Ch3(首次)→ Ch4(UI 端验证配合)→ Ch9(Web API 验证)→ Ch11(FastEndpoints 验证器) |
第一层(一句话直击本质):
使用 C# 特性(Attribute)声明式定义数据约束规则,运行时自动在模型绑定阶段校验数据合法性。
第二层(核心机制展开):
常用验证属性清单:
| 验证属性 | 作用 | 典型用法 | 服务端/客户端 |
|---|---|---|---|
[Required] |
字段不可为空 | [Required] public string Name { get; set; } |
双端 |
[StringLength(n)] |
字符串最大长度 | [StringLength(40)] |
双端 |
[StringLength(n, MinimumLength=m)] |
字符串长度范围 | [StringLength(40, MinimumLength=3)] |
双端 |
[Range(min, max)] |
数值范围 | [Range(0, 9999.99)] |
双端 |
[EmailAddress] |
邮箱格式 | [EmailAddress] |
双端 |
[Phone] |
电话格式 | [Phone] |
双端 |
[Url] |
URL 格式 | [Url] |
双端 |
[RegularExpression(pattern)] |
正则匹配 | [RegularExpression(@"^[A-Z]+$")] |
双端 |
[Compare("OtherProperty")] |
两属性值一致 | [Compare("Password")] |
双端 |
[CreditCard] |
信用卡格式 | [CreditCard] |
双端 |
验证流程:
模型绑定完成 → ModelState 自动触发验证│ 逐属性执行所有 ValidationAttribute.IsValid()│ 将错误信息追加到 ModelState[propertyName].Errors▼
ModelState.IsValid├─ true → 进入业务逻辑└─ false → 返回视图(显示校验错误)
客户端验证(jQuery Unobtrusive Validation)自动从 Data Annotations 生成 JavaScript 规则,无需额外编码。
第三层(架构本质):
Data Annotations 是一种声明式约束元编程(Declarative Constraint Metaprogramming),将验证规则从业务逻辑中解耦并提升至元数据层。核心设计理念:将"合法数据长什么样"作为类型的属性之一,而非散布在控制器代码中。验证本身分为两层——服务端验证(安全最后防线)和客户端验证(用户体验优化)——这体现了纵深防御原则。与 Fluent Validation 等流式验证框架相比,Data Annotations 的优势在于零代码量客户端验证生成,但劣势是复杂规则(如跨属性条件验证)表达力不足,此时需要实现 IValidatableObject.Validate()。
概念卡片 3:路由参数绑定
| 属性 | 说明 |
|---|---|
| 概念名 | 路由参数绑定 / Route Parameter Binding |
| 稳定性 | 稳定 |
| 全书追踪 | Ch2(路由定义)→ Ch3(深度绑定用法)→ Ch9(Web API 路由) |
第一层(一句话直击本质):
URL 路径中的模板占位符 {parameter} 自动注入对应 Action 参数,由模型绑定器按名称匹配完成。
第二层(核心机制展开):
// 路由模板:home/modelbinding/{id?}
// URL: /home/modelbinding/5
// 效果: id = 5public IActionResult ModelBinding(int? id) { ... }
关键机制:
- 路由参数优先级高于查询字符串,但低于表单值
- 在
<form>的action属性中可混合路由参数和查询字符串 - 参数名必须与路由模板中的占位符名完全一致(不区分大小写)
第三层(架构本质):
路由参数绑定是 RESTful URL 设计的基石,将"资源标识"嵌入 URL 路径而非查询字符串,体现了 REST 的资源定位哲学。在 ASP.NET Core 中,路由参数绑定比查询字符串绑定更受信赖(优先级更高),因为路径是 URL 的"核心标识",而查询字符串是"附加过滤"。
概念卡片 4:查询字符串绑定
| 属性 | 说明 |
|---|---|
| 概念名 | 查询字符串绑定 / Query String Binding |
| 稳定性 | 稳定 |
| 全书追踪 | Ch2(基础)→ Ch3(配合模型绑定)→ Chapter 6(缓存 VaryByQuery) |
第一层(一句话直击本质):
URL ? 之后 key=value 对自动映射到 Action 方法参数的同名属性。
第二层(核心机制展开):
// URL: /home/modelbinding/5?color=Red&email=test@example.com
public IActionResult ModelBinding(int? id, string color, string email)
// id = 5, color = "Red", email = "test@example.com"
- 绑定匹配不区分大小写
- 可通过
[FromQuery]特性显式声明绑定源 - 对于复杂类型的属性,同样按名称逐属性匹配
第三层(架构本质):
查询字符串绑定体现了 HTTP GET 请求的幂等性和可书签化设计。由于查询字符串是 URL 的一部分,绑定了查询字符串的 GET 页面可以被缓存、分享和书签。相比之下,表单绑定(POST)的数据不在 URL 中,不可缓存在浏览器历史中。
概念卡片 5:表单绑定
| 属性 | 说明 |
|---|---|
| 概念名 | 表单绑定 / Form Binding |
| 稳定性 | 稳定 |
| 全书追踪 | Ch2(基础 form)→ Ch3(配合 CSRF + 验证)→ Ch4(Tag Helpers 表单) |
第一层(一句话直击本质):
HTTP POST 请求体中的 application/x-www-form-urlencoded 键值对自动绑定到 Action 参数。
第二层(核心机制展开):
<!-- Razor View -->
<form method="POST"><input asp-for="Supplier.CompanyName" placeholder="Company Name"/><input asp-for="Supplier.Country" placeholder="Country"/><input type="submit" value="Insert"/>
</form>
// Controller
[HttpPost]
public IActionResult AddSupplier(Supplier supplier) { ... }
asp-forTag Helper 生成name="Supplier.CompanyName"等属性- 模型绑定器识别前缀
Supplier.并递归绑定到嵌套属性 - 表单绑定在所有绑定源中优先级最高
第三层(架构本质):
表单绑定是传统 Web 1.0「请求-响应」模型的核心,不同于现代 SPA 的 JSON Body 绑定。其设计约束是:表单数据总是扁平的键值对格式,嵌套结构通过 前缀命名约定(Parent.Child)表达。这体现了 Web 标准与框架抽象的折中——HTTP Form 协议天然扁平,框架通过命名约定模拟层次结构。
概念卡片 6:HTTP 错误状态码
| 属性 | 说明 |
|---|---|
| 概念名 | HTTP 错误状态码方法 |
| 稳定性 | 稳定 |
| 全书追踪 | Ch3(首次)→ Ch9(Web API 标准错误)→ Ch10(OData 错误) |
第一层(一句话直击本质):
ControllerBase 内置的方法,统一返回带正确 HTTP 状态码的标准化错误响应。
第二层(核心机制展开):
| 方法 | 状态码 | 适用场景 | 用法示例 |
|---|---|---|---|
BadRequest() |
400 | 请求数据格式错误 | return BadRequest("Invalid data"); |
NotFound() |
404 | 资源不存在 | return NotFound(); |
Unauthorized() |
401 | 未认证(需登录) | return Unauthorized(); |
Forbid() |
403 | 已认证但无权限 | return Forbid(); |
Conflict() |
409 | 资源冲突(并发编辑) | return Conflict(); |
UnprocessableEntity() |
422 | 语义错误(格式正确但逻辑有误) | return UnprocessableEntity(); |
StatusCode(code) |
自定义 | 返回任意状态码 | return StatusCode(418, "I'm a teapot"); |
Problem(detail, statusCode) |
500 | 标准化 Problem Details 错误 | return Problem(detail: ex.Message, statusCode: 500); |
ValidationProblem() |
400 | 模型验证失败,返回验证错误详情 | return ValidationProblem(ModelState); |
第三层(架构本质):
这些方法体现了 HTTP 语义优先 的设计理念。传统 Web Forms 时代,错误以 HTML 页面返回(状态码 200),这违反了 HTTP 协议语义。ASP.NET Core 将 HTTP 状态码提升为一等公民,每个方法都精确映射到 HTTP 规范中的语义。Problem() 和 ValidationProblem() 进一步实现了 RFC 7807(Problem Details for HTTP APIs)标准,为客户端提供机器可读的错误响应结构。
概念卡片 7:过度提交防护(Over-posting / Mass Assignment)
| 属性 | 说明 |
|---|---|
| 概念名 | 过度提交攻击防护 / Over-posting Prevention |
| 稳定性 | 稳定 |
| 全书追踪 | Ch3(首次)→ Ch9(Web API 的 [Bind]) |
第一层(一句话直击本质):
防止攻击者通过 HTTP 请求修改模型中不应被修改的属性(如 IsAdmin),通过限制可绑定属性列表来防御。
第二层(核心机制展开):
攻击原理:
// 用户编辑邮箱的 Form:
// <input name="Email" value="user@example.com"/>
// 攻击者添加隐藏字段:
// <input name="IsAdmin" value="true"/>
// → 如果直接绑定 UserProfile 对象,IsAdmin 被非法设置!
三种防御手段:
方案一:[Bind] 特性(白名单,推荐)
public IActionResult UpdateProfile([Bind(nameof(UserProfile.Email), nameof(UserProfile.Name))]UserProfile profile)
{// 只有 Email 和 Name 会被绑定
}
方案二:[BindNever] 特性(黑名单)
public class UserProfile
{public string Email { get; set; }[BindNever]public bool IsAdmin { get; set; } // 永远不被绑定
}
方案三:ViewModel(最佳实践)
// 只暴露需要编辑的字段
public class UpdateEmailViewModel
{public string Email { get; set; }public string Name { get; set; }
}
第三层(架构本质):
Over-posting 是 ORM 框架的固有风险——实体类的属性同时承担"持久化"和"用户输入"双重职责。ViewModel 模式通过类型层面的职责分离从根本上解决了这个问题:用于接收用户输入的类型不应该与用于持久化的类型是同一个。这与 OWASP Top 10(2013 A8: Insecure Deserialization / 2021 A8: Software and Data Integrity Failures)直接相关。
概念卡片 8:EF Core CRUD 操作
| 属性 | 说明 |
|---|---|
| 概念名 | EF Core CRUD 操作 |
| 稳定性 | 稳定 |
| 全书追踪 | Ch1(读 / 建模)→ Ch3(写 / CRUD 完整)→ Ch5(配合认证的 CRUD)→ Ch9(Web API CRUD) |
第一层(一句话直击本质):
通过 EF Core 的 DbSet<T> 方法和 SaveChanges() 对数据库执行增删改操作,无需手写 SQL。
第二层(核心机制展开):
// ===== CREATE =====
_db.Suppliers.Add(supplier); // 标记为 Added
int affected = _db.SaveChanges(); // 生成 INSERT SQL 并执行// ===== READ =====
List<Supplier> suppliers = _db.Suppliers.ToList(); // SELECT * FROM Suppliers// ===== UPDATE =====
Supplier? supplier = _db.Suppliers.Find(id); // SELECT ... WHERE Id = @id
supplier.Phone = "(603) 555-4568"; // 修改跟踪属性的值
affected = _db.SaveChanges(); // 生成 UPDATE SQL// ===== DELETE =====
_db.Suppliers.Remove(supplier); // 标记为 Deleted
affected = _db.SaveChanges(); // 生成 DELETE SQL
关键机制:
- Change Tracker(变更追踪器):追踪从数据库检索的所有实体状态(Added / Unchanged / Modified / Deleted)
- SaveChanges() 的 SQL 生成过程:遍历变更追踪器 → 对每个变更的实体生成对应的 DML → 封装在一个事务中执行
- affected 返回值:受影响的行数,可用于判断操作是否成功(affected == 0 表示无变更)
第三层(架构本质):
EF Core 的 CRUD 操作体现了 Unit of Work(工作单元) 设计模式——DbContext 作为一个事务边界,SaveChanges() 一次性提交所有变更。变更追踪器是一个微型的 Identity Map,确保同一个数据库行在内存中只有一个 C# 对象实例。这种设计允许多个业务操作在一个事务中执行,保证了 ACID 特性。
概念卡片 9:CSRF 防护 / AntiForgeryToken
| 属性 | 说明 |
|---|---|
| 概念名 | CSRF 防护 / AntiForgeryToken |
| 稳定性 | 稳定 |
| 全书追踪 | Ch3(首次)→ Ch5(结合认证的 CSRF) |
第一层(一句话直击本质):
通过 Cookie 和 Form 双 Token 验证机制,确保 POST 请求确实来自本站页面(而非第三方恶意网站)的防护措施。
第二层(核心机制展开):
CSRF 攻击原理:
1. 用户登录了 bank.com(Cookie 已存入浏览器)
2. 用户访问了 evil.com(含有隐藏的表单)
3. evil.com 的隐藏表单自动提交到 bank.com/transfer
4. 浏览器自动携带 bank.com 的 Cookie → 银行以为是用户操作!
ASP.NET Core 双 Token 防御流程:
GET 请求 /AddSupplier│├─ @Html.AntiForgeryToken()│ 生成两个 Token:Token A 写入 Cookie, Token B 写入 hidden input│ 两个 Token 内容一致(加密后)▼浏览器:Cookie 自动保存 + HTML 渲染到页面│用户点击 Submit(POST 请求)│├─ 浏览器自动发送 Cookie Token A├─ Form 携带 hidden input Token B▼[ValidateAntiForgeryToken] 过滤器│ 解密两个 Token → 比较内容├─ 一致 → 请求来自本站 → 允许└─ 不一致/缺失 → 403 Forbidden(已登录)/ 400 Bad Request(未登录)
实现代码:
<!-- Razor View -->
<form method="POST">@Html.AntiForgeryToken()<!-- 表单字段 -->
</form>
// Controller
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult AddSupplier(Supplier supplier) { ... }
第三层(架构本质):
CSRF 防御的本质是利用同源策略的边界差异。恶意网站可以触发跨域请求并携带 Cookie(浏览器自动行为),但无法读取目标域下的 Cookie 内容。ASP.NET Core 的双 Token 机制正是利用了这一点:恶意网站无法读取 Cookie 中的 Token A,因此无法在 Form 中构造与 Token A 匹配的 Token B。这是 Synchronizer Token Pattern(STP) 的标准实现。
⚠️ 安全风险提示:
- CWE-352(Cross-Site Request Forgery):如果不使用 AntiForgeryToken,攻击者可通过自动提交的表单执行任意操作
- JSON API 端点通常不需要 AntiForgeryToken,但需通过 CORS + Authorization Header 防御
概念卡片 10:ViewModel 模式
| 属性 | 说明 |
|---|---|
| 概念名 | 视图模型模式 / ViewModel Pattern |
| 稳定性 | 稳定 |
| 全书追踪 | Ch2(首次引入 HomeSupplierViewModel)→ Ch3(深度应用)→ Ch4(复杂 ViewModel) |
第一层(一句话直击本质):
一个专门为视图量身定制的数据传输对象,封装"实体数据 + UI 元数据",即将持久化模型(Entity Model)与展示模型(View Model)分离。
第二层(核心机制展开):
本章 ViewModel 设计 —— HomeSupplierViewModel:
public class HomeSupplierViewModel
{public int Affected { get; set; } // 操作影响的行数(元数据)public Supplier? Supplier { get; set; } // 实体数据public IEnumerable<string>? ValidationErrors { get; set; } // 错误信息(元数据)public HomeSupplierViewModel(int affected, Supplier? supplier){Affected = affected;Supplier = supplier;}
}
ViewModel 的三种角色:
- 数据聚合:将多个来源的数据打包(如 Supplier + Affected count)
- 安全隔离:只暴露视图需要的属性,防止 Over-posting
- UI 适配:将数据库字段适配为友好的展示格式
第三层(架构本质):
ViewModel 的核心思想是 单一职责原则 在 Web 层的体现——实体类的职责是"持久化映射",ViewModel 的职责是"展示契约"。直接暴露实体类给视图会导致:① 安全风险(Over-posting);② 耦合(UI 需求变化被迫修改实体类);③ 低效(加载不需要的导航属性)。这种分离与 MVVM(Model-View-ViewModel)中的 VM 概念一致但不完全相同——MVC ViewModel 更轻量,通常没有 INotifyPropertyChanged 的绑定能力。
概念卡片 11:异步控制器动作
| 属性 | 说明 |
|---|---|
| 概念名 | 异步控制器动作 / Async Controller Actions |
| 稳定性 | 稳定 |
| 全书追踪 | Ch3(首次)→ Ch6(异步缓存)→ Ch9(异步 Web API)→ Ch12(异步集成测试) |
第一层(一句话直击本质):
将同步 Action 方法改为 async Task<IActionResult>,用 await 调用 EF Core 的异步数据库方法,使线程在等待 I/O 时不阻塞,返回线程池以服务其他请求。
第二层(核心机制展开):
改造前后对比:
// ❌ 同步版本:线程在数据库查询期间完全阻塞
public IActionResult Index()
{var categories = _db.Categories.ToList(); // 阻塞等待return View(categories);
}// ✅ 异步版本:await 期间线程归还线程池
public async Task<IActionResult> Index()
{List<Category> categories = await _db.Categories.ToListAsync();return View(categories);
}
异步改造三步法:
- 方法签名:
IActionResult→async Task<IActionResult> - EF Core 方法:
ToList()→ToListAsync(),Find()→FindAsync(),SingleOrDefault()→SingleOrDefaultAsync() - 需要
using Microsoft.EntityFrameworkCore;以启用异步扩展方法
关键理解:异步 ≠ 更快。异步 = 更好的可扩展性(更多并发请求)。单个请求的数据库查询耗时不变,但在高并发场景下服务器可以处理更多请求。
第三层(架构本质):
异步 Action 方法的底层机制是 .NET 的 async/await 状态机 和 I/O 完成端口(IOCP)。当执行 await _db.SaveChangesAsync() 时:① 线程将 I/O 请求提交给操作系统;② 线程立即返回线程池;③ I/O 完成后,操作系统通过 IOCP 通知 .NET 运行时;④ 运行时从线程池获取一个线程(可能是另一个线程)继续执行 await 之后的代码。这避免了同步 I/O 中"一个线程等一个请求"的 1:1 绑定,实现了 M:N(M 个线程服务 N 个请求),在 N ≫ M 时显著提升吞吐量。
概念卡片 12:显示模板(Display Templates)
| 属性 | 说明 |
|---|---|
| 概念名 | 显示模板 / Display Templates |
| 稳定性 | 稳定(自 MVC 3 以来) |
| 全书追踪 | Ch3(首次)→ Ch4(深入 Tag Helpers) |
第一层(一句话直击本质):
@Html.DisplayForModel() 和 @Html.DisplayFor(expression) 是 HTML Helper 方法,根据属性类型自动选择渲染方式。
第二层(核心机制展开):
ASP.NET Core 内置显示模板根据属性类型选择渲染方式:
string→ 纯文本decimal→ 格式化数字bool→ 复选框(只读模式)- 可自定义模板:在
Views/Shared/DisplayTemplates/下放置命名模板
第三层(架构本质):
显示模板是 约定优于配置 的又一体现——框架自动匹配合适的渲染方式,但保留自定义能力。这在搭配 Entity Framework 实体类时尤为方便,因为 Model 直接传入视图时,Display Templates 自动为每个属性选择渲染方式。
概念卡片 13:关联数据加载(Include / Eager Loading)
| 属性 | 说明 |
|---|---|
| 概念名 | 关联数据预加载 / Eager Loading with Include |
| 稳定性 | 稳定 |
| 全书追踪 | Ch1(首次使用)→ Ch3(重温)→ Ch6(缓存场景) |
第一层(一句话直击本质):
使用 .Include() 和 .ThenInclude() 在单次数据库查询中同时加载关联实体,避免 N+1 问题。
第二层(核心机制展开):
// 一层关联
Product? model = await _db.Products.Include(p => p.Category).SingleOrDefaultAsync(p => p.ProductId == id);// 两层关联
Product? model = await _db.Products.Include(p => p.Category).Include(p => p.Supplier).SingleOrDefaultAsync(p => p.ProductId == id);
.Include()生成 SQLJOIN- N+1 问题:如果没有 Include,每个 Product 的 Category 将单独查询
.ThenInclude()用于加载更深层的关联(如Category → Subcategory)
第三层(架构本质):
Eager Loading 是 ORM 性能优化的核心手段。EF Core 默认使用 Lazy Loading(按需加载),这会在循环中触发 N+1 问题。Include 强制使用 JOIN 一次性加载所有需要的数据,但代价是可能加载过多冗余数据。正确策略是显式加载(Explicit Loading)——只 Include 视图确实需要的导航属性。
板块③:名词深度词典
| # | 名词 | 英文名 | 标准定义 | 所属概念卡片 | 类比理解 | 常见误解 |
|---|---|---|---|---|---|---|
| 1 | 模型绑定器 | Model Binder | 将 HTTP 请求数据映射到 Action 参数的中间件组件 | 卡片 1 | 类似 JSON.Deserialize,但输入是 HTTP 请求各部分 | 误以为只能绑定简单类型 |
| 2 | 数据注解 | Data Annotations | 装饰在属性上的 C# Attribute,声明验证规则 | 卡片 2 | 类似 TypeScript 的 decorator 或 Java Bean Validation | 误以为只有服务端生效 |
| 3 | ModelState | Model State | 存储模型绑定和验证结果的字典结构 | 卡片 2 | 类似一个“表单结果报告单” | 误以为只存错误不存成功信息 |
| 4 | 绑定优先级 | Binding Precedence | Form > Route > Query String 的绑定源优先级顺序 | 卡片 1 | 类似 CSS 特异性:内联 > ID > Class | 误以为优先级可配置修改 |
| 5 | BadRequest | 400 错误响应 | 请求数据格式/内容有误的标准 HTTP 响应 | 卡片 6 | 类似“格式不对,重新填” | 误以为任何错误都该用 400 |
| 6 | Problem Details | RFC 7807 错误格式 | 标准化的 HTTP 错误响应结构(含 type/title/detail/status) | 卡片 6 | 类似 JSON API 的 error 对象标准 | 误以为可以随意定制字段 |
| 7 | 过度提交 | Over-posting | 攻击者在请求中包含未授权修改的模型属性 | 卡片 7 | 类似在支票上多写一个零 | 误以为 [BindNever] 是唯一解法 |
| 8 | 变更追踪器 | Change Tracker | EF Core 跟踪实体变更状态(Added/Modified/Deleted/Unchanged)的内部机制 | 卡片 8 | 类似 Git 的 staging area(暂存区) | 误以为 SaveChanges 后实体自动释放 |
| 9 | 工作单元 | Unit of Work | 将多个数据库操作封装在单一事务中的设计模式 | 卡片 8 | 类似购物车:所有商品一次性结账 | 误以为每个 Add/Remove 都立即执行 SQL |
| 10 | CSRF | Cross-Site Request Forgery | 跨站请求伪造:恶意网站利用用户已登录状态发起非法请求 | 卡片 9 | 类似冒用你的签名在合同上签字 | 误以为 HTTPS 能防御 CSRF |
| 11 | 同步令牌模式 | Synchronizer Token Pattern | 通过比较 Cookie Token 和 Form Token 来验证请求来源的 CSRF 防御模式 | 卡片 9 | 类似银行要求同时出示身份证和银行卡 | 误以为 Token 必须手动管理 |
| 12 | 视图模型 | ViewModel | 为视图定制的数据传输对象,聚合"实体数据 + 元数据" | 卡片 10 | 类似餐厅菜单(不是厨房库存清单) | 误以为 ViewModel 必须继承实体类 |
| 13 | async/await | 异步编程关键字 | C# 语言层面的异步编程语法糖,编译为状态机 | 卡片 11 | 类似委托他人去买咖啡,自己继续干活 | 误以为 async 让代码跑得更快 |
| 14 | IOCP | I/O Completion Port | Windows 操作系统的异步 I/O 完成通知机制 | 卡片 11 | 类似完成柜台的呼叫铃 | 误以为异步 I/O 需要额外线程 |
| 15 | 线程池 | Thread Pool | 预创建的可复用线程集合,避免频繁创建/销毁线程的开销 | 卡片 11 | 类似出租车队:接完一单立刻接下一单 | 误以为线程池会降低性能 |
| 16 | Eager Loading | 预加载 | 使用 Include 一次性加载关联数据(JOIN) | 卡片 13 | 类似点套餐(一次性上齐所有菜) | 误以为 Include 总是最优选择 |
| 17 | Lazy Loading | 延迟加载 | 访问导航属性时才按需加载关联数据 | 卡片 13 | 类似点单后一道一道上菜 | 误以为延迟加载不会引发 N+1 |
| 18 | N+1 问题 | N+1 Query Problem | 循环中每次访问导航属性触发一次额外查询(共 N+1 次数据库查询) | 卡片 13 | 类似 10 个人分别去餐厅而不是一起团购 | 误以为只有在循环中才会出现 |
| 19 | [Bind] 特性 | Bind Attribute | 限制模型绑定可绑定的属性列表(白名单/黑名单) | 卡片 7 | 类似表单上只允许填特定字段 | 误以为能防御所有注入攻击 |
| 20 | [ValidateAntiForgeryToken] | AntiForgeryToken Filter | Action 过滤器,强制执行 CSRF Token 验证 | 卡片 9 | 类似进入大楼时的双重身份验证 | 误以为 GET 请求也需要加此特性 |
| 21 | SaveChanges | 保存更改 | EF Core 方法,将所有跟踪的实体变更封装在事务中提交到数据库 | 卡片 8 | 类似 Git commit:暂存区 → 仓库 | 误以为返回 int 只是"成功/失败" |
| 22 | RedirectToAction | 重定向到 Action | 返回 HTTP 302 重定向,浏览器跳转到指定 Action | 卡片 8 | 类似挂号后去指定的科室诊室 | 误以为是服务端内部跳转 |
| 23 | 双 Token 机制 | Double-Submit Cookie Pattern | CSRF 防御:Cookie 中的 Token 与 Form 中的 Token 同时提交并比较 | 卡片 9 | 类似电影票:存根+票根同时出示 | 误以为需要 HTTPS 才能安全 |
| 24 | 并发处理 | Concurrency | 服务器同时处理多个 HTTP 请求的能力 | 卡片 11 | 类似银行多个窗口同时服务 | 误以为增加线程等于提升并发 |
| 25 | ToListAsync | 异步列表查询 | EF Core 异步方法,将 IQueryable 转为 List 而不阻塞调用线程 | 卡片 11 | 类似快递送货上门(异步等待)而非自己去取(同步等待) | 误以为 ToListAsync 比 ToList 查询更快 |
| 26 | 输入验证 | Input Validation | 检查用户输入是否符合业务规则 | 卡片 2 | 类似安检机扫描行李 | 误以为客户端验证足够安全 |
板块④:概念→代码双向映射表
| 概念 | 对应的代码/类/方法 | 继承/实现关系 | 所在文件/位置 |
|---|---|---|---|
| 模型绑定 | DefaultModelBinder 自动工作 |
实现 IModelBinder |
无需手动调用 |
| 路由参数绑定 | [FromRoute] / URL 模板 {id} |
IBindingSourceMetadata |
HomeController.ModelBinding(int? id) |
| 查询字符串绑定 | URL ?color=Red&email=test@test.com |
自动按名称匹配 | HomeController.ModelBinding(int? id, string color, string email) |
| 表单绑定 | <input asp-for="Supplier.CompanyName"/> |
Tag Helper 生成 name 属性 |
Views/Home/AddSupplier.cshtml |
| ModelState 验证 | ModelState.IsValid |
ModelStateDictionary |
HomeController.AddSupplier() |
| BadRequest | return BadRequest("message"); |
ControllerBase.BadRequest() |
控制器 Action |
| NotFound | return NotFound(); |
ControllerBase.NotFound() |
HomeController.Details(int? id) |
| Problem Details | return Problem(detail: ex.Message, statusCode: 500); |
ControllerBase.Problem() |
异常处理 Action |
| [Bind] 白名单 | [Bind(nameof(Supplier.CompanyName), ...)] |
Attribute |
Action 参数 |
| Add | _db.Suppliers.Add(supplier); |
DbSet<Supplier>.Add() |
HomeController.AddSupplier() |
| Update | _db.Suppliers.Update(supplierInDb); |
DbSet<Supplier>.Update() |
HomeController.UpdateSupplier(int? id) |
| Remove | _db.Suppliers.Remove(supplierInDb); |
DbSet<Supplier>.Remove() |
HomeController.DeleteSupplier(int? id) |
| SaveChanges | _db.SaveChanges(); |
DbContext.SaveChanges() |
所有写操作 Action |
| AntiForgeryToken | @Html.AntiForgeryToken() |
IHtmlHelper.AntiForgeryToken() |
AddSupplier 等表单视图 |
| 验证 Token | [ValidateAntiForgeryToken] |
ActionFilterAttribute |
POST Action 方法 |
| HomeSupplierViewModel | new HomeSupplierViewModel(affected, supplier) |
自定义类 | Models/HomeSupplierViewModel.cs |
| async Action | async Task<IActionResult> |
— | HomeController.Index() 改造 |
| ToListAsync | await _db.Categories.ToListAsync() |
EntityFrameworkQueryableExtensions |
HomeController.Index() |
| SingleOrDefaultAsync | await _db.Products.SingleOrDefaultAsync(p => ...) |
Entity Framework 扩展 | HomeController.ProductDetail(int? id) |
| Include | .Include(p => p.Category) |
EntityFrameworkQueryableExtensions.Include() |
HomeController.ProductDetail() |
| ViewData | ViewData["MaxPrice"] = maxPrice; |
Controller.ViewData |
HomeController.ProductsThatCostMoreThan() |
板块⑤:语法/API 深度速查表
| API/方法 | 完整签名 | 参数详解 | 返回值 | 使用场景 | ⚠️ 安全注意 | ⚠️ 性能注意 |
|---|---|---|---|---|---|---|
ModelState.IsValid |
bool IsValid { get; } |
无(属性) | bool:全部属性验证通过返回 true |
在 POST Action 中检查表单数据合法性 | — | — |
BadRequest(object? error) |
BadRequest() / BadRequest(object error) / BadRequest(ModelStateDictionary modelState) |
error=可序列化的错误对象;modelState=验证错误字典 |
BadRequestObjectResult → HTTP 400 |
请求参数格式错误时返回 | — | — |
NotFound(object? value) |
NotFound() / NotFound(object value) |
value=可选的响应体 |
NotFoundObjectResult → HTTP 404 |
数据库查询结果为空时返回 | — | — |
Problem(string? detail, ...) |
Problem(string? detail = null, string? instance = null, int? statusCode = null, string? title = null, string? type = null) |
detail=人类可读的错误描述;instance=请求路径;statusCode=HTTP 状态码 |
ObjectResult → 默认 HTTP 500 |
标准化错误响应(RFC 7807) | detail中避免泄露数据库连接字符串等敏感信息 |
— |
ValidationProblem() |
ValidationProblem(ValidationProblemDetails) / ValidationProblem(ModelStateDictionary) / ValidationProblem(string, ...) |
ModelState=当前验证错误字典 |
ObjectResult → HTTP 400 |
模型验证失败时返回结构化错误详情 | — | — |
DbSet<T>.Add(T entity) |
EntityEntry<T> Add(T entity) |
entity=要新增的实体(主键由数据库自动生成) |
EntityEntry<T>(含状态信息) |
新建一条数据库记录 | — | 不立即执行 SQL,由 Change Tracker 追踪 |
DbSet<T>.Update(T entity) |
EntityEntry<T> Update(T entity) |
entity=包含新值的实体(必须有正确的 Id) |
EntityEntry<T> |
更新一条数据库记录 | 使用前务必验证 entity 来源(防 Over-posting) |
— |
DbSet<T>.Remove(T entity) |
EntityEntry<T> Remove(T entity) |
entity=从数据库检索出的完整实体 |
EntityEntry<T> |
删除一条数据库记录 | 删除前确认关联数据是否有 Cascade 设置 |
— |
DbContext.SaveChanges() |
int SaveChanges() / Task<int> SaveChangesAsync(CancellationToken) |
CancellationToken=在异步版本中可取消操作 |
int=受影响的行数 |
提交所有跟踪的变更到数据库 | — | 单事务中执行所有变更,大量实体时可能生成大量 SQL |
DbSet<T>.Find(params object?[] keyValues) |
T? Find(params object?[]? keyValues) / ValueTask<T?> FindAsync(...) |
keyValues=主键值数组 |
T?=找到的实体,null 表示未找到 |
按主键快速查找 | — | 先查 Change Tracker 缓存,再查数据库(性能优于 SingleOrDefault) |
@Html.AntiForgeryToken() |
IHtmlContent AntiForgeryToken() |
无 | 生成 <input type="hidden" name="__RequestVerificationToken" value="..."> |
在表单中生成 CSRF Token | 必须配合 [ValidateAntiForgeryToken] 使用 |
— |
[ValidateAntiForgeryToken] |
Attribute,实现 IAuthorizationFilter |
无参数 | 验证失败抛出 AntiforgeryValidationException |
加在 POST Action 方法上 | GET 请求不需要(但也不会报错) | — |
ToListAsync(CancellationToken) |
Task<List<TSource>> ToListAsync(IQueryable<TSource>, CancellationToken) |
cancellationToken=取消令牌 |
Task<List<T>> |
异步执行 SQL 并将结果转为 List | — | 大量数据时应配合 Take() 限制行数 |
SingleOrDefaultAsync(Expression, CancellationToken) |
Task<TSource?> SingleOrDefaultAsync(Expression<Func<TSource, bool>>, CancellationToken) |
predicate=条件表达式 |
Task<T?>=单个实体或 null |
查询单条记录(期望 0 或 1 条) | — | 结果 > 1 条时抛异常,应确保 WHERE 条件唯一 |
Include(Expression) |
IIncludableQueryable<TEntity, TProperty> Include(Expression<Func<TEntity, TProperty>>) |
navigationPropertyPath=导航属性表达式 |
IIncludableQueryable |
预加载关联数据(Eager Loading) | — | 级联 Include 可能生成巨大的 JOIN,需评估列数 |
ViewData["key"] |
ViewDataDictionary this[string key] { get; set; } |
key=字符串键 |
object? |
从控制器向视图传递少量非模型数据 | 无强类型检查,拼写错误不会编译报错 | — |
RedirectToAction(string action, object? routeValues) |
RedirectToActionResult { get; } |
action=目标 Action 名,routeValues=匿名对象(路由参数) |
RedirectToActionResult → HTTP 302 |
POST 成功后跳转,防止表单重复提交(PRG 模式) | — | — |
板块⑥:易混淆点对比台
对比组 1:路由参数 vs 查询字符串
| 维度 | 路由参数 | 查询字符串 |
|---|---|---|
| 相同点 | 都是 GET 请求中传递数据的方式;都不涉及请求体 | 同左 |
| 不同点 | 路径的一部分(如 /detail/42) |
URL ? 之后(如 /detail?id=42) |
| 不同点 | 通常表示"资源身份" | 通常表示"过滤/排序/分页" |
| 不同点 | 缺失时路由匹配失败(除非标记 ?) |
缺失时参数为 null(不阻止匹配) |
| 代码对比 | 见下方 diff | |
| 选择建议 | 必须的、标识资源身份的参数用路由参数 | 可选的、过滤/分页参数用查询字符串 |
- // 路由参数:值嵌入 URL 路径
- public IActionResult ProductDetail(int? id) // /home/productdetail/42
+ // 查询字符串:值附加在 URL 之后
+ public IActionResult ProductDetail(int? id) // /home/productdetail?id=42
对比组 2:BadRequest() vs ValidationProblem()
| 维度 | BadRequest() | ValidationProblem() |
|---|---|---|
| 相同点 | 都返回 HTTP 400;都用于客户端错误 | 同左 |
| 不同点 | 通用错误,支持自定义消息 | 专用验证错误,自动附带 ModelState 错误 |
| 不同点 | 需手动构建错误响应体 | 自动生成 RFC 7807 格式的 errors 数组 |
| 不同点 | 不要求 ModelState 参数 | 通常传入 ModelState |
| 代码对比 | 见下方 diff | |
| 选择建议 | 通用业务逻辑错误 | 模型验证失败 |
- // 通用错误
- return BadRequest("Invalid supplier data.");
+ // 验证错误(附带字段级错误详情)
+ if (!ModelState.IsValid)
+ {
+ return ValidationProblem(ModelState);
+ }
对比组 3:[Bind] 白名单 vs ViewModel 隔离
| 维度 | [Bind] 白名单 | ViewModel 隔离 |
|---|---|---|
| 相同点 | 都防御 Over-posting 攻击 | 同左 |
| 不同点 | 仍绑定到实体类 | 绑定到专用的 ViewModel 类 |
| 不同点 | 配置在 Action 方法层面 | 架构在类型层面 |
| 不同点 | 每个 Action 需单独配置 | ViewModel 定义一次,所有使用处自动安全 |
| 代码对比 | 见下方 diff | |
| 选择建议 | 快速保护已有代码的临时方案 | 新项目/新功能的最佳实践 |
- // [Bind] 方案:每个 Action 单独声明白名单
- public IActionResult AddSupplier(
- [Bind(nameof(Supplier.CompanyName), nameof(Supplier.Country), nameof(Supplier.Phone))]
- Supplier supplier)
+ // ViewModel 方案:专用 DTO,只含必要字段
+ public class AddSupplierViewModel
+ {
+ [Required] public string CompanyName { get; set; }
+ public string? Country { get; set; }
+ public string? Phone { get; set; }
+ }
对比组 4:Find() vs SingleOrDefault()
| 维度 | Find() / FindAsync() | SingleOrDefault() |
|---|---|---|
| 相同点 | 都查询单条记录;找不到都返回 null | 同左 |
| 不同点 | 先查 Change Tracker 缓存 | 始终查数据库 |
| 不同点 | 只能按主键查询 | 支持任意条件表达式 |
| 不同点 | 不触发 Error(不要求唯一) | 返回多条记录时抛 InvalidOperationException |
| 代码对比 | 见下方 diff | |
| 选择建议 | 按主键快速查找 | 按非主键条件查询 |
- // Find:只能按主键,但查缓存优先
- Supplier? supplier = _db.Suppliers.Find(id);
+ // SingleOrDefault:任何条件,但始终查数据库
+ Supplier? supplier = _db.Suppliers
+ .SingleOrDefault(s => s.SupplierId == id);
对比组 5:ToList() vs ToListAsync()
| 维度 | ToList() | ToListAsync() |
|---|---|---|
| 相同点 | 都将 IQueryable 转为 List<T>;触发数据库查询 |
同左 |
| 不同点 | 同步执行,阻塞当前线程 | 异步执行,不阻塞线程(归还线程池) |
| 不同点 | 返回 List<T> |
返回 Task<List<T>> |
| 不同点 | 适用于低并发场景 | 适用于高并发 / Web 服务 |
| 代码对比 | 见下方 diff | |
| 选择建议 | 控制台应用 / 单元测试 | ASP.NET Core Web 应用 |
- // 同步:线程等待数据库响应
- List<Category> categories = _db.Categories.ToList();
+ // 异步:线程释放,响应后继续执行
+ List<Category> categories = await _db.Categories.ToListAsync();
板块⑦:显隐知识双轨清单
书中显性知识(白纸黑字写的)
- 模型绑定自动从 Route / Query String / Form 提取值并映射到 Action 参数
- 绑定优先级:Form > Route > Query String
- Data Annotations 验证属性包括 [Required]、[StringLength]、[Range]、[RegularExpression] 等
ModelState.IsValid检查所有验证是否通过- 通过 [Bind] 特性和 ViewModel 防御 Over-posting
- EF Core 写操作:
Add()、Update()、Remove()配合SaveChanges() - AntiForgeryToken 防御 CSRF:
@Html.AntiForgeryToken()+[ValidateAntiForgeryToken] - 异步 Action 方法:
async Task<IActionResult>+await ToListAsync() - 异步方法不提升单请求速度,但提升服务器可扩展性
HttpPost和HttpGet特性限定 HTTP 动词- ControllerBase 提供 BadRequest / NotFound / StatusCode / Problem 等方法
书中隐性知识(需推导才能理解的)
- 模型绑定器的替换能力:DefaultModelBinder 不是唯一的绑定器——可通过实现
IModelBinderProvider+IModelBinder完全自定义绑定逻辑。推导:书中展示绑定器自动工作但未提替代方案,但 ASP.NET Core 的 DI 架构暗示其可替换性。 - SaveChanges 的事务性:
SaveChanges()将所有变更包裹在单个数据库事务中——如果任何一个操作失败,所有变更均回滚。推导:书中展示多个 Add/Remove 后调用一次 SaveChanges,但未明确说明它们是 ACID 事务。 - ViewModel 的 Builder 构造器模式:本章 ViewModel 通过构造函数参数
(affected, supplier)初始化,这实际上是一种轻量级的 Builder 模式。推导:因为 ViewModel 需要同时接受操作结果和实体数据,构造器提供了不可变性保证。 - Change Tracker 的 Find 缓存行为:
Find()方法比SingleOrDefault()快,因为它首先检查 Change Tracker 中是否已有该实体。推导:书中分别使用了两种方法,但未解释性能差异的原因。 - 客户端验证的 js 文件依赖:
_ValidationScriptsPartial部分视图依赖jquery.validate.js和jquery.validate.unobtrusive.js。推导:书中只在 Razor View 底部引用了_ValidationScriptsPartial,未明确说明需要这两个 JS 库。 - 异步方法需要 using Microsoft.EntityFrameworkCore:
ToListAsync、SingleOrDefaultAsync等异步方法定义在EntityFrameworkQueryableExtensions类中,需要using Microsoft.EntityFrameworkCore;。推导:书中代码片段未显式包含 using 语句。
版本引发的潜在考点
- .NET 9 → .NET 10:Data Annotations 属性无变化,所有验证属性可直接使用
- EF Core 9 → 10:异步方法签名无变化,
ToListAsync等 API 完全兼容 Problem()和ValidationProblem()在较新版本(.NET 7+)增加了更多重载,书中可能只展示了部分
章节测试预测
- 名词解释题:什么是 Over-posting?如何防御?
- 代码分析题:给出一个不安全的 Controller Action,指出安全漏洞
- 判断题:异步 Action 方法比同步的更快。→ 错误(异步 = 更好扩展性,不 = 更快)
- 编程题:实现供应商编辑功能(含验证、CSRF 防护、异步)
- 错误修复题:给出一个忘记
[ValidateAntiForgeryToken]的代码,要求修复 - 综合题:完整实现一个新实体的 CRUD 页面(如 Shippers)
- 面试题:解释
Find()和SingleOrDefault()的区别
隐性概念捕获(从"最佳实践"/"注意事项"/"常见陷阱"段落提炼)
- PRG 模式(Post-Redirect-Get):
AddSupplierPOST 成功后RedirectToAction("Suppliers"),防止浏览器刷新导致重复提交。来源:书中AddSupplier的 POST Action 末尾。 - 表单插值攻击意识:
@Html.AntiForgeryToken()不能只放在 Layout 中,必须每个独立表单各有一个。来源:书中每个编辑表单都有独立的 Token。 - ModelState 手动添加错误:不仅可以验证 Data Annotations,还可以通过
ModelState.AddModelError("PropertyName", "Error message")添加自定义验证逻辑。来源:书中HomeSupplierViewModel.ValidationErrors列表。 - async void 禁用规则:Action 方法用
async Task<IActionResult>,绝对不要用async void,因为 void 异步方法异常无法被框架捕获。来源:书中所有异步方法均返回 Task。 - SaveChanges 并发异常处理:
SaveChanges()在高并发更新时可能抛出DbUpdateConcurrencyException,需要 catch 处理。来源:书中 CRUD 操作未展示但实践中必须考虑。
板块⑧:本章知识织网图
关系类型图例
→依赖(必须先理解A才能理解B)↔配合使用(A和B常常一起出现)⇢对比(A和B容易混淆)⊃包含(A是B的组成部分)
本章内部织网图
┌──────────────────────┐│ HTTP 请求到达 │└──────────┬───────────┘│▼┌──────────────────────────────────────┐│ 模型绑定 (卡片1) ││ Route → Query String → Form │└──────┬──────────────┬────────────────┘│ │┌─────────┘ └────────────┐▼ ▼┌─────────────────┐ ┌─────────────────┐│ [Bind] / VM │←──→ │ ModelState ││ 防 Over-posting │ ⇢ 对比 │ 绑定结果存储 ││ (卡片7) │ │ │└────────┬────────┘ └────────┬────────┘│ ││ ┌─────────┴─────────┐│ ▼ ▼│ ┌─────────────────┐ ┌─────────────────┐│ │ Data Annotations│ │ ValidationProblem││ │ 验证规则 (卡片2) │─→│ 返回验证错误 ││ └────────┬────────┘ │ (卡片6) ││ │ └─────────────────┘│ ┌─────────┴─────────┐│ ▼ ▼│ ┌─────────────────┐ ┌─────────────────┐│ │ ModelState │ │ DisplayTemplates││ │ .IsValid │ │ (卡片12) ││ │ = true │ └─────────────────┘│ └────────┬────────┘│ ││ ▼│ ┌─────────────────┐ ┌─────────────────────┐│ │ AntiForgeryToken│ │ EF Core CRUD ││ │ CSRF 验证 (↓9) │ │ Add/Update/Remove ││ │ Token A = TokenB│ │ (卡片8) ││ └────────┬────────┘ └──────────┬──────────┘│ │ ││ ▼ ▼│ ┌─────────────────┐ ┌─────────────────────┐│ │ 403 或 继续 │ │ SaveChanges ││ └─────────────────┘ │ 事务提交 (卡片8) ││ └──────────┬──────────┘│ │└──────────────────────┬───────────────┘▼┌─────────────────────────┐│ ViewModel 封装结果 ││ HomeSupplierViewModel ││ (卡片10) │└────────────┬────────────┘│┌────────────┴────────────┐▼ ▼┌─────────────────┐ ┌─────────────────────┐│ View(result) │ │ RedirectToAction ││ 显示错误/结果页 │ │ 重定向 (PRG 模式) │└─────────────────┘ └─────────────────────┘════════════════════════════════════════════════════════════横切关注点(适用所有流程):┌──────────────────────────────────────────────────────┐│ 异步改造 (卡片11) ││ async Task<IActionResult> + await → 所有 DB 操作 ││ 适用:Index / ProductDetail / All CRUD │└──────────────────────────────────────────────────────┘
跨章节织网图
Ch1: 实体模型 + Docker DB│├── Category, Product, Supplier 实体 ───→ Ch3: EF Core CRUD├── NorthwindContext (DbContext) ───────→ Ch3: _db 操作└── Azure SQL Edge Docker ─────────────→ Ch3: 数据库运行时│Ch2: MVC 控制器 + 视图 ││ │├── Controller/Action 模式 ───────────→ Ch3: 扩展 HomeController├── 路由系统 ────────────────────────→ Ch3: 路由参数绑定├── Razor Views + Tag Helpers ───────→ Ch3: 表单视图└── HomeSupplierViewModel ───────────→ Ch3: 扩展使用│Ch3 新增能力│┌────────────────────────────────────────────┤│ │ │ │ │▼ ▼ ▼ ▼ ▼Ch4 Ch5 Ch6 Ch7 Ch9/10/11Tag Helpers 认证授权 缓存 UI测试 Web API CRUD表单增强 CSRF+认证 OutputCache Playwright REST/OData/配合 配合 FastEndpoints
