四、ASP.NET Core 2.0 的基本概念——第 1 部分

在最后三章中,你已经看到了 ASP.NET Core 2.0 是关于从全局的角度,以及设置你的开发环境,包括 Visual Studio 2017(或 Visual Studio Code)和一个与 Git 存储库持续集成和持续交付的 VSTS 管道。

这很有趣,但也很理论化。 现在,是时候做一些实际的事情了,是时候付诸行动了,是时候建立一些自己的东西了!

在本章中,我们将构建一个应用来展示 ASP 的基本概念.NET Core 2.0 框架。 在接下来的章节中,我们将不断地改进这个应用,同时使用和说明 ASP 的各种特性.NET Core 2.0 及其相关技术。

在本章中,我们将涵盖以下主题:

  • StartupProgram
  • 创建页面和服务
  • 使用 Bower 和布局页面
  • 使用依赖注入
  • 使用内置中间件
  • 创建自己的中间件
  • 使用静态文件
  • 使用路由、URL 重定向和 URL 重写
  • 错误处理和模型验证

创建井字游戏

让我们做些有趣的事吧! 让我们来构建一字棋游戏,也被称为 noughts and cross 或 Xs and Os。 玩家将选择谁接受 x,谁接受 o。 然后,他们将轮流在 3×3 网格中标记空间,每回合一个标记。 在水平、垂直或对角线上成功放置三个标记的玩家获胜。

玩家必须输入他们的电子邮件和名字注册创建一个帐户才能开始游戏。 每一场比赛后,他们都会收到一个比赛分数,这个分数会被加到他们的总分中。

排行榜提供有关玩家排名和最高分数的信息。

为了创建游戏,玩家必须向其他玩家发送邀请,然后一个特定的等待页面会显示给他,直到其他玩家做出回应。 其他玩家在收到邀请邮件后,可以确认请求并加入游戏。 当两名玩家在线时,游戏开始。

如上一章所述,我们可以使用 VSTS 及其工作项来组织和安排Tic-Tac-Toe应用的实现。 为此,我们必须创建史诗、特性和产品待办事项列表项,然后进行 sprint 计划,确定优先级,并决定首先执行什么。

正如你在下面的截图中看到的,我们已经决定在第一个 sprint 中处理 5 个产品待办事项列表,并将它们添加到 sprint 待办事项列表中:

你还记得在实现任何新特性之前,接下来需要做什么吗? 你不记得了? 也许特征分支听起来很耳熟?

在上一章中,我们展示了创建开发的最佳实践,它们是独立的,易于维护和发布。 它们包括在 Git 存储库中为每个要添加到应用的新特性创建一个特性分支。

因此,每个开发人员都可以在自己的特定特性分支中处理自己的特定特性,直到他决定可以发布它为止。

最后,所有准备发布的特性都合并到开发(或发布或主)分支中。 然后完成集成测试,如果一切正常,就会交付一个新的应用版本。

我们选择首先处理的特性是用户注册,所以我们要做的第一件事就是创建一个名为 FEA-UserRegistration 的特性分支。 如果你不知道怎么做,你可以转到第 3 章在 VSTS 中创建一个持续集成管道,并得到一个完整的步骤步骤和详细的解释:

构思并实现你的第一个井字游戏功能

在实现用户注册特性之前,我们必须理解它并决定一切应该如何工作。 我们必须定义用户描述和工作流。 为此,我们需要更详细地分析前面提到的Tic-Tac-Toe游戏描述。

如前所述,用户只有拥有用户帐号才能创建并加入游戏。 要创建这个帐户,他必须输入他的名字,他的姓氏,他的电子邮件地址,和一个新的密码。 然后系统验证输入的电子邮件地址是否已经注册。 一个指定的电子邮件地址只能注册一次。 如果电子邮件地址是新的,则生成用户帐户,如果电子邮件地址是已知的,则必须显示一个错误。

让我们来看看用户注册过程和实现它所需要的不同组件:

  1. 主页上有一个用户注册的链接,新用户必须点击注册来创建自己的玩家账户。 单击用户注册链接将用户重定向到专用的注册页面。
  2. 注册页面包含一个注册表单,用户必须在其中输入个人信息,然后进行确认。
  3. JavaScript 客户机验证表单,提交数据并将其发送给通信中间件,然后等待结果。
  4. 通信中间件接收请求并将其路由到注册服务。
  5. 注册服务接收请求,验证数据完整性,检查电子邮件是否已经用于注册,然后注册用户或返回错误消息。
  6. 通信中间件接收结果并将其路由到等待的 JavaScript 客户机。
  7. JavaScript 客户端重定向用户,以便如果结果是成功,他可以开始玩游戏,如果结果是失败,它将显示一条错误消息。

下面的序列图显示了用户注册过程。 它是更容易和更快的理解与更直观的表现:

首先,我们需要创建一个新的空 ASP.NET Core 2.0 Web Application,它将在本章和本书的其余部分用于添加各种组件和包。 然后,我们将逐步添加新的概念和功能,这将让您真正了解发生了什么,以及一切如何工作:

  1. 启动 Visual Studio 2017,点击文件|新建|项目。
  2. 在。net Core 部分选择 ASP.NET Core Web Application,输入应用名称、存储库的位置和解决方案名称,然后单击 OK:

Note that if you have not created a Git repository for your application code yet, you can do it here by ticking the Create new Git repository checkbox.

  1. 选择空模板:

  1. 一个新的空 ASP.NET Core 2.0 Web Application 项目将生成,只包含Program.csStartup.cs文件:

太好了,我们已经创建了我们的项目,现在准备实现我们的第一个功能! 但在此之前,让我们花些时间看看 Visual Studio 2017 在幕后为我们做了什么。

针对项目的.csproj 文件中的不同。net 框架版本

对于 Visual Studio 2017 生成的每个项目,它都会创建一个相应的.csproj文件,该文件包含多个项目范围的设置,例如引用程序集、. net 框架目标版本、包含的文件和文件夹,以及多个其他。

例如,当打开 ASP.NET Core 2.0 项目,你可以看到以下结构:

    <Project Sdk="Microsoft.NET.Sdk.Web"> 
      <PropertyGroup> 
        <TargetFramework>netcoreapp2.0</TargetFramework> 
      </PropertyGroup> 
      <ItemGroup> 
        <Folder Include="wwwroot\" /> 
      </ItemGroup> 
      <ItemGroup> 
        <PackageReference Include="Microsoft.AspNetCore.All"
         Version="2.0.0-preview2-final" /> 
      </ItemGroup> 
    </Project> 

