第6章 绑定模型:检索和验证用户输入(ASP.NET in Action, 2nd Edition)

发布时间 2023-04-10 11:58:15作者: 码农小白修炼记

本章包括

  • 使用请求值创建绑定模型
  • 自定义模型绑定过程
  • 使用DataAnnotations属性验证用户输入

在第5章中,我向您展示了如何使用参数定义路由——可能是日历中的当天,也可能是产品页面的唯一ID。但是,如果用户请求一个给定的产品页面,那么呢?类似地,如果请求包含表单中的数据,例如更改产品名称,该怎么办?您如何处理该请求并访问用户提供的值?

在本章的前半部分,我们将研究如何使用绑定模型从请求中检索这些参数,以便您可以在Razor Pages中使用它们。您将看到如何获取表单或URL中发布的数据并将其绑定到C#对象。这些对象将作为方法参数传递给Razor Page处理程序,或设置为Razor Page PageModel上的属性。当页面处理程序执行时,它可以使用这些值执行一些有用的操作,例如,返回正确的日志条目或更改产品名称。

一旦您的代码在页面处理程序方法中执行,您可能会认为您可以愉快地使用绑定模型,而无需进一步考虑。等等,这些数据是从哪里来的?从用户那里——你知道他们不可信!本章的后半部分着重于如何确保用户提供的值是有效的,并对您的应用程序有意义。

您可以将绑定模型视为RazorPage的输入,接受用户的原始HTTP请求,并通过填充“Plain Old CLR Object”(POCO)使其可用于代码。一旦页面处理程序运行完毕,就可以使用ASP.NET Core的MVC实现中的输出模型——视图模型和API模型。这些用于生成对用户请求的响应。我们将在第7章和第9章中介绍它们。

在我们进一步讨论之前,让我们回顾一下MVC设计模式以及绑定模型如何适合ASP.NET Core。

6.1 了解Razor Pages和MVC中的模型

在本节中,我描述了绑定模型如何适合我们在第4章中讨论的MVC设计模式。我描述了MVC模式中绑定模型和其他“模型”概念之间的区别,以及它们在ASP.NET Core中的使用方式。

MVC就是关注点的分离。前提是,通过将应用程序的每个方面隔离开来,集中于一项职责,可以减少系统中的相互依赖。这种分离使更改更容易,而不会影响应用程序的其他部分。

经典的MVC设计模式有三个独立的组件:

  • 控制器——调用模型上的方法并选择视图
  • 视图——显示构成模型的数据的表示
  • 模型——要显示的数据和更新自身的方法

在此表示中,只有一个模型,即应用程序模型,它表示应用程序的所有业务逻辑以及如何更新和修改其内部状态。ASP.NET Core有多个模型,它比MVC的一些观点更进一步地采用了单一责任原则。

在第4章中,我们看了一个待办事项列表应用程序的示例,它可以显示给定类别和用户名的所有待办事项。使用此应用程序,您可以向使用 todo/listcategory/{category}/{username} 路由的URL发出请求。这将返回一个显示所有相关待办事项的响应,如图6.1所示。

图6.1 显示待办事项列表项目的基本待办事项列表应用程序。用户可以通过更改URL中的类别和用户名参数来过滤项目列表。

该应用程序使用您已经看到的相同MVC构造,例如路由到Razor Page处理程序,以及许多不同的模型。图6.2显示了此应用程序的请求如何映射到MVC设计模式,以及它如何生成最终响应,包括关于模型绑定和请求验证的其他细节。

图6.2 ASP.NET Core中的MVC模式处理查看待办事项列表Razor Pages应用程序中项目子集的请求。

ASP.NET Core Razor Pages使用了几种不同的模型,其中大多数是POCO,而应用程序模型更多地是围绕服务集合的概念。ASP.NET Core中的每个模型都负责处理整个请求的不同方面:

  • 绑定模型——绑定模型是用户在发出请求时提供的所有信息,以及其他上下文数据。这包括从URL解析的路由参数、查询字符串以及请求主体中的表单或JSON数据。绑定模型本身是您定义的一个或多个POCO对象。Razor Pages中的绑定模型通常是通过在页面的PageModel上创建公共属性并用[BindProperty]属性对其进行修饰来定义的。它们也可以作为参数传递给页面处理程序。
    对于本例,绑定模型将包括类别名称open和用户名Andrew。Razor Pages基础结构在页面处理程序执行之前检查绑定模型,以检查提供的值是否有效,尽管页面处理程序即使不有效也会执行,正如我们在第6.3节讨论验证时所看到的。
  • 应用程序模型——应用程序模型实际上根本不是ASP.NET Core模型。它通常是一组不同的服务和类,更像是在应用程序中执行某种业务操作所需的概念。它可能包括域模型(表示应用程序试图描述的内容)和数据库模型(表示存储在数据库中的数据),以及任何其他附加服务。
    在待办事项列表应用程序中,应用程序模型将包含待办事项的完整列表,可能存储在数据库中,并且知道如何仅查找分配给Andrew的Open类别中的待办事项。
    建模是一个巨大的主题,有许多不同的可能方法,因此,这超出了本书的范围,但我们将在第11章中简要介绍创建数据库模型。
  • 页面模型——Razor Page的PageModel有两个主要功能:它通过公开页面处理程序方法作为应用程序的控制器,它作为Razor视图的视图模型。视图生成响应所需的所有数据都显示在PageModel上,例如分配给Andrew的Open类别中的待办事项列表。
    从中派生Razor Pages的PageModel基类包含各种帮助器属性和方法。其中一个是ModelState属性,它将模型验证的结果作为一系列键值对包含。您将在第6.3节中了解有关验证和ModelState属性的更多信息。

