第 4 部分,将模型添加到 ASP.NET Core MVC 应用

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

作者:Rick AndersonJon P Smith

在本教程中,添加了用于管理数据库中的电影的类。 这些类是 MVC 应用的“Model”部分。

这些模型类与 Entity Framework Core (EF Core) 一起使用来处理数据库。 EF Core 是一个对象关系映射 (ORM) 框架,可以简化需要编写的数据访问代码。

创建的模型类称为 POCO 类,它的全称为“Plain Old CLR Object(普通旧 CLR 对象)”。 POCO 类对 EF Core 没有任何依赖性。 它们只定义数据库中存储的数据属性。

在本教程中,首先要创建模型类,然后 EF Core 将创建数据库。

添加数据模型类

右键单击 Models 文件夹 >“添加”>“类”。 命名文件 Movie.cs

使用以下代码更新 Models/Movie.cs 文件:

using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}

Movie 类包含一个 Id 字段,数据需要该字段作为主键。

ReleaseDate 上的 DataType 特性指定了数据的类型 (Date)。 通过此特性:

  • 用户无需在日期字段中输入时间信息。
  • 仅显示日期,而非时间信息。

DataAnnotations 会在后续教程中介绍。

string 后的问号表示属性可为空。 有关详细信息,请参阅可为空引用类型

添加 NuGet 包

Visual Studio 会自动安装所需的包。

生成项目以检查编译器错误。

基架电影页面

使用基架工具为电影模型生成 CreateReadUpdateDelete (CRUD) 页面。

在“解决方案资源管理器”中,右键单击 Controllers 文件夹,并选择“添加”>“新搭建基架的项目”。

上述步骤的视图

在“添加新基架项”对话框中:

  • 在左窗格中,选择“已安装”>“常用”>“MVC”。
  • 选择“视图使用实体框架的 MVC 控制器”。
  • 选择 添加

“添加基架”对话框

完成“使用实体框架添加包含视图的 MVC 控制器”对话框中的内容:

  • 在“模型类”下拉列表中,选择“Movie (MvcMovie.Models)”。
  • 在“数据上下文类”行中,选择 +(加号)。
    • 在“添加数据上下文”对话框中,将生成类名 MvcMovie.Data.MvcMovieContext。
    • 选择 添加
  • 在“数据库提供程序”下拉列表中,选择“SQL Server”。
  • “视图”和“控制器名称”:保留默认值。
  • 选择 添加

添加数据上下文保留默认值

如果收到错误消息,请再次选择“添加”以重试。

基架添加以下包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.VisualStudio.Web.CodeGeneration.Design

