ASP.NET Core 实战-6.绑定模型:检索和验证用户输入
理解 Razor Pages 和 MVC 中的模型
MVC 就是关注点分离。 前提是通过隔离应用程序的每个方面以专注于单一职责,它可以减少系统中的相互依赖性。 这种分离使得在不影响应用程序的其他部分的情况下更容易进行更改。
经典的 MVC 设计模式具有三个独立的组件:
- 控制器——调用模型上的方法并选择一个视图
- 视图 - 显示构成模型的数据表示
- 模型——要显示的数据和更新自身的方法
在这种表示中,只有一个模型,即应用程序模型,它代表了应用程序的所有业务逻辑以及如何更新和修改其内部状态。 ASP.NET Core 有多种模型,这比 MVC 的一些观点更进一步的单一责任原则。
在第 5 章中,我们查看了一个待办事项列表应用程序的示例,该应用程序可以显示给定类别和用户名的所有待办事项。 使用此应用程序,您向使用 todo/listcategory/{category}/{username}
路由的 URL 发出请求。这将返回显示所有相关待办事项的响应,如图 6.1 所示。
该应用程序使用您已经看到的相同 MVC 结构,例如路由到 Razor 页面处理程序,以及许多不同的模型。 图 6.2 显示了对该应用程序的请求如何映射到 MVC 设计模式以及它如何生成最终响应,包括有关模型绑定和请求验证的其他详细信息。
图 6.1 显示待办事项列表项的基本待办事项列表应用程序。 用户可以通过更改 URL 中的类别和用户名参数来过滤项目列表。 |
---|
图 6.2 ASP.NET Core 中的 MVC 模式处理请求以查看待办事项列表 Razor Pages 应用程序中的项目子集。 |
---|
ASP.NET Core Razor Pages 使用了几种不同的模型,其中大部分是 POCO,以及应用程序模型,它更多是围绕服务集合的概念。ASP.NET Core 中的每个模型负责处理不同的方面 总体要求:
-
绑定模型——绑定模型是用户在发出请求时提供的所有信息,以及附加的上下文数据。 这包括从 URL 解析的路由参数、查询字符串以及请求正文中的表单或 JSON 数据等内容。 绑定模型本身是您定义的一个或多个 POCO 对象。 Razor 页面中的绑定模型通常通过在页面的 PageModel 上创建一个公共属性并使用
[BindProperty]
属性对其进行装饰来定义。 它们也可以作为参数传递给页面处理程序。 对于此示例,绑定模型将包括类别名称 open 和用户名 Andrew。 Razor Pages 基础结构在页面处理程序执行之前检查绑定模型以检查提供的值是否有效,尽管页面处理程序将执行,即使它们不是,正如您将在第 6.3 节讨论验证时看到的那样。
-
应用程序模型——应用程序模型根本不是真正的 ASP.NET Core 模型。 它通常是一组不同的服务和类,更多的是一个概念——在您的应用程序中执行某种业务操作所需的任何东西。 它可能包括域模型(代表您的应用程序试图描述的事物)和数据库模型(代表存储在数据库中的数据),以及任何其他附加服务。
在待办事项列表应用程序中,应用程序模型将包含待办事项的完整列表,可能存储在数据库中,并且知道如何仅在分配给
Andrew
的打开类别中找到那些待办事项。 -
页面模型——Razor 页面的
PageModel
有两个主要功能:它通过公开页面处理程序方法充当应用程序的控制器,以及充当 Razor 视图的视图模型。 视图生成响应所需的所有数据都暴露在PageModel
上,例如分配给Andrew
的打开类别中的待办事项列表。
派生 Razor 页面的 PageModel
基类包含各种帮助程序属性和方法。 其中之一是 ModelState
属性,包含作为一系列键值对的模型验证结果。
这些模型构成了任何 Razor Pages 应用程序的主体,处理每个页面处理程序的输入、业务逻辑和输出。 假设您有一个电子商务应用程序,它允许用户通过向 /search/{query}
URL 发送请求来搜索衣服,其中 {query}
包含他们的搜索词:
- 绑定模型——这将从 URL 中获取
{query}
路由参数和请求正文中发布的任何值(可能是排序顺序,或要显示的项目数),并将它们绑定到 C# 类,该类通常充当 一次性数据传输类。 当调用页面处理程序时,这将被设置为 PageModel 上的一个属性。 - 应用程序模型——这是执行逻辑的服务和类。 当页面处理程序调用时,此模型将加载与查询匹配的所有衣服,应用必要的排序和过滤器,并将结果返回给控制器。
- 页面模型——应用程序模型提供的值将与其他元数据一起设置为 Razor 页面的
PageModel
上的属性,例如可用项目的总数,或者用户当前是否可以签出。 Razor 视图将使用此数据将 Razor 视图呈现为 HTML。
所有这些模型的重要一点是,它们的职责定义明确且不同。 将它们分开并避免重用有助于确保您的应用程序保持敏捷且易于更新。
这种分离的明显例外是 PageModel,因为它是定义绑定模型和页面处理程序的地方,它还保存呈现视图所需的数据。 有些人可能认为明显缺乏分离是一种亵渎,但实际上这通常不是问题。 分界线非常明显。 例如,只要您不尝试从 Razor 视图中调用页面处理程序,就不会遇到任何问题!
从请求到模型:使请求有用
到目前为止,您应该熟悉 ASP.NET Core 如何通过在 Razor 页面上执行页面处理程序来处理请求。 您还已经看到了几个页面处理程序,例如
public void OnPost(ProductModel product)
页面处理程序是普通的 C# 方法,因此 ASP.NET Core 框架需要能够以通常的方式调用它们。 当页面处理程序接受参数作为其方法签名的一部分时,例如前面示例中的产品,框架需要一种方法来生成这些对象。 它们究竟来自哪里,又是如何产生的?
模型绑定从请求中提取值并使用它们来创建 .NET 对象。 这些对象作为方法参数传递给正在执行的页面处理程序,或者设置为 PageModel 的属性,这些属性标记有 [BindProperty] 属性。
模型绑定器负责查看传入的请求并查找要使用的值。 然后它创建适当类型的对象,并在称为绑定的过程中将这些值分配给您的模型。
Razor 页面的 PageModel 上的任何属性(在 Razor 页面的 .cshtml.cs 文件中)用 [BindProperty] 属性修饰的都是使用模型绑定从传入请求创建的,如下面的清单所示。 同样,如果您的页面处理程序方法有任何参数,这些也是使用模型绑定创建的。
清单 6.1 模型绑定请求到 Razor 页面中的属性
public class IndexModel: PageModel
{
//用 [BindProperty] 修饰的属性参与模型绑定。
[BindProperty]
public string Category { get; set; }
//除非您使用 SupportsGet,否则属性不是模型绑定的 GET 请求。
[BindProperty(SupportsGet = true)]
public string Username { get; set; }
public void OnGet()
{
}
public void OnPost(ProductModel model)
{
}
}
如前面的清单所示,PageModel
属性对于 GET 请求不是模型绑定的,即使您添加了 [BindProperty] 属性也是如此。 出于安全原因,仅绑定使用 POST 和 PUT 等动词的请求。 如果您确实想绑定 GET 请求,您可以在 [BindProperty] 属性上设置 SupportsGet
属性以选择加入模型绑定。
要为 GET 请求绑定 PageModel 属性,请使用属性的 SupportsGet 属性,例如 [BindProperty(SupportsGet = true)]
。
哪个部分是绑定模型?
清单 6.1 显示了一个使用多个绑定模型的 Razor 页面:Category 属性、Username 属性和 ProductModel 属性(在 OnPost 处理程序中)都是模型绑定的。
以这种方式使用多个模型很好,但我更喜欢使用将所有模型绑定保存在单个嵌套类中的方法,我通常称之为 InputModel。 使用这种方法,清单 6.1 中的 Razor 页面可以编写如下:
public class IndexModel: PageModel { [BindProperty] public InputModel Input { get; set; } public void OnGet() { } public class InputModel { public string Category { get; set; } public string Username { get; set; } public ProductModel Model { get; set; } } }
ASP.NET Core 使用请求的属性自动为您填充绑定模型,例如请求 URL、HTTP 请求中发送的任何标头、请求正文中显式发布的任何数据等。
默认情况下,ASP.NET Core 在创建绑定模型时使用三种不同的绑定源。 它按顺序查看每一个,并获取它找到的与绑定模型名称匹配的第一个值(如果有):
- 表单值——当使用 POST 将表单发送到服务器时,在 HTTP 请求的正文中发送
- 路由值——从 URL 段或匹配路由后通过默认值获取.
- 查询字符串值——在 URL 末尾传递,在路由期间不使用
模型绑定流程如图 6.3 所示。 模型绑定器检查每个绑定源以查看它是否包含可以在模型上设置的值。 或者,模型还可以选择值应来自的特定来源,如您将在第 6.2.3 节中看到的。 一旦绑定了每个属性,模型就会被验证并设置为 PageModel 上的属性或作为参数传递给页面处理程序。您将在本章的后半部分了解验证过程。
图 6.3 模型绑定涉及来自绑定源的映射值,这些值对应于请求的不同部分。 |
---|
PageModel 属性或页面处理程序参数?
在 Razor Pages 中使用模型绑定有两种不同的方法:
- 使用 [BindProperty] 属性装饰 PageModel 上的属性。
- 将参数添加到您的页面处理程序方法。
您应该选择以下哪种方法?
这个问题的答案很大程度上是一个品味问题。 在 PageModel 上设置属性并用 [BindProperty] 标记它们是您在示例中最常看到的方法。 如果您使用这种方法,您将能够在呈现视图时访问绑定模型.
另一种方法是向页面处理程序方法添加参数,在不同的 MVC 阶段之间提供了更多的分离,因为您将无法访问页面处理程序之外的参数。 不利的一面是,如果您确实需要在 Razor 视图中显示这些值,则必须手动将参数复制到可以在视图中访问的属性中。
我选择的方法往往取决于我正在构建的特定 Razor 页面。 如果我正在创建一个表单,我会倾向于 [BindProperty] 方法,因为我通常需要访问 Razor 视图中的请求值。 例如,对于绑定模型是产品 ID 的简单页面,我倾向于使用页面处理程序参数方法,因为它很简单,尤其是当处理程序用于 GET 请求时。
图 6.4 显示了使用模型绑定创建 ProductModel 方法参数的请求示例,该示例在本节开头显示:
public void OnPost(ProductModel product)
图 6.4 使用模型绑定创建用于执行 Razor 页面的模型实例。 |
---|
Id 属性已从 URL 路由参数绑定,但 Name 和 SellPrice 属性已从请求正文绑定。 使用模型绑定的一大优势是您不必自己编写代码来解析请求和映射数据。 这种代码通常是重复的并且容易出错,因此使用内置的常规方法可以让您专注于应用程序的重要方面:业务需求。
模型绑定非常适合减少重复代码。 尽可能利用它,您很少会发现自己必须直接访问 Request 对象。
绑定简单类型
我们将通过考虑一个简单的 Razor 页面处理程序来开始我们的模型绑定之旅。下一个清单显示了一个简单的 Razor 页面,它接受一个数字作为方法参数,并通过将数字乘以自身来平方它。
清单 6.2 接受简单参数的 Razor 页面
public class CalculateSquareModel : PageModel
{
public void OnGet(int number) //方法参数是绑定模型。
{
Square = number * number; //一个更复杂的示例将在应用程序模型中的外部服务中完成这项工作。
}
public int Square { get; set; } //应用程序模型。结果作为属性公开,并由视图用于生成响应。
}
在上一章中,您了解了路由以及它如何选择要执行的 Razor 页面。 您可以将 Razor 页面的路由模板更新为“CalculateSquare/{number}
”,方法是在 .cshtml
文件中的 Razor 页面的 @page
指令中添加 {number}
段,正如我们在第 4 章中讨论的那样:
@page "{number}"
当客户端请求 URL /CalculateSquare/5
时,Razor 页面框架使用路由来解析它以获取路由参数。 这会产生路由值对:
number=5
Razor 页面的 OnGet 页面处理程序包含一个参数 - 一个称为数字的整数 - 这是您的绑定模型。 当 ASP.NET Core 执行此页面处理程序方法时,它将发现预期的参数,浏览与请求关联的路由值,并找到 number=5 对。 然后它可以将 number 参数绑定到此路由值并执行该方法。 页面处理方法本身并不关心这个值是从哪里来的; 它顺其自然,计算值的平方,并将其设置在 Square 属性上。
要欣赏的关键是,当方法执行时,您不必编写任何额外的代码来尝试从 URL 中提取数字。 您需要做的就是创建一个具有正确名称的方法参数(或公共属性),然后让模型绑定发挥作用。
路由值并不是模型绑定器可以用来创建绑定模型的唯一值。 如您之前所见,该框架将通过三个默认绑定源查找与您的绑定模型匹配的内容:
- 表单值
- 路由值
- 查询字符串值
这些绑定源中的每一个都将值存储为名称-值对。 如果没有任何绑定源包含所需的值,则绑定模型将设置为该类型的新默认实例。 在这种情况下,绑定模型的确切值取决于变量的类型:
- 对于值类型,该值将是
default(T)
。 对于int
参数,这将是 0,而对于 bool,它将是 false。 - 对于引用类型,该类型是使用默认(无参数)构造函数创建的。对于像
ProductModel
这样的自定义类型,它将创建一个新对象。 对于像 int 这样的可空类型? 或布尔?,该值将为空。 - 对于字符串类型,该值将为空。
当模型绑定无法绑定方法参数时,考虑页面处理程序的行为很重要。 如果没有任何绑定源包含该值,则传递给该方法的值可能为 null
或可能意外具有默认值(对于值类型)。
清单 6.2 展示了如何绑定单个方法参数。 让我们进行下一个合乎逻辑的步骤,看看如何绑定多个方法参数。
在上一章中,我们讨论了构建货币转换器应用程序的路由。 作为您开发的下一步,您的老板要求您创建一种方法,在该方法中用户以一种货币提供价值,您必须将其转换为另一种货币。 您首先创建一个名为 Convert.cshtml
的 Razor 页面,然后使用 @page 指令自定义页面的路由模板,以使用包含两个路由值的绝对路径:
@page "/{currencyIn}/{currencyOut}"
然后,您创建一个接受您需要的三个值的页面处理程序,如下面的清单所示。
清单 6.3 接受多个绑定参数的 Razor 页面处理程序
public class ConvertModel : PageModel
{
public void OnGet(
string currencyIn,
string currencyOut,
int qty
)
{
/* method implementation */
}
}
如您所见,要绑定三个不同的参数。 问题是,这些值将来自哪里以及它们将被设置为什么? 答案是,视情况而定!表 6.1 显示了各种可能性。 所有这些示例都使用相同的路由模板和页面处理程序,但根据发送的数据,将绑定不同的值。 实际值可能与您的预期不同,因为可用的绑定源提供了相互冲突的值!
表 6.1 将请求数据绑定到来自多个绑定源的页面处理程序参数
URL (route values) | HTTP body data (form values) | 参数值绑定 |
---|---|---|
/GBP/USD | currencyIn=GBP currencyOut=USD qty=0 |
|
/GBP/USD?currencyIn=CAD | QTY=50 | currencyIn=GBP currencyOut=USD qty=50 |
/GBP/USD?qty=100 | qty=50 | currencyIn=GBP currencyOut=USD qty=50 |
/GBP/USD?qty=100 | currencyIn=CAD¤cyOut=EUR&qty=50 | currencyIn=CAD currencyOut=EUR qty=50 |
对于每个示例,请确保您了解为什么绑定值具有它们所具有的值。 在第一个示例中,在表单数据、路由值或查询字符串中找不到 qty 值,因此它的默认值为 0。在其他每个示例中,请求都包含一个或多个 重复值; 在这些情况下,记住模型绑定器查询绑定源的顺序很重要。 默认情况下,表单值将优先于其他绑定源,包括路由值!
默认模型绑定器不区分大小写,因此绑定值 QTY=50 将愉快地绑定到 qty 参数。
虽然这看起来有点压倒性,但同时从所有这些不同的来源绑定是相对不寻常的。 更常见的是,您的值都来自请求正文作为表单值,可能带有来自 URL 路由值的 ID。 如果您不确定事情是如何运作的,那么这种情况更像是一个关于您可以扭曲自己的结的警示故事。
在这些示例中,您愉快地将 qty 整数属性绑定到传入值,但正如我之前提到的,绑定源中存储的值都是字符串。您可以将字符串转换为哪些类型? 模型绑定器将转换几乎任何原始 .NET 类型,例如 int、float、decimal(显然还有字符串),以及任何具有 TypeConverter 的内容。还有一些其他特殊情况可以从字符串转换,例如 类型,但将其视为基元只会让您走得很远!
绑定复杂类型
如果看起来只能绑定简单的原始类型有点限制,那么你是对的! 幸运的是,模型活页夹并非如此。 尽管它只能将字符串直接转换为那些原始类型,但它也能够通过遍历绑定模型公开的任何属性来绑定复杂类型。
如果这不能让您立即感到高兴,那么让我们看看如果简单类型是您唯一的选择,您将如何构建页面处理程序。 想象一下,您的货币转换器应用程序的用户已经到达结账页面并准备兑换一些货币。 伟大的! 您现在需要的只是收集他们的姓名、电子邮件和电话号码。 不幸的是,您的页面处理程序方法必须如下所示:
public IActionResult OnPost(string firstName, string lastName, string phoneNumber, string email)
呸! 四个参数现在可能看起来还不错,但是当需求发生变化并且您需要收集其他详细信息时会发生什么? 方法签名将不断增长。 模型绑定器将非常愉快地绑定值,但它并不是完全干净的代码。 使用 [BindProperty] 方法也无济于事——你仍然需要用大量的属性和属性来弄乱我们的 PageModel!
通过绑定到复杂对象来简化方法参数
当您有许多方法参数时,任何 C# 代码的常见模式是提取一个封装方法所需数据的类。 如果需要添加额外的参数,您可以在该类中添加一个新属性。 此类成为您的绑定模型,它可能看起来像这样。
清单 6.4 用于捕获用户详细信息的绑定模型
public class UserBindingModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}
使用此模型,您现在可以将页面处理程序的方法签名更新为
public IActionResult OnPost(UserBindingModel user)
或者,使用 [BindProperty] 方法,在 PageModel 上创建一个属性:
[BindProperty]
public UserBindingModel User { get; set; }
现在您可以进一步简化页面处理程序签名:
public IActionResult OnPost()
在功能上,模型绑定器对这种新的复杂类型的处理方式略有不同。 模型绑定器不是查找值与参数名称(用户或属性的用户)匹配的参数,而是使用 new UserBindingModel()
创建模型的新实例。
尽管在此示例中模型的名称不是必需的,但模型绑定器还将查找以属性名称为前缀的属性,例如 user.FirstName
和 user.LastName
用于名为 User
的属性。您可以使用这种方法 当您有多个复杂的页面处理程序参数或多个复杂的 [BindProperty]
属性时。 一般来说,为简单起见,您应该尽可能避免这种情况。 对于所有模型绑定,前缀的大小写无关紧要。
一旦设置了绑定模型上可以绑定的所有属性,模型就会传递给页面处理程序(或设置 [BindProperty]
属性),然后处理程序将照常执行。 从这一点开始的行为与拥有大量单独参数时的行为相同——最终会在绑定模型上设置相同的值——但代码更简洁且更易于使用。
对于模型绑定的类,它必须具有默认的公共构造函数。您只能绑定公共和可设置的属性。
使用这种技术,您可以绑定其属性本身就是复杂模型的复杂层次模型。 只要每个属性都暴露了一个可以被模型绑定的类型,绑定器就可以轻松地遍历它。
绑定集合和字典
除了普通的自定义类和原语,您还可以绑定到集合、列表和字典。 想象一下,您有一个页面,用户在其中选择了他们感兴趣的所有货币; 您将显示所有选定对象的费率,如图 6.5 所示。
为此,您可以创建一个接受 List<string>
类型的页面处理程序,例如
public void OnPost(List<string> currencies);
图 6.5 货币转换器应用程序中的选择列表将向应用程序发送所选货币的列表。 模型绑定可以绑定选定的货币并为用户自定义视图以显示选定货币的等值成本。 |
---|
然后,您可以通过提供几种不同格式的值来将数据 POST 到此方法:
currencies[index
]- 其中货币是要绑定的参数的名称,索引是要绑定的项目的索引,例如货币[0]=GBP¤cies[1]=USD。[index]
- 如果您只绑定到单个列表(如本例所示),则可以省略参数名称,例如 [0]=GBP&[1]=USD。currencies
- 或者,您可以省略索引并将货币作为每个值的键发送,例如,货币=GBP¤cies=USD。
键值可以来自路由值和查询值,但更常见的是在表单中发布它们。 字典可以使用类似的绑定,其中字典键在参数命名和省略时替换索引。
如果这一切看起来有点令人困惑,请不要感到太惊慌。 如果您正在构建一个传统的 Web 应用程序并使用 Razor 视图生成 HTML,该框架将负责为您生成正确的名称。
使用 IFORMFILE 绑定文件上传
ASP.NET Core 支持通过公开 IFormFile 接口来上传文件。 您可以将此接口用作您的绑定模型,或者作为页面处理程序的方法参数,或者使用 [BindProperty]
方法,它将填充文件上传的详细信息:
public void OnPost(IFormFile file);
如果您需要接受多个文件,也可以使用 IEnumerable<IFormFile>
:
public void OnPost(IEnumerable<IFormFile> file);
IFormFile 对象公开了几个用于读取上传文件内容的属性和实用方法,其中一些如下所示:
public interface IFormFile
{
string ContentType { get; }
long Length { get; }
string FileName { get; }
Stream OpenReadStream();
}
如您所见,此接口公开了一个 FileName
属性,该属性返回文件上传时使用的文件名。 但是你知道不要相信用户,对吧? 你永远不应该在你的代码中直接使用文件名——总是在你将文件保存在任何地方之前为它生成一个新的文件名。
如果用户只打算上传小文件,则 IFormFile
方法很好。 当您的方法接受 IFormFile
实例时,文件的全部内容会在您接收之前缓冲在内存和磁盘上。 然后,您可以使用 OpenReadStream 方法读取数据。
如果用户将大文件发布到您的网站,您可能会发现内存或磁盘空间不足,因为它会缓冲每个文件。 在这种情况下,您可能需要直接流式传输文件以避免一次保存所有数据。 不幸的是,与模型绑定方法不同,流式传输大文件可能很复杂且容易出错,因此超出了本书的范围。
不要像你一样使用 IFormFile
接口来处理大文件上传 可能会看到性能问题。 请注意,您不能依赖用户不上传大文件,所以最好完全避免文件上传!
对于绝大多数 Razor 页面,简单和复杂类型的模型绑定的默认配置运行良好,但您可能会发现在某些情况下需要进行更多控制。 幸运的是,这是完全可能的,如有必要,您可以通过替换框架内部使用的 ModelBinders 来完全覆盖该过程。
选择绑定源
正如您已经看到的,默认情况下,ASP.NET Core 模型绑定器将尝试从三个不同的绑定源绑定您的绑定模型:表单数据、路由数据和查询字符串。
有时,您可能会发现有必要明确声明要绑定到哪个绑定源。 在其他情况下,这三个来源根本不够用。 最常见的场景是当您想要将方法参数绑定到请求标头值时,或者当请求的主体包含您想要绑定到参数的 JSON 格式数据时。 在这些情况下,您可以使用说明从何处绑定的属性来装饰您的绑定模型,如下面的清单所示。
清单 6.5 为模型绑定选择绑定源
public class PhotosModel: PageModel
{
public void OnPost(
[FromHeader] string userId, //userId 将从请求中的 HTTP 标头绑定。
[FromBody] List<Photo> photos) //photo 对象列表将绑定到请求的正文,通常采用 JSON 格式。
{
/* method implementation */
}
}
在此示例中,页面处理程序使用用户 ID 更新照片集合。 照片中有要标记的用户ID,userId和要标记的Photo对象列表,photos
的方法参数
我没有使用标准绑定源绑定这些方法参数,而是为每个参数添加了属性,指示要使用的绑定源。 [FromHeader]
属性已应用于 userId
参数。 这告诉模型绑定器将该值绑定到名为 userId 的 HTTP 请求标头值。
我们还使用 [FromBody]
属性将照片列表绑定到 HTTP 请求的正文。 这将从请求正文中读取 JSON,并将其绑定到 List<Photo>
方法参数。
熟悉以前版本的 ASP.NET 的开发人员应该注意,在 Razor 页面中绑定到 JSON 请求时,显式需要 [FromBody]
属性。 这与以前不需要属性的 ASP.NET 行为不同。
您不仅限于从请求正文绑定 JSON 数据——您也可以使用其他格式,具体取决于您配置框架使用的 InputFormatter
。 默认情况下,仅配置 JSON 输入格式化程序。
您可以使用一些不同的属性来覆盖默认值并为每个绑定模型(或绑定模型上的每个属性)指定一个绑定源:
[FromHeader]
- 绑定到标头值[FromQuery]
- 绑定到查询字符串值[FromRoute]
—绑定到路由参数[FromForm]
—绑定到请求正文中发布的表单数据[FromBody]
—绑定到请求的正文内容
您可以将这些中的每一个应用于任意数量的处理程序方法参数或属性,如清单 6.5 中所见,除了 [FromBody]
属性——只有一个值可以用 [FromBody]
属性修饰。 此外,由于表单数据是在请求正文中发送的,因此 [FromBody]
和 [FromForm]
属性实际上是互斥的。
只有一个参数可以使用 [FromBody]
属性。 此属性将消耗传入的请求,因为 HTTP 请求正文只能安全地读取一次。
除了这些用于指定绑定源的属性外,还有一些其他属性可以进一步自定义绑定过程:
[BindNever]
- 模型绑定器将完全跳过此参数。[BindRequired]
- 如果参数未提供或为空,则活页夹将添加验证错误。[FromServices]
——这用于指示应该使用依赖注入提供参数
此外,您还有 [ModelBinder]
属性,它使您进入模型绑定的“上帝模式”。 使用此属性,您可以指定确切的绑定源,覆盖要绑定到的参数的名称,并指定要执行的绑定类型。 你很少需要这个,但当你这样做时,至少它就在那里!
通过结合所有这些属性,您应该会发现您能够配置模型绑定器以绑定到页面处理程序想要使用的几乎任何请求数据。 不过,一般来说,您可能会发现很少需要使用它们。 在大多数情况下,默认值应该适合您。
使用模型验证处理用户输入
验证的必要性
数据可以来自 Web 应用程序中的许多不同来源——您可以从文件中加载它,从数据库中读取它,或者接受用户在请求中输入到表单中的值。 尽管您可能倾向于相信服务器上已经存在的数据是有效的(尽管这有时是一个危险的假设!),但您绝对不应该相信作为请求的一部分发送的数据。
验证发生在 Razor Pages 框架中模型绑定之后,但在页面处理程序执行之前,如图 6.2 所示。 图 6.6 显示了模型验证在此过程中的适用位置的更紧凑视图,展示了如何绑定和验证对请求用户个人详细信息的结帐页面的请求。
图 6.6 验证发生在模型绑定之后但页面处理程序执行之前。 无论验证是否成功,页面处理程序都会执行。 |
---|
不过,验证不仅是为了检查安全威胁。 还需要检查非恶意错误:
- 数据格式应正确(电子邮件字段具有有效的电子邮件格式)。
- 数字可能需要在特定范围内(您不能购买 -1 本这本书!)。
- 某些值可能是必需的,但其他值是可选的(配置文件可能需要名称,但电话号码是可选的)。
- 值必须符合您的业务要求(您不能将货币转换为自身,它需要转换为不同的货币)。
看起来其中一些可以在浏览器中轻松处理。 例如,如果用户选择要转换的货币,不要让他们选择相同的货币; 我们都看到了“请输入有效的电子邮件地址”消息。
不幸的是,尽管这种客户端验证对用户很有用,因为它可以为他们提供即时反馈,但您永远不能依赖它,因为它总是可以绕过这些浏览器保护。 当数据到达您的 Web 应用程序时,始终需要使用服务器端验证对其进行验证。
如果这感觉有点多余,比如你会重复逻辑和代码,那么恐怕你是对的。 这是 Web 开发的不幸方面之一; 重复是必要的邪恶。 值得庆幸的是,ASP.NET Core 提供了一些功能来尝试减轻这种负担。
使用 DataAnnotations
属性进行验证
验证属性,或更准确地说是 DataAnnotations
属性,允许您指定绑定模型应遵守的规则。 它们通过描述绑定模型应该包含的数据类型来提供有关模型的元数据,而不是数据本身。
元数据描述其他数据,指定数据应遵守的规则和特征。
您可以将 DataAnnotations 属性直接应用于绑定模型,以指示可接受的数据类型。 例如,这使您可以检查是否提供了必填字段、数字是否在正确的范围内以及电子邮件字段是否是有效的电子邮件地址。
例如,让我们考虑您的货币转换器应用程序的结帐页面。您需要收集有关用户的详细信息才能继续,因此您要求他们提供他们的姓名、电子邮件和电话号码(可选)。 以下清单显示了用表示模型验证规则的验证属性修饰的 UserBindingModel。 这扩展了您在清单 6.4 中看到的示例。
清单 6.6 将 DataAnnotations 添加到绑定模型以提供元数据
public class UserBindingModel
{
[Required] //必须提供标记为必需的值。
[StringLength(100)] //StringLengthAttribute 设置属性的最大长度。
[Display(Name = "Your name")] //自定义用于描述属性的名称
public string FirstName { get; set; }
[Required]
[StringLength(100)]
[Display(Name = "Last name")] //自定义用于描述属性的名称
public string LastName { get; set; }
[Required]
[EmailAddress] //验证 Email 的值是一个有效的电子邮件地址
public string Email { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}
突然之间,您的绑定模型包含大量信息,而以前它的细节非常稀疏。 例如,您已指定应始终提供 FirstName
属性,它的最大长度应为 100 个字符,并且在引用它时(例如在错误消息中)应将其称为“Your name” 的“Name”。
这些属性的好处在于它们清楚地声明了模型的预期状态。 通过查看这些属性,您知道这些属性将包含或应该包含什么。 它们还为 ASP.NET Core 框架提供挂钩,以验证模型绑定期间模型上的数据集是否有效,稍后您将看到。
将 DataAnnotations
应用于模型时,您有大量的属性可供选择。 我在这里列出了一些常见的,但您可以在 System.ComponentModel.DataAnnotations
命名空间中找到更多。
[CreditCard]
- 验证属性是否具有有效的信用卡格式。[EmailAddress]
- 验证属性是否具有有效的电子邮件地址格式。[StringLength(max)]
- 验证字符串最多包含最大字符数。[MinLength(min)]
- 验证一个集合是否至少包含最少的项目数。[Phone]
- 验证属性是否具有有效的电话号码格式。[Range(min, max)]
- 验证属性的值是否介于 min 和 max 之间。[Regular Expression(regex)]
- 验证属性是否符合 regex 正则表达式模式。[Url]
- 验证属性是否具有有效的 URL 格式。[Required]
- 指示属性不能为空。[Compare]
- 允许您确认两个属性具有相同的值(例如,Email 和 ConfirmEmail)。
[EmailAddress] 和其他属性仅验证值的格式是否正确。 他们不验证电子邮件地址是否存在。
DataAnnotations
属性并不是一个新特性——它们自 3.5 版以来一直是 .NET Framework 的一部分——它们在 ASP.NET Core 中的使用几乎与之前版本的 ASP.NET 中相同。
除了验证之外,它们还用于其他目的。 Entity Framework Core(以及其他)使用 DataAnnotations
来定义从 C# 类创建数据库表时要使用的列类型和规则。
如果开箱即用的 DataAnnotation
属性不能涵盖您需要的所有内容,则还可以通过从基本 ValidationAttribute
派生来编写自定义属性。
DataAnnotations
适用于单独验证属性的输入,但不适用于验证业务规则。 您很可能需要在 DataAnnotations 框架之外执行此验证。
无论您使用哪种验证方法,请务必记住,这些技术本身并不能保护您的应用程序。 Razor Pages 框架将确保发生验证,但如果验证失败,它不会自动执行任何操作。 在下一节中,我们将了解如何在服务器上检查验证结果并处理验证失败的情况。
在服务器上验证安全
绑定模型的验证发生在页面处理程序执行之前,但请注意处理程序始终执行,无论验证失败还是成功。 检查验证结果是页面处理程序的责任。
Razor Pages 框架将验证尝试的输出存储在 PageModel
上称为 ModelState
的属性中。 此属性是一个 ModelStateDictionary
对象,其中包含模型绑定后发生的所有验证错误的列表,以及一些用于使用它的实用程序属性。
例如,以下清单显示了 Checkout.cshtml Razor 页面的 OnPost
页面处理程序。 Input
属性被标记为绑定并使用前面清单 6.6 中显示的 UserBindingModel
类型。 这个页面处理程序目前不对数据做任何事情,但是在方法早期检查 ModelState
的模式是这里的关键点。
清单 6.7 检查模型状态以查看验证结果
public class CheckoutModel : PageModel //ModelState 属性在 PageModel 基类中可用。
{
[BindProperty]
public UserBindingModel Input { get; set; } //Input 属性包含模型绑定数据。
public IActionResult OnPost() //在执行页面处理程序之前验证绑定模型。
{
if (!ModelState.IsValid) //如果存在验证错误,IsValid 将为 false。
{
return Page(); //验证失败,因此重新显示有错误的表单并尽早完成该方法。
}
/* Save to the database, update user, return success */
//验证通过,因此使用模型中提供的数据是安全的。
return RedirectToPage("Success");
}
}
如果 ModelState
属性指示发生错误,该方法会立即调用 Page
帮助器方法。 这将返回一个 PageResult
,最终将生成 HTML 以返回给用户,正如您在第 3 章中看到的那样。视图使用 Input
属性中提供的(无效)值在显示时重新填充表单,如图 6.7 所示。 此外,使用 ModelState
属性中的验证错误会自动添加对用户有用的消息。
表单上显示的错误消息是每个验证属性的默认值。 您可以通过在任何验证属性上设置 ErrorMessage
属性来自定义消息。 例如,您可以使用 [Required(ErrorMessage="Required")]
自定义 [Required]
属性。
如果请求成功,页面处理程序会返回一个 RedirectToPageResult
(使用 RedirectToPage()
辅助方法),将用户重定向到 Success.cshtml
Razor 页面。 这种在 POST 成功后返回重定向响应的模式称为 POST-REDIRECT-GET 模式。
图 6.7 当验证失败时,您可以重新显示表单以向用户显示 ModelState 验证错误。 请注意,与其他字段不同,您的姓名字段没有关联的验证错误。 |
---|
您的应用程序不控制此验证; 它内置于现代 HTML5 浏览器中。 另一种方法是通过在页面上运行 JavaScript 并在提交表单之前检查用户输入的值来执行客户端验证。 这是 Razor Pages 中最常用的方法。
使用这种方法,用户可以立即看到表单中的任何错误,甚至在请求发送到服务器之前,如图 6.9 所示。 这提供了更短的反馈周期,提供了更好的用户体验。
如果您正在构建 SPA,则有责任在客户端框架上验证客户端上的数据,然后再将其发布到 Web API。 Web API 仍然会在数据到达服务器时对其进行验证,但客户端框架负责提供流畅的用户体验。
图 6.9 使用客户端验证,单击提交将触发验证,在请求发送到服务器之前显示在浏览器中。 如右窗格所示,未发送任何请求。 |
---|
在 Razor Pages 中组织绑定模型
ASP.NET Core 中的模型绑定有很多等效的方法可供采用,因此没有“正确”的方法可以做到这一点。 以下清单显示了我将如何设计一个简单的 Razor 页面的示例。 此 Razor 页面显示具有给定 ID 的产品的表单,并允许您使用 POST 请求编辑详细信息。 这是一个比我们目前看到的更长的样本,但我强调了以下要点。
清单 6.8 设计一个编辑产品 Razor 页面
public class EditProductModel : PageModel
{
//ProductService 使用 DI 注入并提供对应用程序模型的访问。
private readonly ProductService _productService;
public EditProductModel(ProductService productService)
{
_productService = productService;
}
[BindProperty]
public InputModel Input { get; set; } //单个属性用 BindProperty 标记。
public IActionResult OnGet(int id) //id 参数是来自 OnGet 和 OnPost 处理程序的路由模板的模型绑定。
{
var product = _productService.GetProduct(id); //从应用程序模型加载产品详细信息。
Input = new InputModel //从现有产品的详细信息构建 InputModel 的实例以在表单中进行编辑。
{
Name = product.ProductName,
Price = product.SellPrice,
};
return Page();
}
public IActionResult OnPost(int id) //id 参数是来自 OnGet 和 OnPost 处理程序的路由模板的模型绑定。
{
//如果请求无效,则重新显示表单而不保存。
if (!ModelState.IsValid)
{
return Page();
}
//使用 ProductService 更新应用程序模型中的产品。
_productService.UpdateProduct(id, Input.Name, Input.Price);
//使用 POST-REDIRECT GET 模式重定向到新页面。
return RedirectToPage("Index");
}
//将 InputModel 定义为 Razor 页面中的嵌套类。
public class InputModel
{
[Required]
public string Name { get; set; }
[Range(0, int.MaxValue)]
public decimal Price { get; set; }
}
}
此页面显示典型“编辑表单”的 PageModel
。 这些在许多业务线应用程序中非常常见,除其他外,这是 Razor Pages 非常适合的场景。
此表单显示了与模型绑定相关的几种模式,我在构建 Razor 页面时尝试遵循这些模式:
- 仅使用
[BindProperty]
绑定单个属性。 我倾向于使用[BindProperty]
装饰的单个属性来进行模型绑定。 当需要绑定多个值时,我会创建一个单独的类InputModel
来保存这些值,并使用[BindProperty]
装饰该单个属性。 像这样装饰单个属性会使忘记添加属性变得更加困难,这意味着您的所有 Razor 页面都使用相同的模式。 - 将绑定模型定义为嵌套类。 我将
InputModel
定义为 Razor 页面中的嵌套类。 绑定模型通常高度特定于该单个页面,因此这样做可以使您正在处理的所有内容保持在一起。此外,我通常为我的所有页面使用确切的类名称 InputModel。再次,这增加了 Razor 页面的一致性. - 不要使用
[BindProperties]
。 除了[BindProperty]
属性之外,还有一个[BindProperties]
属性(注意拼写不同)可以直接应用于 Razor Page PageModel。 这将导致模型中的所有属性都是模型绑定的,如果您不小心,这可能会使您容易受到过度发布攻击。 我建议你不要使用[BindProperties]
属性,而是坚持使用[BindProperty]
绑定单个属性。 - 在页面处理程序中接受路由参数。 对于简单的路由参数,例如清单 6.8 中传递给
OnGet
和OnPost
处理程序的id
,我将参数添加到页面处理程序方法本身。 这避免了 GET 请求的笨拙SupportsGet=true
语法。 - 始终在使用数据之前进行验证。 我之前说过,所以我再说一遍。 验证用户输入!
Razor Pages 中的模型绑定到此结束。 您看到了 ASP.NET Core 框架如何使用模型绑定来简化从请求中提取值并将它们转换为您可以快速使用的普通 .NET 对象的过程。 本章最重要的方面是关注验证——这是所有 Web 应用程序共同关心的问题,使用 DataAnnotations
可以很容易地为模型添加验证。
总结
- Razor Pages 使用三个不同的模型,每个模型负责请求的不同方面。 绑定模型封装了作为请求的一部分发送的数据。 应用程序模型表示应用程序的状态。 PageModel 是 Razor 页面的支持类,它公开了 Razor 视图用来生成响应的数据。
- 模型绑定从请求中提取值并使用它们来创建页面处理程序在执行时可以使用的 .NET 对象。
PageModel
上标有[BindProperty]
属性的任何属性以及页面处理程序的方法参数都将参与模型绑定。- 用
[BindProperty]
修饰的属性不受 GET 请求的约束。 要绑定 GET 请求,您必须改用[BindProperty(SupportsGet = true)]
。 - 默认情况下,有 3 个绑定源:POST 表单值、路由值和查询字符串。 在尝试绑定绑定模型时,绑定程序将按顺序询问这些。
- 将值绑定到模型时,参数和属性的名称不区分大小写。
- 您可以绑定到简单类型或复杂类型的属性。
- 要绑定复杂类型,它们必须具有默认构造函数和可设置的公共属性。
- 简单类型必须可转换为字符串才能自动绑定; 例如,数字、日期和布尔值。
- 集合和字典可以分别使用
[index]=value
和[key] =value
语法进行绑定。 - 您可以使用应用于方法的
[From*]
属性来自定义绑定模型的绑定源,例如[FromHeader]
和[FromBody]
。 这些可用于绑定到非默认绑定源,例如标头或 JSON 正文内容。 - 与以前版本的 ASP.NET 相比,绑定 JSON 属性时需要
[FromBody]
属性(以前不需要)。 - 验证对于检查安全威胁是必要的。 检查数据格式是否正确,并确认它符合预期值并符合您的业务规则。
- ASP.NET Core 提供
DataAnnotations
属性以允许您以声明方式定义预期值。 - 模型绑定后会自动进行验证,但您必须手动检查验证结果并通过询问
ModelState
属性在页面处理程序中采取相应措施。 - 客户端验证提供比单独的服务器端验证更好的用户体验,但您应该始终使用服务器端验证。
- 客户端验证使用 JavaScript 和应用于 HTML 元素的属性来验证表单值。