这些模型构成所有Razor Pages应用程序的主体,处理每个页面处理程序的输入、业务逻辑和输出。假设您有一个电子商务应用程序,它允许用户通过向/search/{query} URL发送请求来搜索衣服,其中{query}包含他们的搜索词:

  • 绑定模型——这将从URL中获取{query}路由参数和请求正文中发布的所有值(可能是排序顺序或要显示的项目数),并将它们绑定到C#类,该类通常充当一次性数据传输类。当调用页面处理程序时,这将被设置为PageModel上的属性。
  • 应用程序模型——这是执行逻辑的服务和类。当页面处理程序调用时,该模型将加载与查询匹配的所有衣服,应用必要的排序和过滤器,并将结果返回给控制器。
  • 页面模型——应用程序模型提供的值将设置为Razor Page的PageModel上的属性,以及其他元数据,例如可用项目的总数,或用户当前是否可以签出。Razor视图将使用这些数据将Razor的视图呈现为HTML。

关于所有这些模型的重要一点是,它们的职责是明确的。将它们分开并避免重复使用将有助于确保您的应用程序保持灵活和易于更新。

这种分离的一个明显的例外是PageModel,因为它是定义绑定模型和页面处理程序的地方,它还保存呈现视图所需的数据。有些人可能认为表面上的不分离是亵渎,但实际上这并不是一个普遍的问题。分界线很明显。例如,只要不尝试从Razor视图内部调用页面处理程序,就不会遇到任何问题!

现在您已经正确地介绍了ASP.NET Core中的各种模型,现在是时候关注如何使用它们了。本章将介绍从传入请求构建的绑定模型,它们是如何创建的,以及值来自何处?

6.2 从请求到模型:使请求变得有用

在本节中,您将了解

  • ASP.NET Core如何从请求创建绑定模型
  • 如何绑定简单类型,如int和string,以及复杂类
  • 如何选择在绑定模型中使用请求的哪些部分

现在,您应该熟悉ASP.NET Core如何通过在Razor Page上执行页面处理程序来处理请求。您还看到了几个页面处理程序,例如:

public void OnPost(ProductModel product)

页面处理程序是普通的C#方法,因此ASP.NET Core框架需要能够以通常的方式调用它们。当页面处理程序接受参数作为其方法签名的一部分时,例如前面示例中的产品,框架需要一种方法来生成这些对象。它们究竟来自何处,又是如何产生的?

我已经暗示,在大多数情况下,这些值来自请求本身。但是服务器接收的HTTP请求是一系列字符串,ASP.NET Core如何将其转换为.NET对象?这就是模型绑定的作用所在。

定义:模型绑定从请求中提取值,并使用它们创建.NET对象。这些对象作为方法参数传递给正在执行的页面处理程序,或者设置为PageModel的属性,这些属性用[BindProperty]属性标记。

模型绑定器负责查看传入的请求并查找要使用的值。然后,它创建适当类型的对象,并在称为绑定的过程中将这些值分配给模型。

注意:Razor Pages和MVC中的模型绑定是来自请求的对象的单向填充,而不是桌面或移动开发时使用的双向数据绑定。

Razor Page的PageModel(在Razor页面的.cshtml.cs文件中)上用[BindProperty]属性修饰的任何属性都是使用模型绑定从传入请求中创建的,如以下列表所示。类似地,如果页面处理程序方法有任何参数,这些参数也会使用模型绑定创建。

set; } [BindProperty(SupportsGet = true)] public string Username { get; set; } //属性不是GET请求的模型绑定,除非您使用SupportsGet。 public void OnGet() { } //选择页面处理程序时,页面处理程序的参数也会绑定到模型。 public void OnPost(ProductModel model) { } }

如前列表所示,PageModel属性对于GET请求不是模型绑定的,即使您添加了[BindProperty]属性。出于安全原因,仅绑定使用POST和PUT等动词的请求。如果确实要绑定GET请求,可以在[BindProperty]属性上设置SupportsGet属性以选择加入模型绑定。

提示:要为GET请求绑定PageModel属性,请使用SupportsGet属性,例如[BindProperty(SupportsGet=true)]。

哪个部分是绑定模型?
清单6.1显示了使用多个绑定模型的RazorPage:Category属性、Username属性和ProductModel属性(在OnPost处理程序中)都是模型绑定的。
以这种方式使用多个模型很好,但我更喜欢使用一种方法,将所有模型绑定保持在一个嵌套的类中,我通常称之为InputModel。使用这种方法,清单6.1中的Razor Page可以写成如下:

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; }
    }
}

这种方法具有一些结构的优势,您将在第6.4节中了解更多。

ASP.NET Core使用请求的属性(例如请求URL、HTTP请求中发送的任何标头、请求正文中显式POST的任何数据等)自动填充绑定模型。
默认情况下,ASP.NET Core在创建绑定模型时使用三种不同的绑定源。它将按顺序查看其中的每一个,并获取找到的与绑定模型名称匹配的第一个值(如果有的话):

  • 表单值——当使用POST将表单发送到服务器时,在HTTP请求正文中发送
  • 路由值——从URL段获取,或在匹配路由后通过默认值获取,如第5章所示
  • 查询字符串值——在URL末尾传递,在路由过程中不使用

模型绑定过程如图6.3所示。模型绑定器检查每个绑定源,以查看它是否包含可以在模型上设置的值。或者,模型也可以选择值应该来自的特定来源,如第6.2.3节所示。绑定每个属性后,将验证模型,并将其设置为PageModel上的属性或作为参数传递给页面处理程序。您将在本章的后半部分了解验证过程。

图6.3 模型绑定涉及映射来自绑定源的值,这些值对应于请求的不同部分。

PageModel属性还是页面处理程序参数?
在Razor Pages中使用模型绑定有两种不同的方法:

    • 用[BindProperty]属性修饰PageModel上的属性。
    • 向页面处理程序方法添加参数。

您应该选择以下哪种方法?
这个问题的答案很大程度上取决于品味。在PageModel上设置属性并用[BindProperty]标记它们是示例中最常见的方法。如果使用这种方法,您将能够在渲染视图时访问绑定模型,如第7章和第8章所示。
另一种方法是将参数添加到页面处理程序方法中,在不同的MVC阶段之间提供了更多的分离,因为您将无法访问页面处理程序之外的参数。另一方面,如果确实需要在Razor视图中显示这些值,则必须手动将参数复制到视图中可以访问的属性。
我选择的方法往往取决于我正在构建的特定Razor页面。如果我正在创建表单,我会倾向于[BindProperty]方法,因为我通常需要访问Razor视图中的请求值。对于简单页面(例如,绑定模型是产品ID),我倾向于支持页面处理程序参数方法,因为它简单,尤其是当处理程序用于GET请求时。我在第6.4节中对我的方法提出了一些更具体的建议。

