三十二、创建示例项目

在本章中,您将创建贯穿本书这一部分的示例项目。该项目包含一个使用简单控制器和 Razor 页面显示的数据模型。

创建项目

从 Windows 开始菜单打开一个新的 PowerShell 命令提示符,并运行清单 32-1 中所示的命令。

Tip

你可以从 https://github.com/apress/pro-asp.net-core-3 下载本章以及本书其他章节的示例项目。如果在运行示例时遇到问题,请参见第 1 章获取帮助。

dotnet new globaljson --sdk-version 3.1.101 --output Advanced
dotnet new web --no-https --output Advanced --framework netcoreapp3.1
dotnet new sln -o Advanced

dotnet sln Advanced add Advanced

Listing 32-1.Creating the Project

如果您使用的是 Visual Studio,请打开Advanced文件夹中的Advanced.sln文件。选择项目➤平台属性,导航到调试页面,将 App URL 字段更改为 http://localhost:5000 ,如图 32-1 所示。这将更改用于接收 HTTP 请求的端口。选择文件➤保存全部保存配置更改。

img/338050_8_En_32_Fig1_HTML.jpg

图 32-1。

更改 HTTP 端口

如果您使用的是 Visual Studio 代码,请打开Advanced文件夹。当系统提示添加构建和调试项目所需的资产时,点击是按钮,如图 32-2 所示。

img/338050_8_En_32_Fig2_HTML.jpg

图 32-2。

添加项目资产

将 NuGet 包添加到项目中

该数据模型将使用实体框架核心来存储和查询 SQL Server LocalDB 数据库中的数据。要添加实体框架核心的 NuGet 包,使用 PowerShell 命令提示符来运行在Advanced项目文件夹中的清单 32-2 中显示的命令。

dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.1
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 3.1.1

Listing 32-2.Adding Packages to the Project

如果您使用的是 Visual Studio,则可以通过选择“项目➤管理 NuGet 包”来添加包。注意选择要添加到项目中的包的正确版本。

如果您没有遵循前面章节中的示例,您将需要安装用于创建和管理实体框架核心迁移的全局工具包。运行清单 32-3 中所示的命令,删除软件包的任何现有版本,并安装本书所需的版本。

dotne.t tool uninstall --global dotnet-ef
dotnet tool install --global dotnet-ef --version 3.1.1

Listing 32-3.Installing a Global Tool Package

添加数据模型

这个应用的数据模型将由三个类组成,分别代表人、他们工作的部门以及他们的位置。创建一个Models文件夹,并用清单 32-4 中的代码向其中添加一个名为Person.cs的类文件。

using System.Collections.Generic;

namespace Advanced.Models {

    public class Person {

        public long PersonId { get; set; }
        public string Firstname { get; set; }
        public string Surname { get; set; }
        public long DepartmentId { get; set; }
        public long LocationId { get; set; }

        public Department Department {get; set; }
        public Location Location { get; set; }
    }
}

Listing 32-4.The Contents of the Person.cs File in the Models Folder

将名为Department.cs的类文件添加到Models文件夹中,并使用它来定义清单 32-5 中所示的类。

using System.Collections.Generic;

namespace Advanced.Models {
    public class Department {

        public long Departmentid { get; set; }
        public string Name { get; set; }

        public IEnumerable<Person> People { get; set; }
    }
}

Listing 32-5.The Contents of the Department.cs File in the Models Folder

将名为Location.cs的类文件添加到Models文件夹中,并使用它来定义清单 32-6 中所示的类。

using System.Collections.Generic;

namespace Advanced.Models {
    public class Location {

        public long LocationId { get; set; }
        public string City { get; set; }
        public string State { get; set; }

        public IEnumerable<Person> People { get; set; }
    }
}

Listing 32-6.The Contents of the Location.cs File in the Models Folder

三个数据模型类中的每一个都定义了一个键属性,当存储新对象时,数据库将分配该键属性的值,并且定义了定义类之间关系的外键属性。这些由导航属性补充,导航属性将与实体框架核心Include方法一起使用,以将相关数据合并到查询中。

为了创建将提供对数据库访问的实体框架核心上下文类,将名为DataContext.cs的文件添加到Models文件夹中,并添加清单 32-7 中所示的代码。

using Microsoft.EntityFrameworkCore;

namespace Advanced.Models {
    public class DataContext: DbContext {

        public DataContext(DbContextOptions<DataContext> opts)
            : base(opts) { }

        public DbSet<Person> People { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Location> Locations { get; set; }
    }
}

Listing 32-7.The Contents of the DataContext.cs File in the Models Folder

上下文类定义了用于查询数据库中的PersonDepartmentLocation数据的属性。

准备种子数据

将名为SeedData.cs的类添加到Models文件夹中,并添加清单 32-8 中所示的代码,以定义将用于填充数据库的种子数据。

using Microsoft.EntityFrameworkCore;
using System.Linq;

namespace Advanced.Models {
    public static class SeedData {