基架创建以下内容:

  • 电影控制器:Controllers/MoviesController.cs
  • “创建”、“删除”、“详细信息”、“编辑”和“索引”页面的 Razor 视图文件:Views/Movies/*.cshtml
  • 数据库上下文类:Data/MvcMovieContext.cs

基架更新以下内容:

  • MvcMovie.csproj 项目文件中插入所需的包引用。
  • Program.cs 文件中注册数据库上下文。
  • 将数据库连接字符串添加到 appsettings.json 文件。

自动创建这些文件和文件更新被称为“基架”。

尚且不能使用基架页面,因为该数据库不存在。 运行应用并选择“Movie App”链接会导致“无法打开数据库”或“无此类表: Movie”错误消息。

生成应用以验证没有任何错误。

初始迁移

使用 EF Core迁移功能来创建数据库。 迁移是可用于创建和更新数据库以匹配数据模型的一组工具。

从“工具”菜单中,选择“NuGet 程序包管理器”>“包管理器控制台”

在包管理器控制台 (PMC) 中输入以下命令:

Add-Migration InitialCreate

  • Add-Migration InitialCreate:生成 Migrations/{timestamp}_InitialCreate.cs 迁移文件。 InitialCreate 参数是迁移名称。 可以使用任何名称,但是按照惯例,会选择可说明迁移的名称。 因为这是首次迁移,所以生成的类包含用于创建数据库架构的代码。 数据库架构基于在 MvcMovieContext 类中指定的模型。

将显示以下警告,该警告将在后面的步骤中解决:

未对实体类型“Movie”上的十进制属性“Price”指定任何存储类型。 This will cause values to be silently truncated if they do not fit in the default precision and scale. 使用“HasColumnType”显式指定可容纳“OnModelCreating”中所有值的 SQL Server 列类型,使用“HasPrecision”指定精度和小数位数,或者使用“HasConversion”配置值转换器。

在 PMC 中,输入以下命令:

Update-Database

  • Update-Database:将数据库更新到上一个命令创建的最新迁移。 此命令在用于创建数据库的 Migrations/{time-stamp}_InitialCreate.cs 文件中运行 Up 方法。

有关 EF Core 的 PMC 工具的详细信息,请参阅 EF Core 工具引用 - Visual Studio 中的 PMC

测试应用

运行应用并选择“Movie App”链接。

如果收到类似于以下内容的异常,可能是因为遗漏了迁移步骤中的 Update-Database 命令:

SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.

注意

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

检查生成的数据库上下文类和注册

对于 EF Core,使用模型执行数据访问。 模型由实体类和表示数据库会话的上下文对象构成。 上下文对象允许查询并保存数据。 数据库上下文派生自 Microsoft.EntityFrameworkCore.DbContext 并指定要包含在数据模型中的实体。

基架创建 Data/MvcMovieContext.cs 数据库上下文类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; } = default!;
    }
}

前面的代码创建一个 DbSet<Movie> 属性,该属性表示数据库中的电影。

依赖项注入

ASP.NET Core 通过依赖关系注入 (DI) 生成。 服务(如数据库上下文)在 Program.cs 中向 DI 注册。 这些服务通过构造函数参数提供给需要它们的组件。

Controllers/MoviesController.cs 文件中,构造函数使用依赖关系注入MvcMovieContext 数据库上下文注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

基架在 Program.cs 中生成了以下突出显示的代码:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));

ASP.NET Core 配置系统读取“MvcMovieContext”数据库连接字符串。

检查生成的数据库连接字符串

基架向 appsettings.json 文件添加了一个连接字符串:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-4ebefa10-de29-4dea-b2ad-8a8dc6bcf374;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

进行本地开发时,ASP.NET Core 配置系统appsettings.json 文件中读取 ConnectionString 键。

InitialCreate

检查 Migrations/{timestamp}_InitialCreate.cs 迁移文件:

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    /// <inheritdoc />
    public partial class InitialCreate : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}

在上述代码中:

  • InitialCreate.Up 创建 Movie 表,并将 Id 配置为主键。
  • InitialCreate.Down 还原 Up 迁移所做的架构更改。

控制器中的依赖项注入

打开 Controllers/MoviesController.cs 文件并检查构造函数:

public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }

构造函数使用依赖关系注入将数据库上下文 (MvcMovieContext) 注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

测试“创建”页。 输入并提交数据。

测试“编辑”、“详细信息”和“删除”页 。

强类型模型和 @model 指令

在本教程之前的内容中,已经介绍了控制器如何使用 ViewData 字典将数据或对象传递给视图。 ViewData 字典是一个动态对象,提供了将信息传递给视图的方便的后期绑定方法。

MVC 提供将强类型模型对象传递给视图的功能。 此强类型方法启用编译时代码检查。 基架机制在 MoviesController 类和视图中传递了一个强类型模型。

检查 Controllers/MoviesController.cs 文件中生成的 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);
}

id 参数通常作为路由数据传递。 例如,https://localhost:5001/movies/details/1 的设置如下:

  • 控制器被设置为 movies 控制器(第一个 URL 段)。
  • 操作被设置为 details(第二个 URL 段)。
  • id 被设置为 1(最后一个 URL 段)。

id 可以通过查询字符串传入,如以下示例所示:

https://localhost:5001/movies/details?id=1

在未提供 id 值的情况下,id 参数可定义为可以为 null 的类型 (int?)。

Lambda 表达式会被传入 FirstOrDefaultAsync 方法以选择与路由数据或查询字符串值相匹配的电影实体。

var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);

如果找到了电影,Movie 模型的实例则会被传递到 Details 视图:

return View(movie);

检查 Views/Movies/Details.cshtml 文件的内容:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

视图文件顶部的 @model 语句可指定视图期望的对象类型。 创建影片控制器时,将包含以下 @model 语句:

@model MvcMovie.Models.Movie

@model 指令允许访问控制器传递给视图的影片。 Model 对象为强类型对象。 例如,在 Details.cshtml 视图中,代码通过强类型的 Model 对象将每个电影字段传递给 DisplayNameForDisplayFor HTML 帮助程序。 CreateEdit 方法以及视图也传递一个 Movie 模型对象。

检查电影控制器中的 Index.cshtml 视图和 Index 方法。 请注意代码在调用 View 方法时是如何创建 List 对象的。 代码将此 Movies 列表从 Index 操作方法传递给视图:

// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

如果数据上下文的 Movie 属性为 null,则代码返回问题详细信息

创建电影控制器后,基架将以下 @model 语句包含在 Index.cshtml 文件的顶部:

@model IEnumerable<MvcMovie.Models.Movie>

@model 指令允许使用强类型的 Model 对象访问控制器传递给视图的电影列表。 例如,在 Index.cshtml 视图中,代码使用 foreach 语句通过强类型 Model 对象对电影进行循环遍历:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

因为 Model 对象为强类型(作为 IEnumerable<Movie> 对象),因此循环中的每个项都被类型化为 Movie。 编译器还有一些其他优势,比如验证代码中使用的类型。

其他资源

在本教程中,添加了用于管理数据库中的电影的类。 这些类是 MVC 应用的“Model”部分。

这些模型类与 Entity Framework Core (EF Core) 一起使用来处理数据库。 EF Core 是一个对象关系映射 (ORM) 框架,可以简化需要编写的数据访问代码。

创建的模型类称为 POCO 类,它的全称为“Plain Old CLR Object(普通旧 CLR 对象)”。 POCO 类对 EF Core 没有任何依赖性。 它们只定义数据库中存储的数据属性。

在本教程中,首先要创建模型类,然后 EF Core 将创建数据库。

添加数据模型类

右键单击 Models 文件夹 >“添加”>“类”。 命名文件 Movie.cs

使用以下代码更新 Models/Movie.cs 文件:

using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}

Movie 类包含一个 Id 字段,数据需要该字段作为主键。

ReleaseDate 上的 DataType 特性指定了数据的类型 (Date)。 通过此特性:

  • 用户无需在日期字段中输入时间信息。
  • 仅显示日期,而非时间信息。

DataAnnotations 会在后续教程中介绍。

string 后的问号表示属性可为空。 有关详细信息,请参阅可为空引用类型

添加 NuGet 包

Visual Studio 会自动安装所需的包。

生成项目以检查编译器错误。

基架电影页面

使用基架工具为电影模型生成 CreateReadUpdateDelete (CRUD) 页面。

在“解决方案资源管理器”中,右键单击 Controllers 文件夹,并选择“添加”>“新搭建基架的项目”。

上述步骤的视图

在“添加新基架项”对话框中:

  • 在左窗格中,选择“已安装”>“常用”>“MVC”。
  • 选择“视图使用实体框架的 MVC 控制器”。
  • 选择 添加

“添加基架”对话框

完成“使用实体框架添加包含视图的 MVC 控制器”对话框中的内容:

  • 在“模型类”下拉列表中,选择“Movie (MvcMovie.Models)”。
  • 在“数据上下文类”行中,选择 +(加号)。
    • 在“添加数据上下文”对话框中,将生成类名 MvcMovie.Data.MvcMovieContext。
    • 选择 添加
  • 在“数据库提供程序”下拉列表中,选择“SQL Server”。
  • “视图”和“控制器名称”:保留默认值。
  • 选择 添加

添加数据上下文保留默认值

如果收到错误消息,请再次选择“添加”以重试。

基架添加以下包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.VisualStudio.Web.CodeGeneration.Design

基架创建以下内容:

  • 电影控制器:Controllers/MoviesController.cs
  • “创建”、“删除”、“详细信息”、“编辑”和“索引”页面的 Razor 视图文件:Views/Movies/*.cshtml
  • 数据库上下文类:Data/MvcMovieContext.cs

基架更新以下内容:

  • MvcMovie.csproj 项目文件中插入所需的包引用。
  • Program.cs 文件中注册数据库上下文。
  • 将数据库连接字符串添加到 appsettings.json 文件。

自动创建这些文件和文件更新被称为“基架”。

尚且不能使用基架页面,因为该数据库不存在。 运行应用并选择“Movie App”链接会导致“无法打开数据库”或“无此类表: Movie”错误消息。

生成应用以验证没有任何错误。

初始迁移

使用 EF Core迁移功能来创建数据库。 迁移是可用于创建和更新数据库以匹配数据模型的一组工具。

从“工具”菜单中,选择“NuGet 包管理器”>“包管理器控制台”。

在包管理器控制台 (PMC) 中输入以下命令:

Add-Migration InitialCreate
Update-Database

  • Add-Migration InitialCreate:生成 Migrations/{timestamp}_InitialCreate.cs 迁移文件。 InitialCreate 参数是迁移名称。 可以使用任何名称,但是按照惯例,会选择可说明迁移的名称。 因为这是首次迁移,所以生成的类包含用于创建数据库架构的代码。 数据库架构基于在 MvcMovieContext 类中指定的模型。

  • Update-Database:将数据库更新到上一个命令创建的最新迁移。 此命令在用于创建数据库的 Migrations/{time-stamp}_InitialCreate.cs 文件中运行 Up 方法。

Update-Database 命令将生成如下警告:

未对实体类型“Movie”上的十进制属性“Price”指定任何存储类型。 This will cause values to be silently truncated if they do not fit in the default precision and scale. 使用“HasColumnType”显式指定可容纳“OnModelCreating”中所有值的 SQL Server 列类型,使用“HasPrecision”指定精度和小数位数,或者使用“HasConversion”配置值转换器。

忽略前面的警告,它将在后面的教程中进行修复。

有关 EF Core 的 PMC 工具的详细信息,请参阅 EF Core 工具引用 - Visual Studio 中的 PMC

测试应用

运行应用并选择“Movie App”链接。

如果收到类似于以下内容的异常,可能是因为遗漏了迁移步骤中的 Update-Database 命令:

SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.

注意

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

检查生成的数据库上下文类和注册

对于 EF Core,使用模型执行数据访问。 模型由实体类和表示数据库会话的上下文对象构成。 上下文对象允许查询并保存数据。 数据库上下文派生自 Microsoft.EntityFrameworkCore.DbContext 并指定要包含在数据模型中的实体。

基架创建 Data/MvcMovieContext.cs 数据库上下文类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; }
    }
}

前面的代码创建一个 DbSet<Movie> 属性,该属性表示数据库中的电影。

依赖项注入

ASP.NET Core 通过依赖关系注入 (DI) 生成。 服务(如数据库上下文)在 Program.cs 中向 DI 注册。 这些服务通过构造函数参数提供给需要它们的组件。

Controllers/MoviesController.cs 文件中,构造函数使用依赖关系注入MvcMovieContext 数据库上下文注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

基架在 Program.cs 中生成了以下突出显示的代码:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));

ASP.NET Core 配置系统读取“MvcMovieContext”数据库连接字符串。

检查生成的数据库连接字符串

基架向 appsettings.json 文件添加了一个连接字符串:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovieContext-ea7a4069-f366-4742-bd1c-3f753a804ce1.db"
  }
}

进行本地开发时,ASP.NET Core 配置系统appsettings.json 文件中读取 ConnectionString 键。

InitialCreate

检查 Migrations/{timestamp}_InitialCreate.cs 迁移文件:

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}

在上述代码中:

  • InitialCreate.Up 创建 Movie 表,并将 Id 配置为主键。
  • InitialCreate.Down 还原 Up 迁移所做的架构更改。

控制器中的依赖项注入

打开 Controllers/MoviesController.cs 文件并检查构造函数:

public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }

构造函数使用依赖关系注入将数据库上下文 (MvcMovieContext) 注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

测试“创建”页。 输入并提交数据。

测试“编辑”、“详细信息”和“删除”页 。

强类型模型和 @model 指令

在本教程之前的内容中,已经介绍了控制器如何使用 ViewData 字典将数据或对象传递给视图。 ViewData 字典是一个动态对象,提供了将信息传递给视图的方便的后期绑定方法。

MVC 提供将强类型模型对象传递给视图的功能。 此强类型方法启用编译时代码检查。 基架机制在 MoviesController 类和视图中传递了一个强类型模型。

检查 Controllers/MoviesController.cs 文件中生成的 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);
}

id 参数通常作为路由数据传递。 例如,https://localhost:5001/movies/details/1 的设置如下:

  • 控制器被设置为 movies 控制器(第一个 URL 段)。
  • 操作被设置为 details(第二个 URL 段)。
  • id 被设置为 1(最后一个 URL 段)。

id 可以通过查询字符串传入,如以下示例所示:

https://localhost:5001/movies/details?id=1

在未提供 id 值的情况下,id 参数可定义为可以为 null 的类型 (int?)。

Lambda 表达式会被传入 FirstOrDefaultAsync 方法以选择与路由数据或查询字符串值相匹配的电影实体。

var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);

如果找到了电影,Movie 模型的实例则会被传递到 Details 视图:

return View(movie);

检查 Views/Movies/Details.cshtml 文件的内容:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

视图文件顶部的 @model 语句可指定视图期望的对象类型。 创建影片控制器时,将包含以下 @model 语句:

@model MvcMovie.Models.Movie

@model 指令允许访问控制器传递给视图的影片。 Model 对象为强类型对象。 例如,在 Details.cshtml 视图中,代码通过强类型的 Model 对象将每个电影字段传递给 DisplayNameForDisplayFor HTML 帮助程序。 CreateEdit 方法以及视图也传递一个 Movie 模型对象。

检查电影控制器中的 Index.cshtml 视图和 Index 方法。 请注意代码在调用 View 方法时是如何创建 List 对象的。 代码将此 Movies 列表从 Index 操作方法传递给视图:

// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

如果数据上下文的 Movie 属性为 null,则代码返回问题详细信息

创建电影控制器后,基架将以下 @model 语句包含在 Index.cshtml 文件的顶部:

@model IEnumerable<MvcMovie.Models.Movie>

@model 指令允许使用强类型的 Model 对象访问控制器传递给视图的电影列表。 例如,在 Index.cshtml 视图中,代码使用 foreach 语句通过强类型 Model 对象对电影进行循环遍历:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

因为 Model 对象为强类型(作为 IEnumerable<Movie> 对象),因此循环中的每个项都被类型化为 Movie。 编译器还有一些其他优势,比如验证代码中使用的类型。

其他资源

在本教程中,添加了用于管理数据库中的电影的类。 这些类是 MVC 应用的“Model”部分。

这些模型类与 Entity Framework Core (EF Core) 一起使用来处理数据库。 EF Core 是一个对象关系映射 (ORM) 框架,可以简化需要编写的数据访问代码。

创建的模型类称为 POCO 类,它的全称为“Plain Old CLR Object(普通旧 CLR 对象)”。 POCO 类对 EF Core 没有任何依赖性。 它们只定义数据库中存储的数据属性。

在本教程中,首先要创建模型类,然后 EF Core 将创建数据库。

添加数据模型类

右键单击 Models 文件夹 >“添加”>“类”。 命名文件 Movie.cs

使用以下代码更新 Models/Movie.cs 文件:

using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models;

public class Movie
{
    public int Id { get; set; }
    public string? Title { get; set; }
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }
    public string? Genre { get; set; }
    public decimal Price { get; set; }
}

Movie 类包含一个 Id 字段,数据需要该字段作为主键。

ReleaseDate 上的 DataType 特性指定了数据的类型 (Date)。 通过此特性:

  • 用户无需在日期字段中输入时间信息。
  • 仅显示日期,而非时间信息。

DataAnnotations 会在后续教程中介绍。

string 后的问号表示属性可为空。 有关详细信息,请参阅可为空引用类型

添加 NuGet 包

Visual Studio 会自动安装所需的包。

生成项目以检查编译器错误。

基架电影页面

使用基架工具为电影模型生成 CreateReadUpdateDelete (CRUD) 页面。

在“解决方案资源管理器”中,右键单击 Controllers 文件夹,并选择“添加”>“新搭建基架的项目”。

上述步骤的视图

在“添加新基架项”对话框中:

  • 在左窗格中,选择“已安装”>“常用”>“MVC”。
  • 选择“视图使用实体框架的 MVC 控制器”。
  • 选择 添加

“添加基架”对话框

完成“使用实体框架添加包含视图的 MVC 控制器”对话框中的内容:

  • 在“模型类”下拉列表中,选择“Movie (MvcMovie.Models)”。
  • 在“数据上下文类”行中,选择 +(加号)。
    • 在“添加数据上下文”对话框中,将生成类名 MvcMovie.Data.MvcMovieContext。
    • 选择 添加
  • 在“数据库提供程序”下拉列表中,选择“SQL Server”。
  • “视图”和“控制器名称”:保留默认值。
  • 选择 添加

“添加数据上下文”保持默认设置 如果收到错误消息,请再次选择“添加”进行重试。

基架添加以下包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.VisualStudio.Web.CodeGeneration.Design

基架创建以下内容:

  • 电影控制器:Controllers/MoviesController.cs
  • “创建”、“删除”、“详细信息”、“编辑”和“索引”页面的 Razor 视图文件:Views/Movies/*.cshtml
  • 数据库上下文类:Data/MvcMovieContext.cs

基架更新以下内容:

  • MvcMovie.csproj 项目文件中插入所需的包引用。
  • Program.cs 文件中注册数据库上下文。
  • 将数据库连接字符串添加到 appsettings.json 文件。

自动创建这些文件和文件更新被称为“基架”。

尚且不能使用基架页面,因为该数据库不存在。 运行应用并选择“Movie App”链接会导致“无法打开数据库”或“无此类表: Movie”错误消息。

生成应用以验证没有任何错误。

初始迁移

使用 EF Core迁移功能来创建数据库。 迁移是可用于创建和更新数据库以匹配数据模型的一组工具。

从“工具”菜单中,选择“NuGet 包管理器”>“包管理器控制台”。

在包管理器控制台 (PMC) 中输入以下命令:

Add-Migration InitialCreate
Update-Database

  • Add-Migration InitialCreate:生成 Migrations/{timestamp}_InitialCreate.cs 迁移文件。 InitialCreate 参数是迁移名称。 可以使用任何名称,但是按照惯例,会选择可说明迁移的名称。 因为这是首次迁移,所以生成的类包含用于创建数据库架构的代码。 数据库架构基于在 MvcMovieContext 类中指定的模型。

  • Update-Database:将数据库更新到上一个命令创建的最新迁移。 此命令在用于创建数据库的 Migrations/{time-stamp}_InitialCreate.cs 文件中运行 Up 方法。

Update-Database 命令将生成如下警告:

No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

忽略前面的警告,它将在后面的教程中进行修复。

有关 EF Core 的 PMC 工具的详细信息,请参阅 EF Core 工具引用 - Visual Studio 中的 PMC

测试应用

运行应用并选择“Movie App”链接。

如果收到类似于以下内容的异常,可能是因为遗漏了迁移步骤中的 Update-Database 命令:

SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.

注意

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

检查生成的数据库上下文类和注册

对于 EF Core,使用模型执行数据访问。 模型由实体类和表示数据库会话的上下文对象构成。 上下文对象允许查询并保存数据。 数据库上下文派生自 Microsoft.EntityFrameworkCore.DbContext 并指定要包含在数据模型中的实体。

基架创建 Data/MvcMovieContext.cs 数据库上下文类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; }
    }
}

前面的代码创建一个 DbSet<Movie> 属性,该属性表示数据库中的电影。

依赖项注入

ASP.NET Core 通过依赖关系注入 (DI) 生成。 服务(如数据库上下文)在 Program.cs 中向 DI 注册。 这些服务通过构造函数参数提供给需要它们的组件。

Controllers/MoviesController.cs 文件中,构造函数使用依赖关系注入MvcMovieContext 数据库上下文注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

基架在 Program.cs 中生成了以下突出显示的代码:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));

ASP.NET Core 配置系统读取“MvcMovieContext”数据库连接字符串。

检查生成的数据库连接字符串

基架向 appsettings.json 文件添加了一个连接字符串:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Data Source=MvcMovieContext-ea7a4069-f366-4742-bd1c-3f753a804ce1.db"
  }
}

进行本地开发时,ASP.NET Core 配置系统appsettings.json 文件中读取 ConnectionString 键。

InitialCreate

检查 Migrations/{timestamp}_InitialCreate.cs 迁移文件:

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}

在上述代码中:

  • InitialCreate.Up 创建 Movie 表,并将 Id 配置为主键。
  • InitialCreate.Down 还原 Up 迁移所做的架构更改。

控制器中的依赖项注入

打开 Controllers/MoviesController.cs 文件并检查构造函数:

public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }

构造函数使用依赖关系注入将数据库上下文 (MvcMovieContext) 注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

测试“创建”页。 输入并提交数据。

测试“编辑”、“详细信息”和“删除”页 。

强类型模型和 @model 指令

在本教程之前的内容中,已经介绍了控制器如何使用 ViewData 字典将数据或对象传递给视图。 ViewData 字典是一个动态对象,提供了将信息传递给视图的方便的后期绑定方法。

MVC 提供将强类型模型对象传递给视图的功能。 此强类型方法启用编译时代码检查。 基架机制在 MoviesController 类和视图中传递了一个强类型模型。

检查 Controllers/MoviesController.cs 文件中生成的 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);
}

id 参数通常作为路由数据传递。 例如,https://localhost:5001/movies/details/1 的设置如下:

  • 控制器被设置为 movies 控制器(第一个 URL 段)。
  • 操作被设置为 details(第二个 URL 段)。
  • id 被设置为 1(最后一个 URL 段)。

id 可以通过查询字符串传入,如以下示例所示:

https://localhost:5001/movies/details?id=1

在未提供 id 值的情况下,id 参数可定义为可以为 null 的类型 (int?)。

Lambda 表达式会被传入 FirstOrDefaultAsync 方法以选择与路由数据或查询字符串值相匹配的电影实体。

var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);

如果找到了电影,Movie 模型的实例则会被传递到 Details 视图:

return View(movie);

检查 Views/Movies/Details.cshtml 文件的内容:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

视图文件顶部的 @model 语句可指定视图期望的对象类型。 创建影片控制器时,将包含以下 @model 语句:

@model MvcMovie.Models.Movie

@model 指令允许访问控制器传递给视图的影片。 Model 对象为强类型对象。 例如,在 Details.cshtml 视图中,代码通过强类型的 Model 对象将每个电影字段传递给 DisplayNameForDisplayFor HTML 帮助程序。 CreateEdit 方法以及视图也传递一个 Movie 模型对象。

检查电影控制器中的 Index.cshtml 视图和 Index 方法。 请注意代码在调用 View 方法时是如何创建 List 对象的。 代码将此 Movies 列表从 Index 操作方法传递给视图:

// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

如果数据上下文的 Movie 属性为 null,则代码返回问题详细信息

创建电影控制器后,基架将以下 @model 语句包含在 Index.cshtml 文件的顶部:

@model IEnumerable<MvcMovie.Models.Movie>

@model 指令允许使用强类型的 Model 对象访问控制器传递给视图的电影列表。 例如,在 Index.cshtml 视图中,代码使用 foreach 语句通过强类型 Model 对象对电影进行循环遍历:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

因为 Model 对象为强类型(作为 IEnumerable<Movie> 对象),因此循环中的每个项都被类型化为 Movie。 编译器还有一些其他优势,比如验证代码中使用的类型。

其他资源

在本教程中,添加了用于管理数据库中的电影的类。 这些类是 MVC 应用的“Model”部分。

这些模型类与 Entity Framework Core (EF Core) 一起使用来处理数据库。 EF Core 是一个对象关系映射 (ORM) 框架,可以简化需要编写的数据访问代码。

创建的模型类称为 POCO 类,它的全称为“Plain Old CLR Object(普通旧 CLR 对象)”。 POCO 类对 EF Core 没有任何依赖性。 它们只定义数据库中存储的数据属性。

在本教程中,首先要创建模型类,然后 EF Core 将创建数据库。

添加数据模型类

右键单击 Models 文件夹 >“添加”>“类”。 命名文件 Movie.cs

使用以下代码更新 Models/Movie.cs 文件:

using System.ComponentModel.DataAnnotations;

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

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string? Genre { get; set; }
        public decimal Price { get; set; }
    }
}

Movie 类包含一个 Id 字段,数据需要该字段作为主键。

ReleaseDate 上的 DataType 特性指定了数据的类型 (Date)。 通过此特性:

  • 用户无需在日期字段中输入时间信息。
  • 仅显示日期,而非时间信息。

DataAnnotations 会在后续教程中介绍。

string 后的问号表示属性可为空。 有关详细信息,请参阅可为空引用类型

添加 NuGet 包

从“工具”菜单中选择“NuGet 包管理器”>“包管理器控制台 (PMC)”

PMC 菜单

在 PMC 中运行以下命令:

Install-Package Microsoft.EntityFrameworkCore.Design
Install-Package Microsoft.EntityFrameworkCore.SqlServer

上述命令添加:

  • EF Core SQL Server 提供程序。 提供程序包将 EF Core 包作为依赖项进行安装。
  • 在本教程后面的基架步骤中会自动安装包使用的实用程序。

生成项目以检查编译器错误。

基架电影页面

使用基架工具为电影模型生成 CreateReadUpdateDelete (CRUD) 页面。

在“解决方案资源管理器”中,右键单击 Controllers 文件夹,并选择“添加”>“新搭建基架的项目”。

上述步骤的视图

在“添加基架”对话框中,选择“包含视图的 MVC 控制器(使用实体框架)”>“添加”。

“添加基架”对话框

完成“使用实体框架添加包含视图的 MVC 控制器”对话框中的内容:

  • 在“模型类”下拉列表中,选择“Movie (MvcMovie.Models)”。
  • 在“数据上下文类”行中,选择 +(加号)。
    • 在“添加数据上下文”对话框中,将生成类名 MvcMovie.Data.MvcMovieContext。
    • 选择 添加
  • “视图”和“控制器名称”:保留默认值。
  • 选择 添加

添加数据上下文保留默认值

如果收到错误消息,请再次选择“添加”以重试。

基架更新以下内容:

  • MvcMovie.csproj 项目文件中插入所需的包引用。
  • Program.cs 文件中注册数据库上下文。
  • 将数据库连接字符串添加到 appsettings.json 文件。

基架创建以下内容:

  • 电影控制器:Controllers/MoviesController.cs
  • “创建”、“删除”、“详细信息”、“编辑”和“索引”页面的 Razor 视图文件:Views/Movies/*.cshtml
  • 数据库上下文类:Data/MvcMovieContext.cs

自动创建这些文件和文件更新被称为“基架”。

尚且不能使用基架页面,因为该数据库不存在。 运行应用并选择“Movie App”链接会导致“无法打开数据库”或“无此类表: Movie”错误消息。

生成应用

构建应用程序。 编译器会生成几个有关如何处理 null 值的警告。 有关详细信息,请参阅此 GitHub 问题可为 null 的引用类型

若要消除可为空引用类型的警告,请从 MvcMovie.csproj 文件中删除以下行:

<Nullable>enable</Nullable>

我们计划在下一版本中修复此问题。

初始迁移

使用 EF Core迁移功能来创建数据库。 迁移是可用于创建和更新数据库以匹配数据模型的一组工具。

从“工具”菜单中,选择“NuGet 包管理器”>“包管理器控制台”。

在包管理器控制台 (PMC) 中输入以下命令:

Add-Migration InitialCreate
Update-Database

  • Add-Migration InitialCreate:生成 Migrations/{timestamp}_InitialCreate.cs 迁移文件。 InitialCreate 参数是迁移名称。 可以使用任何名称,但是按照惯例,会选择可说明迁移的名称。 因为这是首次迁移,所以生成的类包含用于创建数据库架构的代码。 数据库架构基于在 MvcMovieContext 类中指定的模型。

  • Update-Database:将数据库更新到上一个命令创建的最新迁移。 此命令在用于创建数据库的 Migrations/{time-stamp}_InitialCreate.cs 文件中运行 Up 方法。

Update-Database 命令将生成如下警告:

No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

忽略前面的警告,它将在后面的教程中进行修复。

有关 EF Core 的 PMC 工具的详细信息,请参阅 EF Core 工具引用 - Visual Studio 中的 PMC

测试应用

运行应用并选择“Movie App”链接。

如果收到类似于以下内容的异常,可能是因为遗漏了迁移步骤

SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.

注意

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

检查生成的数据库上下文类和注册

对于 EF Core,使用模型执行数据访问。 模型由实体类和表示数据库会话的上下文对象构成。 上下文对象允许查询并保存数据。 数据库上下文派生自 Microsoft.EntityFrameworkCore.DbContext 并指定要包含在数据模型中的实体。

基架创建 Data/MvcMovieContext.cs 数据库上下文类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<MvcMovie.Models.Movie> Movie { get; set; }
    }
}

前面的代码创建一个 DbSet<Movie> 属性,该属性表示数据库中的电影。

依赖项注入

ASP.NET Core 通过依赖关系注入 (DI) 生成。 服务(如数据库上下文)在 Program.cs 中向 DI 注册。 这些服务通过构造函数参数提供给需要它们的组件。

Controllers/MoviesController.cs 文件中,构造函数使用依赖关系注入MvcMovieContext 数据库上下文注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

基架在 Program.cs 中生成了以下突出显示的代码:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext")));

ASP.NET Core 配置系统读取“MvcMovieContext”数据库连接字符串。

检查生成的数据库连接字符串

基架向 appsettings.json 文件添加了一个连接字符串:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-7dc5;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

进行本地开发时,ASP.NET Core 配置系统appsettings.json 文件中读取 ConnectionString 键。

InitialCreate

检查 Migrations/{timestamp}_InitialCreate.cs 迁移文件:

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace MvcMovie.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Movie",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                    Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Movie", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Movie");
        }
    }
}

在上述代码中:

  • InitialCreate.Up 创建 Movie 表,并将 Id 配置为主键。
  • InitialCreate.Down 还原 Up 迁移所做的架构更改。

控制器中的依赖项注入

打开 Controllers/MoviesController.cs 文件并检查构造函数:

public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }

构造函数使用依赖关系注入将数据库上下文 (MvcMovieContext) 注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

测试“创建”页。 输入并提交数据。

测试“编辑”、“详细信息”和“删除”页 。

强类型模型和 @model 指令

在本教程之前的内容中,已经介绍了控制器如何使用 ViewData 字典将数据或对象传递给视图。 ViewData 字典是一个动态对象,提供了将信息传递给视图的方便的后期绑定方法。

MVC 提供将强类型模型对象传递给视图的功能。 此强类型方法启用编译时代码检查。 基架机制在 MoviesController 类和视图中传递了一个强类型模型。

检查 Controllers/MoviesController.cs 文件中生成的 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);
}

id 参数通常作为路由数据传递。 例如,https://localhost:5001/movies/details/1 的设置如下:

  • 控制器被设置为 movies 控制器(第一个 URL 段)。
  • 操作被设置为 details(第二个 URL 段)。
  • id 被设置为 1(最后一个 URL 段)。

id 可以通过查询字符串传入,如以下示例所示:

https://localhost:5001/movies/details?id=1

在未提供 id 值的情况下,id 参数可定义为可以为 null 的类型 (int?)。

Lambda 表达式会被传入 FirstOrDefaultAsync 方法以选择与路由数据或查询字符串值相匹配的电影实体。

var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);

如果找到了电影,Movie 模型的实例则会被传递到 Details 视图:

return View(movie);

检查 Views/Movies/Details.cshtml 文件的内容:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class = "col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class = "col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

视图文件顶部的 @model 语句可指定视图期望的对象类型。 创建影片控制器时,将包含以下 @model 语句:

@model MvcMovie.Models.Movie

@model 指令允许访问控制器传递给视图的影片。 Model 对象为强类型对象。 例如,在 Details.cshtml 视图中,代码通过强类型的 Model 对象将每个电影字段传递给 DisplayNameForDisplayFor HTML 帮助程序。 CreateEdit 方法以及视图也传递一个 Movie 模型对象。

检查电影控制器中的 Index.cshtml 视图和 Index 方法。 请注意代码在调用 View 方法时是如何创建 List 对象的。 代码将此 Movies 列表从 Index 操作方法传递给视图:

// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

创建电影控制器后,基架将以下 @model 语句包含在 Index.cshtml 文件的顶部:

@model IEnumerable<MvcMovie.Models.Movie>

@model 指令允许使用强类型的 Model 对象访问控制器传递给视图的电影列表。 例如,在 Index.cshtml 视图中,代码使用 foreach 语句通过强类型 Model 对象对电影进行循环遍历:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

因为 Model 对象为强类型(作为 IEnumerable<Movie> 对象),因此循环中的每个项都被类型化为 Movie。 编译器还有一些其他优势,比如验证代码中使用的类型。

其他资源

在本教程中,添加了用于管理数据库中的电影的类。 这些类是 MVC 应用的“Model”部分。

这些模型类与 Entity Framework Core (EF Core) 一起使用来处理数据库。 EF Core 是一个对象关系映射 (ORM) 框架,可以简化需要编写的数据访问代码。

创建的模型类称为 POCO 类,它的全称为“Plain Old CLR Object(普通旧 CLR 对象)”。 POCO 类对 EF Core 没有任何依赖性。 它们只定义数据库中存储的数据属性。

在本教程中,首先要创建模型类,然后 EF Core 将创建数据库。

添加数据模型类

右键单击 Models 文件夹 >“添加”>“类”。 命名文件 Movie.cs

使用以下代码更新 Models/Movie.cs 文件:

using System;
using System.ComponentModel.DataAnnotations;

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

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}

Movie 类包含一个 Id 字段,数据需要该字段作为主键。

ReleaseDate 上的 DataType 特性指定了数据的类型 (Date)。 通过此特性:

  • 用户无需在日期字段中输入时间信息。
  • 仅显示日期,而非时间信息。

DataAnnotations 会在后续教程中介绍。

添加 NuGet 包

从“工具”菜单中选择“NuGet 包管理器”>“包管理器控制台 (PMC)”

PMC 菜单

在 PMC 中运行以下命令:

Install-Package Microsoft.EntityFrameworkCore.Design

上述命令添加:

  • EF Core SQL Server 提供程序。 提供程序包将 EF Core 包作为依赖项进行安装。
  • 在本教程后面的基架步骤中会自动安装包使用的实用程序。

生成项目以检查编译器错误。

基架电影页面

使用基架工具为电影模型生成 CreateReadUpdateDelete (CRUD) 页面。

在“解决方案资源管理器”中,右键单击 Controllers 文件夹,并选择“添加”>“新搭建基架的项目”。

上述步骤的视图

在“添加基架”对话框中,选择“包含视图的 MVC 控制器(使用实体框架)”>“添加”。

“添加基架”对话框

完成“使用实体框架添加包含视图的 MVC 控制器”对话框中的内容:

  • 在“模型类”下拉列表中,选择“Movie (MvcMovie.Models)”。
  • 在“数据上下文类”行中,选择 +(加号)。
    • 在“添加数据上下文”对话框中,将生成类名 MvcMovie.Data.MvcMovieContext。
    • 选择 添加
  • “视图”和“控制器名称”:保留默认值。
  • 选择 添加

添加数据上下文保留默认值

基架更新以下内容:

  • MvcMovie.csproj 项目文件中插入所需的包引用。
  • Startup.cs 文件的 Startup.ConfigureServices 中注册数据库上下文。
  • 将数据库连接字符串添加到 appsettings.json 文件。

基架创建以下内容:

  • 电影控制器:Controllers/MoviesController.cs
  • “创建”、“删除”、“详细信息”、“编辑”和“索引”页面的 Razor 视图文件:Views/Movies/*.cshtml
  • 数据库上下文类:Data/MvcMovieContext.cs

自动创建这些文件和文件更新被称为“基架”。

尚且不能使用基架页面,因为该数据库不存在。 运行应用并选择“Movie App”链接会导致“无法打开数据库”或“无此类表: Movie”错误消息。

初始迁移

使用 EF Core迁移功能来创建数据库。 迁移是可用于创建和更新数据库以匹配数据模型的一组工具。

从“工具”菜单中,选择“NuGet 包管理器”>“包管理器控制台”。

在包管理器控制台 (PMC) 中输入以下命令:

Add-Migration InitialCreate
Update-Database

  • Add-Migration InitialCreate:生成 Migrations/{timestamp}_InitialCreate.cs 迁移文件。 InitialCreate 参数是迁移名称。 可以使用任何名称,但是按照惯例,会选择可说明迁移的名称。 因为这是首次迁移,所以生成的类包含用于创建数据库架构的代码。 数据库架构基于在 MvcMovieContext 类中指定的模型。

  • Update-Database:将数据库更新到上一个命令创建的最新迁移。 此命令在用于创建数据库的 Migrations/{time-stamp}_InitialCreate.cs 文件中运行 Up 方法。

Update-Database 命令将生成如下警告:

No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

忽略前面的警告,它将在后面的教程中进行修复。

有关 EF Core 的 PMC 工具的详细信息,请参阅 EF Core 工具引用 - Visual Studio 中的 PMC

测试应用

运行应用并选择“Movie App”链接。

如果收到类似于以下内容的异常,可能是因为遗漏了迁移步骤

SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.

注意

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

检查生成的数据库上下文类和注册

对于 EF Core,使用模型执行数据访问。 模型由实体类和表示数据库会话的上下文对象构成。 上下文对象允许查询并保存数据。 数据库上下文派生自 Microsoft.EntityFrameworkCore.DbContext 并指定要包含在数据模型中的实体。

基架创建 Data/MvcMovieContext.cs 数据库上下文类:

using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<Movie> Movie { get; set; }
    }
}

前面的代码创建一个 DbSet<Movie> 属性,该属性表示数据库中的电影。

ASP.NET Core 通过依赖关系注入 (DI) 生成。 服务(如数据库上下文)必须在 Startup 中向 DI 注册。 需要这些服务的组件是通过构造函数参数提供的。

Controllers/MoviesController.cs 文件中,构造函数使用依赖关系注入MvcMovieContext 数据库上下文注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

基架在 Startup.ConfigureServices 中生成了以下突出显示的代码:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}

ASP.NET Core 配置系统读取“MvcMovieContext”数据库连接字符串。

检查生成的数据库连接字符串

基架向 appsettings.json 文件添加了一个连接字符串:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

进行本地开发时,ASP.NET Core 配置系统appsettings.json 文件中读取 ConnectionString 键。

InitialCreate

检查 Migrations/{timestamp}_InitialCreate.cs 迁移文件:

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Movie",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
                ReleaseDate = table.Column<DateTime>(type: "datetime2", nullable: false),
                Genre = table.Column<string>(type: "nvarchar(max)", nullable: true),
                Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Movie", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Movie");
    }
}

在上述代码中:

  • InitialCreate.Up 创建 Movie 表,并将 Id 配置为主键。
  • InitialCreate.Down 还原 Up 迁移所做的架构更改。

控制器中的依赖项注入

打开 Controllers/MoviesController.cs 文件并检查构造函数:

public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }

构造函数使用依赖关系注入将数据库上下文 (MvcMovieContext) 注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

测试“创建”页。 输入并提交数据。

测试“编辑”、“详细信息”和“删除”页 。

强类型模型和 @model 指令

在本教程之前的内容中,已经介绍了控制器如何使用 ViewData 字典将数据或对象传递给视图。 ViewData 字典是一个动态对象,提供了将信息传递给视图的方便的后期绑定方法。

MVC 提供将强类型模型对象传递给视图的功能。 此强类型方法启用编译时代码检查。 基架机制在 MoviesController 类和视图中传递了一个强类型模型。

检查 Controllers/MoviesController.cs 文件中生成的 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);
}

id 参数通常作为路由数据传递。 例如,https://localhost:5001/movies/details/1 的设置如下:

  • 控制器被设置为 movies 控制器(第一个 URL 段)。
  • 操作被设置为 details(第二个 URL 段)。
  • id 被设置为 1(最后一个 URL 段)。

id 可以通过查询字符串传入,如以下示例所示:

https://localhost:5001/movies/details?id=1

在未提供 id 值的情况下,id 参数可定义为可以为 null 的类型 (int?)。

Lambda 表达式会被传入 FirstOrDefaultAsync 方法以选择与路由数据或查询字符串值相匹配的电影实体。

var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);

如果找到了电影,Movie 模型的实例则会被传递到 Details 视图:

return View(movie);

检查 Views/Movies/Details.cshtml 文件的内容:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

视图文件顶部的 @model 语句可指定视图期望的对象类型。 创建影片控制器时,将包含以下 @model 语句:

@model MvcMovie.Models.Movie

@model 指令允许访问控制器传递给视图的影片。 Model 对象为强类型对象。 例如,在 Details.cshtml 视图中,代码通过强类型的 Model 对象将每个电影字段传递给 DisplayNameForDisplayFor HTML 帮助程序。 CreateEdit 方法以及视图也传递一个 Movie 模型对象。

检查电影控制器中的 Index.cshtml 视图和 Index 方法。 请注意代码在调用 View 方法时是如何创建 List 对象的。 代码将此 Movies 列表从 Index 操作方法传递给视图:

// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

创建电影控制器后,基架将以下 @model 语句包含在 Index.cshtml 文件的顶部:

@model IEnumerable<MvcMovie.Models.Movie>

@model 指令允许使用强类型的 Model 对象访问控制器传递给视图的电影列表。 例如,在 Index.cshtml 视图中,代码使用 foreach 语句通过强类型 Model 对象对电影进行循环遍历:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

因为 Model 对象为强类型(作为 IEnumerable<Movie> 对象),因此循环中的每个项都被类型化为 Movie。 编译器还有一些其他优势,比如验证代码中使用的类型。

Entity Framework Core 的 SQL 日志记录

日志配置通常由 appsettings.{Environment}.json 文件的 Logging 部分提供。 若要记录 SQL 语句,请将 "Microsoft.EntityFrameworkCore.Database.Command": "Information" 添加到 appsettings.Development.json 文件:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-2;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
     ,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*"
}

有了前面的 JSON,SQL 语句就会显示在命令行和 Visual Studio 输出窗口中。

有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录和此 GitHub 问题

其他资源

在本教程中,添加了用于管理数据库中的电影的类。 这些类是 MVC 应用的“Model”部分。

这些模型类与 Entity Framework Core (EF Core) 一起使用来处理数据库。 EF Core 是一个对象关系映射 (ORM) 框架,可以简化需要编写的数据访问代码。

创建的模型类称为 POCO 类,它的全称为“Plain Old CLR Object(普通旧 CLR 对象)”。 POCO 类对 EF Core 没有任何依赖性。 它们只定义数据库中存储的数据属性。

在本教程中,首先要创建模型类,然后 EF Core 将创建数据库。

添加数据模型类

右键单击 Models 文件夹 >“添加”>“类”。 命名文件 Movie.cs

使用以下代码更新 Movie.cs 文件:

using System;
using System.ComponentModel.DataAnnotations;

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

        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}

Movie 类包含一个 Id 字段,数据需要该字段作为主键。

ReleaseDate 上的 DataType 特性指定了数据的类型 (Date)。 通过此特性:

  • 用户无需在数据字段中输入时间信息。
  • 仅显示日期,而非时间信息。

DataAnnotations 会在后续教程中介绍。

添加 NuGet 包

从“工具”菜单中选择“NuGet 包管理器”>“包管理器控制台 (PMC)”

PMC 菜单

在 PMC 中运行以下命令:

Install-Package Microsoft.EntityFrameworkCore.SqlServer

前面的命令添加 EF Core SQL Server 提供程序。 提供程序包将 EF Core 包作为依赖项进行安装。 在本教程后面的基架步骤中会自动安装其他包。

创建数据库上下文类

需要一个数据库上下文类来协调 Movie 模型的 EF Core 功能(创建、读取、更新和删除)。 数据库上下文派生自 Microsoft.EntityFrameworkCore.DbContext 并指定要包含在数据模型中的实体。

创建一个“Data”文件夹。

使用以下代码添加 Data/MvcMovieContext.cs 文件:

using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
    public class MvcMovieContext : DbContext
    {
        public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
            : base(options)
        {
        }

        public DbSet<Movie> Movie { get; set; }
    }
}

前面的代码为实体集创建 DbSet<Movie> 属性。 在实体框架术语中,实体集通常与数据表相对应。 实体对应表中的行。

注册数据库上下文

ASP.NET Core 通过依赖关系注入 (DI) 生成。 在应用程序启动过程中,必须向 DI 注册服务(如 EF Core DB 上下文)。 需要这些服务(如 Razor Pages)的组件通过构造函数参数提供。 本教程的后续部分介绍了用于获取 DB 上下文实例的构造函数代码。 本部分会将数据库上下文注册到 DI 容器。

Startup.cs 顶部添加以下 using 语句:

using MvcMovie.Data;
using Microsoft.EntityFrameworkCore;

将以下突出显示的代码添加到 Startup.ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}

通过调用 DbContextOptions 中的一个方法将数据库连接字符串在配置文件中的名称传递给上下文对象。 进行本地开发时,ASP.NET Core 配置系统appsettings.json 文件中读取连接字符串。

检查数据库连接字符串

将连接字符串添加到 appsettings.json 文件中:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

生成项目以检查编译器错误。

基架电影页面

使用基架工具为电影模型生成“创建”、“读取”、“更新”和“删除”(CRUD) 页面。

在解决方案资源管理器中,右键单击 Controllers 文件夹 >“添加”>“新搭建基架的项目”。

上述步骤的视图

在“添加基架”对话框中,选择“包含视图的 MVC 控制器(使用实体框架)”>“添加”。

“添加基架”对话框

填写“添加控制器”对话框:

  • 模型类:Movie(MvcMovie.Models)
  • 数据上下文类:MvcMovieContext (MvcMovie.Data)

“添加数据”上下文

  • 视图:将每个选项保持为默认选中状态
  • 控制器名称:保留默认的 MoviesController
  • 选择“添加”

Visual Studio 将创建:

  • 电影控制器 (Controllers/MoviesController.cs)
  • “创建”、“删除”、“详细信息”、“编辑”和“索引”页面的 Razor 视图文件 (*Views/Movies/`.cshtml`)

自动创建这些文件称为“基架”。

你还不能使用基架页面,因为该数据库不存在。 如果运行应用并单击“Movie App”链接,则会出现“无法打开数据库”或“无此类表:Movie”错误消息。

初始迁移

使用 EF Core迁移功能来创建数据库。 迁移是可用于创建和更新数据库以匹配数据模型的一组工具。

从“工具”菜单中选择“NuGet 包管理器”>“包管理器控制台 (PMC)”

在 PMC 中,输入以下命令:

Add-Migration InitialCreate
Update-Database
  • Add-Migration InitialCreate:生成 Migrations/{timestamp}_InitialCreate.cs 迁移文件。 InitialCreate 参数是迁移名称。 可以使用任何名称,但是按照惯例,会选择可说明迁移的名称。 因为这是首次迁移,所以生成的类包含用于创建数据库架构的代码。 数据库架构基于在 MvcMovieContext 类中指定的模型。

  • Update-Database:将数据库更新到上一个命令创建的最新迁移。 此命令在用于创建数据库的 Migrations/{time-stamp}_InitialCreate.cs 文件中运行 Up 方法。

    数据库更新命令生成以下警告:

    No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'HasColumnType()'.

    你可以忽略该警告,它将后面的教程中得到修复。

有关 EF Core 的 PMC 工具的详细信息,请参阅 EF Core 工具引用 - Visual Studio 中的 PMC

InitialCreate 类

检查 Migrations/{timestamp}_InitialCreate.cs 迁移文件:

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Movie",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", 
                                 SqlServerValueGenerationStrategy.IdentityColumn),
                Title = table.Column<string>(nullable: true),
                ReleaseDate = table.Column<DateTime>(nullable: false),
                Genre = table.Column<string>(nullable: true),
                Price = table.Column<decimal>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Movie", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Movie");
    }
}

Up 方法创建 Movie 表,并将 Id 配置为主键。 Down 方法可还原 Up 迁移所做的架构更改。

测试应用

  • 运行应用并单击“Movie App”链接。

    如果遇到类似于以下情况的异常:

SqlException: Cannot open database "MvcMovieContext-1" requested by the login. The login failed.

可能缺失迁移步骤

  • 测试“创建”页。 输入并提交数据。

    注意

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

  • 测试“编辑”、“详细信息”和“删除”页 。

控制器中的依赖项注入

打开 Controllers/MoviesController.cs 文件并检查构造函数:

public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }

构造函数使用依赖关系注入将数据库上下文 (MvcMovieContext) 注入到控制器中。 数据库上下文将在控制器中的每个 CRUD 方法中使用。

强类型模型和 @model 关键词

在本教程之前的内容中,已经介绍了控制器如何使用 ViewData 字典将数据或对象传递给视图。 ViewData 字典是一个动态对象,提供了将信息传递给视图的方便的后期绑定方法。

MVC 还提供将强类型模型对象传递给视图的功能。 此强类型方法启用编译时代码检查。 基架机制通过 MoviesController 类和视图使用了此方法(即传递强类型模型)。

检查 Controllers/MoviesController.cs 文件中生成的 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);
}

id 参数通常作为路由数据传递。 例如 https://localhost:5001/movies/details/1 的设置如下:

  • 控制器被设置为 movies 控制器(第一个 URL 段)。
  • 操作被设置为 details(第二个 URL 段)。
  • ID 被设置为 1(最后一个 URL 段)。

还可以使用查询字符串传入 id,如下所示:

https://localhost:5001/movies/details?id=1

在未提供 ID 值的情况下,id 参数可定义为可以为 null 的类型 (int?)。

Lambda 表达式会被传入 FirstOrDefaultAsync 以选择与路由数据或查询字符串值相匹配的电影实体。

var movie = await _context.Movie
    .FirstOrDefaultAsync(m => m.Id == id);

如果找到了电影,Movie 模型的实例则会被传递到 Details 视图:

return View(movie);

检查 Views/Movies/Details.cshtml 文件的内容:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

视图文件顶部的 @model 语句可指定视图期望的对象类型。 创建影片控制器时,将包含以下 @model 语句:

@model MvcMovie.Models.Movie

@model 指令允许访问控制器传递给视图的影片。 Model 对象为强类型对象。 例如,在 Details.cshtml 视图中,代码通过强类型的 Model 对象将每个电影字段传递给 DisplayNameForDisplayFor HTML 帮助程序。 CreateEdit 方法以及视图也传递一个 Movie 模型对象。

检查电影控制器中的 Index.cshtml 视图和 Index 方法。 请注意代码在调用 View 方法时是如何创建 List 对象的。 代码将此 Movies 列表从 Index 操作方法传递给视图:

// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

创建电影控制器后,基架将以下 @model 语句包含在 Index.cshtml 文件的顶部:

@model IEnumerable<MvcMovie.Models.Movie>

@model 指令使你能够使用强类型的 Model 对象访问控制器传递给视图的电影列表。 例如,在 Index.cshtml 视图中,代码使用 foreach 语句通过强类型 Model 对象对电影进行循环遍历:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

因为 Model 对象为强类型(作为 IEnumerable<Movie> 对象),因此循环中的每个项都被类型化为 Movie。 除其他优点之外,这意味着可对代码进行编译时检查。

其他资源