图6.4显示了使用本节开头所示示例的模型绑定创建ProductModel方法参数的请求示例:

public void OnPost(ProductModel product)

图6.4 使用模型绑定创建用于执行RazorPage的模型实例。

Id属性已从URL路由参数绑定,但Name和SellPrice属性已从请求主体绑定。使用模型绑定的最大优点是您不必自己编写代码来解析请求和映射数据。这类代码通常重复且容易出错,因此使用内置的常规方法可以让您专注于应用程序的重要方面:业务需求。

提示:模型绑定对于减少重复代码非常有用。尽可能利用它,您很少会发现自己必须直接访问Request对象。

如果需要的话,这些功能可以让您完全定制模型绑定的工作方式,但您很少会发现自己需要对此进行深入研究。对于大多数情况,它都是按原样工作的,您将在本节的其余部分中看到。

6.2.1 绑定简单类型

我们将通过一个简单的RazorPage处理程序来开始我们的模型绑定之旅。下一个列表显示了一个简单的RazorPage,它将一个数字作为方法参数,并通过将该数字乘以其自身来对其进行平方。

//更复杂的示例将在应用程序模型的外部服务中完成这项工作。 } public int Square { get; set; } //结果作为属性公开,并由视图用于生成响应。 }

在上一章中,您了解了路由以及它如何选择RazorPage来执行。您可以将Razor Page的路由模板更新为“CalculateSquare/{number}”,方法是在.cshtml文件中的Razor Page@Page指令中添加一个{number}段,如我们在第5章中所讨论的:

@page "{number}"

当客户端请求URL /CalculateSquare/5 时,RazorPage框架使用路由来解析它的路由参数。这将生成路由值对:

number=5

RazorPage的OnGet页面处理程序包含一个名为number的整数,它是绑定模型。当ASP.NET Core执行此页面处理程序方法时,它将发现期望的参数,快速浏览与请求相关联的路由值,并找到number=5。然后,它可以将number参数绑定到此路由值并执行该方法。页面处理程序方法本身不关心该值来自何处;它沿着它的快乐之路,计算值的平方,并将其设置在square属性上。

值得赞赏的是,在执行方法时,您不必编写任何额外的代码来尝试从URL中提取数字。您需要做的就是创建一个具有正确名称的方法参数(或公共属性),并让模型绑定发挥作用。

路由值不是模型绑定器可以用来创建绑定模型的唯一值。如前所述,框架将查看三个默认绑定源,以找到与绑定模型匹配的源:

  • Form values
  • Route values
  • Query string values

这些绑定源中的每一个都将值存储为"名称-值"对。如果没有一个绑定源包含所需的值,则将绑定模型设置为该类型的默认值。在这种情况下,绑定模型的确切值取决于变量的类型:

  • 对于值类型,值将为默认值(T)。对于int参数,这将是0,对于bool,则为false。
  • 对于引用类型,使用默认(无参数)构造函数创建类型。对于自定义类型(如ProductModel),这将创建一个新对象。对于可空类型 int?, bool?,该值将为空。
  • 对于字符串类型,值将为空。

警告:当模型绑定无法绑定方法参数时,必须考虑页面处理程序的行为。如果没有绑定源包含该值,则传递给该方法的值可能为空,或者可能意外地具有默认值(对于值类型)。

清单6.2显示了如何绑定单个方法参数。让我们进行下一个逻辑步骤,看看如何绑定多个方法参数。

在上一章中,我们讨论了构建货币转换器应用程序的路由。作为您开发的下一步,您的老板要求您创建一种方法,在该方法中,用户以一种货币提供价值,您必须将其转换为另一种货币。首先创建名为Convert.cshtml的Razor页面,然后使用@Page指令自定义页面的路由模板,以使用包含两个路由值的绝对路径:

@page "/{currencyIn}/{currencyOut}"

然后创建一个页面处理程序,接受所需的三个值,如下面的列表所示。

清单6.3 接受多个绑定参数的RazorPage处理程序

public class ConvertModel : PageModel
{
    public void OnGet(string currencyIn, string currencyOut, int qty)
    {
        /* method implementation */
    }
}

如您所见,有三个不同的参数要绑定。问题是,这些值将从何而来,它们将被设置为什么?答案是,这要看具体情况!表6.1显示了各种可能性。所有这些示例都使用相同的路由模板和页面处理程序,但根据发送的数据,将绑定不同的值。实际值可能与您预期的不同,因为可用的绑定源提供了冲突的值!

表6.1 将请求数据绑定到来自多个绑定源的页面处理程序参数

URL(路由值)

HTTP正文数据(表单值)

参数值已绑定

/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& currencyOut=EUR&qty=50

currencyIn=CAD currencyOut=EUR qty=50

对于每个示例,请确保您了解绑定值具有它们所具有的值的原因。在第一个示例中,在表单数据、路由值或查询字符串中找不到数量值,因此它的默认值为0。在其他每个示例中,请求包含一个或多个重复值;在这些情况下,必须记住模型绑定器查询绑定源的顺序。默认情况下,表单值将优先于其他绑定源,包括路由值!

注:默认模型绑定器不区分大小写,因此QTY=50的绑定值将很好地绑定到QTY参数。

虽然这可能看起来有点令人难以接受,但同时从所有这些不同的来源绑定是相对不寻常的。更常见的是,所有值都来自请求体作为表单值,可能带有来自URL路由值的ID。在这种情况下,如果你不确定其工作原理,可能会陷入困境。

在这些示例中,您很高兴地将qty integer属性绑定到传入值,但正如我前面提到的,绑定源中存储的值都是字符串。您可以将字符串转换为什么类型?模型绑定器将转换几乎所有基本的.NET类型,例如int、float、decimal(显然还有字符串),以及任何具有TypeConverter的东西。还有一些其他特殊情况可以从字符串转换,例如Type,但将其视为基元会让您有很长的路要走!

6.2.2 绑定复合类型