        public static void SeedDatabase(DataContext context) {
            context.Database.Migrate();
            if (context.People.Count() == 0 && context.Departments.Count() == 0 &&
                context.Locations.Count() == 0) {

                Department d1 = new Department { Name = "Sales" };
                Department d2 = new Department { Name = "Development" };
                Department d3 = new Department { Name = "Support" };
                Department d4 = new Department { Name = "Facilities" };

                context.Departments.AddRange(d1, d2, d3, d4);
                context.SaveChanges();

                Location l1 = new Location { City = "Oakland", State = "CA" };
                Location l2 = new Location { City = "San Jose", State = "CA" };
                Location l3 = new Location { City = "New York", State = "NY" };
                context.Locations.AddRange(l1, l2, l3);

                context.People.AddRange(
                    new Person {
                        Firstname = "Francesca", Surname = "Jacobs",
                        Department = d2, Location = l1
                    },
                    new Person {
                        Firstname = "Charles", Surname = "Fuentes",
                        Department = d2, Location = l3
                    },
                    new Person {
                        Firstname = "Bright", Surname = "Becker",
                        Department = d4, Location = l1
                    },
                    new Person {
                        Firstname = "Murphy", Surname = "Lara",
                        Department = d1, Location = l3
                    },
                    new Person {
                        Firstname = "Beasley", Surname = "Hoffman",
                        Department = d4, Location = l3
                    },
                    new Person {
                        Firstname = "Marks", Surname = "Hays",
                        Department = d4, Location = l1
                    },
                    new Person {
                        Firstname = "Underwood", Surname = "Trujillo",
                        Department = d2, Location = l1
                    },
                    new Person {
                        Firstname = "Randall", Surname = "Lloyd",
                        Department = d3, Location = l2
                    },
                    new Person {
                        Firstname = "Guzman", Surname = "Case",
                        Department = d2, Location = l2
                    });
                context.SaveChanges();
            }
        }
    }
}

Listing 32-8.The Contents of the SeedData.cs File in the Models Folder

静态SeedDatabase方法确保所有挂起的迁移都已经应用到数据库。如果数据库是空的,它会植入数据。实体框架核心将负责将对象映射到数据库的表中,并且在存储数据时将自动分配关键属性。

配置实体框架核心服务和中间件

对清单 32-9 中所示的Startup类进行修改,该类配置实体框架核心并设置本书这一部分将用来访问数据库的DataContext服务。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Advanced.Models;

namespace Advanced {
    public class Startup {

        public Startup(IConfiguration config) {
            Configuration = config;
        }

        public IConfiguration Configuration { get; set; }

        public void ConfigureServices(IServiceCollection services) {
            services.AddDbContext<DataContext>(opts => {
                opts.UseSqlServer(Configuration[
                    "ConnectionStrings:PeopleConnection"]);
                opts.EnableSensitiveDataLogging(true);
            });
        }

        public void Configure(IApplicationBuilder app, DataContext context) {

            app.UseDeveloperExceptionPage();
            app.UseRouting();

            app.UseEndpoints(endpoints => {
                endpoints.MapGet("/", async context => {
                    await context.Response.WriteAsync("Hello World!");
                });
            });

            SeedData.SeedDatabase(context);
        }
    }
}

Listing 32-9.Preparing Services and Middleware in the Startup.cs File in the Advanced Folder

为了定义将用于应用数据的连接字符串,在appsettings.json文件中添加清单 32-10 中所示的配置设置。连接字符串应该在一行中输入。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "PeopleConnection": "Server=(localdb)\\MSSQLLocalDB;Database=People;MultipleActiveResultSets=True"
  }
}

Listing 32-10.Defining a Connection String in the appsettings.json File in the Advanced Folder

除了连接字符串之外,清单 32-10 增加了实体框架核心的日志细节,以便发送到数据库的 SQL 查询被记录。

创建和应用迁移

要创建将建立数据库模式的迁移,请使用 PowerShell 命令提示符来运行在Advanced项目文件夹中的清单 32-11 中所示的命令。

dotnet ef migrations add Initial

Listing 32-11.Creating an Entity Framework Core Migration

创建迁移后,使用清单 32-12 中所示的命令将其应用到数据库。

dotnet ef database update

Listing 32-12.Applying the Migration to the Database

应用显示的日志消息将显示发送到数据库的 SQL 命令。

Note

如果需要重置数据库,那么运行dotnet ef database drop --force命令,然后运行清单 32-12 中的命令。

添加引导 CSS 框架

按照前面章节中建立的模式,我将使用 Bootstrap CSS 框架来设计示例应用生成的 HTML 元素的样式。要安装引导包,运行Advanced项目文件夹中清单 32-13 所示的命令。这些命令依赖于库管理器包。

