• 向 Movie 模型添加了验证逻辑。
  • 确保每当用户创建或编辑电影时,都会强制执行验证规则。

坚持 DRY 原则

MVC 的设计原则之一是 DRY(“不要自我重复”)。 ASP.NET Core MVC 支持你仅指定一次功能或行为,然后使它应用到整个应用中。 这可以减少所需编写的代码量,并使编写的代码更少出错,更易于测试和维护。

MVC 和 Entity Framework Core Code First 提供的验证支持是 DRY 原则在实际操作中的极佳示例。 可以在一个位置(模型类中)以声明方式指定验证规则,并且在应用中的所有位置强制执行。

将验证规则添加到电影模型

DataAnnotations 命名空间提供一组内置验证特性,可通过声明方式应用于类或属性。 DataAnnotations 还包含 DataType 等格式特性,有助于格式设置但不提供任何验证。

更新 Movie 类以使用内置的 RequiredStringLengthRegularExpression 和 Range 验证特性。

复制
 
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string? Title { get; set; }

        [Display(Name = "Release Date")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string? Genre { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string? Rating { get; set; }
    }
}

验证特性指定要对应用这些特性的模型属性强制执行的行为:

  • Required 和 MinimumLength 特性表示属性必须有值;但用户可输入空格来满足此验证。

  • RegularExpression 特性用于限制可输入的字符。 在上述代码中,即“Genre”(分类):

    • 只能使用字母。
    • 第一个字母必须为大写。 允许使用空格,但不允许使用数字和特殊字符。
  • RegularExpression“Rating”(分级):

    • 要求第一个字符为大写字母。
    • 允许在后续空格中使用特殊字符和数字。 “PG-13”对“分级”有效,但对于“分类”无效。
  • Range 特性将值限制在指定范围内。

  • StringLength 特性使你能够设置字符串属性的最大长度,以及可选的最小长度。

  • 从本质上来说,需要值类型(如 decimalintfloatDateTime),但不需要 [Required] 特性。

让 ASP.NET Core 强制自动执行验证规则有助于提升你的应用的可靠性。 同时它能确保你无法忘记验证某些内容,并防止你无意中将错误数据导入数据库。

验证错误 UI

运行应用并导航到电影控制器。

选择“新建”链接以添加新电影。 使用无效值填写表单。 当 jQuery 客户端验证检测到错误时,会显示一条错误消息。

添加验证

 备注

可能无法在小数字段中输入十进制逗号。 若要使 jQuery 验证支持使用逗号(“,”)表示小数点的的非英语区域设置,以及支持非美国英语日期格式,必须执行使应用全球化的步骤。 有关添加十进制逗号的说明,请参阅 GitHub 问题 4076

请注意表单如何自动呈现每个包含无效值的字段中相应的验证错误消息。 客户端(使用 JavaScript 和 jQuery)和服务器端(若用户禁用 JavaScript)都必定会遇到这些错误。

明显的好处在于不需要在 MoviesController 类或 Create.cshtml 视图中更改单个代码行来启用此验证 UI。 在本教程前面创建的控制器和视图会自动选取验证规则,这些规则是通过在 Movie 模型类的属性上使用验证特性所指定的。 使用 Edit 操作方法测试验证后,即已应用相同的验证。

存在客户端验证错误时,不会将表单数据发送到服务器。 可通过使用 Fiddler 工具或 F12 开发人员工具在 HTTP Post 方法中设置断点来对此进行验证。

验证工作原理

你可能想知道在不对控制器或视图中的代码进行任何更新的情况下,验证 UI 是如何生成的。 下列代码显示两种 Create 方法。

复制
 
// GET: Movies/Create
public IActionResult Create()
{
    return View();
}

// POST: Movies/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (ModelState.IsValid)
    {
        _context.Add(movie);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}

