概述

当用户从浏览器向服务器上运行的 Web 程序提交数据时,HTTP 请求将携带数据对象并将其传输至 Web 程序。这些数据对象称为数据传输模型(Data Transfer Model)数据传输对象(Data Transfer Objects DTO)

DTO 可以通过 form 表单提交或 AJAX 请求进行传输。一旦数据到达 Web 程序,ASP.NET Core 就会将它们转换为强类型对象。这便是模型绑定

本节你将学习:

  • 如何设计 DTO

  • 通过 HTML form 提交数据

  • 模型绑定的基础知识

HTML Form 和基本的模型绑定

假设我们的 ASP.NET Core Web 程序托管在 http://localhost:5000 上。用户访问网站时获得如下 HTML 表单:

  1. <form action="film/create" method="GET">
  2. <label for="name">Name</label>
  3. <input type="text" name="name" value="Transformer" /><br/>
  4. <label for="year">Year</label>
  5. <input type="text" name="year" value="2017" /><br/>
  6. <input type="submit" name="submit" value="Create" />
  7. </form>

当用户单击 Create 按钮时,会发生下面这些事情:

  1. 表单中包含的信息将发送回运行在 http://localhost:5000 上的 Web 程序

  2. 每个 标签都会生成一个键值对。name 属性对应名称,value 属性对应值

  3. action=”film/create” 指明数据发送的目的地。ASP.NET Core Web 程序将字符串 film/create 解析为某个控制器的方法。解析 action 字符串并查找方法的过程称为 URL 路由,目标方法称为 action

    1. 如果不出意外 string action =”film/create” 会将数据路由到 FilmController 类的 Create 方法
  4. method =”GET” 要求浏览器通过 URL 传递键值对。因此,用户将在浏览器的地址栏中看到 http://localhost:5000/film/create?name=Transformer&year=2017&submit=Create。你可能已经注意到,即使由 Create 按钮生成的键值对已经附加到 URL 中了,你也可以使用它们来确定如何处理数据

现在让我们看一眼 ASP.NET Core 的模型绑定。请牢记,通过 HTTP 请求发送给 Web 程序的所有值都是字符串类型的。例如,键值对 year=2017 中的 2017 实际上是一个字符串。但是,当我们设计处理这些值的 action 方法时,方法签名可以是:

  1. public IActionResult Create(string name, int year)
  2. {
  3. // logic ...
  4. }

模型绑定的基本思路是:

  1. 模型绑定不区分大小写。因此参数名可以是 name 也可以是 Name。使用 name 是因为 C# 方法参数命名规范是驼峰命名法

  2. 模型绑定将帮助我们进行一些简单的类型转换。这也是我们可以将参数 year 直接声明为 int 类型的原因。如果用户输入的年份值不是有效整数呢?URL 路由将尝试查找其他参数与表单数据匹配的 Create 方法,如果找不到,则 ASP.NET Core 框架将向浏览器返回 404 Not Found

  3. 用不到的表单项可以忽略掉。上例中我们就忽略了 submit = Create 键值对

高级模型绑定

很多时候并不推荐通过 URL 传递键值对。有时是出于安全考虑,不希望用户直接看到表单值;有时单纯是因为表单数据结构太复杂,无法通过 URL 字符串进行传递。例如,提交数组这类情况,我们应该将 HTML 表单的 method 属性设置为 POST,并设计一个合适的 DTO 来组装表单中的数据。

以下面的表单为例:

  1. <form action="film/createorupdate" method="POST">
  2. <span>ID</span>
  3. <input type="text" name="id" value="101" /><br />
  4. <span>Name</span>
  5. <input type="text" name="name" value="Transformer" /><br/>
  6. <span>Year</span>
  7. <select name="year">
  8. <option value="2015">2015</option>
  9. <option value="2016">2016</option>
  10. <option value="2017">2017</option>
  11. </select><br/>
  12. <span>Genre</span>
  13. <input type="checkbox" name="genres" value="action" /><span>Action</span>
  14. <input type="checkbox" name="genres" value="comedy" /><span>Comedy</span>
  15. <input type="checkbox" name="genres" value="war" /><span>War</span><br/>
  16. <span>In Store</span>
  17. <input type="radio" name="isinstore" value="true" /><span>Yes</span>
  18. <input type="radio" name="isinstore" value="false" /><span>No</span><br/>
  19. <input type="submit" name="operation" value="Create" />
  20. <input type="submit" name="operation" value="Update" />
  21. </form>