如果只能够绑定简单的原始类型似乎有点限制,那么你是对的!幸运的是,模型绑定并非如此。虽然它只能将字符串直接转换为那些原始类型,但它也可以通过遍历绑定模型公开的所有属性来绑定复杂类型。

如果这一点不能让您立即感到满意,那么让我们来看看如果简单类型是您唯一的选择,您将如何构建页面处理程序。想象一下,您的货币转换器应用程序的用户已经到达一个结账页面,准备兑换一些货币。太棒了你现在只需要收集他们的姓名、电子邮件和电话号码。不幸的是,页面处理程序方法必须如下所示:

public IActionResult OnPost(string firstName, string lastName, string phoneNumber, string email)

讨厌!四个参数现在看起来可能不那么糟糕,但是当需求发生变化并且需要收集其他细节时会发生什么?方法签名将继续增长。模型绑定器将很好地绑定值,但它不是完全干净的代码。使用[BindProperty]方法也没有什么帮助——你仍然需要用大量的特征和属性来扰乱我们的PageModel!

通过绑定到复杂对象简化方法参数

当您有许多方法参数时,任何C#代码的常见模式都是提取一个封装方法所需数据的类。如果需要添加额外的参数,可以向该类添加新属性。这个类成为您的绑定模型,它可能看起来像这样。

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()

从功能上讲,模型绑定器对这种新的复杂类型有点不同。模型绑定器使用新的UserBindingModel()创建模型的新实例,而不是查找具有与参数名称(用户或属性的user)匹配的值的参数。

注意:您不必为方法使用自定义类;这取决于你的要求。如果页面处理程序只需要一个整数,那么绑定到简单参数更有意义。

接下来,模型绑定器循环遍历绑定模型的所有属性,如清单6.4中的FirstName和LastName。对于这些属性中的每一个,它都会查询绑定源的集合,并尝试查找匹配的名称-值对。如果找到一个,它将设置该属性的值并继续下一个。

提示:虽然在本示例中模型的名称不是必需的,但模型绑定器还将查找以属性名称为前缀的属性,例如名为user的属性的user.FirstName和user.LastName。当页面处理程序有多个复杂参数或多个复杂[BindProperty]属性时,可以使用此方法。一般来说,为了简单起见,您应该尽可能避免这种情况。对于所有模型绑定,前缀的大小写无关紧要。

一旦设置了可以绑定到绑定模型上的所有属性,则将模型传递给页面处理程序(或设置了[BindProperty]属性),并照常执行处理程序。从这一点开始的行为与当您有许多单独的参数时相同,您将在绑定模型上设置相同的值,但代码更干净,更易于使用。

提示:对于要绑定模型的类,它必须具有默认的公共构造函数。只能绑定公共和可设置的属性。

使用此技术,您可以绑定属性本身为复杂模型的复杂分层模型。只要每个属性公开一个可以绑定模型的类型,绑定器就可以轻松地遍历它。

绑定集合和字典

与普通的自定义类和原语一样,您可以绑定到集合、列表和字典。假设您有一个页面,其中用户选择了他们感兴趣的所有货币;您将显示所有选定对象的费率,如图6.5所示。要实现这一点,您可以创建一个页面处理程序,它接受List<string>类型,例如

public void OnPost(List<string> currencies);

图6.5 货币转换器应用程序中的选择列表将向应用程序发送所选货币的列表。模型绑定可以绑定所选货币,并为用户自定义视图,以显示所选货币的等价转换。

然后,您可以通过提供几种不同格式的值将数据POST到该方法:

  • currency[index]——其中currency是要绑定的参数的名称,index是要绑定项目的索引,例如,currency[0]=GBP&currency[1]=USD。
  • [index]——如果您只绑定到一个列表(如本示例中所示),则可以省略参数的名称,例如,[0]=GBP&[1]=USD。
  • currencies——或者,您可以省略索引并将currencies作为每个值的关键字,例如,currencies=GBP&currencies=USD.。

键值可以来自路由值和查询值,但以表单形式对它们进行POST更为常见。字典可以使用类似的绑定,其中在命名参数和省略参数时,字典键都会替换索引。如果这一切看起来有点混乱,不要感到太惊慌。如果您正在构建一个传统的Web应用程序,并使用Razor视图生成HTML,那么框架将负责为您生成正确的名称。正如您将在第8章中看到的,Razor视图将确保您POST的任何表单数据都将以正确的格式生成。

使用IFORMFILE绑定文件上传

许多网站的一个共同特点是能够上传文件。这可能是一个相对罕见的活动,例如用户为其Stack Overflow配置文件上传配置文件图片,或者它可能是应用程序的一部分,例如将照片上传到Facebook。

允许用户将文件上载到应用程序
将文件上传到网站是一项非常常见的活动,但您应该仔细考虑您的应用程序是否需要该功能。每当用户可以上传文件时,道路上就充满了危险。
您应该小心地将传入的文件视为潜在的恶意文件:不要信任提供的文件名,注意上载的大型文件,并且不允许在服务器上执行这些文件。
文件还提出了数据应该存储在数据库、文件系统或其他存储中的问题?这些问题都没有一个直截了当的答案,你应该仔细考虑选择其中一个而不是另一个的含义。更好的是,如果你可以避免,不要让用户上传文件!

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方法读取数据。

如果用户将大文件发布到您的网站,您可能会发现内存或磁盘空间开始耗尽,因为它会缓冲每个文件。在这种情况下,您可能需要直接流式传输文件,以避免一次保存所有数据。不幸的是,与模型绑定方法不同,流式传输大型文件可能很复杂,容易出错,因此不在本书的范围之内。有关详细信息,请参阅Microsoft的“在ASP.NET Core中上载文件”文档,网址为http://mng.bz/SH7X.

提示:不要使用IFormFile接口处理大型文件上载,因为您可能会看到性能问题。请注意,您不能依赖用户不上传大型文件,因此最好还是完全避免文件上传!

对于绝大多数RazorPages来说,简单和复杂类型的模型绑定的默认配置工作得很好,但您可能会发现某些情况下需要更多的控制。幸运的是,这是完全可能的,如果需要,您可以通过替换框架内部使用的ModelBinder来完全覆盖该过程。

然而,很少需要这种级别的定制——我发现,更常见的是指定页面绑定模型使用哪个绑定源。

6.2.3 选择绑定源

正如您已经看到的,默认情况下,ASP.NET Core模型绑定器将尝试从三个不同的绑定源绑定绑定模型:表单数据、路由数据和查询字符串。

有时,您可能会发现需要明确声明要绑定到哪个绑定源。在其他情况下,这三个源根本不够。最常见的场景是当您希望将方法参数绑定到请求头值时,或者当请求的主体包含要绑定到参数的JSON格式数据时。在这些情况下,您可以用说明从何处绑定的属性来修饰绑定模型,如下面的列表所示。

清单6.5 为模型绑定选择绑定源

public class PhotosModel: PageModel
{
    //userId将从请求中的HTTP头绑定。
    //照片对象列表将绑定到请求主体,通常为JSON格式。
    public void OnPost( [FromHeader] string userId, [FromBody] List<Photo> photos)
    {
        /* method implementation */
    }
}

在此示例中,页面处理程序使用用户ID更新照片集合。照片中有要标记的用户ID的方法参数userId,以及要标记的照片对象列表照片。

我没有使用标准绑定源绑定这些方法参数,而是为每个参数添加了属性,指示要使用的绑定源。[FromHeader]属性已应用于userId参数。这告诉模型绑定器将值绑定到名为userId的HTTP请求头值。

我们还使用[FromBody]属性将照片列表绑定到HTTP请求的主体。这将从请求主体读取JSON,并将其绑定到List<Photo>方法参数。

警告:熟悉ASP.NET早期版本的开发人员应注意,在Razor Pages中绑定JSON请求时,显式需要[FromBody]属性。这与以前的ASP.NET行为不同,在ASP.NET行为中不需要属性。

您不限于从请求体绑定JSON数据,也可以使用其他格式,具体取决于您配置框架使用的InputFormatters。默认情况下,只配置JSON输入格式化程序。在第9章讨论WebAPI时,您将看到如何添加XML格式器。

您可以使用几个不同的属性来覆盖默认值,并为每个绑定模型(或绑定模型上的每个属性)指定绑定源:

  • [FromHeader]——绑定到标头值
  • [FromQuery]——绑定到查询字符串值
  • [FromRoute]——绑定到路由参数
  • [FromForm]——绑定到请求正文中发布的表单数据
  • [FromBody]——绑定到请求的正文内容

您可以将其中的每一个应用于任意数量的处理程序方法参数或属性,如清单6.5所示,除了[FromBody]属性之外,只有一个值可以用[FromBody]属性修饰。此外,当表单数据在请求的正文中发送时,[FromBody]和[FromForm]属性实际上是互斥的。

提示:只有一个参数可以使用[FromBody]属性。此属性将使用传入的请求,因为HTTP请求体只能安全地读取一次。

除了这些用于指定绑定源的属性之外,还有一些其他属性用于进一步定制绑定过程:

  • [BindNever]——模型绑定器将完全跳过此参数。
  • [BindRequired]——如果未提供参数或参数为空,绑定程序将添加验证错误。
  • [FromServices]——这用于指示应使用依赖注入提供参数(有关详细信息,请参阅第10章)。

此外,您还有[ModelBinder]属性,该属性使您进入模型绑定的“上帝模式”。使用此属性,您可以指定确切的绑定源,重写要绑定到的参数的名称,并指定要执行的绑定类型。你很少需要这个,但当你需要的时候,至少它就在那里!

通过组合所有这些属性,您应该可以将模型绑定器配置为绑定到页面处理程序想要使用的几乎所有请求数据。但总的来说,你可能会发现你很少需要使用它们;在大多数情况下,默认值应该适用于您。

这就结束了关于模型绑定的本节。如果一切顺利,那么页面处理程序应该可以访问填充的绑定模型,并且可以执行其逻辑。是时候处理请求了,对吧?没什么好担心的?

不要那么快!你怎么知道你收到的数据是有效的?你没有收到试图SQL注入攻击的恶意数据,或是一个充满字母的电话号码?

绑定器相对盲目地分配请求中发送的值,您很乐意将其插入到自己的方法中?如何阻止邪恶的小吉米向您的应用程序发送恶意值?

除了基本的保障措施,没有什么能阻止他,这就是为什么你总是验证输入是重要的。ASP.NET Core提供了一种开箱即用的声明性方式实现这一点的方法,这是本章后半部分的重点。

6.3 通过模型验证处理用户输入

在本节中,我将讨论

  • 什么是验证,为什么需要验证?
  • 使用DataAnnotations属性描述所需的数据
  • 如何在页面处理程序中验证绑定模型

一般来说,验证是一个相当大的主题,您需要在构建的每个应用程序中考虑它。ASP.NET Core使其成为框架的一个组成部分,从而使向应用程序添加验证相对容易。

6.3.1 验证的需要

数据可以来自Web应用程序中的许多不同来源。您可以从文件中加载数据,从数据库中读取数据,或者接受用户在请求中键入表单的值。尽管您可能倾向于相信服务器上已经存在的数据是有效的(尽管这有时是一个危险的假设!),但您绝对不应该相信作为请求一部分发送的数据。

验证发生在模型绑定之后的RazorPages框架中,但在页面处理程序执行之前,如图6.2所示。图6.6展示了模型验证在这个过程中的适用范围,展示了如何绑定和验证请求用户个人详细信息的结账页面请求。

图6.6 验证发生在模型绑定之后但在页面处理程序执行之前。无论验证是否成功,页面处理程序都会执行。

在方法中使用用户提供的数据之前,应始终对其进行验证。你不知道浏览器可能向你发送了什么。“小鲍比桌子”的经典例子(https://xkcd.com/327/)强调需要始终验证用户发送的任何数据。

不过,验证不仅仅是检查安全威胁;还需要检查非恶意错误:

  • 数据格式应正确(电子邮件字段具有有效的电子邮件格式)。
  • 数字可能需要在一个特定的范围内(你不能买-1本书!)。
  • 某些值可能是必需的,但其他值是可选的(配置文件可能需要名称,但电话号码是可选的)。
  • 值必须符合您的业务要求(您不能将货币转换为其本身,需要将其转换为其他货币)。

在浏览器中似乎可以很容易地处理其中的一些问题。例如,如果用户正在选择要转换的货币,不要让他们选择相同的货币;我们都看到了“请输入有效的电子邮件地址”的消息。

不幸的是,尽管这种客户端验证对用户很有用,因为它可以给他们即时反馈,但你永远不能依赖它,因为它总是可以绕过这些浏览器保护。在数据到达Web应用程序时,始终需要使用服务器端验证来验证数据。

警告:始终在应用程序的服务器端验证用户输入。

如果这感觉有点多余,就像你要复制逻辑和代码一样,那么恐怕你是对的。这是Web开发的一个不幸方面;口是心非是必要的罪恶。值得庆幸的是,ASP.NET Core提供了一些功能来减轻这种负担。

提示:Blazor,新的C#SPA框架有望解决其中一些问题。有关详细信息,请参阅 https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor 和《blazor in Action》(曼宁,2021)。

如果你必须为每个应用程序编写新的验证代码,那么这将是乏味的,并且可能容易出错。幸运的是,您可以使用.NETCore和.NET5.0提供的一组属性显著简化验证代码。

6.3.2 使用DataAnnotations属性进行验证

Validation属性,或者更准确地说是DataAnnotations属性,允许您指定绑定模型应该遵守的规则。它们通过描述绑定模型应该包含的数据类型(而不是数据本身)来提供关于模型的元数据。

定义:元数据描述其他数据,指定数据应遵循的规则和特征。

您可以将DataAnnotations属性直接应用于绑定模型,以指示可接受的数据类型。例如,这允许您检查是否提供了必填字段、数字是否在正确范围内以及电子邮件字段是否为有效电子邮件地址。

例如,让我们考虑一下货币转换器应用程序的结账页面。在继续之前,您需要收集有关用户的详细信息,因此您要求他们提供姓名、电子邮件以及电话号码(可选)。下面的列表显示了用表示模型验证规则的验证属性修饰的UserBindingModel。这扩展了您在清单6.4中看到的示例。

set; } [Required] [StringLength(100)] [Display(Name = "Last name")] public string LastName { get; set; } [Required] [EmailAddress] //验证电子邮件的值是否为有效的电子邮件地址 public string Email { get; set; } [Phone] [Display(Name = "Phone number")] public string PhoneNumber { get; set; } }

突然,您的绑定模型包含了大量信息,而以前它在细节上非常稀少。例如,您已指定始终应提供FirstName属性,该属性的最大长度应为100个字符,并且当引用该属性时(例如,在错误消息中),应将其称为“Your name”而不是“FirstName”。

这些属性的优点在于它们清楚地声明了模型的预期状态。通过查看这些属性,您知道属性将包含或应该包含什么。它们还为ASP.NET Core框架提供桥梁,以验证模型绑定期间模型上的数据集是否有效,如稍后所见。

在将DataAnnotations应用于模型时,您有大量属性可供选择。我在这里列出了一些常见的,但您可以在System.ComponentModel.DataAnnotations命名空间中找到更多。要获得更完整的列表,我建议在Visual Studio/Visual Studio代码中使用IntelliSense,或者您可以直接在GitHub上查看源代码(http://mng.bz/5jxZ).

  • [CreditCard]-验证属性是否具有有效的信用卡格式。
  • [EmailAddress]-验证属性是否具有有效的电子邮件地址格式。
  • [StringLength(max)]-验证字符串最多包含最多个字符。
  • [MinLength(min)]-验证集合至少具有最小数量的项。
  • [Phone]-验证属性是否具有有效的电话号码格式。
  • [Range(min,max)]-验证属性的值是否介于min和max之间。
  • [RegularExpression(正则表达式)]-验证属性是否符合正则表达式模式。
  • [Url]-验证属性是否具有有效的Url格式。
  • [Required]-表示属性不能为空。
  • [Compare]-允许您确认两个属性具有相同的值(例如,电子邮件和确认电子邮件)。 

警告:[EmailAddress]和其他属性一样,仅验证值的格式是否正确。他们不会验证电子邮件地址是否存在。

DataAnnotations属性并不是一项新功能,自3.5版以来,它们一直是.NET Framework的一部分,在ASP.NET Core中的使用与以前版本的ASP.NET几乎相同。

除了验证之外,它们还用于其他目的。实体框架核心(以及其他)使用DataAnnotations来定义从C#类创建数据库表时要使用的列类型和规则。您可以在第12章和Jon P.Smith的《Entity Framework Core in Action》第二版(Manning,2021)中阅读更多关于实体框架核心的内容。

如果现成提供的DataAnnotation属性没有涵盖您所需的所有内容,也可以通过从基本ValidationAttribute派生来编写自定义属性。您将在第19章中看到如何为货币转换器应用程序创建自定义属性。

或者,如果您不是基于属性的方法的爱好者,ASP.NET Core足够灵活,您可以使用更喜爱的技术完全替换验证基础结构。例如,如果您愿意,您可以使用流行的FluentValidation库(https://github.com/JeremySkinner/FluentValidation)而不是DataAnnotations属性。你将在第20章中看到如何做到这一点。

提示:DataAnnotations适合单独验证属性的输入,但不适合验证业务规则。您很可能需要在DataAnnotations框架之外执行此验证。

无论您使用哪种验证方法,重要的是要记住这些技术本身并不能完全保护您的应用程序。RazorPages框架将确保验证发生,但如果验证失败,它不会自动执行任何操作。在下一节中,我们将讨论如何检查服务器上的验证结果,并处理验证失败的情况。

6.3.3 在服务器上验证安全性

绑定模型的验证发生在页面处理程序执行之前,但请注意,无论验证是否失败,处理程序始终执行。页面处理程序负责检查验证结果。

注意:验证会自动发生,但处理验证失败是页面处理程序的责任。

Razor Pages框架将验证结果的输出存储在PageModel上名为ModelState的属性中。此属性是一个ModelStateDictionary对象,它包含模型绑定后发生的所有验证错误的列表,以及用于处理它的一些实用程序属性。

例如,下面的列表显示了Check-out.cshtml RazorPage的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();    //验证失败,因此重新显示有错误的表单并尽早完成该方法。
        }
 
        /* 保存到数据库,更新用户,返回成功 */
 
        return RedirectToPage("Success");    //验证通过,因此使用模型中提供的数据是安全的。
    }
}