第一个 (HTTP GET) Create 操作方法显示初始的“创建”表单。 第二个 ([HttpPost]) 版本处理表单发布。 第二个 Create 方法([HttpPost] 版本)调用 ModelState.IsValid 以检查电影是否有任何验证错误。 调用此方法将评估已应用于对象的任何验证特性。 如果对象有验证错误,则 Create 方法会重新显示此表单。 如果没有错误,此方法则将新电影保存在数据库中。 在我们的电影示例中,在检测到客户端上存在验证错误时,表单不会发布到服务器。当存在客户端验证错误时,第二个 Create 方法永远不会被调用。 如果在浏览器中禁用 JavaScript,客户端验证将被禁用,而你可以测试 HTTP POST Create 方法 ModelState.IsValid 检测任何验证错误。

可以在 [HttpPost] Create 方法中设置断点,并验证方法从未被调用,客户端验证在检测到存在验证错误时不会提交表单数据。 如果在浏览器中禁用 JavaScript,然后提交错误的表单,将触发断点。 在没有 JavaScript 的情况下仍然可以进行完整的验证。

以下图片显示如何在 FireFox 浏览器中禁用 JavaScript。

添加验证

以下图片显示如何在 Chrome 浏览器中禁用 JavaScript。

添加验证

禁用 JavaScript 后,发布无效数据并单步执行调试程序。

添加验证

Create.cshtml 视图模板的一部分在以下标记中显示:

复制
 
<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>

            @*Markup removed for brevity.*@

操作方法使用上述标记来显示初始表单,并在发生错误时重新显示此表单。

输入标记帮助程序使用 DataAnnotations 特性,并在客户端上生成 jQuery 验证所需的 HTML 特性。 验证标记帮助程序用于显示验证错误。 有关详细信息,请参阅验证

此方法真正好的一点是:无论是控制器还是 Create 视图模板都不知道强制实施的实际验证规则或显示的特定错误消息。 仅可在 Movie 类中指定验证规则和错误字符串。 这些相同的验证规则自动应用于 Edit 视图和可能创建用于编辑模型的任何其他视图模板。

需要更改验证逻辑时,可以通过将验证特性添加到模型在同一个位置实现此操作。(在此示例中为 Movie 类)。 无需担心对应用程序的不同部分所强制执行规则的方式不一致 - 所有验证逻辑都将定义在一个位置并用于整个应用程序。 这使代码非常简洁,并且更易于维护和改进。 这意味着对 DRY 原则的完全遵守。

使用 DataType 特性

打开 Movie.cs 文件并检查 Movie 类。 除了一组内置的验证特性,System.ComponentModel.DataAnnotations 命名空间还提供格式特性。 我们已经在发布日期和价格字段中应用了 DataType 枚举值。 以下代码显示具有适当 DataType 特性的 ReleaseDate 和 Price 属性。

复制
 
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

DataType 属性仅提供相关提示来帮助视图引擎设置数据格式(并提供元素/属性,例如向 URL 提供 <a> 和向电子邮件提供 <a href="mailto:EmailAddress.com">)。 可以使用 RegularExpression 特性验证数据的格式。 DataType 属性用于指定比数据库内部类型更具体的数据类型,它们不是验证属性。 在此示例中,我们只想跟踪日期,而不是时间。 DataType 枚举提供了多种数据类型,例如日期、时间、电话号码、货币、电子邮件地址等。 应用程序还可通过 DataType 特性自动提供类型特定的功能。 例如,可以为 DataType.EmailAddress 创建 mailto: 链接,并且可以在支持 HTML5 的浏览器中为 DataType.Date 提供日期选择器。 DataType 特性发出 HTML 5 data-(读作 data dash)特性供 HTML 5 浏览器理解。 DataType 特性不提供任何验证。

DataType.Date 不指定显示日期的格式。 默认情况下,数据字段根据基于服务器的 CultureInfo 的默认格式进行显示。

DisplayFormat 特性用于显式指定日期格式:

复制
 
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }

ApplyFormatInEditMode 设置指定在文本框中显示值以进行编辑时也应用格式。 (你可能不想为某些字段执行此操作 — 例如对于货币值,你可能不希望文本框中的货币符号可编辑。)