我们很少在单个表单中组合多个操作,示例是个例外(既有 Create 又有 Update),只是用它来演示如何设计 DTO。

由于 method 设置为 POST,所以提交数据时键值对将封装在 HTTP 请求的报文体中,用户将无法再在 URL 看到键值对。

设计 DTO 前,你需要收集整理表单中所有带 name 属性的元素。请注意,只有 name 属性是用于生成键值对的,id 属性是用于 CSS 和 JavaScript 来索引元素的。

通常,键与值是一对一的,例如电影的 ID 和电影名称。对于这类元素,我们一般在 DTO 中为它们创建非集合类型的属性。

但有时一个键可能对应多个值,或者多个元素拥有相同的名称(多个 HTML 标签的 name 相同)。根据是否可以选择多个值,我们为它们创建集合类型属性或非集合类型属性。在上例中,电影年份 select 是单值下拉列表,仅允许选择一个值,所以我们为它创建非集合类型属性。而电影类型的 checkbox 是可以多选的,所以我们必须为它创建集合类型属性。

根据从上例 form 中收集整理的信息,我们设计的 DTO 大致如下:

  1. public class CreateOrUpdateDTO
  2. {
  3. public int ID { get; set; }
  4. public string Name { get; set; }
  5. public int Year { get; set; }
  6. public Genre[] Genres { get; set; }
  7. public bool IsInStore { get; set; }
  8. public Operation Operation { get; set; }
  9. }
  10. public enum Genre
  11. {
  12. Action,
  13. Comedy,
  14. War,
  15. // ...
  16. }
  17. public enum Operation
  18. {
  19. Create,
  20. Update,
  21. // ...
  22. }

当 HTTP 请求被路由到目标 action 方法时,模型绑定引擎将从 HTTP 请求的报文体中提取数据,并将这些键值对转换为 DTO 类型的实例。因此,action 方法如下所示:

  1. public IActionResult CreateOrUpdate(CreateOrUpdateDTO dto)
  2. {
  3. if (dto.Operation == Operation.Create)
  4. {
  5. // create a new film
  6. }
  7. else if (dto.Operation == Operation.Update)
  8. {
  9. // update the existing film by ID
  10. }
  11. else
  12. {
  13. // ...
  14. }
  15. }

领域模型作为 DTO

在实际项目中,我们很少使用一个表单完成多个操作。如果把上例的 Create 和 Update 操作分开,我们就能得到两个表单。

Create:

  1. <form action="film/create" method="POST">
  2. <span>Name</span>
  3. <input type="text" name="name" value="Transformer" /><br/>
  4. <!--other elements-->
  5. <input type="submit" value="Create" />
  6. </form>

Update:

  1. <form action="film/update" method="POST">
  2. <input type="hidden" name="id" value="101" />
  3. <span>Name</span>
  4. <input type="text" name="name" value="Transformer" /><br/>
  5. <!--other elements-->
  6. <input type="submit" value="Update" />
  7. </form>

分离操作后,就不再需要 Operation 属性,enum Operation 也不需要了。DTO 简化为:

  1. public class CreateOrUpdateDTO
  2. {
  3. public int ID { get; set; }
  4. public string Name { get; set; }
  5. public int Year { get; set; }
  6. public ICollection<Genre> Genres { get; set; }
  7. public bool IsInStore { get; set; }
  8. }

不难看出,这个 DTO 和 Film 的领域模型简直是一模一样:

  1. public class Film
  2. {
  3. public int ID { get; set; }
  4. public string Name { get; set; }
  5. public int Year { get; set; }
  6. public ICollection<Genre> Genres { get; set; }
  7. public bool IsInStore { get; set; }
  8. }

所以,在这种情况,我们根本不需要创建 CreateOrUpdateDTO。完全可以直接用 Film 领域模型作为 DTO。所以,最后两个 action 方法就变成了:

  1. public IActionResult Create(Film film)
  2. {
  3. // create a new film
  4. }
  5. public IActionResult Update(Film film)
  6. {
  7. // update the existing film by ID
  8. }