libman init -p cdnjs
libman install twitter-bootstrap@4.3.1 -d wwwroot/lib/twitter-bootstrap

Listing 32-13.Installing the Bootstrap CSS Framework

如果您使用的是 Visual Studio,则可以通过在解决方案资源管理器中右键单击“高级”项目项并从弹出菜单中选择“添加➤客户端库”来安装客户端包。

配置服务和中间件

我将在这个项目中启用运行时 Razor 视图编译。在Advanced项目文件夹中运行清单 32-14 中所示的命令,安装将提供运行时编译服务的包。

dotnet add package Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation --version 3.1.1

Listing 32-14.Adding a Package to the Example Project

本书这一部分中的示例应用将使用 MVC 控制器和 Razor 页面来响应请求。将清单 32-15 中所示的语句添加到Startup类中,以配置应用将使用的服务和中间件。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Advanced.Models;

namespace Advanced {
    public class Startup {

        public Startup(IConfiguration config) {
            Configuration = config;
        }

        public IConfiguration Configuration { get; set; }

        public void ConfigureServices(IServiceCollection services) {
            services.AddDbContext<DataContext>(opts => {
                opts.UseSqlServer(Configuration[
                    "ConnectionStrings:PeopleConnection"]);
                opts.EnableSensitiveDataLogging(true);
            });
            services.AddControllersWithViews().AddRazorRuntimeCompilation();
            services.AddRazorPages().AddRazorRuntimeCompilation();
        }

        public void Configure(IApplicationBuilder app, DataContext context) {

            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseRouting();

            app.UseEndpoints(endpoints => {
                endpoints.MapControllerRoute("controllers",
                    "controllers/{controller=Home}/{action=Index}/{id?}");
                endpoints.MapDefaultControllerRoute();
                endpoints.MapRazorPages();
            });

            SeedData.SeedDatabase(context);
        }
    }
}

Listing 32-15.Adding Services and Middleware in the Startup.cs File in the Advanced Folder

除了默认的控制器路由之外,我还添加了一个匹配以controllers开头的 URL 路径的路由,这将使后面章节中的例子在控制器和 Razor 页面之间切换时更容易理解。这是我在前面章节中采用的相同约定,我将以/pages开头的 URL 路径路由到 Razor 页面。

创建控制器和视图

要使用控制器显示应用的数据,在Advanced项目文件夹中创建一个名为Controllers的文件夹,并向其中添加一个名为HomeController.cs的类文件,其内容如清单 32-16 所示。

using Advanced.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;

namespace Advanced.Controllers {
    public class HomeController : Controller {
        private DataContext context;

        public HomeController(DataContext dbContext) {
            context = dbContext;
        }

        public IActionResult Index([FromQuery] string selectedCity) {
            return View(new PeopleListViewModel {
                People = context.People
                    .Include(p => p.Department).Include(p => p.Location),
                Cities = context.Locations.Select(l => l.City).Distinct(),
                SelectedCity = selectedCity
            });
        }
    }

    public class PeopleListViewModel {
        public IEnumerable<Person> People { get; set; }
        public IEnumerable<string> Cities { get; set; }
        public string SelectedCity { get; set; }

        public string GetClass(string city) =>
            SelectedCity == city ? "bg-info text-white" : "";
    }
}

Listing 32-16.The Contents of the HomeController.cs File in the Controllers Folder

为了给控制器提供一个视图,创建Views/Home文件夹并添加一个名为Index.cshtml的 Razor 视图,其内容如清单 32-17 所示。

@model PeopleListViewModel

<h4 class="bg-primary text-white text-center p-2">People</h4>

<table class="table table-sm table-bordered table-striped">
    <thead>
        <tr>
            <th>ID</th><th>Name</th><th>Dept</th><th>Location</th>
        </tr>
    </thead>
    <tbody>
        @foreach (Person p in Model.People) {
            <tr class="@Model.GetClass(p.Location.City)">
                <td>@p.PersonId</td>
                <td>@p.Surname, @p.Firstname</td>
                <td>@p.Department.Name</td>
                <td>@p.Location.City, @p.Location.State</td>
            </tr>
        }
    </tbody>
</table>

<form asp-action="Index" method="get">
    <div class="form-group">
        <label for="selectedCity">City</label>
        <select name="selectedCity" class="form-control">
            <option disabled selected>Select City</option>
            @foreach (string city in Model.Cities) {
                <option selected="@(city == Model.SelectedCity)">
                    @city
                </option>
            }
        </select>
    </div>
    <button class="btn btn-primary" type="submit">Select</button>
</form>

Listing 32-17.The Contents of the Index.cshtml File in the Views/Home Folder