可以单独使用 DisplayFormat 特性,但通常建议使用 DataType 特性。 DataType 特性传达数据的语义而不是传达如何在屏幕上呈现数据,并提供 DisplayFormat 不具备的以下优势:

  • 浏览器可启用 HTML5 功能(例如显示日历控件、区域设置适用的货币符号、电子邮件链接等)

  • 默认情况下,浏览器将根据区域设置采用正确的格式呈现数据。

  • DataType 特性使 MVC 能够选择正确的字段模板来呈现数据(如果 DisplayFormat 由自身使用,则使用的是字符串模板)。

 备注

jQuery 验证不适用于 Range 属性和 DateTime。 例如,以下代码将始终显示客户端验证错误,即便日期在指定的范围内:

[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]

需要禁用 jQuery 日期验证才能使用具有 DateTime 的 Range 特性。 通常,在模型中编译固定日期是不恰当的,因此不推荐使用 Range 特性和 DateTime

以下代码显示组合在一行上的特性:

复制
 
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [StringLength(60, MinimumLength = 3)]
        public string Title { get; set; }

        [Display(Name = "Release Date"), DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
        public string Genre { get; set; }

        [Range(1, 100), DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }

        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
        public string Rating { get; set; }
    }
}


打开电影控制器,并检查 Details 方法:

复制
 
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

创建此操作方法的 MVC 基架引擎添加显示调用方法的 HTTP 请求的注释。 在此情况下,它是包含三个 URL 段的 GET 请求,这三个段为 Movies 控制器、Details 方法和 id 值。 回顾这些在 Program.cs 中定义的段。

复制
 
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

EF 可以使用 FirstOrDefaultAsync 方法轻松搜索数据。 该方法中内置的一个重要安全功能是,代码会先验证搜索方法已经找到电影,然后再执行操作。 例如,一个黑客可能通过将链接创建的 URL 从 http://localhost:{PORT}/Movies/Details/1 更改为类似 http://localhost:{PORT}/Movies/Details/12345 的值(或者不代表任何实际电影的其他值)将错误引入站点。 如果未检查是否有空电影,则应用可能引发异常。

检查 Delete 和 DeleteConfirmed 方法。

复制
 
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    var movie = await _context.Movie.FindAsync(id);
    _context.Movie.Remove(movie);
    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}

请注意,HTTP GET Delete 方法不删除指定的电影,而是返回可在其中提交 (HttpPost) 删除的电影视图。 执行删除操作以响应 GET 请求(或者说,执行编辑操作、创建操作或更改数据的任何其他操作)会打开安全漏洞。

删除数据的 [HttpPost] 方法命名为 DeleteConfirmed,以便为 HTTP POST 方法提供一个唯一的签名或名称。 下面显示了两个方法签名:

复制
 
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
复制
 
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{

公共语言运行时 (CLR) 需要重载方法拥有唯一的参数签名(相同的方法名称但不同的参数列表)。 但是,这里需要两个 Delete 方法 -- 一个用于 GET,另一个用于 POST -- 这两个方法拥有相同的参数签名。 (它们都需要接受单个整数作为参数。)

可通过两种方法解决此问题,一种是为方法提供不同的名称。 这正是前面的示例中的基架机制进行的操作。 但是,这会造成一个小问题:ASP.NET 按名称将 URL 段映射到操作方法,如果重命名方法,则路由通常无法找到该方法。 该示例中也提供了解决方案,即向 DeleteConfirmed 方法添加 ActionName("Delete") 属性。 该属性对路由系统执行映射,以便包括 POST 请求的 /Delete/ 的 URL可找到 DeleteConfirmed 方法。

对于名称和签名相同的方法,另一个常用解决方法是手动更改 POST 方法的签名以包括额外(未使用)的参数。 这正是前面的文章中添加 notUsed 参数时进行的操作。 这里为了 [HttpPost] Delete 方法可以执行同样的操作:

复制
 
// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)

相关文章: