概述
当用户从浏览器向服务器上运行的 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 表单:
<form action="film/create" method="GET">
<label for="name">Name</label>
<input type="text" name="name" value="Transformer" /><br/>
<label for="year">Year</label>
<input type="text" name="year" value="2017" /><br/>
<input type="submit" name="submit" value="Create" />
</form>
当用户单击 Create 按钮时,会发生下面这些事情:
表单中包含的信息将发送回运行在 http://localhost:5000 上的 Web 程序
每个 标签都会生成一个键值对。name 属性对应名称,value 属性对应值
action=”film/create” 指明数据发送的目的地。ASP.NET Core Web 程序将字符串 film/create 解析为某个控制器的方法。解析 action 字符串并查找方法的过程称为 URL 路由,目标方法称为 action
- 如果不出意外 string action =”film/create” 会将数据路由到 FilmController 类的 Create 方法
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 方法时,方法签名可以是:
public IActionResult Create(string name, int year)
{
// logic ...
}
模型绑定的基本思路是:
模型绑定不区分大小写。因此参数名可以是 name 也可以是 Name。使用 name 是因为 C# 方法参数命名规范是驼峰命名法
模型绑定将帮助我们进行一些简单的类型转换。这也是我们可以将参数 year 直接声明为 int 类型的原因。如果用户输入的年份值不是有效整数呢?URL 路由将尝试查找其他参数与表单数据匹配的 Create 方法,如果找不到,则 ASP.NET Core 框架将向浏览器返回 404 Not Found
用不到的表单项可以忽略掉。上例中我们就忽略了 submit = Create 键值对
高级模型绑定
很多时候并不推荐通过 URL 传递键值对。有时是出于安全考虑,不希望用户直接看到表单值;有时单纯是因为表单数据结构太复杂,无法通过 URL 字符串进行传递。例如,提交数组这类情况,我们应该将 HTML 表单的 method 属性设置为 POST,并设计一个合适的 DTO 来组装表单中的数据。
以下面的表单为例:
<form action="film/createorupdate" method="POST">
<span>ID</span>
<input type="text" name="id" value="101" /><br />
<span>Name</span>
<input type="text" name="name" value="Transformer" /><br/>
<span>Year</span>
<select name="year">
<option value="2015">2015</option>
<option value="2016">2016</option>
<option value="2017">2017</option>
</select><br/>
<span>Genre</span>
<input type="checkbox" name="genres" value="action" /><span>Action</span>
<input type="checkbox" name="genres" value="comedy" /><span>Comedy</span>
<input type="checkbox" name="genres" value="war" /><span>War</span><br/>
<span>In Store</span>
<input type="radio" name="isinstore" value="true" /><span>Yes</span>
<input type="radio" name="isinstore" value="false" /><span>No</span><br/>
<input type="submit" name="operation" value="Create" />
<input type="submit" name="operation" value="Update" />
</form>
我们很少在单个表单中组合多个操作,示例是个例外(既有 Create 又有 Update),只是用它来演示如何设计 DTO。
由于 method 设置为 POST,所以提交数据时键值对将封装在 HTTP 请求的报文体中,用户将无法再在 URL 看到键值对。
设计 DTO 前,你需要收集整理表单中所有带 name 属性的元素。请注意,只有 name 属性是用于生成键值对的,id 属性是用于 CSS 和 JavaScript 来索引元素的。
通常,键与值是一对一的,例如电影的 ID 和电影名称。对于这类元素,我们一般在 DTO 中为它们创建非集合类型的属性。
但有时一个键可能对应多个值,或者多个元素拥有相同的名称(多个 HTML 标签的 name 相同)。根据是否可以选择多个值,我们为它们创建集合类型属性或非集合类型属性。在上例中,电影年份 select 是单值下拉列表,仅允许选择一个值,所以我们为它创建非集合类型属性。而电影类型的 checkbox 是可以多选的,所以我们必须为它创建集合类型属性。
根据从上例 form 中收集整理的信息,我们设计的 DTO 大致如下:
public class CreateOrUpdateDTO
{
public int ID { get; set; }
public string Name { get; set; }
public int Year { get; set; }
public Genre[] Genres { get; set; }
public bool IsInStore { get; set; }
public Operation Operation { get; set; }
}
public enum Genre
{
Action,
Comedy,
War,
// ...
}
public enum Operation
{
Create,
Update,
// ...
}
当 HTTP 请求被路由到目标 action 方法时,模型绑定引擎将从 HTTP 请求的报文体中提取数据,并将这些键值对转换为 DTO 类型的实例。因此,action 方法如下所示:
public IActionResult CreateOrUpdate(CreateOrUpdateDTO dto)
{
if (dto.Operation == Operation.Create)
{
// create a new film
}
else if (dto.Operation == Operation.Update)
{
// update the existing film by ID
}
else
{
// ...
}
}
领域模型作为 DTO
在实际项目中,我们很少使用一个表单完成多个操作。如果把上例的 Create 和 Update 操作分开,我们就能得到两个表单。
Create:
<form action="film/create" method="POST">
<span>Name</span>
<input type="text" name="name" value="Transformer" /><br/>
<!--other elements-->
<input type="submit" value="Create" />
</form>
Update:
<form action="film/update" method="POST">
<input type="hidden" name="id" value="101" />
<span>Name</span>
<input type="text" name="name" value="Transformer" /><br/>
<!--other elements-->
<input type="submit" value="Update" />
</form>
分离操作后,就不再需要 Operation 属性,enum Operation 也不需要了。DTO 简化为:
public class CreateOrUpdateDTO
{
public int ID { get; set; }
public string Name { get; set; }
public int Year { get; set; }
public ICollection<Genre> Genres { get; set; }
public bool IsInStore { get; set; }
}
不难看出,这个 DTO 和 Film 的领域模型简直是一模一样:
public class Film
{
public int ID { get; set; }
public string Name { get; set; }
public int Year { get; set; }
public ICollection<Genre> Genres { get; set; }
public bool IsInStore { get; set; }
}
所以,在这种情况,我们根本不需要创建 CreateOrUpdateDTO。完全可以直接用 Film 领域模型作为 DTO。所以,最后两个 action 方法就变成了:
public IActionResult Create(Film film)
{
// create a new film
}
public IActionResult Update(Film film)
{
// update the existing film by ID
}