您可以看到TargetFramework设置,它允许您定义应该包含哪些. net 框架版本,以及用于构建和执行源代码。

在我们的例子中,它被设置为netcoreapp2.0,这是使用。net Core 2.0 框架的特定值:

    <TargetFramework>netcoreapp2.0</TargetFramework>

Note that you can refer to multiple .NET Framework versions within your library projects. In this case, you have to replace the TargetFramework element with the TargetFrameworks element.

For instance, if you want to cross-target .NET Core 2.0 and .NET 4.7, you have to use the following settings:

<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>

在调试模式下按F5键执行应用时,可以看到应用的Debug文件夹(\bin\Debug)中已经创建了多个文件夹和文件:

如果您更改了.csproj文件并添加了其他目标框架,您将看到将生成其他文件夹。 然后,每个特定。net 框架版本的 dll 被放入相应的文件夹中。 下面的示例使用了。net Core 和。net 4.7 的TargetFrameworks设置:

使用 Microsoft.AspNetCore.All 元包

在依赖项| NuGet 部分的解决方案资源管理器中,您可以看到一些非常有趣的东西,特别是针对 ASP.NET Core 2.0 项目:Microsoft.AspNetCore.All元包:

Microsoft.AspNetCore.All项目依赖项是在您创建 ASP. php 时自动添加的.NET Core 2.0 Web 应用。 对于这种类型的项目,默认情况下会这样做。

然而,Microsoft.AspNetCore.All不是一个标准的 NuGet 包,因为它不包含任何代码或 dll。 相反,它充当一个元包,引用它所依赖的其他包。 更具体地说,它包含了 ASP 的所有包.NET Core 和实体框架 Core,以及它们的内部和外部依赖,并利用。NET Core 运行时存储。

在这个示例中,您可以看到检索到各种各样的包,例如 Application Insights、Authentication、Authorization、Azure App Services 等。

在旧版本的。net Core(版本 1.0 和 1.1)中,你必须自己添加这些 NuGet 包。 现在微软已经创建了 ASP 的概念.NET Core 元包,你可以在一个地方找到所有东西。

此外,包裁减排除了未使用的二进制文件,因此在部署应用时不会发布它们。

使用 Program 类

Program类是 ASP 的主要入口.NET Core 2.0 应用。 事实上,ASP。 在这方面,NET Core 2.0 应用非常类似于标准的。NET Framework 控制台应用。 两者都有一个在运行应用时执行的Main方法。 即使是接受字符串数组作为参数的Main方法的基本签名也是一样的,如下面的代码中所示。 毫无疑问,这是由于 ASP.NET Core 应用实际上是一个托管 web 应用的控制台应用:

    using Microsoft.AspNetCore; 
    using Microsoft.AspNetCore.Hosting; 

    namespace TicTacToe 
    { 
      public class Program 
      { 
        public static void Main(string[] args) 
        { 
            BuildWebHost(args).Run(); 
        } 

        public static IWebHost BuildWebHost(string[] args) => 
            WebHost.CreateDefaultBuilder(args) 
                .UseStartup<Startup>() 
                .Build(); 
      } 
    } 

通常,您不需要以任何方式接触Program类。 默认情况下,运行应用所需的所有东西都已经在那里并预先配置好了。

但是,您可能希望激活一些更高级的功能。

例如,您可以在服务器启动期间启用错误捕获并显示一个错误页面。 在这种情况下,你只需要使用以下指令:

    WebHost.CaptureStartupErrors(true); 

默认情况下,此设置是不启用的,这意味着在发生错误时,主机将退出。 这可能不是我们想要的行为,我们建议相应地修改这个参数。