如果ModelState属性指示发生错误,则该方法立即调用Page助手方法。这将返回一个PageResult,它将最终生成返回给用户的HTML,如第4章所示。该视图使用Input属性中提供的(无效)值在显示表单时重新填充表单,如图6.7所示。此外,还会使用ModelState属性中的验证错误自动添加对用户有用的消息。

注意:表单上显示的错误消息是每个验证属性的默认值。您可以通过在任何验证属性上设置ErrorMessage属性来自定义消息。例如,您可以使用[Required(ErrorMessage="Required")]自定义[Required]属性。

如果请求成功,页面处理程序将返回RedirectToPageResult(使用RedirectToPage()帮助器方法),将用户重定向到Success.cshtml Razor页面。这种在成功POST后返回重定向响应的模式称为POST-DIRECT-GET模式。

图6.7 当验证失败时,您可以重新显示表单以向用户显示ModelState验证错误。请注意,与其他字段不同,“您的姓名”字段没有关联的验证错误。

POST-RDIRECT-GET
POST-RDIRECT-GET设计模式是一种Web开发模式,可防止用户多次意外提交同一表单。用户通常使用标准浏览器POST机制提交表单,向服务器发送数据。例如,这是正常的提交方式。
如果服务器采用幼稚的方法,并以200 OK响应和一些HTML显示,则用户仍将使用相同的URL。如果用户随后刷新浏览器,他们将向服务器进行额外的POST,可能会再次提交!浏览器有一些机制可以避免这种情况,如下图所示,但用户体验并不理想。

POST后刷新浏览器窗口会向用户显示警告消息。

POST-DIRECT-GET模式表示,为了响应成功的POST,您应该向新URL返回REDIRECT响应,然后浏览器将对新URL进行GET。如果用户现在刷新浏览器,他们将刷新对新URL的最后GET调用。没有额外的POST,因此不会发生额外的付款或副作用。
使用清单6.7所示的模式,在ASP.NET Core MVC应用程序中很容易实现这种模式。通过在成功POST后返回RedirectToPageResult,如果用户在浏览器中刷新页面,您的应用程序将是安全的。

您可能会想,为什么ASPNETCore不会自动为您处理无效的请求——如果验证失败,并且您得到了结果,为什么页面处理程序会被执行?是否存在忘记检查验证结果的风险?

这是正确的,在某些情况下,最好的做法是自动生成验证检查和响应。事实上,这正是我们在第9章中讨论WebAPI时将使用的方法。

然而,对于RazorPages应用程序,即使验证失败,您通常仍希望生成HTML响应。这允许用户看到问题并可能纠正它。这很难实现自动化。

例如,您可能会发现在重新显示RazorPage之前需要加载其他数据,例如加载可用货币列表。使用IsValid模式,这变得更简单、更明确。尝试自动完成这项工作可能会导致您与边缘案例和变通方法作斗争。

此外,通过在页面处理程序中显式包含IsValid检查,可以更容易地控制其他验证检查失败时发生的情况。例如,如果用户尝试更新产品,DataAnnotations验证将不知道具有所请求ID的产品是否存在,只知道ID是否具有正确的格式。通过将验证转移到处理程序方法,可以以相同的方式处理数据和业务规则验证失败。

我希望我已经明白了在ASP.NET Core中验证用户输入是多么重要,但以防万一:validate!好了,我们很好。尽管如此,仅在服务器上执行验证可能会给用户留下稍差的体验。你有多少次在网上填写表格,提交表格,去吃零食,回来发现你打错了什么,不得不重做。如果能立即得到反馈,不是更好吗?

6.3.4 在客户端验证用户体验

您可以通过几种不同的方式向应用程序添加客户端验证。HTML5有许多浏览器都会使用的几种内置验证行为。如果您在页面上显示电子邮件地址字段并使用“email”HTML输入类型,浏览器将自动阻止您提交无效格式,如图6.8所示。

图6.8 默认情况下,现代浏览器将在提交表单之前自动验证电子邮件类型的字段。

您的应用程序不控制此验证;它内置于现代HTML5浏览器中。另一种方法是通过在页面上运行Java-Script并在提交表单之前检查用户输入的值来执行客户端验证。这是RazorPages中最常用的方法。

在下一章中,我将详细介绍如何生成客户端验证助手,在这里,您将再次看到DataAnnotations属性。通过用这些属性装饰视图模型,您可以为Razor引擎提供必要的元数据,使其生成适当的HTML。

使用这种方法,即使在请求发送到服务器之前,用户也会立即看到表单中的任何错误,如图6.9所示。这提供了更短的反馈周期,提供了更好的用户体验。

如果您正在构建SPA,则客户端框架有责任在将数据发布到Web API之前验证客户端的数据。当数据到达服务器时,Web API仍将验证数据,但客户端框架负责提供流畅的用户体验。

图6.9 对于客户端验证,单击提交将触发验证,在将请求发送到服务器之前,验证将显示在浏览器中。如右窗格所示,不发送请求。

当您使用RazorPages生成HTML时,您可以免费获得大部分验证。它自动为大多数内置属性配置客户端验证,而不需要额外的工作,如第7章所示。不幸的是,如果您使用了自定义ValidationAttributes,则默认情况下这些属性只在服务器上运行;您需要对属性进行一些额外的连接,以使其在客户端也能工作。尽管如此,自定义验证属性对于处理应用程序中的常见验证场景非常有用,如第20章所示。

ASP.NET Core中的模型绑定框架为您提供了许多关于如何组织RazorPages的选项:页面处理程序参数或PageModel属性;一个结合模型或多个结合模型;用于定义绑定模型类的位置的选项。在下一节中,我就如何组织RazorPages提供了一些建议。

6.4 在RazorPages中组织绑定模型