为了启用标签助手并添加视图中默认可用的名称空间,将名为_ViewImports.cshtml的 Razor 视图导入文件添加到Views文件夹中,其内容如清单 32-18 所示。

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using Advanced.Models
@using Advanced.Controllers

Listing 32-18.The Contents of the _ViewImports.cshtml File in the Views Folder

为了指定控制器视图的默认布局,将一个名为_ViewStart.cshtml的 Razor 视图开始启动文件添加到Views文件夹中,其内容如清单 32-19 所示。

@{
    Layout = "_Layout";
}

Listing 32-19.The Contents of the _ViewStart.cshtml File in the Views Folder

要创建布局,创建Views/Shared文件夹并添加一个名为_Layout.cshtml的 Razor 布局,其内容如清单 32-20 所示。

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div class="m-2">
        @RenderBody()
    </div>
</body>
</html>

Listing 32-20.The Contents of the _Layout.cshtml File in the Views/Shared Folder

创建 Razor 页面

要使用 Razor 页面显示应用的数据,创建Pages文件夹并向其中添加一个名为Index.cshtml的 Razor 页面,其内容如清单 32-21 所示。

@page "/pages"
@model IndexModel

<h4 class="bg-primary text-white text-center p-2">People</h4>

<table class="table table-sm table-bordered table-striped">
    <thead>
        <tr>
            <th>ID</th><th>Name</th><th>Dept</th><th>Location</th>
        </tr>
    </thead>
    <tbody>
        @foreach (Person p in Model.People) {
            <tr class="@Model.GetClass(p.Location.City)">
                <td>@p.PersonId</td>
                <td>@p.Surname, @p.Firstname</td>
                <td>@p.Department.Name</td>
                <td>@p.Location.City, @p.Location.State</td>
            </tr>
        }
    </tbody>
</table>

<form asp-page="Index" method="get">
    <div class="form-group">
        <label for="selectedCity">City</label>
        <select name="selectedCity" class="form-control">
            <option disabled selected>Select City</option>
            @foreach (string city in Model.Cities) {
                <option selected="@(city == Model.SelectedCity)">
                    @city
                </option>
            }
        </select>
    </div>
    <button class="btn btn-primary" type="submit">Select</button>
</form>

@functions {

    public class IndexModel: PageModel {
        private DataContext context;

        public IndexModel(DataContext dbContext) {
            context = dbContext;
        }

        public IEnumerable<Person> People { get; set; }

        public IEnumerable<string> Cities { get; set; }

        [FromQuery]
        public string SelectedCity { get; set; }

        public void OnGet() {
            People = context.People.Include(p => p.Department)
                .Include(p => p.Location);
            Cities = context.Locations.Select(l => l.City).Distinct();
        }

        public string GetClass(string city) =>
            SelectedCity == city ? "bg-info text-white" : "";
    }
}

Listing 32-21.The Contents of the Index.cshtml File in the Pages Folder

要启用标签助手并添加 Razor 页面的视图部分中默认可用的名称空间,将名为_ViewImports.cshtml的 Razor 视图导入文件添加到Pages文件夹中,其内容如清单 32-22 所示。

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using Advanced.Models
@using Microsoft.AspNetCore.Mvc.RazorPages
@using Microsoft.EntityFrameworkCore

Listing 32-22.The Contents of the _ViewImports.cshtml File in the Pages Folder

为了指定 Razor 页面的默认布局,将一个名为_ViewStart.cshtml的 Razor 视图开始文件添加到Pages文件夹中,其内容如清单 32-23 所示。

@{
    Layout = "_Layout";
}

Listing 32-23.The Contents of the _ViewStart.cshtml File in the Pages Folder

为了创建布局,将一个名为_Layout.cshtml的 Razor 布局添加到Pages文件夹中,其内容如清单 32-24 所示。

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div class="m-2">
        <h5 class="bg-secondary text-white text-center p-2">Razor Page</h5>
        @RenderBody()
    </div>
</body>
</html>

Listing 32-24.The Contents of the _Layout.cshtml File in the Pages Folder

运行示例应用

通过从调试菜单中选择启动而不调试或运行而不调试,或者通过运行在Advanced项目文件夹中的清单 32-25 中显示的命令,启动应用。

dotnet run

Listing 32-25.Running the Example Application

使用浏览器请求http://localhost:5000/controllershttp://localhost:5000/pages。使用 select 元素选择一个城市,点击 Select 按钮高亮显示表格中的行,如图 32-3 所示。

img/338050_8_En_32_Fig3_HTML.jpg

图 32-3。

运行示例应用

摘要

在这一章中,我展示了如何创建贯穿本书这一部分的示例应用。该项目是用空模板创建的,它包含一个依赖于实体框架核心的数据模型,并使用控制器和 Razor 页面处理请求。在下一章中,我将介绍 Blazor,它是 ASP.NET Core 的新成员。