另外两个有用的参数是PreferHostingUrlsUseUrls。 您可以指示主机是否应该侦听由您提供的Microsoft.AspNetCore.Hosting.Server.IServeror特定 url 定义的标准 url。 根据您的需要,url 可以有不同的格式,例如:

  • 带有主机和端口的 IPV4 地址(例如https://192.168.57.12:5000)
  • 带端口的 IPV6 地址(例如https://[0:0:0:0:0:ffff:4137:270a]:5500)
  • 主机名(例如https://mycomputer:90)
  • Localhost(例如,https://localhost:443)
  • Unix 套接字(例如,http://unix:/run/dan-live.sock)

下面是一个如何设置这些参数的例子:

    WebHost.PreferHostingUrls(true); 
    WebHost.UseUrls("http://localhost:5000"); 

最后,通过设置以下参数,您可以将应用与 Application Insights 集成,这是一个可扩展的应用性能管理服务,允许在运行时监视应用并检测性能异常,以及诊断问题和了解用户的操作:

    WebHost.UseApplicationInsights();

下面是一个Program类的例子,它包含了前面展示的所有概念:

    public class Program 
    { 
      public static void Main(string[] args) 
      { 
        BuildWebHost(args).Run(); 
      } 

      public static IWebHost BuildWebHost(string[] args) => 
        WebHost.CreateDefaultBuilder(args) 
            .CaptureStartupErrors(true) 
            .UseStartup<Startup>() 
            .PreferHostingUrls(true) 
            .UseUrls("http://localhost:5000") 
            .UseApplicationInsights() 
            .Build(); 
    } 

与 Startup 类一起工作

另一个自动生成的元素,它存在于所有类型的 ASP.NET Core 2.0 项目,是Startup类。 正如前面看到的,Program类主要处理宿主环境周围的一切。 类是关于服务和中间件的预加载和配置的。 这两个类是所有 ASP 的基础.NET Core 2.0 应用。

让我们来看看Startup类的基本结构,以便更好地理解它提供了什么以及如何最好地利用它的功能:

    using Microsoft.AspNetCore.Builder; 
    using Microsoft.AspNetCore.Hosting; 
    using Microsoft.AspNetCore.Http; 
    using Microsoft.Extensions.DependencyInjection; 

    namespace TicTacToe 
    { 
      public class Startup 
      { 
        public void ConfigureServices(IServiceCollection services) 
        { 
        } 

        public void Configure(IApplicationBuilder app,
         IHostingEnvironment env) 
        { 
          if (env.IsDevelopment()) 
          { 
            app.UseDeveloperExceptionPage(); 
          } 

          app.Run(async (context) => 
          { 
            await context.Response.WriteAsync("Hello World!"); 
          }); 
        } 
      } 
    } 

有两种方法需要你的注意,因为你会经常使用它们:

  • ConfigureServices方法,由运行时调用,用于向容器添加服务
  • 用于配置 HTTP 管道的Configure方法

我们在本章开始时说过,我们需要更多的实际工作,所以让我们回到我们的井字游戏,看看如何使用Startup类在一个真实的例子!

我们将使用 MVC 来实现应用,但由于您已经使用了空白的 ASP.NET Core 2.0 Web 应用模板,Visual Studio 2017 在项目生成过程中没有添加任何内容。 你必须自己添加一切; 这是一个很好的机会,可以更好地了解一切事物是如何运作的!

首先要做的是将 MVC 添加到服务配置中。 你可以使用ConfigureServices方法,然后添加 MVC 中间件:

    public void ConfigureServices(IServiceCollection services) 
    { 
      services.AddMvc(); 
    } 

你可能会说这太简单了,那么有什么问题呢? 没有圈套! 一切都在 ASP.NET Core 2.0 是围绕着简单性、清晰性和开发人员生产力而开发的。

你可以在配置 MVC 中间件和设置路由路径时再次看到这一点(我们将在后面更详细地解释路由):

    app.UseMvc(routes => 
    { 
      routes.MapRoute( 
        name: "default", 
        template: "{controller=Home}/{action=Index}/{id?}"); 
    }); 

再次,非常清晰和简短的说明,使我们作为开发人员的生活更容易和更高效。 现在是成为一名开发人员的好时机!

在下一步中,您需要在您的 ASP 中启用静态内容的使用.NET Core 2.0 应用能够使用 HTML, CSS, JavaScript 和图像。

你知道怎么做吗? 是的,您是对的,您需要添加另一个中间件。 你可以像之前一样调用相应的 app 方法:

    app.UseStaticFiles(); 

作为一名开发人员,您需要能够快速分析和理解 HTML、CSS 和 JavaScript 行为和问题。 为此,ASP.NET Core 2.0 包含一个非常方便的特性浏览器链接。 启用后,它会在 Visual Studio 2017 之间建立一个专用的通信通道,以提高开发人员的生产力。

启用浏览器链接真的很容易:

    app.UseBrowserLink(); 

下面是一个示例,你可以在井字游戏中使用Startup.cs类,在配置了前面看到的各种服务设置后:

    public class Startup 
    { 
      public void ConfigureServices(IServiceCollection services) 
      { 
        services.AddMvc(); 
      } 

      public void Configure(IApplicationBuilder app,
       IHostingEnvironment env) 
      { 
        if (env.IsDevelopment()) 
        { 
          app.UseDeveloperExceptionPage(); 
          app.UseBrowserLink(); 
        } 
        else 
        { 
          app.UseExceptionHandler("/Home/Error"); 
        } 

        app.UseStaticFiles(); 

        app.UseMvc(routes => 
        { 
          routes.MapRoute( 
            name: "default", 
              template: "{controller=Home}/{action=Index}/{id?}"); 
        }); 
      } 
    } 

编制项目基本结构

你肯定想看到一些东西运行,并创建井字游戏。 现在我们已经从功能的角度定义了一切应该如何工作,我们需要从创建应用的基本项目结构开始。

ASP.NET Core 2.0 web 应用,最好使用以下项目结构:

  • 一个包含应用的所有控制器的文件夹。
  • 一个文件夹Services,包含应用的所有服务(例如,外部通信服务)。
  • 包含应用所有视图的Views文件夹。 这个文件夹应该包含一个Shared子文件夹以及每个控制器一个文件夹。
  • 一个_ViewImports.cshtml文件,用于定义在所有视图中可用的一些名称空间。
  • 一个_ViewStart.cshtml文件,用于定义在每个视图呈现开始时执行的一些代码(例如,为所有视图设置布局页面)。
  • 一个_Layout.cshtml文件,用于定义所有视图的公共布局。

让我们创建项目结构:

  1. 打开 Visual Studio 2017,打开井字游戏 您创建的 NET Core 2.0 项目,创建三个新文件夹ControllersServicesViews,并在Views文件夹中创建一个子文件夹Shared:

  1. Views文件夹中创建一个新的视图页面_ViewImports.cshtml:
        @using TicTacToe 
        @addTagHelper*, Microsoft.AspNetCore.Mvc.TagHelpers 
  1. Views文件夹中创建一个新的视图页面_ViewStart.cshtml:
        @{ Layout = "~/Views/Shared/_Layout.cshtml"; }
  1. 右键单击Views/Shared文件夹,选择“添加|新项目”,在搜索框中输入Layout,选择“MVC 视图布局页面”,单击“添加”:

Note that the layout page concept will be detailed a little bit later in this chapter, but don't worry too much, it is not a very complicated concept.

创建井字游戏主页

既然基本的项目结构已经到位,我们需要实现不同的组件,需要一起工作,以提供Tic-Tac-Toe游戏 web 应用:

  1. 如前所述,更新Program.csStartup.cs文件。
  2. 添加一个新控制器,在Controllers文件夹的解决方案资源管理器中右键单击,然后选择 Add |控制器:

  1. 在弹出的 Add Scaffold 窗口中,选择 MVC Controller - Empty 并将新控制器命名为HomeController:

  1. 您的 MVC 主控制器将自动生成,其中包含一个方法。 现在需要添加相应的视图,方法是右键单击Index方法名,并从菜单中选择 add view:

  1. 添加视图窗口有助于定义需要生成的内容。 保留默认模板为空,并启用我们将在本章下一节中修改的布局页面:

  1. 祝贺您,您的视图将自动生成,您可以通过按F5来测试您的应用。 我们将在本章稍后完成:

通过使用 Bower 和布局页面,使您的网页具有更现代的外观

在上一节中,您了解了如何创建一个基本的 web 页面。 知道如何在技术上做到这一点是一回事,但创建成功的 web 应用不仅在于技术实现,还在于如何使您的应用具有视觉吸引力和用户友好性。 虽然这本书不是关于网页设计和用户体验的,但我们想给你一些快速和简单的方法来构建更好的 web 应用。

为此,我们建议使用 Bower(https://bower.io),即 Web 中自称的包管理器,与 ASP 结合使用.NET Core 布局页面。

在过去的几年中,Bower 在 web 开发社区中取得了一些显著的成功。 它有助于安装带有静态内容(如 HTML、CSS、JavaScript、字体和图像)的客户端包,包括它们的依赖项。

Visual Studio 2017 中对 Bower 有一些很棒的集成和支持; 您只需要正确地配置它,以有效地使用它。 让我们看看怎么做:

  1. 右键单击“一字棋”项目,选择“添加|新项目”,在搜索框中输入Bower,选择“凉亭配置文件”,单击“添加”:

  1. 添加 Bower 配置文件应该已经添加了一个bower.json文件。 用以下内容更新此文件:
        { 
          "name": "asp.net", 
          "private": true, 
          "dependencies": { 
            "bootstrap": "3.3.6", 
            "jquery": "2.2.0", 
            "jquery-validation": "1.14.0", 
            "jquery-validation-unobtrusive": "3.2.6" 
          } 
        }
  1. 添加 Bower 配置文件应该已经添加了一个.bowerrc文件。 更新这个文件并定义资产应该放置的目录:
        { 
          "directory": "wwwroot/lib" 
        } 
  1. 右键单击bower.json文件,点击恢复包:

  1. 客户端包(bootstrapjquery和更多)被下载到您已经定义的文件夹(wwwroot/lib)中。 静态内容现在可以在你的应用中使用:

  1. wwwroot文件夹中,创建一个名为css的文件夹。 在这个文件夹中添加一个新的样式表site.css:
        body { 
          padding-top: 50px; 
          padding-bottom: 20px; 
        } 

        /* Set padding to keep content from hitting the edges */ 
        .body-content { 
          padding-left: 15px; 
          padding-right: 15px; 
        } 

        /* Set width on the form input elements since they're 100% wide
           by default */ 
        input, 
        select, 
        textarea { 
          max-width: 280px; 
        } 

        /* styles for validation helpers */ 
        .field-validation-error { 
          color: #b94a48; 
        } 

        .field-validation-valid { 
          display: none; 
        } 

        input.input-validation-error { 
          border: 1px solid #b94a48; 
        } 

        input[type="checkbox"].input-validation-error { 
          border: 0 none; 
        } 

        .validation-summary-errors { 
          color: #b94a48; 
        } 

        .validation-summary-valid { 
          display: none; 
        } 

一个成功的 web 应用应该有一个共同的布局和一致的用户体验时,从一个页面导航到另一个页面。 这是用户采用和用户满意度的关键。 ASP.NET Core 布局页面是正确的解决方案。

它们可以用于在 web 应用中定义视图模板。 您的所有视图可以使用相同的模板,也可以根据您的特定需求使用不同的模板。

我们将使用更新后的布局页面,如下所示,用于我们的示例应用:

    <!DOCTYPE html> 
    <html> 
    <head> 
      <meta charset="utf-8" /> 
      <meta name="viewport" content="width=device-width,
       initial-scale=1.0" /> 
      <title>@ViewData["Title"] - TicTacToe</title> 

      <environment include="Development"> 
        <link rel="stylesheet"
         href="~/lib/bootstrap/dist/css/bootstrap.css" /> 
        <link rel="stylesheet" href="~/css/site.css" /> 
      </environment> 
      <environment exclude="Development"> 
        <link rel="stylesheet"
         href="https://ajax.aspnetcdn.com/ajax/bootstrap
         /3.3.7/css/bootstrap.min.css" 
         asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css" 
         asp-fallback-test-class="sr-only"
         asp-fallback-test-property="position" asp-fallback-test-
          value="absolute" /> 
        <link rel="stylesheet" href="~/css/site.min.css"
         asp-append-version="true" /> 
      </environment> 
    </head> 
    <body> 
      <nav class="navbar navbar-inverse navbar-fixed-top"> 
        <div class="container"> 
        <div class="navbar-header"> 
         <button type="button" class="navbar-toggle"
           data-toggle="collapse" data-target=".navbar-collapse"> 
          <span class="sr-only">Toggle navigation</span> 
          <span class="icon-bar"></span> 
          <span class="icon-bar"></span> 
          <span class="icon-bar"></span> 
         </button> 
         <a asp-area="" asp-controller="Home" asp-action="Index"
          class="navbar-brand">Tic-Tac-Toe</a> 
        </div> 
        <div class="navbar-collapse collapse"> 
         <ul class="nav navbar-nav"> 
           <li><a asp-area="" asp-controller="Home"
             asp-action="Index">Home</a></li> 
           <li><a asp-area="" asp-controller="Home" 
             asp-action="About">About</a></li> 
           <li><a asp-area="" asp-controller="Home"
             asp-action="Contact">Contact</a></li> 
         </ul> 
        </div> 
        </div> 
      </nav> 
      <div class="container body-content"> 
        @RenderBody() 
        <hr /> 
        <footer> 
          <p>&copy; 2017 - TicTacToe</p> 
        </footer> 
      </div> 

      <environment include="Development"> 
       <script src="~/lib/jquery/dist/jquery.js"></script> 
       <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> 
       <script src="~/js/site.js" asp-append-version="true"></script> 
      </environment> 
      <environment exclude="Development"> 
       <script src="https://ajax.aspnetcdn.com/ajax/jquery/
        jquery-2.2.0.min.js" 
        asp-fallback-src="~/lib/jquery/dist/jquery.min.js" 
        asp-fallback-test="window.jQuery" 
         crossorigin="anonymous" 
         integrity="sha384-K+ctZQ+LL8q6tP7I94W+qzQsfRV2a+
         AfHIi9k8z8l9ggpc8X+Ytst4yBo/hH+8Fk"> 
       </script> 
       <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/
        3.3.7/bootstrap.min.js" 
        asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js" 
        asp-fallback-test="window.jQuery&&window.jQuery
         .fn&&window.jQuery.fn.modal" 
        crossorigin="anonymous" 
        integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA
         7l2mCWNIpG9mGCD8wGNIcPD7Txa"> 
       </script> 
       <script src="~/js/site.min.js" asp-append-version="true">
       </script> 
      </environment> 

      @RenderSection("Scripts", required: false) 
    </body> 
    </html> 

在下一节中创建用户注册页面之前,让我们更新之前创建的主页,以显示Tic-Tac-Toe游戏的一些基本信息,同时使用前面的布局页面:

    @{ 
      ViewData["Title"] = "Home Page"; 
      Layout = "~/Views/Shared/_Layout.cshtml"; 
    } 
    <div class="row"> 
    <div class="col-lg-12"> 
      <h2>Tic-Tac-Toe</h2> 
      <div class="alert alert-info"> 
        <p>Tic-Tac-Toe is a two-player turn-based game.</p> 
        <p>Two players will choose who takes the Xs and who
           takes the Os. They will then be taking turns and
           mark spaces in a 3×3 grid by putting their marks,
           one mark per turn.</p> 
        <p>A player who succeeds in placing three of his
           marks in a horizontal, vertical, or diagonal row
           wins the game.</p> 
      </div> 
      <p> 
        <h3>Register by clicking <a asp-controller="UserRegistration"
            asp-view="Index">here</a></h3> 
      </p> 
    </div> 
    </div> 

当启动应用时,你会看到新的主页设计:

创建用户注册一字棋界面

现在将第二个组件,即用户注册页面与其表单集成在一起,它将允许新用户注册来玩Tic-Tac-Toe游戏。

  1. 向项目中添加一个名为Models的新文件夹。
  2. 通过右键单击项目中的Models文件夹,选择 Add | Class,并将其命名为UserModel:
        public class UserModel 
        { 
          public Guid Id { get; set; } 
          public string FirstName { get; set; } 
          public string LastName { get; set; } 
          public string Email { get; set; } 
          public string Password { get; set; } 
          public bool IsEmailConfirmed { get; set; } 
          public System.DateTime? EmailConfirmationDate { get; set; } 
          public int Score { get; set; } 
        } 
  1. 添加一个新控制器并将其命名为UserRegistrationController(如果您不知道如何做到这一点,请参考创建井字游戏主页节)。
  2. 右键单击名为Index的方法并选择 Add View。 这一次,选择创建模板,像前面提到的模型UserModel,并启用布局页面的使用:

Note that you can leave the layout page empty if you want to use the _ViewStart.cshtml file in the Shared folder to define a unified common layout for all your views.

The _ViewStart.cshtml file is used to share settings between views, while the _ViewImports file is used to share using namespaces and inject dependency injection instances. Visual Studio 2017 includes two templates for these files.

  1. 从视图中移除自动生成的IdIsEmailConfirmedEmailConfirmationDateScore元素; 对于用户注册表单,我们不需要它们。
  2. 视图现在已经准备好了; 点击F5,点击主页注册链接显示:

使用依赖注入来鼓励应用中的松散耦合

开发应用时最大的问题之一是组件间的依赖关系。 这些依赖关系使单独维护和发展组件变得困难,因为修改可能会严重影响其他依赖组件。 但请放心,有一些机制允许分解这些依赖项,其中之一是依赖项注入(DI)。

依赖注入允许组件一起工作,同时提供松耦合。 一个组件只需要知道另一个组件实现的契约就可以使用它。 使用 DI 容器,组件不会直接实例化,也不会使用静态引用来查找另一个组件的实例。 相反,DI 容器负责在运行时检索正确的实例。

当一个组件在设计时考虑到依赖注入,它在默认情况下是非常演进的,并且不依赖于任何其他组件或行为。 例如,身份验证服务可以使用提供者来进行使用 DI 的身份验证,如果添加了新的提供者,现有的提供者将不会受到影响。

ASP.NET Core 2.0 包含了一个非常简单的内置 DI 容器,它支持构造函数注入。 要使服务对容器可用,您必须将其添加到Startup类的ConfigureService方法中。 在不知道的情况下,你已经在 MVC 中做过了:

    public void ConfigureServices(IServiceCollection services) 
    { 
      services.AddMvc(); 
    } 

实际上,您必须对您自己的自定义服务做同样的事情,您必须在这个方法中声明它们。 当你知道你在做什么时,这真的很容易做到!

然而,有多种方式注入你的服务,你需要选择哪一种最适合你的需求:

  • 瞬时注入:每次调用方法时创建一个实例(例如,无状态服务):
        services.AddTransient<IExampleService, ExampleService>();
  • 作用域注入:为每个请求管道(例如,有状态服务)创建一个实例:
        services.AddScoped<IExampleService, ExampleService>(); 
  • 单例注入:为整个应用创建一个单实例:
        services.AddSingleton<IExampleService, ExampleService>(); 

Note that you should add the instances for your services by yourself if you do not want the container to automatically dispose of them. The container will call the Dispose method of each service instance it creates by itself.

Here is an example of how to instantiate your services by yourself: services.AddSingleton(new ExampleService());

现在您已经了解了如何使用 DI,让我们应用您的知识并为我们的示例应用创建下一个组件。

使用实例创建井字用户服务

我们已经创建了一个主页以及一个用户注册页面。 用户可以单击注册链接并填写注册表单,但表单数据尚未以任何方式处理。 我们将添加一个用户服务,该服务将负责处理与用户相关的任务,例如用户注册请求。 此外,您将应用一些 ASP。 之前看到的 NET Core 2.0 DI 机制:

  1. Services文件夹中添加一个名为UserService.cs的新类。
  2. 为用户注册添加一个新方法,使用上一节中创建的模型作为参数:
        public class UserService 
        { 
          public Task<bool>RegisterUser(UserModel userModel) 
          { 
            return Task.FromResult(true); 
          } 
        }
  1. 右键单击类,选择 Quick Actions 和 Refactorings,然后单击 Extract Interface:

  1. 在弹出窗口中保留所有的默认值,然后单击 OK:

  1. Visual Studio 2017 将生成一个名为IUserService.cs的新文件,其中包含提取的接口定义,如下所示:
        public interface IUserService 
        { 
          Task<bool>RegisterUser(UserModeluserModel); 
        } 
  1. 更新之前创建的UserRegistrationController并应用构造函数注入机制:
        public class UserRegistrationController : Controller 
        { 
          private IUserService _userService; 
          public UserRegistrationController(IUserService userService) 
          { 
            _userService = userService; 
          } 

          public IActionResult Index() 
          { 
            return View(); 
          } 
        } 
  1. 添加一些简单的代码来处理UserRegistrationController中的用户注册(我们将在本章后面添加验证):
        [HttpPost] 
        public async Task<IActionResult> Index(UserModel userModel) 
        { 
          await _userService.RegisterUser(userModel); 
          return Content($"User {userModel.FirstName} 
           {userModel.LastName} has been registered sucessfully"); 
        } 
  1. Startup类,在ConfigureServices方法中声明UserService,使其可用于应用:
        public void ConfigureServices(IServiceCollection services) 
        {             
          services.AddMvc(); 
          services.AddSingleton<IUserService, UserService>(); 
        } 
  1. F5测试您的应用,填写注册页面,然后点击 OK。 你应该得到以下输出:

非常好,您已经创建了多个组件的Tic-Tac-Toe应用,进展非常好! 请保持敏锐,因为下一节非常重要,因为它详细解释了中间件。

使用中间件)

如前所述,Startup类负责在 ASP 中添加和配置中间件.NET Core 2.0 应用。 但什么是中间件? 何时以及如何使用它,以及如何创建自己的中间件? 这些都是我们现在要讨论的问题。

本质上,多个中间件组成了 ASP 的功能.NET Core 2.0 应用。 您现在可能已经注意到,即使是最基本的功能,比如提供静态内容,也由它们来执行。

中间件是 ASP 的一部分.NET Core 2.0 请求管道用于处理请求和响应。 当它们链接在一起时,它们可以将传入的请求从一个传递给另一个,并在管道中调用下一个中间件之前和之后执行操作:

使用中间件可以使您的应用更加灵活和完善,因为您可以在Startup类的Configure方法中轻松地添加和删除中间件。

此外,在Configure方法中调用中间件的顺序就是调用它们的顺序。 为了保证更好的性能、功能和安全性,建议按照以下顺序调用中间件:

  1. 异常处理中间件)。
  2. 静态文件中间件)。
  3. 验证仿真中间件。
  4. MVC middlewares.

如果您不按此顺序调用它们,您可能会得到一些意想不到的行为,甚至错误,因为中间件操作可能在请求管道中应用得太晚或太早。

例如,如果您不首先调用异常处理中间件,那么您可能无法捕获在调用它之前发生的所有异常。 另一个例子是在静态文件中间件之后调用响应压缩中间件。 在这种情况下,您的静态文件将不会被压缩,这可能不是期望的行为。 所以,要注意中间件调用的顺序; 它可以带来巨大的不同。

下面是一些你可以在你的应用中使用的内置中间件(列表不是详尽的; 还有更多):

| 身份验证 | OAuth 2 和 OpenID 认证,基于最新版本的 IdentityModel | | CORS | 基于 HTTP 报头的跨源资源共享保护 | | 响应缓存 | HTTP 响应缓存 | | 响应压缩 | HTTP 响应 gzip 压缩 | | 路由 | HTTP 请求路由框架 | | 会话 | 基本的本地和分布式会话对象管理 | | 静态文件 | HTML, CSS, JavaScript,和图像支持包括目录浏览 | | URL 重写 | URL SEO 优化和重写 |

内置的中间件将足以满足最基本的需求和标准用例,但您肯定需要创建自己的中间件。 有两种方法:在 Startup 类中内联创建它们,或者在自包含类中创建它们。

首先让我们看看如何定义内联中间件; 以下是可采用的方法:

  • Run
  • Map
  • MapWhen
  • Use

Run方法用于添加中间件并立即返回响应,从而使请求管道短路。 它不调用以下任何中间件,并结束请求管道。 因此,建议将它放在中间件调用的末尾(参见前面讨论的中间件排序)。

如果请求路径以特定路径开始,那么Map方法允许执行某个分支并添加相应的中间件,这意味着您可以有效地对请求管道进行分支。

MapWhen方法提供了与分支请求管道和添加特定中间件基本相同的概念,但是可以控制分支条件,因为它是基于Func<HttpContext, bool>谓词的结果。

Use方法添加中间件,并允许调用在线的下一个中间件或使请求管道短路。 但是,如果您希望在执行特定操作之后传递请求,则必须使用带有当前上下文作为参数的next.Invoke手动调用下一个中间件。

下面是一些如何使用这些扩展方法的例子:

    private static void ApiPipeline(IApplicationBuilder app) 
    { 
      app.Run(async context => 
      { 
        await context.Response.WriteAsync("Branched to Api Pipeline."); 
      }); 
    } 

    private static void WebPipeline(IApplicationBuilder app) 
    { 
      app.MapWhen(context => 
      { 
        return context.Request.Query.ContainsKey("usr"); 
      }, UserPipeline); 

      app.Run(async context => 
      { 
        await context.Response.WriteAsync("Branched to Web Pipeline."); 
      }); 
    } 

    private static void UserPipeline(IApplicationBuilder app) 
    { 
      app.Run(async context => 
      { 
        await context.Response.WriteAsync("Branched to User Pipeline."); 
      }); 
    } 

    public void Configure(IApplicationBuilder app, IHostingEnvironmentenv) 
    { 
      app.Map("/api", ApiPipeline); 
      app.Map("/web", WebPipeline); 

      app.Use(next =>async context => 
      { 
        await context.Response.WriteAsync("Called Use."); 
        await next.Invoke(context); 
      }); 

      app.Run(async context => 
      { 
        await context.Response.WriteAsync("Finished with Run."); 
      }); 
    } 

如前所述,您可以内联创建中间件,但不建议用于更高级的场景。 在这种情况下,我们建议您将中间件放在自包含的类中,这样做的过程非常简单。 中间件只是一个具有特定结构的类,它是通过扩展方法公开的。

让我们为Tic-Tac-Toe应用创建一个基本的通信中间件:

  1. 在项目中创建一个名为Middlewares的新文件夹,然后添加一个名为CommunicationMiddleware.cs的新类,使用以下代码:
        public class CommunicationMiddleware 
        { 
          private readonly RequestDelegate _next; 
          private readonly IUserService _userService; 

          public CommunicationMiddleware(RequestDelegate next,
           IUserService userService) 
          { 
            _next = next; 
            _userService = userService; 
          } 

          public async Task Invoke(HttpContext context) 
          { 
            await _next.Invoke(context); 
          } 
        }
  1. 在项目中创建一个名为Extensions的新文件夹,然后添加一个名为CommunicationMiddlewareExtension.cs的新类,代码如下:
        public static class CommunicationMiddlewareExtension 
        { 
          public static IApplicationBuilder
           UseCommunicationMiddleware(this IApplicationBuilder app) 
          { 
            return app.UseMiddleware<CommunicationMiddleware>(); 
          } 
        } 
  1. Startup类中为TicTacToe.Extensions添加一个 using 指令,然后在Configure方法中添加通信中间件:
        using TicTacToe.Extensions; 
        ... 
        public void Configure(IApplicationBuilder app,
         IHostingEnvironment env) 
        { 
          ... 
          app.UseCommunicationMiddleware(); 
          app.UseMvc(routes => 
          { 
            routes.MapRoute( 
             name: "default", 
             template: "{controller=Home}/{action=Index}/{id?}"); 
          }); 
        } 
  1. 在通信中间件实现中设置一些断点,并按F5启动应用。 你会看到,如果一切正常工作,断点将被命中:

这只是一个如何创建自己的中间件的基本示例; 在这个部分和其他部分之间没有可见的功能更改。 在下一章中,您将进一步实现完成Tic-Tac-Toe应用的各种功能,在本章中看到的通信中间件将很快完成一些实际工作。

使用静态文件

在使用 web 应用时,大多数时候,你必须使用 HTML、CSS、JavaScript 和图像,这些都被 ASP 视为静态文件。 2.0 NET Core。

对这些文件的访问在默认情况下是不可用的,但是在本章的开始部分,您看到了需要做些什么才能允许静态文件在应用中使用。 事实上,你必须在Startup类中添加和配置相应的中间件,以便能够提供静态文件:

    app.UseStaticFiles();

Note that by default all static files served by this middleware are public and anyone can access them. If you need to protect some of your files, you need to either store them outside the wwwroot folder or you need to use the FileResult controller action, which supports the authorization middleware.

此外,出于安全原因,目录浏览在默认情况下是禁用的。 但是,如果你需要让用户看到文件夹和文件,你可以很容易地激活它:

  1. 在调用AddMvc()方法之后,在Startup类的ConfigureService方法中添加DirectoryBrowsingMiddleware:
        services.AddDirectoryBrowser(); 
  1. Startup类的Configure方法中,调用UseDirectoryBrowser方法(在调用UseCommunicationMiddleware方法之后)来激活目录浏览:
        app.UseDirectoryBrowser(); 

  1. Startup类中移除对UseDirectoryBrowser方法的调用; 对于示例应用,我们不需要它

使用路由、URL 重定向和 URL 重写

在构建应用时,路由用于将传入请求映射到路由处理程序(URL 匹配),并为响应生成 URL (URL 生成)。

ASP 的路由能力.NET Core 2.0 结合并统一了以前存在的 MVC 和 Web API 的路由功能。 它们被从头开始重新构建,以创建一个公共路由框架,将所有不同的功能集中在一个地方,对所有类型的 ASP 都可用.NET Core 2.0 项目。

让我们看看路由在内部是如何工作的,以便更好地理解它在应用中是如何有用的,以及如何将它应用到我们的Tic-Tac-Toe示例中。

对于每个接收到的请求,将根据请求 URL 检索一个匹配的路由。 路由按照它们在路由集合中出现的顺序进行处理。

更具体地说,传入的请求被分派到相应的处理程序。 大多数情况下,这是基于 URL 中的数据完成的,但您也可以在更高级的场景中使用请求中的任何数据。

如果你正在使用 MVC 中间件,你可以在Startup类中定义和创建你的路由,如本章开头所示。 这是最简单的方式开始与 URL 匹配和 URL 生成:

    app.UseMvc(routes => 
    { 
      routes.MapRoute( 
        name: "default", 
        template: "{controller=Home}/{action=Index}/{id?}"); 
    }); 

还有一个专用的路由中间件,您可以使用它在应用中处理路由,您在前面关于中间件的部分中已经看到了它。 你只需要在Startup类中添加它:

    public void ConfigureServices(IServiceCollection services) 
    { 
      services.AddRouting(); 
    }

下面是如何使用它调用Startup类中的UserRegistration服务的示例:

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc();
      services.AddSingleton<IUserService, UserService>();
      services.AddRouting();
    }
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
      if (env.IsDevelopment())
      {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
      }
      else
      {
        app.UseExceptionHandler("/Home/Error");
      }
      app.UseStaticFiles();
      var routeBuilder = new RouteBuilder(app);
      routeBuilder.MapGet("CreateUser", context =>
      {
        var firstName = context.Request.Query["firstName"];
        var lastName = context.Request.Query["lastName"];
        var email = context.Request.Query["email"];
        var password = context.Request.Query["password"];
        var userService = 
          context.RequestServices.GetService<IUserService>();
        userService.RegisterUser(new UserModel { FirstName = firstName,
         LastName = lastName, Email = email, Password = password });
        return context.Response.WriteAsync($"User {firstName} 
         {lastName} has been sucessfully created.");
      });
      var newUserRoutes = routeBuilder.Build();
      app.UseRouter(newUserRoutes);
      app.UseCommunicationMiddleware();
      app.UseMvc(routes =>
      {
        routes.MapRoute(
         name: "default",
         template: "{controller=Home}/{action=Index}/{id?}");
      });
      app.UseStatusCodePages("text/plain",
      "HTTP Error - Status Code: {0}");
    }

如果你用一些查询字符串参数调用它,你会得到以下结果:

另一个重要的中间件是URL 重写中间件。 它提供 URL 重定向和 URL 重写功能。 然而,这两者之间有一个你需要理解的关键区别。

URL 重定向需要到服务器的往返,并在客户端完成。 客户端首先接收到移动的永久301或移动的临时302HTTP 状态码,该状态码指示要使用的新的重定向 URL。 然后,客户端调用新的 URL 来检索所请求的资源,因此它对客户端是可见的。

另一方面,URL 重写纯粹是服务器端。 服务器将在内部从不同的资源地址检索所请求的资源。 客户端不会知道资源已经从另一个 URL 提供,因为它对客户端是不可见的。

回到Tic-Tac-Toe应用,我们可以使用 URL 重写来为注册新用户提供更有意义的 URL。 不使用UserRegistration/Index,我们可以使用一个更短的 URL,例如/NewUser:

    var options = new RewriteOptions() 
      .AddRewrite("NewUser", "/UserRegistration/Index", false); 
    app.UseRewriter(options);

在这里,用户认为页面从/NewUser开始被服务,而实际上在用户没有注意到的情况下,页面从/UserRegistration/Index开始被服务:

向应用添加错误处理

在开发应用时,问题不是错误和 bug 是否会发生,而是它们什么时候会发生。 构建应用是一项非常复杂的任务,几乎不可能考虑运行时可能发生的所有情况。 即使您认为您已经考虑了所有的事情,但是环境的行为并不如预期的那样,例如,服务不可用或者处理请求花费的时间比预期的要多。

对于这个问题,您有两个解决方案,它们需要同时应用于单元测试和错误处理。 从应用的角度来看,单元测试将确保开发期间的正确行为,而错误处理帮助您在运行时为环境问题做好准备。 我们将看看如何为你的 ASP 添加有效的错误处理.NET Core 2.0 应用。

默认情况下,如果根本没有错误处理,并且如果异常发生,您的应用将停止,用户将不能再使用它,在最坏的情况下,将会出现服务中断。

在开发期间要做的第一件事是激活默认的开发异常页面; 它显示发生的异常的详细信息。 你已经在本章的开头看到了如何做到这一点:

    if (env.IsDevelopment()) 
    { 
      app.UseDeveloperExceptionPage(); 
    } 

在默认的开发异常页面上,您可以深入研究原始异常细节,以分析堆栈跟踪。 您有多个选项卡,允许查看查询字符串参数、客户端 cookie 和请求头。

这些是一些有力的指标,有助于更好地理解发生了什么以及为什么会发生。 它们可以帮助你在开发期间更快速地发现问题并解决问题。

下面是一个发生异常的例子:

但是,不建议在生产环境中使用默认的开发异常页,因为它包含了太多关于系统的信息,可能会被用来危害系统。

对于生产环境,建议配置一个带有静态内容的专用错误页面。 在下面的示例中,您可以看到在开发期间使用了默认的开发异常页面,如果应用被配置为在非开发环境中运行,则会显示一个特定的错误页面:

    if (env.IsDevelopment()) 
    { 
      app.UseDeveloperExceptionPage(); 
      app.UseBrowserLink(); 
    } 
    else 
    { 
      app.UseExceptionHandler("/Home/Error"); 
    } 

缺省情况下,400599之间的 HTTP 错误码不显示信息。 例如,这包括404(未找到)和500(内部服务器错误)。 用户只会看到一个空白页面,这不是很友好。

您应该激活Startup类中的特定UseStatusCodePages中间件。 它将帮助您定制在本例中需要显示的内容。 有意义的信息将帮助用户更好地理解应用中发生的事情,从而提高客户满意度。

最基本的配置可能只是显示一条文本消息:

    app.UseStatusCodePages("text/plain", "HTTP Error - Status Code: {0}"); 

但是,你可以更进一步。 例如,您可以针对特定的 HTTP 错误状态码重定向到特定的错误页面。

下面的示例展示了如何将移动的临时302(找到的)HTTP 状态码发送到客户端,然后将它们重定向到特定的错误页面:

    app.UseStatusCodePagesWithRedirects("/error/{0}"); 

这个例子展示了如何将原始的 HTTP 状态码返回给客户端,然后将它们重定向到一个特定的错误页面:

    app.UseStatusCodePagesWithReExecute("/error/{0}"); 

You can disable HTTP status code pages for specific requests as shown here:

var statusCodePagesFeature = context.Features.Get<IStatusCodePagesFeature>(); if (statusCodePagesFeature != null) { statusCodePagesFeature.Enabled = false; }

既然我们已经了解了如何在外部处理错误,那么让我们看看如何在应用内部处理它们。

如果我们回到UserRegisterController实现,我们可以看到它有多个缺陷。 如果字段没有正确填写或者根本没有填写,该怎么办? 如果模型定义没有得到尊重怎么办? 目前,我们不要求任何东西,也不验证任何东西。

让我们来解决这个问题,看看如何构建一个更健壮的应用:

  1. 更新UserModel,根据需要使用 decorator 设置一些属性,并要求特定的数据类型:
        public class UserModel 
        { 
          public Guid Id { get; set; } 
          [Required()] 
          public string FirstName { get; set; } 
          [Required()] 
          public string LastName { get; set; } 
          [Required(), DataType(DataType.EmailAddress)] 
          public string Email { get; set; } 
          [Required(), DataType(DataType.Password)] 
          public string Password { get; set; } 
          public bool IsEmailConfirmed { get; set; } 
          public System.DateTime? EmailConfirmationDate { get; set; } 
          public int Score { get; set; } 
        } 
  1. 更新UserRegistrationController中特定的Index方法,然后添加ModelState验证代码:
        [HttpPost] 
        public async Task<IActionResult> Index(UserModel userModel) 
        { 
          if (ModelState.IsValid) 
          { 
            await _userService.RegisterUser(userModel); 
            return Content($"User {userModel.FirstName}  
             {userModel.LastName} has been registered sucessfully"); 
          } 
          return View(userModel); 
        }  
  1. 如果你没有填写必要的字段,或者你给出了一个错误的电子邮件地址,然后点击确定,你现在会得到一个相应的错误消息:

总结

在本章中,你已经学习了 ASP 的一些基本概念.NET 2.0。 有很多东西要理解,有很多东西要看,我们希望你在自己尝试这些东西时获得了一些乐趣。 你肯定取得了巨大的进步!

一开始,您创建了Tic-Tac-Toe项目; 然后,您开始实现它的不同组件。 我们学习了ProgramStartup类,了解了如何使用 Bower 和布局页面,学习了如何应用依赖注入,并使用了静态文件。

此外,我们还介绍了用于更高级场景的中间件和路由。 最后,我们通过一个实际示例说明了如何为您的应用添加有效的错误处理。

在下一章中,我们将继续并介绍更多的概念,如 WebSockets、全球化、本地化和配置,以及一次性构建和在多个环境中运行。