在本节中,我就如何在RazorPages中配置绑定模型提供一些一般性建议。如果您遵循本节中的模式,您的RazorPages将遵循一致的布局,使其他人更容易理解应用程序中每个RazorPage的工作原理。

注意:这个建议只是个人喜好,所以如果你有不同意的地方,可以随意调整。重要的是要理解我为什么提出每一个建议,并接受这些建议。在适当的情况下,我也会偏离这些指南!

ASP.NET Core中的模型绑定有很多等效的方法,因此没有“正确”的方法。下面的列表显示了我如何设计简单RazorPage的示例。此RazorPage显示具有给定ID的产品的表单,并允许您使用POST请求编辑详细信息。这是一个比我们目前所看到的要长得多的样本,但我强调了以下要点。

清单6.8 设计编辑产品RazorPage

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);    //从应用程序模型加载产品详细信息。
 
        //根据现有产品的详细信息构建一个InputModel实例,以便在表单中进行编辑。
        Input = new InputModel
        {
            Name = product.ProductName, 
            Price = product.SellPrice
        };
        return Page();
    }
 
     public IActionResult OnPost(int id)    //id参数是从OnGet和OnPost处理程序的路由模板中绑定的模型。
    {
        //如果请求无效,请在不保存的情况下重新显示表单。
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _productService.UpdateProduct(id, Input.Name, Input.Price);    //使用ProductService更新应用程序模型中的产品。
        return RedirectToPage("Index");    //使用POST-DIRECT-GET模式重定向至新页面。
    }

    //将InputModel定义为RazorPage中的嵌套类。 
    public class InputModel
    {
        [Required]
        public string Name { get; set; }

        [Range(0, int.MaxValue)]
        public decimal Price { get; set; }
    }
}

本页显示了典型的“编辑表单”的PageModel。这些在许多业务应用程序中非常常见,RazorPages非常适合这种情况。您将在第8章中看到如何创建表单的HTML端。

注意:本示例的目的仅在于强调模型绑定方法。从逻辑角度来看,代码过于简单化。例如,它不检查具有所提供ID的产品是否存在,也不包括任何错误处理。

此表单显示了我在构建RazorPages时尝试遵循的与模型绑定相关的几个模式:

  • 仅使用[BindProperty]绑定单个属性。一般来说,我喜欢用[BindProperty]修饰单个属性来进行模型绑定。当需要绑定多个值时,我创建一个单独的类InputModel来保存这些值,然后用[BindProperty]来修饰这个属性。像这样装饰一个属性会让你很难忘记添加属性,这意味着所有RazorPages都使用相同的模式。
  • 将绑定模型定义为嵌套类。我将InputModel定义为RazorPage中的嵌套类。绑定模型通常高度特定于单个页面,因此这样做可以将您正在处理的所有内容保持在一起。此外,我通常对所有页面使用完全相同的类名InputModel。同样,这会为RazorPages增加一致性。
  • 不要使用[BindProperties]。除了[BindProperty]属性之外,还有一个[BindProperties]属性(注意拼写不同)可以直接应用于RazorPage PageModel。这将导致模型中的所有属性都绑定到模型,如果不小心,可能会导致过度发布攻击。我建议您不要使用[BindProperties]属性,而是坚持使用[BindProperty]绑定单个属性。
  • 接受页面处理程序中的路由参数。对于简单的路由参数,例如清单6.8中传递给OnGet和OnPost处理程序的id,我向页面处理程序方法本身添加了参数。这避免了GET请求的笨拙SupportsGet=true语法。
  • 始终在使用数据之前进行验证。我以前说过,所以我会再说一遍。验证用户输入!

RazorPages中的模型绑定研究到此结束。您看到了ASP.NET Core框架如何使用模型绑定来简化从请求中提取值并将其转换为可以快速使用的普通.NET对象的过程。本章最重要的方面是对验证的关注,这是所有Web应用程序的共同关注点,使用DataAnnotations可以方便地将验证添加到模型中。

在下一章中,我们将通过学习如何创建视图来继续我们的RazorPages之旅。特别是,您将学习如何使用Razor模板引擎响应请求生成HTML。

总结

  • RazorPages使用三个不同的模型,每个模型负责请求的不同方面。绑定模型封装作为请求一部分发送的数据。应用程序模型表示应用程序的状态。PageModel是RazorPage的支持类,它公开Razor视图用于生成响应的数据。
  • 模型绑定从请求中提取值,并使用它们创建页面处理程序在执行时可以使用的.NET对象。
  • PageModel上标记有[BindProperty]属性的任何属性以及页面处理程序的方法参数都将参与模型绑定。
  • 用[BindProperty]修饰的属性不绑定GET请求。要绑定GET请求,必须改用[BindProperty(SupportsGet=true)]。
  • 默认情况下,有三个绑定源:POSTed表单值、路由值和查询字符串。当试图绑定绑定模型时,绑定器将按顺序询问这些。
  • 将值绑定到模型时,参数和属性的名称不区分大小写。
  • 您可以绑定到简单类型或复杂类型的属性。
  • 要绑定复杂类型,它们必须具有默认构造函数和可设置的公共属性。
  • 简单类型必须转换为字符串才能自动绑定;例如,数字、日期和布尔值。
  • 可以分别使用[index]=value和[key]=value语法绑定集合和字典。
  • 您可以使用应用于方法的[From*]属性(例如[FromHeader]和[FromBody])自定义绑定模型的绑定源。这些可用于绑定到非默认绑定源,例如标头或JSON正文内容。
  • 与以前版本的ASP.NET不同,绑定JSON属性时需要[FromBody]属性(以前不需要)。
  • 验证对于检查安全威胁是必要的。检查数据格式是否正确,并确认其符合预期值,并且符合您的业务规则。
  • ASP.NET Core提供DataAnnotations属性,允许您以声明方式定义期望的值。
  • 验证在模型绑定后自动发生,但您必须手动检查验证结果,并通过询问ModelState属性在页面处理程序中相应操作。
  • 客户端验证比单独使用服务器端验证提供了更好的用户体验,但您应该始终使用服务器端的验证。
  • 客户端验证使用应用于HTML元素的JavaScript和属性来验证表单值。