C-9-和--NET5-高级教程-十七-

100 阅读50分钟

C#9 和 .NET5 高级教程(十七)

原文:Pro C# 9 with .NET 5

协议:CC BY-NC-SA 4.0

三十、ASP.NET 核心的 RESTful 服务

前一章介绍了 ASP.NET 核心,讨论了一些新特性,创建了项目,并更新了 AutoLot 中的代码。Mvc 和 AutoLot。包含自动 Lot 的 Api。Dal 和 Serilog 测井。本章重点介绍如何完成自动 Lot。Api RESTful 服务。

Note

本章的示例代码在本书 repo 的Chapter 30目录中。请随意继续你在第二十九章开始的解决方案。

介绍 ASP.NET 核心 RESTful 服务

ASP.NET MVC 框架几乎一发布就开始获得关注,微软发布了 ASP.NET Web API 和 ASP.NET MVC 4 以及 Visual Studio 2012。ASP.NET Web API 2 随 Visual Studio 2013 一起发布,然后随 Visual Studio 2013 Update 1 更新到 2.2 版。

从一开始,ASP.NET Web API 就被设计成一个基于服务的框架,用于构建REpresentationalSstateTtransfer(RESTful)服务。它基于 MVC 框架减去 V (视图),优化创建无头服务。这些服务可以被任何技术调用,而不仅仅是微软旗下的那些。对 Web API 服务的调用基于核心 HTTP 动词(Get、Put、Post、Delete ),通过统一资源标识符(URI ),如下所示:

http://www.skimedic.com:5001/api/cars

如果这看起来像一个统一资源定位器(URL),那是因为它就是!URL 只是一个指向网络上物理资源的 URI。

对 Web API 的调用使用特定主机上的HyperTextTtransferProtocol(HTTP)方案(在本例中为 www.skimedic.com )、特定端口(上例中为 5001),后跟路径(api/cars)和可选的查询和片段(本例中未显示)。Web API 调用也可以在消息体中包含文本,这一点你会在本章中看到。正如前一章所讨论的,ASP.NET 核心将 Web API 和 MVC 统一到一个框架中。

RESTful 服务的控制器动作

回想一下,动作返回一个IActionResult(或者异步操作返回一个Task<IActionResult>)。除了返回特定 HTTP 状态代码的ControllerBase中的 helper 方法之外,action 方法还可以以格式化的 JavaScript 对象符号(JSON)响应的形式返回内容。

Note

严格来说,动作方法可以返回多种格式。JSON 包含在本书中,因为它是最常见的。

格式化的 JSON 响应结果

大多数 RESTful APIs 使用 JSON(发音为“Jay-saw”)从客户端接收数据,并向客户端发送数据。这里显示了一个简单的 JSON 示例,包含两个值:

[
  "value1",
  "value2"
]

Note

第二十章使用System.Text.Json深入讨论了 JSON 序列化。

API 也使用 HTTP 状态代码来传达成功或失败。在表 29-3 中的前一章中列出了一些在ControllerBase类中可用的 HTTP 状态助手方法。成功的请求返回 200 范围内的状态代码,200 (OK)是最常见的成功代码。事实上,它是如此普遍,以至于你不必显式地返回一个 OK。如果没有抛出异常,并且代码没有指定状态代码,那么将向客户端返回 200 以及任何数据。

要设置以下示例,请向自动 Lot 添加一个新的控制器。Api 项目。在Controllers目录中添加一个名为ValuesController.cs的新文件,并更新代码以匹配以下内容:

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
}

Note

如果使用 Visual Studio,有一个架子工负责控制器。要访问它,右击自动 Lot 中的Controllers文件夹。Api 项目并选择添加控制器。选择 MVC 控制器-空。

该代码使用一个值(api)和一个令牌([controller])为控制器设置路由。这个路由模板将匹配类似于 www.skimedic.com/ api / values 的 URL。下一个属性(ApiController)选择几个特定于 API 的特性(在下一节讨论)。最后,控制器继承自ControllerBase。正如在第二十九章中所讨论的,ASP.NET 核心将经典 ASP.NET 中所有可用的不同控制器类型整合为一个,命名为Controller,带有一个基类ControllerBaseController类提供特定于视图的功能(MVC 中的 V ),而ControllerBase为 MVC 风格的应用提供所有其余的核心功能。

有几种方法可以将内容作为 JSON 从 action 方法中返回。以下示例都返回相同的 JSON 以及 200 状态代码。不同之处主要在文体上。将以下代码添加到您的ValuesController类中:

[HttpGet]
public IActionResult Get()
{
  return Ok(new string[] { "value1", "value2" });
}
[HttpGet("one")]
public IEnumerable<string> Get1()
{
  return new string[] { "value1", "value2" };
}
[HttpGet("two")]
public ActionResult<IEnumerable<string>> Get2()
{
  return new string[] { "value1", "value2" };
}
[HttpGet("three")]
public string[] Get3()
{
  return new string[] { "value1", "value2" };
}
[HttpGet("four")]
public IActionResult Get4()
{
    return new JsonResult(new string[] { "value1", "value2" });
}

要对此进行测试,请运行 AutoLot。Api 应用,你会看到 Swagger UI 中列出了从ValuesController开始的所有方法,如图 30-1 所示。回想一下,在确定路由时,Controller后缀被从名称中去掉,因此ValuesController上的端点被映射为Values,而不是ValuesController

img/340876_10_En_30_Fig1_HTML.jpg

图 30-1。

Swagger 文档页面

要执行其中一种方法,请单击“获取”按钮、“尝试”按钮,然后单击“执行”按钮。一旦方法执行完毕,UI 就会更新以显示结果,图 30-2 中只显示了 Swagger UI 的相关部分。

img/340876_10_En_30_Fig2_HTML.jpg

图 30-2。

Swagger 服务器响应信息

您将看到,执行每个方法都会产生相同的 JSON 结果。

ApiController 属性

在 ASP.NET 核心 2.1 中添加的ApiController属性在与ControllerBase类结合时提供了特定于 REST 的规则、约定和行为。这些约定和行为将在以下几节中概述。

属性路由要求

使用ApiController属性时,控制器必须使用属性路由。这只是强化了许多人认为的最佳实践。

自动 400 响应

如果模型绑定有问题,该操作将自动返回 HTTP 400(错误请求)响应代码。这将替换以下代码:

if (!ModelState.IsValid)
{
  return BadRequest(ModelState);
}

ASP.NET 核心使用ModelStateInvalidFilter动作过滤器来做前面的检查。当出现绑定或验证错误时,HTTP 400 响应的主体中会包含错误的详细信息。这里显示了一个示例:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "|7fb5e16a-4c8f23bbfc974667.",
  "errors": {
    "": [
      "A non-empty request body is required."
    ]
  }
}

这种行为可以通过在Startup.cs类的ConfigureServices()方法中进行配置来禁用。

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressModelStateInvalidFilter = true;
    });

绑定源参数推断

模型绑定引擎将根据表 30-1 中列出的约定推断在哪里检索值。

表 30-1。

绑定源推理约定

|

来源

|

参数界限

| | --- | --- | | FromBody | 除了有特殊含义的内置类型,如IFormCollectionCancellationToken之外,对于复杂类型参数进行推断。只能存在一个FromBody参数,否则将抛出异常。如果简单类型需要绑定(例如,stringint),那么FromBody属性仍然是必需的。 | | FromForm | 为类型IFormFileIFormFileCollection的动作参数推断。当参数标有FromForm时,将推断出多部分/表单数据内容类型。 | | FromRoute | 对于匹配路由令牌名称的任何参数名称进行推断。 | | FromQuery | 推断出任何其他行动参数。 |

这种行为可以通过在Startup.cs类的ConfigureServices()方法中进行配置来禁用。

services.AddControllers().ConfigureApiBehaviorOptions(options =>
{
  //suppress all binding inference
  options.SuppressInferBindingSourcesForParameters= true;
  //suppress multipart/form-data content type inference
  options. SuppressConsumesConstraintForFormFileParameters = true;
});

错误状态代码的问题详细信息

ASP.NET 核心将错误结果(状态为 400 或更高)转换为带有ProblemDetails的结果。这里列出了ProblemDetails类型:

public class ProblemDetails
{
  public string Type { get; set; }
  public string Title { get; set; }
  public int? Status { get; set; }
  public string Detail { get; set; }
  public string Instance { get; set; }
  public IDictionary<string, object> Extensions { get; }
    = new Dictionary<string, object>(StringComparer.Ordinal);
}

为了测试这种行为,向ValuesController添加另一个方法,如下所示:

[HttpGet("error")]
public IActionResult Error()
{
  return NotFound();
}

运行应用并使用 Swagger UI 来执行新的Error端点。结果仍然是 404 ( NotFound)状态代码,但是在响应的主体中返回了附加信息。以下是一个示例响应(您的traceId会有所不同):

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Not Found",
  "status": 404,
  "traceId": "00-9a609e7e05f46d4d82d5f897b90da624-a6484fb34a7d3a44-00"
}

这种行为可以通过在Startup.cs类的ConfigureServices()方法中进行配置来禁用。

services.AddControllers()
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressMapClientErrors = true;
    });

当行为被禁用时,对Error端点的调用返回一个 404,没有任何附加信息。

更新 Swagger/OpenAPI 设置

Swagger(也称为 OpenAPI)是一个用于记录 RESTful APIs 的开放标准。将 Swagger 添加到 ASP.NET 核心 API 的两个主要选择是 Swashbuckle 和 NSwag。ASP.NET 核心 5 现在包括 Swashbuckle 作为新项目模板的一部分。为自动 Lot 生成的swagger.json文件。Api 包含站点、每个端点以及端点中涉及的任何对象的信息。

Swagger UI 是一个基于 web 的 UI,它提供了一个交互式界面来检查和测试应用的端点(就像你在本章前面所做的那样)。通过将文档添加到生成的swagger.json文件中,可以增强这种体验。

更新启动类中的 Swagger 调用

默认的 API 模板在Startup.csConfigureService()方法中添加了生成swagger.json文件的代码。

services.AddSwaggerGen(c =>
{
  c.SwaggerDoc("v1", new OpenApiInfo { Title = "AutoLot.Api", Version = "v1" });
});

默认代码的第一个变化是向OpenApiInfo添加元数据。将AddSwaggerGen()调用更新为以下内容,这将更新标题并添加描述和许可信息:

services.AddSwaggerGen(c =>
{
  c.SwaggerDoc("v1",
    new OpenApiInfo
    {
      Title = "AutoLot Service",
      Version = "v1",
      Description = "Service to support the AutoLot dealer site",
      License = new OpenApiLicense
      {
        Name = "Skimedic Inc",
        Url = new Uri("http://www.skimedic.com")
      }
    });
});

下一步是将UseSwagger()UseSwaggerUI()移出开发专用块,进入Configure()中的主执行路径。另外,将标题从“自动锁定”更新为“自动锁定服务 v1”。Api v1。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ApplicationDbContext context)
{
  if (env.IsDevelopment())
  {
    //If in development environment, display debug info
    app.UseDeveloperExceptionPage();
    //Original code
    //app.UseSwagger();
    //app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "AutoLot.Api v1"));
    //Initialize the database
    if (Configuration.GetValue<bool>("RebuildDataBase"))
    {
      SampleDataInitializer.ClearAndReseedDatabase(context);
    }
  }

  // Enable middleware to serve generated Swagger as a JSON endpoint.
  app.UseSwagger();
  // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
  // specifying the Swagger JSON endpoint.
  app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "AutoLot Service v1"); });
...
}

前面的代码选择使用 Swagger ( app.UseSwagger())和 Swagger UI ( app.useSwaggerUI())。它还为swagger.json文件配置端点。

添加 XML 文档文件

。NET Core 可以通过检查三斜线(///)注释的方法,从您的项目中生成一个 XML 文档文件。若要使用 Visual Studio 启用此功能,请右键单击自动标注。Api 项目并打开“属性”窗口。选择构建页面,选中 XML 文档文件复选框,并输入AutoLot.Api.xml作为文件名。同样,在“抑制警告”文本框中输入 1591 ,如图 30-3 所示。

img/340876_10_En_30_Fig3_HTML.jpg

图 30-3。

添加 XML 文档文件并取消 1591

相同的设置可以直接输入到项目文件中。下面显示了要添加的PropertyGroup:

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <DocumentationFile>AutoLot.Api.xml</DocumentationFile>
    <NoWarn>1701;1702;1591;</NoWarn>
  </PropertyGroup>

NoWarn 1591 设置为没有 XML 注释的方法关闭编译器警告。

Note

1701 和 1702 警告是早期经典的延续。方法公开的。NET 核心编译器。

要查看这个过程的运行情况,请将ValuesController的 Get 方法更新为:

/// <summary>
/// This is an example Get method returning JSON
/// </summary>
/// <remarks>This is one of several examples for returning JSON:
/// <pre>
/// [
///   "value1",
///   "value2"
/// ]
/// </pre>
/// </remarks>
/// <returns>List of strings</returns>
[HttpGet]
public IActionResult Get()
{
  return Ok(new string[] { "value1", "value2" });
}

当您构建项目时,会在项目的根目录下创建一个名为AutoLot.Api.xml的新文件。打开文件以查看您刚刚添加的注释。

<?xml version="1.0"?>
<doc>
  <assembly>
    <name>AutoLot.Api</name>
  </assembly>
  <members>
    <member name="M:AutoLot.Api.Controllers.ValuesController.Get">
      <summary>
        This is an example Get method returning JSON
      </summary>
      <remarks>This is one of several examples for returning JSON:
        <pre>
        [
          "value1",
          "value2"
        ]
        </pre>
      </remarks>
      <returns>List of strings</returns>    </member>
  </members>
</doc>

Note

使用 Visual Studio 时,如果在类或方法定义前输入三个反斜杠,Visual Studio 将为您剔除初始 XML 注释。

下一步是将 XML 注释合并到生成的swagger.json文件中。

向 SwaggerGen 添加 XML 注释

生成的 XML 注释必须添加到swagger.json生成过程中。首先向Startup类添加以下using语句:

using System.IO;
using System.Reflection;

通过调用AddSwaggerGen()方法中的IncludeXmlComments()方法,XML 文档文件被添加到 Swagger 中。导航到Startup类的ConfigureServices()方法,并将AddSwaggerGen()方法更新为以下内容,以添加 XML 文档文件:

services.AddSwaggerGen(c =>
{
  c.SwaggerDoc("v1",
    new OpenApiInfo
    {
      Title = "AutoLot Service",
      Version = "v1",
      Description = "Service to support the AutoLot dealer site",
      License = new OpenApiLicense
      {
        Name = "Skimedic Inc",
        Url = new Uri("http://www.skimedic.com")
      }
    });
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

运行应用并检查 Swagger UI。注意集成到 Swagger UI 中的 XML 注释,如图 30-4 所示。

img/340876_10_En_30_Fig4_HTML.jpg

图 30-4。

集成到 Swagger UI 中的 XML 文档

除了 XML 文档之外,应用端点上的附加配置可以改进文档。

API 端点的附加文档选项

Swagger 文档还有一些额外的属性。要使用它们,首先将下面的using语句添加到ValuesController.cs文件中:

using Microsoft.AspNetCore.Http;
using Swashbuckle.AspNetCore.Annotations;

Produces属性表示端点的内容类型。ProducesResponseType属性使用StatusCodes枚举来指示端点可能的返回代码。更新ValuesControllerGet()方法,指定application/json为返回类型,动作结果将返回 200 OK 或 400 Bad 请求。

[HttpGet]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<IEnumerable<string>> Get()
{
  return new string[] {"value1", "value2"};
}

虽然ProducesResponseType属性将响应代码添加到文档中,但是该信息不能被定制。幸运的是,Swashbuckle 为此添加了SwaggerResponse属性。将Get()方法更新如下:

[HttpGet]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
public ActionResult<IEnumerable<string>> Get()
{
  return new string[] {"value1", "value2"};
}

在 Swagger 注释被选取并添加到生成的文档之前,它们必须被启用。打开Startup.cs并导航至Configure()方法。将对AddSwaggerGen()的呼叫更新为:

services.AddSwaggerGen(c =>
{
  c.EnableAnnotations();
...
});

现在,当您查看 Swagger UI 的 responses 部分时,您将看到定制的消息,如图 30-5 所示。

img/340876_10_En_30_Fig5_HTML.jpg

图 30-5。

更新了 Swagger UI 中的响应

Note

Swashbuckle 支持许多额外的定制。更多信息请咨询 https://github.com/domaindrivendev/Swashbuckle.AspNetCore 的文档。

构建 API 操作方法

自动手枪的大部分功能。Api 应用可分为以下几种方法:

  • GetOne()

  • GetAll()

  • UpdateOne()

  • AddOne()

  • DeleteOne()

主要的 API 方法将在通用的基本 API 控制器中实现。首先在 AutoLot 的Controllers目录中创建一个名为Base的新文件夹。Api 项目。在这个文件夹中,添加一个名为BaseCrudController.cs的新类。将using语句和类定义更新如下:

using System;
using System.Collections.Generic;
using AutoLot.Dal.Exceptions;
using AutoLot.Models.Entities.Base;
using AutoLot.Dal.Repos.Base;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace AutoLot.Api.Controllers.Base
{
  [ApiController]
  public abstract class BaseCrudController<T, TController> : ControllerBase
    where T : BaseEntity, new()
    where TController : BaseCrudController<T, TController>
  {
  }
}

这个类是publicabstract,并且继承了ControllerBase。该类接受两个泛型参数。第一种类型被限制为从BaseEntity派生,并有一个默认的构造函数,第二种类型从BaseCrudController派生(表示派生的控制器)。当ApiController属性被添加到基类中时,派生的控制器将获得该属性提供的功能。

Note

此类中没有定义路线。它将使用派生类来设置。

构造函数

下一步是添加两个受保护的类级变量:一个保存IRepo<T>的实例,另一个保存IAppLogging<T>的实例。这两者都应该使用构造函数来设置。

protected readonly IRepo<T> MainRepo;
protected readonly IAppLogging<TController> Logger;
protected BaseCrudController(IRepo<T> repo, IAppLogging<TController> logger)
{
  MainRepo = repo;
  Logger = logger;
}

Get 方法

有两个 HTTP Get 方法,GetOne()GetAll()。两者都使用传递给控制器的回购。首先,添加GetAll()方法。此方法用作派生控制器的路由模板的端点。

/// <summary>
/// Gets all records
/// </summary>
/// <returns>All records</returns>
/// <response code="200">Returns all items</response>
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpGet]
public ActionResult<IEnumerable<T>> GetAll()
{
  return Ok(MainRepo.GetAllIgnoreQueryFilters());
}

下一个方法基于id获得一条记录,该记录作为必需的 route 参数传递,并被添加到派生控制器的 route 中。

/// <summary>
/// Gets a single record
/// </summary>
/// <param name="id">Primary key of the record</param>
/// <returns>Single record</returns>
/// <response code="200">Found the record</response>
/// <response code="204">No content</response>
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(204, "No content")]
[HttpGet("{id}")]
public ActionResult<T> GetOne(int id)
{
  var entity = MainRepo.Find(id);
  if (entity == null)
  {
    return NotFound();
  }
  return Ok(entity);
}

路线值自动分配给id参数(implicit [FromRoute])。

UpdateOne 方法

HTTP Put 谓词表示对记录的更新。此处列出了该方法,并附有解释:

/// <summary>
/// Updates a single record
/// </summary>
/// <remarks>
/// Sample body:
/// <pre>
/// {
///   "Id": 1,
///   "TimeStamp": "AAAAAAAAB+E="
///   "MakeId": 1,
///   "Color": "Black",
///   "PetName": "Zippy",
///   "MakeColor": "VW (Black)",
/// }
/// </pre>
/// </remarks>
/// <param name="id">Primary key of the record to update</param>
/// <returns>Single record</returns>
/// <response code="200">Found and updated the record</response>
/// <response code="400">Bad request</response>
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpPut("{id}")]
public IActionResult UpdateOne(int id,T entity)
{
  if (id != entity.Id)
  {
    return BadRequest();
  }

  try
  {
    MainRepo.Update(entity);
  }
  catch (CustomException ex)
  {
    //This shows an example with the custom exception
    //Should handle more gracefully
    return BadRequest(ex);
  }
  catch (Exception ex)
  {
    //Should handle more gracefully
    return BadRequest(ex);
  }

  return Ok(entity);
}

该方法首先基于具有所需的Id路由参数的派生控制器的路由,将路由设置为HttpPut请求。路由值被分配给id参数(implicit [FromRoute],实体从请求体中分配(implicit [FromBody])。还要注意没有对ModelState有效性的检查。这也由ApiController属性自动完成。如果ModelState无效,将向客户端返回一个 400 (BadRequest)。

该方法检查以确保路由值(id)与正文中的id匹配。如果没有,则返回一个BadRequest。如果是,回购用于更新记录。如果更新因异常而失败,则向客户端返回 400。如果全部成功,则向客户机返回 200 (OK ),并将更新后的记录作为响应体传入。

Note

这个例子中的异常处理(以及其他例子)非常不完善。生产应用应该利用到目前为止您在本书中学到的所有知识,按照需求的指示优雅地处理问题。

添加一个方法

HTTP Post 谓词表示对记录的插入。此处列出了该方法,并附有解释:

/// <summary>
/// Adds a single record
/// </summary>
/// <remarks>
/// Sample body:
/// <pre>
/// {
///   "Id": 1,
///   "TimeStamp": "AAAAAAAAB+E="
///   "MakeId": 1,
///   "Color": "Black",
///   "PetName": "Zippy",
///   "MakeColor": "VW (Black)",
/// }
/// </pre>
/// </remarks>
/// <returns>Added record</returns>
/// <response code="201">Found and updated the record</response>
/// <response code="400">Bad request</response>
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(201, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpPost]
public ActionResult<T> AddOne(T entity)
{
  try
  {
    MainRepo.Add(entity);
  }
  catch (Exception ex)
  {
    return BadRequest(ex);
  }
  return CreatedAtAction(nameof(GetOne), new {id = entity.Id}, entity);
}

该方法首先将路由定义为 HTTP Post。因为是新记录,所以没有路径参数。如果回购成功添加记录,则响应为CreatedAtAction()。这将向客户机返回一个 HTTP 201,新创建的实体的 URL 作为Location头值。响应的主体是新添加的实体 JSON。

DeleteOne 方法

HTTP Delete 谓词表示记录的移除。一旦从主体内容创建了实例,就可以使用 repo 来处理删除。下面列出了整个方法:

/// <summary>
/// Deletes a single record
/// </summary>
/// <remarks>
/// Sample body:
/// <pre>
/// {
///   "Id": 1,
///   "TimeStamp": "AAAAAAAAB+E="
/// }
/// </pre>
/// </remarks>
/// <returns>Nothing</returns>
/// <response code="200">Found and deleted the record</response>
/// <response code="400">Bad request</response>
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(400, "The request was invalid")]
[HttpDelete("{id}")]
public ActionResult<T> DeleteOne(int id, T entity)
{
  if (id != entity.Id)
  {
    return BadRequest();
  }
  try
  {
    MainRepo.Delete(entity);
  }
  catch (Exception ex)
  {
    //Should handle more gracefully
    return new BadRequestObjectResult(ex.GetBaseException()?.Message);
  }
  return Ok();
}

该方法首先将路由定义为 HTTP Delete,并将id作为必需的路由参数。将路由中的id与正文中实体的其余部分发送的id进行比较,如果它们不匹配,则返回一个BadRequest。如果回购成功删除记录,则响应为 OK;如果有错误,响应是一个BadRequest

这就完成了基本控制器。

小车控制器

自动手枪。Api app 需要一个额外的 HTTP Get 方法来基于一个Make值获取Car记录。这将进入一个名为CarsController的新类别。在Controllers文件夹中创建一个名为CarsController的新的空 API 控制器。将using语句更新如下:

using System.Collections.Generic;
using AutoLot.Api.Controllers.Base;
using Microsoft.AspNetCore.Mvc;
using AutoLot.Models.Entities;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Http;
using Swashbuckle.AspNetCore.Annotations;

CarsController源自BaseCrudController并定义控制器路线。构造函数接受特定于实体的 repo 和记录器的一个实例。以下是初始控制器布局:

namespace AutoLot.Api.Controllers
{
  [Route("api/[controller]")]
  public class CarsController : BaseCrudController<Car, CarsController>
  {
    public CarsController(ICarRepo carRepo, IAppLogging<CarsController> logger) : base(carRepo, logger)
    {
    }
  }
}

CarsController用另一个动作方法扩展了基类,该方法获取特定品牌的所有汽车。添加以下代码,解释如下:

/// <summary>
/// Gets all cars by make
/// </summary>
/// <returns>All cars for a make</returns>
/// <param name="id">Primary key of the make</param>
/// <response code="200">Returns all cars by make</response>
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SwaggerResponse(200, "The execution was successful")]
[SwaggerResponse(204, "No content")]

[HttpGet("bymake/{id?}")]
public ActionResult<IEnumerable<Car>> GetCarsByMake(int? id)
{
  if (id.HasValue && id.Value>0)
  {
    return Ok(((ICarRepo)MainRepo).GetAllBy(id.Value));
  }
  return Ok(MainRepo.GetAllIgnoreQueryFilters());
}

HTTP Get 属性使用bymake常量扩展路由,然后使用 make 的可选id进行过滤,例如:

https://localhost:5021/api/cars/bymake/5

接下来,它检查是否为id传递了一个值。如果没有,它将获取所有车辆。如果传入了一个值,它将使用CarRepoGetAllBy()方法来获取汽车的制造商。由于基类的MainRepo保护属性被定义为IRepo<T>,所以必须将其强制转换回ICarRepo接口。

剩余的控制器

其余的特定于实体的控制器都是从BaseCrudController派生出来的,但是没有添加任何额外的功能。将四个名为CreditRisksControllerCustomersControllerMakesControllerOrdersController的空 API 控制器添加到Controllers文件夹中。其余的控制器都显示在这里:

//CreditRisksController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Models.Entities;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;

namespace AutoLot.Api.Controllers
{
  [Route("api/[controller]")]
  public class CreditRisksController : BaseCrudController<CreditRisk, CreditRisksController>
  {
    public CreditRisksController(
      ICreditRiskRepo creditRiskRepo, IAppLogging<CreditRisksController> logger)
      : base(creditRiskRepo, logger)
    {
    }
  }
}

//CustomersController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Models.Entities;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;

namespace AutoLot.Api.Controllers
{
  [Route("api/[controller]")]
  public class CustomersController : BaseCrudController<Customer, CustomersController>
  {
    public CustomersController(
      ICustomerRepo customerRepo, IAppLogging<CustomersController> logger)
      : base(customerRepo, logger)
    {
    }
  }
}

//MakesController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Models.Entities;
using Microsoft.AspNetCore.Mvc;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.Logging;

namespace AutoLot.Api.Controllers
{
  [Route("api/[controller]")]
  public class MakesController : BaseCrudController<Make, MakesController>
  {
    public MakesController(IMakeRepo makeRepo, IAppLogging<MakesController> logger)
      : base(makeRepo, logger)
    {
    }
  }
}

//OrdersController.cs
using AutoLot.Api.Controllers.Base;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Models.Entities;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;

namespace AutoLot.Api.Controllers
{
  [Route("api/[controller]")]
  public class OrdersController : BaseCrudController<Order, OrdersController>
  {
    public OrdersController(IOrderRepo orderRepo, IAppLogging<OrdersController> logger) : base(orderRepo, logger)
    {
    }
  }
}

这就完成了所有的控制器,您可以使用 Swagger UI 来测试所有的功能。如果您要添加/更新/删除记录,请将appsettings.development.json文件中的RebuildDataBase值更新为 true。

{
...
  "RebuildDataBase": true,
...
}

异常过滤器

当 Web API 应用中出现异常时,不会显示错误页面,因为用户通常是另一个应用,而不是人。任何信息都必须作为 JSON 与 HTTP 状态代码一起发送。正如在第二十九章中所讨论的,ASP.NET 内核允许创建在未处理异常事件中运行的过滤器。可以在控制器级别或动作级别全局应用过滤器。对于这个应用,您将构建一个异常过滤器来发回格式化的 JSON(连同 HTTP 500 ),如果站点运行在调试模式下,还将包含一个堆栈跟踪。

Note

过滤器是 ASP.NET 核心的一个非常强大的功能。在这一章中,我们只研究异常过滤器,但是还可以创建更多的过滤器,从而在构建 ASP.NET 核心应用时节省大量时间。有关过滤器的完整信息,请参考 https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters 的文档。

创建 CustomExceptionFilter

创建一个名为Filters的新目录,并在该目录中添加一个名为CustomExceptionFilterAttribute.cs的新类。将using声明更新如下:

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;

将类更改为public并从ExceptionFilterAttribute继承。覆盖OnException()方法,如下所示:

namespace AutoLot.Api.Filters
{
  public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
  {
    public override void OnException(ExceptionContext context)
    {
    }
  }
}

不像 ASP.NET 核心中的大多数过滤器有一个 before 和 after 事件处理程序,异常过滤器只有一个处理程序:OnException()(或OnExceptionAsync())。这个处理程序有一个参数,ExceptionContext。该参数提供对ActionContext以及抛出的异常的访问。

过滤器还参与依赖注入,允许在代码中访问容器中的任何项目。在这个例子中,我们需要将一个IWebHostEnvironment实例注入过滤器。这将用于确定运行时环境。如果环境是Development,响应也应该包括堆栈跟踪。添加一个类级变量来保存IWebHostEnvironment的实例,并添加构造函数,如下所示:

private readonly IWebHostEnvironment _hostEnvironment;
public CustomExceptionFilterAttribute(IWebHostEnvironment hostEnvironment)
{
  _hostEnvironment = hostEnvironment;
}

OnException()事件处理程序中的代码检查抛出的异常类型,并构建适当的响应。如果环境为Development,则堆栈跟踪包含在响应消息中。构建一个包含发送给调用请求的值的动态对象,并在IActionResult中返回。更新后的方法如下所示:

public override void OnException(ExceptionContext context)
{
  var ex = context.Exception;
  string stackTrace = _hostEnvironment.IsDevelopment() ? context.Exception.StackTrace : string.Empty;
  string message = ex.Message;
  string error;
  IActionResult actionResult;
  switch (ex)
  {
    case DbUpdateConcurrencyException ce:
      //Returns a 400
      error = "Concurrency Issue.";
      actionResult = new BadRequestObjectResult(
        new {Error = error, Message = message, StackTrace = stackTrace});
      break;
    default:
      error = "General Error.";
      actionResult = new ObjectResult(
        new {Error = error, Message = message, StackTrace = stackTrace})
      {
        StatusCode = 500
      };
      break;
  }
  //context.ExceptionHandled = true; //If this is uncommented, the exception is swallowed
  context.Result = actionResult;
}

如果您希望异常过滤器接收异常并将响应设置为 200(例如,记录错误但不将其返回给客户端),请在设置Result(在前面的示例中被注释掉)之前添加以下行:

context.ExceptionHandled = true;

向处理管道添加过滤器

过滤器可以应用于动作方法、控制器或应用的全局。滤镜的代码前的由外向内执行(全局、控制器、动作方法),滤镜的代码后的由内向外执行(动作方法、控制器、全局)。

在应用级别添加过滤器是在Startup类的ConfigureServices()方法中完成的。打开Startup.cs类并将下面的using语句添加到文件的顶部:

using AutoLot.Api.Filters;

更新AddControllers()方法以添加自定义过滤器。

services
  .AddControllers(config => config.Filters.Add(new CustomExceptionFilterAttribute(_env)))
  .AddJsonOptions(options =>
  {
    options.JsonSerializerOptions.PropertyNamingPolicy = null;
    options.JsonSerializerOptions.WriteIndented = true;
  })
  .ConfigureApiBehaviorOptions(options =>
  {
...
  });

测试异常过滤器

要测试异常过滤器,打开WeatherForecastController.cs文件,并将Get()动作更新为如下所示的代码:

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
  _logger.LogAppWarning("This is a test");
  throw new Exception("Test Exception");
...
}

使用 Swagger 运行应用并练习该方法。Swagger UI 中显示的结果应该与以下输出相匹配(为简洁起见,堆栈跟踪被缩短):

{
  "Error": "General Error.",
  "Message": "Test Exception",
  "StackTrace": "   at AutoLot.Api.Controllers.WeatherForecastController.Get() in D:\\Projects\\Books\\csharp9-wf\\Code\\New\\Chapter_30\\AutoLot.Api\\Controllers\\WeatherForecastController.cs:line 31\r\n   "
}

添加跨来源请求支持

API 应该有适当的策略,允许或阻止来自另一个服务器的客户端与 API 通信。这些类型的请求被称为跨来源请求 (CORS)。虽然当您在一个全 ASP.NET 的核心世界中本地运行您的机器时这是不需要的,但是想要与您的 API 通信的 JavaScript 框架需要它,即使所有都在本地运行。

Note

有关 CORS 支持的更多信息,请参阅位于 https://docs.microsoft.com/en-us/aspnet/core/security/cors 的文档文章。

创建 CORS 策略

ASP.NET 核心为配置核心提供了丰富的支持,包括允许/禁止标头、方法、来源、凭证等方法。在本例中,我们将尽可能开放所有内容。配置从创建 CORS 策略并将该策略添加到服务集合开始。策略被命名(这个名称将在Configure()方法中使用),然后是规则。下面的例子创建了一个名为AllowAll的策略,然后就这么做了。将以下代码添加到Startup.cs类中的ConfigureServices()方法中:

services.AddCors(options =>
{
  options.AddPolicy("AllowAll", builder =>
  {
    builder
      .AllowAnyHeader()
      .AllowAnyMethod()
      .AllowAnyOrigin();
  });
});

将 CORS 策略添加到 HTTP 管道处理

最后一步是将 CORS 策略添加到 HTTP 管道处理中。将下面一行添加到Startup.csConfigure()方法中,确保它在app.UseRouting()app.UseEndpoints()方法调用之间:

public void Configure(
    IApplicationBuilder app,
    IWebHostEnvironment env,
    ApplicationDbContext context)
{
  ...
            //opt-in to routing
            app.UseRouting();
            //Add CORS Policy
            app.UseCors("AllowAll");
            //enable authorization checks
            app.UseAuthorization();
...
}

摘要

本章继续我们对 ASP.NET 岩心的研究。我们首先学习了从 action 方法返回 JSON,然后我们看了一下ApiController属性及其对 API 控制器的影响。接下来,更新了通用 Swashbuckle 实现,以包括应用的 XML 文档和来自 action 方法属性的信息。

接下来,构建基本控制器,它拥有应用的大部分功能。之后,派生的、实体特定的控制器被添加到项目中。最后两个步骤增加了应用范围的异常过滤器和对跨源请求的支持。

在下一章中,您将完成 ASP.NET 核心 Web 应用 AutoLot.Mvc 的构建

三十一、ASP.NET 核心的 MVC 应用

第二十九章奠定了 ASP.NET 核心的基础,在第三十章,我们建立了 RESTful 服务。在这一章中,我们将使用 MVC 模式构建 web 应用。我们首先将 V 放回 MVC。

Note

本章的示例代码在本书 repo 的Chapter 31目录中。请随意继续处理您在第二十九章中开始并在第三十章中更新的解决方案。

介绍 ASP.NET 核心中的“V”

在构建 ASP.NET 核心服务时,只使用了 MVC 模式的 M (模型)和 C (控制器)。用户界面是使用 V 或者 MVC 模式的视图创建的。视图是使用 HTML、JavaScript、CSS 和 Razor 代码构建的。它们可选地具有基本布局页面,并且从控制器动作方法或视图组件呈现。如果你在经典的 ASP.NET MVC 中工作过,这听起来应该很熟悉。

查看结果和行动方法

正如在第二十九章中简要提到的,ViewResultPartialView结果是使用Controller帮助器方法从动作方法返回的ActionResult。一个PartialViewResult被设计成在另一个视图中呈现,并且不使用布局页面,而一个ViewResult通常与一个布局页面一起呈现。

ASP.NET 核心中的约定(就像在 ASP.NET MVC 中一样)是让ViewPartialView呈现一个与方法同名的*.cshtml文件。视图必须位于以控制器命名的文件夹(减去控制器后缀)或Shared文件夹(都位于父Views文件夹下)。例如,以下代码将呈现位于Views\SampleViews\Shared文件夹中的SampleAction.cshtml视图:

[Route("[controller]/[action]")]
public class SampleController: Controller
{
  public ActionResult SampleAction()
  {
    return View();
  }
}

Note

首先搜索以控制器命名的视图文件夹。如果找不到视图,则搜索Shared文件夹。如果还是找不到,就会抛出一个异常。

要使用不同于操作方法名称的名称来呈现视图,请传入文件名(不带扩展名cshtml)。下面的代码将呈现CustomViewName.cshtml视图:

public ActionResult SampleAction()
{
  return View("CustomViewName");
}

最后两个重载用于传入成为视图模型的数据对象。第一个示例使用默认视图名称,第二个示例指定不同的视图名称。

public ActionResult SampleAction()
{
  var sampleModel = new SampleActionViewModel();
  return View(sampleModel);
}
public ActionResult SampleAction()
{
  var sampleModel = new SampleActionViewModel();
  return View("CustomViewName",sampleModel);
}

下一节将使用一个视图详细探索 Razor 视图引擎,该视图是从一个名为RazorSyntax()的动作方法在HomeController上呈现的。动作方法将从注入到方法中的CarRepo类的实例中获得一个Car记录,并将Car实例作为模型传递给视图。

打开自动 Lot 中的HomeController。Mvc 应用的Controllers目录,并添加下面的using语句:

using AutoLot.Dal.Repos.Interfaces;

接下来,向控制器添加RazorSyntax()方法。

[HttpGet]
public IActionResult RazorSyntax([FromServices] ICarRepo carRepo)
{
  var car = carRepo.Find(1);
  return View(car);
}

action 方法用HTTPGet属性修饰,只要请求是 HTTP Get,就将该方法设置为/Home/RazorSyntax的应用端点。ICarRepo参数上的FromServices属性通知 ASP.NET 核心,该参数不应该与任何传入数据绑定,而是该方法从依赖注入容器接收ICarRepo的实例。该方法获取一个Car的实例,并使用View方法返回一个ViewResult。由于视图没有被命名,ASP.NET 核心将在Views\Home目录或Views\Shared目录中寻找名为RazorSyntax.cshtml的视图。如果在任一位置都找不到视图,将向客户机(浏览器)返回一个异常。

运行应用,将浏览器导航到https://localhost:5001/Home/RazorSyntax(如果您使用 Visual Studio 和 IIS,您将需要更新端口)。由于项目中没有可以满足请求的视图,所以浏览器会返回一个异常。回想一下第二十九章,如果环境是Development,Startup类的Configure()方法将UseDeveloperExceptionPage()方法添加到 HTTP 管道中。图 31-1 显示了该方法的实际效果。

img/340876_10_En_31_Fig1_HTML.jpg

图 31-1。

来自开发人员例外页面的错误消息

开发人员异常页面提供了调试应用的大量信息,包括原始异常详细信息和堆栈跟踪。现在,注释掉Configure()方法中的那一行,用“标准”错误处理程序替换它,就像这样:

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

再次运行 app,导航到http://localhost:5001/Home/RazorSyntax,会看到标准错误页面,如图 31-2 所示。

img/340876_10_En_31_Fig2_HTML.jpg

图 31-2。

来自标准错误页面的错误消息

Note

本章中的示例 URL 都使用 Kestrel 和端口 5001。如果您将 Visual Studio 与 IIS Express 一起使用,请使用 IIS 的launchsettings.json配置文件中的 URL。

标准的错误处理程序将错误重定向到HomeControllerError动作方法。记得返回Configure()方法来使用开发者异常页面:

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

有关自定义错误处理和其他可用选项的更多信息,请查阅文档: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-5.0

Razor 视图引擎和 Razor 语法

Razor 视图引擎是对 Web 表单视图引擎的改进,它使用 Razor 作为核心语言。Razor 是嵌入到视图中的服务器端代码,基于 C#,纠正了 Web 表单视图引擎的许多问题。Razor 与 HTML 和 CSS 的结合使得代码比使用 Web Forms 视图引擎语法更加简洁易读。

首先,右击自动 Lot 中的Views\Home文件夹,添加一个新视图。Mvc 项目并选择添加➤新项。从添加新项目-自动 Lot 中选择 Razor 视图-空。Mvc 对话框并将视图命名为RazorSyntax.cshtml

Note

当你右击Views\Home文件夹时,还有一个菜单选项:添加➤视图。但是,该对话框会将您带回“添加新项目”对话框。

Razor 视图通常使用@model指令进行强类型化(注意小写的 m )。通过在视图顶部添加以下内容,将新视图的类型更新为Car实体:

@model AutoLot.Models.Entities.Car

在页面顶部添加一个<h1>标签。这与剃刀无关;它只是在页面上添加了一个标题。

<h1>Razor Syntax</h1>

Razor 语句块以一个@符号开始,或者是自包含语句(如foreach)或者是用大括号括起来的,如下例所示:

@for (var i = 0; i < 15; i++)
{
    //do something
}

@{
    //Code Block
    var foo = "Foo";
    var bar = "Bar";
    var htmlString = "<ul><li>one</li><li>two</li></ul>";
}

要将变量值输出到视图,只需在变量名中使用@符号。这相当于Response.Write()。注意,当直接输出到浏览器时,语句后面没有结束分号。

@foo
<br />
@htmlString
<br />
@foo.@bar
<br />

前面的例子(@foo.@bar)结合了两个变量,它们之间有一个句点(.)。这不是导航属性链的常用 C#“点”符号。它只是两个变量到响应流的输出,它们之间有一个物理周期。如果你需要在一个变量上“打点”,在变量上使用@,然后像平常一样写你的代码。

@foo.ToUpper()

如果你想输出原始的 HTML,你可以使用一个叫做的 HTML 助手。这些是内置在 Razor 视图引擎中的助手。以下是输出原始 HTML 的代码行:

@Html.Raw(htmlString)
<hr />

代码块可以混合标记和代码。以标记开头的行被解释为 HTML,而所有其他行被解释为代码。如果一行以而非代码的文本开始,您必须使用内容指示器(@:<text></text>内容块指示器。请注意,线条可以来回转换。这里有一个例子:

@{
    @:Straight Text
    <div>Value:@Model.Id</div>
    <text>
        Lines without HTML tag
    </text>
    <br />
}

如果想转义@符号,就用双@。Razor 也足够智能来处理电子邮件地址,所以它们不需要被转义。如果您需要 Razor 像 Razor 令牌一样处理@符号,请添加括号。

foo@foo.com
<br />
@@foo
<br />
test@foo
<br/>
test@(foo)
<br />

前面的代码分别输出foo@foo.com@foo``test@footestFoo

剃刀评论以@*开头,以*@结尾。

@*
    Multiline Comments
    Hi.
*@

Razor 也支持内联函数。以下示例函数对字符串列表进行排序:

@functions {
    public static IList<string> SortList(IList<string> strings)  {
        var list = from s in strings orderby s select s;
        return list.ToList();
    }
}

下面的代码创建一个字符串列表,使用SortList()函数对它们进行排序,然后将排序后的列表输出到浏览器:

@{
    var myList = new List<string> {"C", "A", "Z", "F"};
    var sortedList = SortList(myList);
}
@foreach (string s in sortedList)
{
    @s@:&nbsp;
}
<hr/>

下面是另一个示例,它创建了一个可用于将字符串设置为粗体的委托:

@{
    Func<dynamic, object> b = @<strong>@item</strong>;
}
This will be bold: @b("Foo")

Razor 还包含 HTML 助手,它们是由 ASP.NET 核心提供的方法。两个例子是DisplayForModel()EditorForModel()。前者使用视图模型上的反射来显示在网页中。后者也使用反射为编辑表单创建 HTML(注意,它不提供Form标签,只提供模型的标记)。HTML 助手将在本章后面详细讨论。

最后,新的 ASP.NET 核心是标签助手。标记帮助器结合了标记和代码,将在本章后面介绍。

视图

视图是带有cshtml扩展名的特殊代码文件,使用 HTML 标记、CSS、JavaScript 和 Razor 语法的组合编写。

视图目录

文件夹是 ASP.NET 核心项目中使用 MVC 模式存储视图的地方。在Views文件夹的根目录下,有两个文件:_ViewStart.cshtml_ViewImports.cshtml

在呈现任何其他视图(不包括部分视图和布局)之前,_ViewStart.cshtml执行它的代码。该文件通常用于为没有指定默认布局的视图设置默认布局。布局在“布局”一节中有更详细的讨论。这里显示的是_ViewStart.cshtml文件:

@{
    Layout = "_Layout";
}

_ViewImports.cshtml文件用于导入共享指令,如using语句。这些内容适用于_ViewImports文件的同一目录或子目录中的所有视图。为 AutoLot.Models.Entities 添加一条using语句

@using AutoLot.Mvc
@using AutoLot.Mvc.Models
@using AutoLot.Models.Entities
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

标签助手将覆盖@addTegHelper行。

Note

为什么_ViewStart.html_ViewImports.cshtml_Layout.cshtml的前导下划线?Razor 视图引擎最初是为 WebMatrix 创建的,它不允许任何以下划线开头的文件被直接呈现。所有核心文件(如布局和配置)的名称都以下划线开头。这不是 MVC 所关心的约定,因为 MVC 没有 WebMatrix 的问题,但是下划线的传统仍然存在。

如前所述,每个控制器在Views文件夹下都有自己的目录,其中存储了特定的视图。这些名称与控制器的名称相匹配(减去单词控制器)。例如,Views\Cars目录保存了CarsController的所有视图。视图通常以呈现它们的操作方法命名,尽管名称可以更改,如前所示。

共享目录

Views下有一个名为Shared的特殊目录。该目录包含所有控制器和动作都可用的视图。如上所述,如果在特定于控制器的目录中找不到请求的视图文件,则会搜索共享文件夹。

显示模板文件夹

DisplayTemplates文件夹保存自定义模板,这些模板控制类型的呈现方式,促进代码重用和显示一致性。当调用DisplayFor() / DisplayForModel()方法时,Razor 视图引擎会寻找一个与正在呈现的类型同名的模板,例如,Car.cshtml表示一个Car类。如果找不到自定义模板,则使用反射来呈现标记。搜索路径从Views\{CurrentControllerName}\DisplayTemplates文件夹开始,如果没有找到,则在Views\Shared\DisplayTemplates文件夹中查找。这两种方法都采用可选参数来指定模板名称。

日期时间显示模板

Views\Shared文件夹下创建一个名为DisplayTemplates的新文件夹。将名为DateTime.cshtml的新视图添加到该文件夹中。清除所有生成的代码和注释,并用以下内容替换它们:

@model DateTime?
@if (Model == null)
{
  @:Unknown
}
else
{
  if (ViewData.ModelMetadata.IsNullableValueType)
  {
    @:@(Model.Value.ToString("d"))
  }
  else
  {
    @:@(((DateTime)Model).ToString("d"))
  }
}

注意,强类型化视图的@model指令使用了小写的m。当提到 Razor 中模型的赋值时,使用大写的M。在这个例子中,模型定义是可空的。如果传递到视图中的模型值为 null,模板将显示单词Unknown。否则,它使用可空类型的Value属性或实际模型本身,以短日期格式显示日期。

汽车展示模板

Views目录下新建一个名为Cars的目录,在Cars目录下增加一个名为DisplayTemplates的目录。将名为Car.cshtml的新视图添加到该文件夹中。清除所有生成的代码和注释,并用下面的代码替换它们,这将显示一个Car实体:

@model AutoLot.Models.Entities.Car
<dl class="row">
  <dt class="col-sm-2">
    @Html.DisplayNameFor(model => model.MakeId)
  </dt>
  <dd class="col-sm-10">
    @Html.DisplayFor(model => model.MakeNavigation.Name)
  </dd>
  <dt class="col-sm-2">
    @Html.DisplayNameFor(model => model.Color)
  </dt>
  <dd class="col-sm-10">
    @Html.DisplayFor(model => model.Color)
  </dd>
  <dt class="col-sm-2">
    @Html.DisplayNameFor(model => model.PetName)
  </dt>
  <dd class="col-sm-10">
    @Html.DisplayFor(model => model.PetName)
  </dd>
</dl>

HTML 帮助器显示属性的名称,除非该属性是用Display(Name="")DisplayName("")属性修饰的,在这种情况下使用显示值。DisplayFor()方法显示表达式中指定的模型属性的值。注意,MakeNavigation的导航属性被用来获取品牌名称。

如果您运行应用并导航到RazorSyntax页面,您可能会惊讶于没有使用Car显示模板。这是因为模板在Cars视图文件夹中,而RazorSyntax动作方法和视图是从HomeController中调用的。HomeController中的动作方法将只在HomeShared目录中搜索视图,因此不会找到汽车显示模板。

如果您将Car.cshtml模板移动到Shared\DisplayTemplates目录中,RazorSyntax视图将使用显示模板。

带彩色显示模板的汽车

以下模板类似于Car模板。不同之处在于,这个模板根据模型的Color属性值来改变颜色文本的颜色。将名为CarWithColors.cshtml的新模板添加到Cars\DisplayTemplates目录中,并将标记更新为:

@model Car
<hr />
<div>
  <dl class="row">
    <dt class="col-sm-2">
      @Html.DisplayNameFor(model => model.PetName)
    </dt>
    <dd class="col-sm-10">
      @Html.DisplayFor(model => model.PetName)
    </dd>
    <dt class="col-sm-2">
      @Html.DisplayNameFor(model => model.MakeNavigation)
    </dt>
    <dd class="col-sm-10">
      @Html.DisplayFor(model => model.MakeNavigation.Name)
    </dd>
    <dt class="col-sm-2">
      @Html.DisplayNameFor(model => model.Color)
    </dt>
    <dd class="col-sm-10" style="color:@Model.Color">
      @Html.DisplayFor(model => model.Color)
    </dd>
  </dl>
</div>

要使用这个模板而不是Car.cshtml模板,用模板的名称调用DisplayForModel()(注意位置规则仍然适用)。

@Html.DisplayForModel("CarWithColors")

EditorTemplates 文件夹

除了模板用于编辑之外,EditorTemplates文件夹与DisplayTemplates文件夹的工作方式相同。

汽车编辑模板

Views\Cars目录下创建一个名为EditorTemplates的新目录。将名为Car.cshtml的新视图添加到该文件夹中。清除所有生成的代码和注释,并用下面的代码替换它们,它代表编辑一个Car实体的标记:

@model Car
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
    <label asp-for="PetName" class="col-form-label"></label>
    <input asp-for="PetName" class="form-control" />
    <span asp-validation-for="PetName" class="text-danger"></span>
</div>
<div class="form-group">
    <label asp-for="MakeId" class="col-form-label"></label>
    <select asp-for="MakeId" class="form-control" asp-items="ViewBag.MakeId"></select>
</div>
<div class="form-group">
    <label asp-for="Color" class="col-form-label"></label>
    <input asp-for="Color" class="form-control"/>
    <span asp-validation-for="Color" class="text-danger"></span>
</div>

编辑器模板使用了几个标签助手(asp-forasp-itemsasp-validation-forasp-validation-summary)。这些将在本章后面讨论。

这个模板由EditorFor() / EditorForModel() HTML 助手调用。像显示模板一样,这些方法将寻找名为Car.cshtml的视图或方法中命名的视图。

布局

与 Web 窗体母版页类似,MVC 支持视图之间共享的布局,以使站点的页面具有一致的外观和感觉。导航到Views\Shared文件夹并打开_Layout.cshtml文件。这是一个成熟的 HTML 文件,带有<head><body>标签。

该文件是其他视图呈现的基础。此外,由于页面的大部分内容(如导航和任何页眉和/或页脚标记)由布局页面处理,因此视图页面保持小而简单。在文件中向下滚动,直到看到下面一行 Razor 代码:

@RenderBody()

该行指示布局页面在哪里呈现视图。现在向下滚动到右</body>标签之前的那一行。以下代码行为布局创建一个新部分,并使其成为可选部分:

@await RenderSectionAsync("scripts", required: false)

通过将true作为第二个参数传入,也可以将节标记为required。它们也可以同步渲染,如下所示:

@RenderSection("Header",true)

视图文件的@section块中的任何代码和/或标记都不会用@RenderBody()调用来呈现,而是呈现在布局的节定义的位置。例如,假设您有一个包含以下部分实现的视图:

@section Scripts {
  <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
}

视图中的代码被呈现到布局中,代替了节定义。如果布局具有以下定义:

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)

然后添加视图的部分,导致这个标记被发送到浏览器:

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>

ASP.NET 核心中的两个新选项是IgnoreBodyIgnoreSection。放置在布局中的这些方法将分别不呈现视图的主体或特定部分。这些功能可以根据条件逻辑(如安全级别)打开或关闭布局中的视图功能。

指定视图的默认布局

如前所述,默认布局页面是在_ViewStart.cshtml文件中定义的。任何未指定布局的视图将使用位于视图目录或其上方的第一个_ViewStart.cshtml文件中定义的布局。

局部视图

分部视图在概念上类似于 Web 窗体中的用户控件。局部视图对于封装 UI 很有用,这有助于减少重复的代码和/或标记。局部视图不使用布局,而是被注入到另一个视图中,或者使用视图组件进行渲染(本章稍后将介绍)。

更新布局和局部

有时,布局文件会变得很大,难以处理。管理这一点的一个技巧是将布局分割成一组重点突出的部分。

创造分音

Shared文件夹下创建一个名为Partials的新文件夹。创建三个名为_Head.cshtml_JavaScriptFiles.cshtml_Menu.cshtml的空视图。

头部偏角

剪切布局中位于<head></head>标签之间的内容,并将其粘贴到_Head.cshtml文件中。

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - AutoLot.Mvc</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />

_Layout.cshtml中,用调用替换删除的标记,以呈现新的部分:

<head>
  <partial name="Partials/_Head"/>
</head>

<partial>标签是标签助手的另一个例子。name属性是部分的名称,路径从视图的当前目录开始,在本例中是Views\Shared

菜单部分

对于部分菜单,剪切掉<header></header>标签(不是<head></head>标签)之间的所有标记,并将其粘贴到_Menu.cshtml文件中。更新_Layout来渲染Menu的局部。

<header>
  <partial name="Partials/_Menu"/>
</header>

JavaScript 文件部分

此时的最后一步是剪切出 JavaScript 文件的<script>标签,并将它们粘贴到JavaScriptFiles片段中。确保将RenderSection标签留在原位。这里是JavaScriptFiles偏:

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

以下是_Layout.cshtml文件的当前标记:

<!DOCTYPE html>
<html lang="en">
<head>
  <partial name="Partials/_Head" />
</head>
<body>
  <header>
    <partial name="Partials/_Menu" />
  </header>
  <div class="container">
    <main role="main" class="pb-3">
      @RenderBody()
    </main>
  </div>

  <footer class="border-top footer text-muted">
    <div class="container">
      &copy; 2021 - AutoLot.Mvc - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
    </div>
  </footer>
  <partial name="Partials/_JavaScriptFiles" />
  @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

向视图发送数据

有多种方法可以将数据发送到视图中。当视图是强类型时,可以在呈现视图时发送数据(通过动作方法或通过<partial>标签助手)。

强类型视图和视图模型

当模型或ViewModel被传递到视图方法中时,该值被赋给强类型视图的@model属性,如下所示(注意小写的 m ):

@model IEnumerable<Order>

@model为视图设置类型,然后可以通过使用@Model Razor 命令来访问,就像这样(注意大写的 M ):

@foreach (var item in Model)
{
  //Do something interesting here
}

RazorViewSyntax()动作方法演示了视图从动作方法中获取数据。

[HttpGet]
public IActionResult RazorSyntax([FromServices] ICarRepo carRepo)
{
  var car = carRepo.Find(1);
  return View(car);
}

模型值可以通过<partial>传入,如下所示:

<partial name="Partials/_CarListPartial" model="@Model"/>

ViewBag、ViewData 和 TempData

ViewBagViewDataTempData对象是向视图发送少量数据的机制。表 31-1 列出了将数据从控制器传递到视图(除了Model属性)或从控制器传递到控制器的三种机制。

表 31-1。

向视图发送数据的其他方法

|

数据传输对象

|

使用说明

| | --- | --- | | TempData | 这是一个短命的对象,只在当前请求和下一个请求期间工作。通常在重定向到另一个操作方法时使用。 | | ViewData | 允许在名称-值对中存储值的字典(例如,ViewData["Title"] = "My Page")。 | | ViewBag | ViewData字典的动态包装器(例如ViewBag.Title = "My Page")。 |

ViewBagViewData都指向同一个对象;它们只是提供了不同的方法来访问数据。

让我们再来看看您之前创建的_HeadPartial.cshtml文件(重要的一行用粗体显示):

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - AutoLot.Mvc</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />

您会注意到<title>属性使用ViewData来设置值。由于ViewData是一个剃刀构造,它以@符号开始。要看到这一点,用下面的代码更新RazorSyntax.cshtml视图:

@model AutoLot.Models.Entities.Car
@{
    ViewData["Title"] = "RazorSyntax";
}
<h1>Razor Syntax</h1>
...

现在,当您运行应用并导航到https://localhost:5001/Home/RazorSyntax时,您将看到标题“Razor Syntax–AutoLot”。Mvc”在浏览器标签。

标签助手

标签助手是 ASP.NET 核心中引入的新功能。标签助手是表示服务器端代码的标记(自定义标签或标准标签上的属性)。然后,服务器端代码帮助形成发出的 HTML。它们极大地改善了 MVC 视图的开发体验和可读性。

与作为 Razor 方法调用的 HTML 帮助器不同,标记帮助器是添加到标准 HTML 元素或独立自定义标记的属性。如果您正在使用 Visual Studio 进行开发,那么对于内置的标记助手来说,IntelliSense 还有一个额外的好处。

例如,下面的 HTML 助手为客户的FullName属性创建一个标签:

@Html.Label("FullName","Full Name:",new {@class="customer"})

这会生成以下 HTML:

<label class="customer" for="FullName">Full Name:</label>

对于一直使用 ASP.NET MVC 和 Razor 的 C# 开发人员来说,HTML helper 语法可能已经很好理解了。但这并不直观,尤其是对于一个用 HTML/CSS/JavaScript 而不是 C# 工作的人来说。

标签助手版本如下所示:

<label class="customer" asp-for="FullName">Full Name:</label>

它们产生相同的输出,但是标记帮助器,通过集成到 HTML 标记中,使您保持“在标记中”

有许多内置的标记帮助器,它们被设计用来代替它们各自的 HTML 帮助器。然而,并不是所有的 HTML 助手都有相关的标签助手。表 31-2 列出了更常用的标签助手,它们对应的 HTML 助手,以及可用的属性。我们将在本章的其余部分详细介绍它们。

表 31-2。

常用的内置标签助手

|

标签助手

|

HTML 助手

|

可用属性

| | --- | --- | --- | | Form | Html.BeginForm Html.BeginRouteForm Html.AntiForgeryToken | asp-route—用于命名路线(不能与控制器或动作属性一起使用)。asp-antiforgery—是否需要添加防伪(默认为真)。asp-area—该地区的名称。asp-controller—控制器的名称。asp-action—动作的名称。asp-route-<ParameterName>—将参数添加到路线中,例如asp-route-id="1"asp-page—Razor 页面的名称。asp-page-handler—Razor 页面处理程序的名称。asp-all-route-data—附加路线值字典。 | | Form Action``(button or input type=image) | N/A | asp-route—用于命名路线(不能与控制器或动作属性一起使用)。asp-antiforgery—是否需要添加防伪(默认为真)。asp-area—该地区的名称。asp-controller—控制器的名称。asp-action—动作的名称。asp-route-<ParameterName>—将参数添加到路线中,例如asp-route-id="1"asp-page—Razor 页面的名称。asp-page-handler—Razor 页面处理程序的名称。asp-all-route-data—附加路线值字典。 | | Anchor | Html.ActionLink | asp-route—用于命名路线(不能与控制器或动作属性一起使用)。asp-area—区域的名称。asp-controller—定义控制器。asp-action—定义动作。asp-protocol —HTTP 或 HTTPS。asp-fragment —URL 片段。asp-host—主机名称。asp-route-<ParameterName>—将参数添加到路线中,例如asp-route-id="1"asp-page—Razor 页面的名称。asp-page-handler—Razor 页面处理程序的名称。asp-all-route-data—附加路径值的字典。 | | Input | Html.TextBox/TextBoxFor Html.Editor/EditorFor | asp-for—一个模型属性。可以浏览模型(Customer.Address.AddressLine1)和使用表达式(asp-for="@localVariable")。idname属性自动生成。任何 HTML5 data-val属性和type属性都是自动生成的。 | | TextArea | Html.TextAreaFor | asp-for—一个模型属性。可以浏览模型(Customer.Address.Description)和使用表达式(asp-for="@localVariable")。idname属性自动生成。任何 HTML5 data-val属性和type属性都是自动生成的。 | | Label | Html.LabelFor | asp-for—一个模型属性。可以浏览模型(Customer.Address.AddressLine1)和使用表达式(asp-for="@localVariable")。显示Display属性的值(如果存在);否则使用属性名。 | | Partial | Html.Partial(Async)Html.RenderPartial(Async) | name—局部视图的路径和名称。for—当前表单上的模型表达式将成为分部中的模型。model—局部模型中的对象。view-data——ViewData为偏科。 | | Select | Html.DropDownListFor Html.ListBoxFor | asp-for—一个模型属性。可以浏览模型(Customer.Address.AddressLine1)和使用表达式(asp-for="@localVariable")。asp-items—指定options元素。自动生成selected="selected"属性。自动生成idname属性。任何 HTML5 data-val属性都是自动生成的。 | | Validation Message (Span) | Html.ValidationMessageFor | asp-validation-for—一个模型属性。可以浏览模型(Customer.Address.AddressLine1)和使用表达式(asp-for="@localVariable")。将data-valmsg-for属性添加到span中。 | | Validation Summary (Div) | Html.ValidationSummaryFor | asp-validation-summary—选择AllModelOnlyNone中的一个。将data-valmsg-summary属性添加到div。 | | Link | N/A | asp-append-version—将文件的哈希作为版本指示符附加到文件名(作为查询字符串),用于缓存破坏。href—源的内容传递网络版本的地址。asp-fallback-href—主文件不可用时使用的后备文件;通常与 CDN 源一起使用。asp-fallback-href-include—回退时要包含的文件的成组文件列表。asp-fallback-href-exclude—回退时要排除的文件的全局文件列表。asp-fallback-test-*—用于回退测试的属性。包括classpropertyvalueasp-href-include—要包含的文件的成组文件模式。asp-href-exclude—要排除的文件的组合文件模式。 | | Script | N/A | asp-append-version—将文件的哈希作为版本指示符附加到文件名(作为查询字符串),用于缓存破坏。src—源的内容传递网络版本的地址asp-fallback-src—主文件不可用时使用的后备文件;通常与 CDN 源一起使用。asp-fallback-src-include—回退时要包含的文件的成组文件列表。asp-fallback-src-exclude—回退时要排除的文件的全局文件列表。asp-fallback-test—回退测试中使用的脚本方法。asp-src-include—要包含的文件的 globbed 文件模式。asp-src-exclude—要排除的文件的组合文件模式。 | | Image | N/A | asp-append-version—将文件的哈希作为版本指示符附加到文件名(作为查询字符串),用于缓存破坏。 | | Environment | N/A | names—触发内容呈现的单个主机环境名称或逗号分隔的名称列表(忽略大小写)。include—触发内容呈现的单个主机环境名称或逗号分隔的名称列表(忽略大小写)。exclude—要从内容呈现中排除的单个主机环境名称或逗号分隔的名称列表(忽略大小写)。 |

启用标签助手

标记助手必须对任何想要使用它们的代码可见。标准模板中的_ViewImports.html文件已经包含以下行:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

这使得Microsoft.AspNetCore.Mvc.TagHelpers集合中的所有标签助手(包含所有内置标签助手)对位于或低于_ViewImports.cshtml文件目录级别的所有视图可用。

表单标签帮助器

表单标签助手取代了Html.BeginFormHtml.BeginRouteForm HTML 助手。例如,要创建一个表单,该表单提交给带有一个参数(Id)的CarsController上的Edit动作的 HTTP Post版本,请使用以下代码和标记:

<form method="post" asp-controller="Cars" asp-action="Edit"
  asp-route-id="@Model.Id" >
<!-- Omitted for brevity -->
</form>

从严格的 HTML 角度来看,Form标签可以在没有表单标签助手属性的情况下工作。如果这些属性都不存在,那么它只是一个普通的旧 HTML 表单,必须手动添加防伪标记。然而,一旦添加了一个asp-标签,防伪标记就被添加到表单中。可以通过在表单标签中添加asp-antiforgery="false"来禁用防伪标记。防伪标记将在后面介绍。

汽车创造形式

Car实体的创建表单提交给CarsControllerCreate动作方法。在Views\Cars目录中添加一个名为Create.cshtml的新的空 Razor 视图。将视图更新为以下内容:

@model Car

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

<h1>Create a New Car</h1>
<hr/>
<div class="row">
  <div class="col-md-4">
    <form asp-controller="Cars" asp-action="Create">
    </form>
  </div>
</div>

这不是一个完整的视图,但是它足以显示我们到目前为止所讨论的内容以及表单标记帮助器。回顾一下,第一行将视图强类型化为Car实体类。Razor 块为页面设置特定于视图的标题。HTML <form>标签具有asp-controllerasp-action属性,它们在服务器端执行以形成标签并添加防伪标记。

为了渲染这个视图,将名为CarsController的新控制器添加到Controllers文件夹中。将代码更新为以下内容(该代码将在本章稍后更新):

using Microsoft.AspNetCore.Mvc;

namespace AutoLot.Mvc.Controllers
{
  [Route("[controller]/[action]")]
  public class CarsController : Controller
  {
    public IActionResult Create()
    {
      return View();
    }
  }
}

现在运行应用并导航到http://localhost:5001/Cars/Create。检查源代码会发现表单具有基于asp-controllerasp-action的动作属性,方法被设置为post,并且__RequestVerificationToken被添加为隐藏的表单输入。

<form action="/Cars/Create" method="post">
  <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Hqg5HsrvCtOkkLRHY4ukxwvix0vkQ3vOvezvtJWdl0P5lwbI5-FFWXh8KCFZo7eKxveCuK8NRJywj8Jz23pP2nV37fIGqqcITRyISGgq7tRYZDuPv8NMIYz2nCWRiDbxOvlkg61DTDW9BrJxr8H63Y">
</form>

在本章中,Create视图将被更新。

表单操作标签帮助器

表单动作标签帮助器用于按钮和图像,以更改包含它们的表单的动作。例如,添加到编辑表单的以下按钮将导致 post 请求转到Create端点:

<button type="submit" asp-action="Create">Index</button>

锚标记辅助对象

锚点标签辅助对象替换了Html.ActionLink HTML 辅助对象。例如,要创建 RazorSyntax 视图的链接,请使用以下代码:

<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="RazorSyntax">
  Razor Syntax
</a>

要将 Razor 语法页面添加到菜单中,请将_Menu.cshtml更新为以下内容,在 Home 和 Privacy 菜单项之间添加新菜单项(锚标签周围的<li>标签用于引导菜单):

...
<li class="nav-item">
  <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
  <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="RazorSyntax">Razor Syntax</a>
</li>
<li class="nav-item">
  <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>

输入标签助手

标签助手是最通用的标签助手之一。除了自动生成 HTML idname属性,以及任何 HTML5 data-val验证属性之外,tag helper 还基于目标属性的数据类型构建适当的 HTML 标记。表 31-3 列出了基于。属性的 NET Core 类型。

表 31-3。

生成的 HTML 类型。NET 类型使用输入标记帮助器

|

.NET 类型

|

生成的 HTML 类型

| | --- | --- | | Bool | type="checkbox" | | String | type="text" | | DateTime | type="datetime" | | ByteIntSingleDouble | type="number" |

此外,Input标签助手将根据数据注释添加 HTML5 type属性。表 31-4 列出了一些最常见的注释和生成的 HTML5 type属性。

表 31-4。

生成的 HTML5 类型属性.NET 数据注释

|

.NET 数据注释

|

生成的 HTML5 类型属性

| | --- | --- | | EmailAddress | type="email" | | Url | type="url" | | HiddenInput | type="hidden" | | Phone | type="tel" | | DataType(DataType.Password) | type="password" | | DataType(DataType.Date) | type="date" | | DataType(DataType.Time) | type="time" |

Car.cshtml编辑器模板包含用于PetNameColor属性的<input>标签。提醒一下,这里只列出了这些标签:

<input asp-for="PetName" class="form-control" />
<input asp-for="Color" class="form-control"/>

输入标签帮助器将nameid属性添加到呈现的标签、属性的现有值(如果有)和 HTML5 验证属性中。这两个字段都是必需的,并且字符串长度限制为 50。以下是这两个属性的呈现标记:

<input class="form-control" type="text" data-val="true" data-val-length="The field Pet Name must be a string with a maximum length of 50." data-val-length-max="50" data-val-required="The Pet Name field is required." id="PetName" maxlength="50" name="PetName" value="Zippy">

<input class="form-control valid" type="text" data-val="true" data-val-length="The field Color must be a string with a maximum length of 50." data-val-length-max="50" data-val-required="The Color field is required." id="Color" maxlength="50" name="Color" value="Black" aria-describedby="Color-error" aria-invalid="false">

TextArea 标签帮助器

<textarea>标签助手自动添加idname属性以及为属性定义的任何 HTML5 验证标签。例如,下面的代码行为Description属性创建了一个textarea标记:

<textarea asp-for="Description"></textarea>

选择标签助手

标签助手从模型属性和集合中构建输入选择标签。与其他输入标签助手一样,idname被添加到标记中,任何 HTML5 data-val属性也是如此。如果模型属性值与选择列表项的值之一匹配,则该选项会将选定的属性添加到标记中。

例如,假设一个模型有一个名为Country的属性和一个名为CountriesSelectList,其列表定义如下:

public List<SelectListItem> Countries { get; } = new List<SelectListItem>
{
  new SelectListItem { Value = "MX", Text = "Mexico" },
  new SelectListItem { Value = "CA", Text = "Canada" },
  new SelectListItem { Value = "US", Text = "USA"  },
};

以下标记将使用适当的选项呈现select标记:

<select asp-for="Country" asp-items="Model.Countries"></select>

如果Country属性的值被设置为 CA,下面的完整标记将被输出到视图中:

<select id="Country" name="Country">
  <option value="MX">Mexico</option>
  <option selected="selected" value="CA">Canada</option>
  <option value="US">USA</option>
</select>

验证标签助手

验证消息和验证摘要标签帮助器与Html.ValidationMessageForHtml.ValidationSummaryFor HTML 帮助器非常相似。第一个应用于模型上特定属性的 HTML span,第二个应用于div标签并代表整个模型。验证总结可选择All错误、ModelOnly(不包括模型属性错误)或None

Car.cshtml文件的EditorTemplate中调用验证标签助手(这里以粗体显示):

<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
    <label asp-for="PetName" class="col-form-label"></label>
    <input asp-for="PetName" class="form-control" />
    <span asp-validation-for="PetName" class="text-danger"></span>
</div>
<div class="form-group">
    <label asp-for="MakeId" class="col-form-label"></label>
    <select asp-for="MakeId" class="form-control" asp-items="ViewBag.MakeId"></select>
</div>
<div class="form-group">
    <label asp-for="Color" class="col-form-label"></label>
    <input asp-for="Color" class="form-control"/>
    <span asp-validation-for="Color" class="text-danger"></span>
</div>

这些帮助器将显示来自绑定和验证的ModelState错误,如图 31-3 所示。

img/340876_10_En_31_Fig3_HTML.jpg

图 31-3。

有效的验证标签助手

环境标签助手

标签助手通常用于根据站点运行的环境有条件地加载 JavaScript 和 CSS 文件(或任何标记)。打开_Head.cshtml partial 并将标记更新如下:

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - AutoLot.Mvc</title>
<environment include="Development">
  <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
</environment>
<environment exclude="Development">
  <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
</environment>
<link rel="stylesheet" href="~/css/site.css" />

当环境设置为Development时,第一个<environment>标签助手使用include=”Development”属性来包含所包含的文件。在前面的代码中,加载了 Bootstrap 的非精简版本。当环境不是而是 Development时,第二个标签助手使用exclude=”Development”来使用包含的文件,并加载缩小版本的bootstrap.css。在开发和非开发环境中,site.css文件不会改变,所以它被列在<environment>标签助手之外。

此外,将_JavaScriptFiles.cshtml部分更新为以下内容(注意,Development部分中的文件不再具有.min扩展名):

<environment include="Development">
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
</environment>
<environment exclude="Development">
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

链接标签帮助器

<link>标签助手具有用于本地和远程的属性。与本地文件一起使用的asp-append-version属性将文件的散列作为查询字符串参数添加到发送给浏览器的 URL 中。当文件改变时,散列也改变,更新发送到浏览器的 URL。由于链接已更改,浏览器会清除该文件的缓存并重新加载它。将_Head.cshtml文件中的bootstrap.csssite.css链接标签更新如下:

<environment include="Development">
  <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" asp-append-version="true"/>
</environment>
<environment exclude="Development">
  <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
</environment>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>

发送到浏览器的site.css文件的链接现在如下所示(您的散列会有所不同):

<link href="/css/site.css?v=v9cmzjNgxPHiyLIrNom5fw3tZj3TNT2QD7a0hBrSa4U" rel="stylesheet">

当从内容交付网络加载 CSS 文件时,标记助手提供了一种测试机制来确保文件被正确加载。该测试寻找特定 CSS 类的特定属性值,如果该属性不匹配,tag helper 将加载回退文件。更新_Head.cshtml文件中的exclude=”Development”以匹配以下内容:

<environment exclude="Development">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
    asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
    crossorigin="anonymous"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"/>
</environment>

脚本标签帮助器

<script>标签助手类似于<link>标签助手,具有缓存破坏和 CDN 回退设置。asp-append-version属性对脚本和链接样式表的作用是一样的。asp-fallback-*属性也用于 CDN 文件源。asp-fallback-test只是检查 JavaScript 的真实性,如果失败,就从后备源加载文件。

更新_JavaScriptFiles.cshtml片段以使用缓存破坏和 CDN 回退功能(注意 MVC 模板已经在site.js脚本标签上有了asp-append-version)。

<environment include="Development">
  <script src="~/lib/jquery/dist/jquery.js" asp-append-version="true"></script>
  <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
    asp-fallback-src="~/lib/jquery/dist/jquery.min.js" asp-fallback-test="window.jQuery"
    crossorigin="anonymous"
    integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
  </script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"
    asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"
    asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
    crossorigin="anonymous"
    integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o">
  </script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

需要用<environment><script>标签助手来更新_ValidationScriptsPartial.cshtml

<environment include="Development">
  <script src="~/lib/jquery-validation/dist/jquery.validate.js" asp-append-version="true"></script>
  <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js"
    asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
    asp-fallback-test="window.jQuery && window.jQuery.validator"
    crossorigin="anonymous"
    integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
  </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
    asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
    asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
    crossorigin="anonymous"
    integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
  </script>
</environment>

图像标签帮助器

图像标签帮助器提供了asp-append-version属性,其工作原理与链接和脚本标签帮助器中描述的一样。

自定义标签助手

自定义标记帮助器有助于消除重复代码。为了 AutoLot。Mvc,自定义标签助手将取代 HTML 来导航Car CRUD 屏幕。

奠定基础

定制标签助手使用一个UrlHelperFactoryIActionContextAccessor来创建基于路由的链接。我们还将添加一个字符串扩展方法来删除控制器名称中的Controller后缀。

更新 Startup.cs

要从非Controller派生类中创建UrlFactory的实例,必须将IActionContextAccessor添加到服务集合中。首先将以下名称空间添加到Startup.cs:

using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection.Extensions;

接下来,将下面一行添加到ConfigureServices()方法:

services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();

创建字符串扩展方法

当在代码中引用控制器名称时,ASP.NET 核心经常需要原始字符串值,没有Controller后缀。这防止了在没有调用string.Replace()的情况下使用nameof()方法。随着时间的推移,这变得越来越乏味,我们将创建一个字符串扩展方法来处理这个问题。

AutoLot.Services项目添加一个名为Utilities的新文件夹,并在该文件夹中添加一个名为StringExtensions.cs的新静态类。将代码更新为以下内容,以添加RemoveController()扩展方法:

using System;

namespace AutoLot.Mvc.Extensions
{
  public static class StringExtensions
  {
    public static string RemoveController(this string original)
      => original.Replace("Controller", "", StringComparison.OrdinalIgnoreCase);
  }
}

创建基类

在 AutoLot 的根目录下创建一个名为TagHelpers的新文件夹。Mvc 项目。在这个文件夹中,创建一个名为Base的新文件夹,在那个文件夹中,创建一个名为ItemLinkTagHelperBase.cs的类,将该类公共化、抽象化,并继承自TagHelper。将以下using语句添加到新文件中:

using AutoLot.Mvc.Controllers;
using AutoLot.Services.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AutoLot.Mvc.TagHelpers.Base
{
  public abstract class ItemLinkTagHelperBase : TagHelper
  {
  }
}

添加一个接受IActionContextAccessorIUrlHelperFactory实例的构造函数。使用UrlHelperFactoryActionContextAccessor创建一个IUrlHelper的实例,并将其存储在一个类级变量中。代码如下所示:

protected readonly IUrlHelper UrlHelper;
protected ItemLinkTagHelperBase(IActionContextAccessor contextAccessor, IUrlHelperFactory urlHelperFactory)
{
  UrlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);
}

添加一个公共属性来保存该项的Id,如下所示:

public int? ItemId { get; set; }

当一个标签助手被调用时,Process()方法被调用。Process()方法有两个参数,一个TagHelperContext和一个TagHelperOutputTagHelperContext用于获取标签上的任何其他属性,以及一个对象字典,用于与其他以子元素为目标的标签助手进行通信。TagHelperOutput用于创建渲染输出。

由于这是一个基类,我们将添加一个名为BuildContent()的方法,派生类可以从Process()方法中调用该方法。添加以下方法和代码:

protected void BuildContent(TagHelperOutput output,
  string actionName, string className, string displayText, string fontAwesomeName)
{
  output.TagName = "a"; // Replaces <item-list> with <a> tag
  var target = (ItemId.HasValue)
    ? UrlHelper.Action(actionName, nameof(CarsController).RemoveController(), new {id = ItemId})
    : UrlHelper.Action(actionName, nameof(CarsController).RemoveController());
  output.Attributes.SetAttribute("href", target);
  output.Attributes.Add("class",className);
  output.Content.AppendHtml($@"{displayText} <i class=""fas fa-{fontAwesomeName}""></i>");
}

前面的代码清单引用了字体 Awesome,它将在本章的后面添加到项目中。

项目详细信息标签帮助器

TagHelpers文件夹中创建一个名为ItemDetailsTagHelper.cs的新类。使类public继承ItemLinkTagHelperBase。将以下using语句添加到新文件中:

using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AutoLot.Mvc.TagHelpers
{
  public class ItemDetailsTagHelper : ItemLinkTagHelperBase
  {
  }
}

添加一个公共构造函数来接收所需的对象实例,并将它们传递给基类。

public ItemDetailsTagHelper(
    IActionContextAccessor contextAccessor,
    IUrlHelperFactory urlHelperFactory)
      : base(contextAccessor, urlHelperFactory) { }

覆盖Process()方法,调用基类中的BuildContent()方法。

public override void Process(TagHelperContext context, TagHelperOutput output)
{
  BuildContent(output,nameof(CarsController.Details),"text-info","Details","info-circle");
}

这将创建带有字体 Awesome info 图像的详细信息链接。为了防止编译器错误,在CarsController中添加一个基本的Details()方法。

public IActionResult Details()
{
  return View();
}

项目删除标签帮助器

TagHelpers文件夹中创建一个名为ItemDeleteTagHelper.cs的新类。使类public继承ItemLinkTagHelperBase。将以下using语句添加到新文件中:

using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AutoLot.Mvc.TagHelpers
{
  public class ItemDeleteTagHelper : ItemLinkTagHelperBase
  {
  }
}

添加一个公共构造函数来接收所需的对象实例,并将它们传递给基类。

public ItemDeleteTagHelper(
    IActionContextAccessor contextAccessor,
    IUrlHelperFactory urlHelperFactory)
      : base(contextAccessor, urlHelperFactory) { }

覆盖Process()方法,调用基类中的BuildContent()方法。

public override void Process(TagHelperContext context, TagHelperOutput output)
{
  BuildContent(output,nameof(CarsController.Delete),"text-danger","Delete","trash");
}

这创建了带有字体 Awesome 垃圾桶图像的Delete链接。为了防止编译器错误,在CarsController中添加一个基本的Delete()方法。

public IActionResult Delete()
{
  return View();
}

项目编辑标签助手

TagHelpers文件夹中创建一个名为ItemEditTagHelper.cs的新类。使类public继承ItemLinkTagHelperBase。将以下using语句添加到新文件中:

using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AutoLot.Mvc.TagHelpers
{
  public class ItemEditTagHelper : ItemLinkTagHelperBase
  {
  }
}

添加一个公共构造函数来接收所需的对象实例,并将它们传递给基类。

public ItemEditTagHelper(
    IActionContextAccessor contextAccessor,
    IUrlHelperFactory urlHelperFactory)
      : base(contextAccessor, urlHelperFactory) { }

覆盖Process()方法,调用基类中的BuildContent()方法。

public override void Process(TagHelperContext context, TagHelperOutput output)
{
  BuildContent(output,nameof(CarsController.Edit),"text-warning","Edit","edit");
}

这将创建带有字体 Awesome 铅笔图像的编辑链接。为了防止编译器错误,在CarsController中添加一个基本的Edit()方法。

public IActionResult Edit()
{
  return View();
}

项目创建标签帮助器

TagHelpers文件夹中创建一个名为ItemCreateTagHelper.cs的新类。使类public继承ItemLinkTagHelperBase。将以下using语句添加到新文件中:

using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AutoLot.Mvc.TagHelpers
{
  public class ItemCreateTagHelper : ItemLinkTagHelperBase
  {
  }
}

添加一个公共构造函数来接收所需的对象实例,并将它们传递给基类:

public ItemCreateTagHelper(
    IActionContextAccessor contextAccessor,
    IUrlHelperFactory urlHelperFactory)
      : base(contextAccessor, urlHelperFactory) { }

覆盖Process()方法,调用基类中的BuildContent()方法。

public override void Process(TagHelperContext context, TagHelperOutput output)
{
  BuildContent(output,nameof(CarsController.Create),"text-success","Create new","plus");
}

这将创建带有字体 Awesome plus 图像的创建链接。

项目列表标签帮助器

TagHelpers文件夹中创建一个名为ItemEditTagHelper.cs的新类。使类public继承ItemLinkTagHelperBase。将以下using语句添加到新文件中:

using AutoLot.Mvc.Controllers;
using AutoLot.Mvc.TagHelpers.Base;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace AutoLot.Mvc.TagHelpers
{
  public class ItemListTagHelper : ItemLinkTagHelperBase
  {
  }
}

添加一个公共构造函数来接收所需的对象实例,并将它们传递给基类。

public ItemListTagHelper(
    IActionContextAccessor contextAccessor,
    IUrlHelperFactory urlHelperFactory)
      : base(contextAccessor, urlHelperFactory) { }

覆盖Process()方法,调用基类中的BuildContent()方法。

public override void Process(TagHelperContext context, TagHelperOutput output)
{
  BuildContent(output,nameof(CarsController.Index),"text-default","Back to List","list");
}

这将创建带有字体 Awesome 列表图像的编辑链接。为了防止编译器错误,在CarsController中添加一个基本的Index()方法。

public IActionResult Index()
{
  return View();
}

使自定义标记助手可见

要使定制标签助手可见,必须对任何使用标签助手或添加到_ViewImports.cshtml文件的视图执行@addTagHelper命令。打开Views文件夹根目录下的_ViewImports.cshtml文件,添加下面一行:

@addTagHelper *, AutoLot.Mvc

HTML 助手

来自 ASP.NET MVC 的 HTML 助手仍然被支持,并且有一些仍然被广泛使用。表 31-5 列出了那些仍然常用的助手。

表 31-5。

常用的 HTML 助手

|

HTML 助手

|

使用

| | --- | --- | | Html.DisplayFor() | 显示由表达式定义的对象 | | Html.DisplayForModel() | 使用默认模板或自定义模板显示模型 | | Html.DisplayNameFor() | 如果显示名称存在,则获取显示名称;如果没有显示名称,则获取属性名称 | | Html.EditorFor() | 显示由表达式定义的对象的编辑器 | | Html.EditorForModel() | 使用默认模板或自定义模板显示模型的编辑器 |

HTML 助手的显示

DisplayFor()助手显示一个由表达式定义的对象。如果显示的类型有一个显示模板,那么它将用于创建该项的 HTML。例如,如果一个视图的模型是Car实体,那么这个CarMake信息可以用下面的代码显示:

@Html.DisplayFor(x=>x.MakeNavigation);

如果名为Make.cshtml的视图存在于DisplayTemplates文件夹中,那么该视图将用于呈现值(记住模板名称查找是基于对象的类型,而不是其属性名称)。如果有一个名为ShowMake.cshtml的视图(例如),它可以通过以下调用来呈现对象:

@Html.DisplayFor(x=>x.MakeNavigation, "ShowMake");

如果没有指定模板,并且没有用于类名的模板,则使用反射来创建用于显示的 HTML。

DisplayForModel HTML 帮助器

DisplayForModel()助手显示视图的模型。如果显示的类型有一个显示模板,那么它将用于创建该项的 HTML。继续前面的例子,用Car实体作为它的模型的视图,全部的Car信息可以用这个来显示:

@Html.DisplayForModel();

就像DisplayFor()助手一样,如果一个以类型命名的显示模板存在,它将被使用。也可以使用命名模板。例如,要用CarWithColors.html显示模板显示Car,使用下面的调用:

@Html.DisplayForModel("CarWithColors");

如果没有指定模板,并且没有用于类名的模板,则使用反射来创建用于显示的 HTML。

EditorFor 和 EditorForModel HTML 助手

EditorFor()EditorForModel()辅助功能与其对应的显示辅助功能相同。不同之处在于,在EditorTemplates目录中搜索模板,并显示 HTML 编辑器,而不是对象的只读表示。

管理客户端库

在完成视图之前,是时候更新客户端库(CSS 和 JavaScript)了。LibraryManager 项目(最初由 Mads Kristensen 构建)现在是 Visual Studio (VS2019)的一部分,也可以作为. NET 核心全局工具使用。LibraryManager 使用一个简单的 JSON 文件从 CDNJS 中提取 CSS 和 JavaScript 工具。comUNPKG。com ,JSDeliver,或者文件系统。

将库管理器作为. NET 核心全局工具安装

库管理器现在内置于 Visual Studio 中。要将其作为. NET 核心全局工具安装,请输入以下命令:

dotnet tool install --global Microsoft.Web.LibraryManager.Cli --version 2.1.113

当前版本可在 https://www.nuget.org/packages/Microsoft.Web.LibraryManager.Cli/ 找到。

将客户端库添加到自动 Lot。手动音量调节

当自动手枪。Mvc 项目已创建(使用 Visual Studio 或。NET Core CLI),在wwwroot\lib文件夹中安装了几个 JavaScript 和 CSS 文件。删除整个lib文件夹及其包含的所有文件,因为库管理器将替换所有文件。

添加 libman.json 文件

libman.json文件控制安装的内容、来源以及安装文件的目的地。

可视化工作室

如果您使用的是 Visual Studio,请右键单击 AutoLot。Mvc 项目并选择管理客户端库。这将把libman.json文件添加到项目的根目录中。Visual Studio 中还有一个将库管理器绑定到 MSBuild 进程的选项。右键单击libman.json文件并选择“在构建时启用恢复”这将提示您允许将另一个 NuGet 包(Microsoft.Web.LibraryManager.Build)恢复到项目中。允许安装软件包。

命令行

使用以下命令创建一个新的libman.json文件(这将默认提供者设置为 cdnjs.com ):

libman init --default-provider cdnjs

更新 libman.json 文件

当搜索要安装的库时,CDNJS.com 有一个很好的、人类可读的 API 可以使用。使用以下 URL 列出所有可用的库:

https://api.cdnjs.com/libraries?output=human

当您找到想要安装的资源库时,请使用列出的资源库名称更新 URL,以查看所有版本和每个版本的文件。例如,要查看 jQuery 的所有可用内容,请输入以下内容:

https://api.cdnjs.com/libraries/jquery?output=human

一旦确定了要安装的版本和文件,添加库名(和版本)、目的地(通常是wwwroot/lib/<library name>)和要加载的文件。例如,要加载 jQuery,请在库的 JSON 数组中输入以下内容:

{
  "library": "jquery@3.5.1",
  "destination": "wwwroot/lib/jquery",
  "files": [ "jquery.js"]
},

添加该应用所需的所有文件后,整个libman.json文件如下所示:

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "defaultDestination": "wwwroot/lib",
  "libraries": [
    {
      "library": "jquery@3.5.1",
      "destination": "wwwroot/lib/jquery",
      "files": [ "jquery.js", "jquery.min.js" ]
    },
    {
      "library": "jquery-validate@1.19.2",
      "destination": "wwwroot/lib/jquery-validation",
      "files": [ "jquery.validate.js", "jquery.validate.min.js", "additional-methods.js", "additional-methods.min.js" ]
    },
    {
      "library": "jquery-validation-unobtrusive@3.2.11",
      "destination": "wwwroot/lib/jquery-validation-unobtrusive",
      "files": [ "jquery.validate.unobtrusive.js", "jquery.validate.unobtrusive.min.js" ]
    },
    {
      "library": "twitter-bootstrap@4.5.3",
      "destination": "wwwroot/lib/bootstrap",
      "files": [
        "css/bootstrap.css",
        "js/bootstrap.bundle.js",
        "js/bootstrap.js"
      ]
    },
    {
      "library": "font-awesome@5.15.1",
      "destination": "wwwroot/lib/font-awesome/",
      "files": [
        "js/all.js",
        "css/all.css",
        "sprites/brands.svg",
        "sprites/regular.svg",
        "sprites/solid.svg",
        "webfonts/fa-brands-400.eot",
        "webfonts/fa-brands-400.svg",
        "webfonts/fa-brands-400.ttf",
        "webfonts/fa-brands-400.woff",
        "webfonts/fa-brands-400.woff2",
        "webfonts/fa-regular-400.eot",
        "webfonts/fa-regular-400.svg",
        "webfonts/fa-regular-400.ttf",
        "webfonts/fa-regular-400.woff",
        "webfonts/fa-regular-400.woff2",
        "webfonts/fa-solid-900.eot",
        "webfonts/fa-solid-900.svg",
        "webfonts/fa-solid-900.ttf",
        "webfonts/fa-solid-900.woff",
        "webfonts/fa-solid-900.woff2"
      ]
    }
  ]
}

Note

如果你想知道为什么没有列出任何缩小的文件,这将很快涵盖。

一旦保存了文件(在 Visual Studio 中),这些文件将被加载到项目的wwwroot\lib文件夹中。如果从命令行运行,请输入以下命令来重新加载所有文件:

libman restore

还提供了其他命令行选项。输入libman -h浏览所有选项。

更新 JavaScript 和 CSS 参考

许多 JavaScript 和 CSS 文件的位置随着库管理器的移动而改变。Bootstrap 和 jQuery 是从\dist文件夹中加载的。我们还在应用中添加了字体 Awesome。

引导文件的位置需要更新到~/lib/boostrap/css而不是~/lib/boostrap/dist/css。在最后加上字体 Awesome,就在site.css之前。将_Head.cshtml文件更新为以下内容:

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - AutoLot.Mvc</title>
<environment include="Development">
  <link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.css" asp-append-version="true"/>
</environment>
<environment exclude="Development">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      asp-fallback-href="~/lib/bootstrap/css/bootstrap.css"
      asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
      crossorigin="anonymous"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"/>
</environment>
<link rel="stylesheet" href="~/lib/font-awesome/css/all.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>

接下来,更新_JavaScriptFiles.cshtml以将\dist从 jQuery 和引导位置中取出。

<environment include="Development">
  <script src="~/lib/jquery/jquery.js" asp-append-version="true"></script>
  <script src="~/lib/bootstrap/js/bootstrap.bundle.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"
    asp-fallback-src="~/lib/jquery/jquery.min.js"
    asp-fallback-test="window.jQuery"
    crossorigin="anonymous"
    integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
  </script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"
    asp-fallback-src="~/lib/bootstrap/js/bootstrap.bundle.min.js"
    asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
    crossorigin="anonymous"
    integrity="sha384-xrRywqdh3PHs8keKZN+8zzc5TX0GRTLCcmivcbNJWm2rs5C8PRhcEn3czEjhAO9o">
  </script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>

最后的改变是更新_ValidationScriptsPartial.cshtml局部视图中jquery.validate的位置。

<environment include="Development">
  <script src="~/lib/jquery-validation/jquery.validate.js" asp-append-version="true"></script>
  <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js"
    asp-fallback-src="~/lib/jquery-validation/jquery.validate.min.js"
    asp-fallback-test="window.jQuery && window.jQuery.validator"
    crossorigin="anonymous"
    integrity="sha256-F6h55Qw6sweK+t7SiOJX+2bpSAa3b/fnlrVCJvmEj1A=">
  </script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"
  asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
  asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"
  crossorigin="anonymous"
  integrity="sha256-9GycpJnliUjJDVDqP0UEu/bsm9U+3dnQUH8+3W10vkY=">
  </script>
</environment>

完成小车控制器和汽车视图

该部分完成了CarsControllerCars视图。如果您将appsettings.development.json中的RebuildDatabase标志设置为true,那么您在测试这些视图时所做的任何更改都将在您下次启动应用时被重置。

小车控制器

CarsController是自动 Lot 的焦点。Mvc 应用,具有创建、读取、更新和删除功能。这个版本的CarsController直接使用数据访问层。在本章的后面,你将创建另一个版本的CarsController,它使用自动手枪。用于数据访问的 Api 服务。

首先更新CarsController类的using语句,使其符合以下内容:

using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Models.Entities;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;

前面,您添加了带有路由的控制器类。现在是时候通过依赖注入来添加ICarRepoIAppLogging<CarsController>实例了。添加两个类级变量来保存实例,并添加一个将被注入这两个项的构造函数。

private readonly ICarRepo _repo;
private readonly IAppLogging<CarsController> _logging;
public CarsController(ICarRepo repo, IAppLogging<CarsController> logging)
{
  _repo = repo;
  _logging = logging;
}

汽车列表局部视图

列表视图(一个是全部汽车清单,一个是制造商汽车列表)都共享一个局部视图。在Views\Cars目录下创建一个名为Partials的新目录。在这个目录中,添加一个名为_CarListPartial.cshtml的新视图,并清除现有代码。将IEnumerable<Car>设置为类型(它不需要完全限定,因为我们将AutoLot.Models.Entities名称空间添加到了_ViewImports.cshtml文件中)。

@model IEnumerable< Car>

接下来,添加一个 Razor 块,其中包含一组布尔变量,指示是否应该显示Makes。当该部分被整个库存清单使用时,应显示Makes。当它只显示一个Make时,应该隐藏Make字段。

@{
    var showMake = true;
    if (bool.TryParse(ViewBag.ByMake?.ToString(), out bool byMake))
    {
        showMake = !byMake;
    }
}

下一个标记使用ItemCreateTagHelper创建一个到 Create HTTP Get 方法的链接。当使用自定义标签助手时,名称是小写的 kebab。这意味着TagHelper后缀被删除,然后每个 Pascal 大小写的单词被小写,并用连字符分隔,就像烤肉串一样:

<p>
  <item-create></item-create>
</p>

为了设置表格和表格标题,Razor HTML 助手用于获取每个属性的DisplayNameDisplayName将选择DisplayDisplayName属性的值,如果没有设置,它将使用属性名。本节使用一个剃刀块来显示基于前面设置的视图级变量的Make信息。

<table class="table">
  <thead>
  <tr>
    @if (showMake)
    {
      <th>
        @Html.DisplayNameFor(model => model.MakeId)
      </th>
    }
    <th>
      @Html.DisplayNameFor(model => model.Color)
    </th>
    <th>
      @Html.DisplayNameFor(model => model.PetName)
    </th>
    <th></th>
  </tr>
  </thead>

最后一部分循环遍历记录,并使用DisplayFor Razor HTML 助手显示表记录。这个助手将寻找一个匹配属性类型的DisplayTemplate模板名,如果没有找到,将以默认方式创建标记。对象上的每个属性也将检查显示模板,如果找到就使用它。例如,如果Car有一个DateTime属性,那么本章前面显示的DisplayTemplate将被调用。

这个块还使用了在上一节中添加的item-edititem-detailsitem-delete定制标记助手。请注意,当将值传递给自定义标记助手的公共属性时,属性名称是小写的,并作为属性添加到标记中。

  <tbody>
    @foreach (var item in Model)
    {
      <tr>
        @if (showMake)
        {
          <td>
            @Html.DisplayFor(modelItem => item.MakeNavigation.Name)
          </td>
        }
        <td>
          @Html.DisplayFor(modelItem => item.Color)
        </td>
        <td>
          @Html.DisplayFor(modelItem => item.PetName)
        </td>
        <td>
          <item-edit item-id="@item.Id"></item-edit> |
          <item-details item-id="@item.Id"></item-details> |
          <item-delete item-id="@item.Id"></item-delete>
        </td>
      </tr>
    }
    </tbody>
</table>

索引视图

_CarListPartial部分就位的情况下,Index视图很小。在Views\Cars目录中创建一个名为Index.cshtml的新视图。清除所有生成的代码,并添加以下内容:

@model IEnumerable<Car>
@{
  ViewData["Title"] = "Index";
}
<h1>Vehicle Inventory</h1>
<partial name="Partials/_CarListPartial" model="@Model"/>

分部_CarListPartial由包含视图的模型值(IEnumerable<Car>)调用,该值通过model属性传递。这将局部视图的模型设置为传递给<partial>标签辅助对象的对象。

要查看这个视图的运行情况,请将CarsController Index()方法更新如下:

[Route("/[controller]")]
[Route("/[controller]/[action]")]
public IActionResult Index()
  => View(_repo.GetAllIgnoreQueryFilters());

现在您已经有了Index视图,运行应用并导航到https://localhost:5001/Cars/Index,您将看到如图 31-4 所示的列表。

img/340876_10_En_31_Fig4_HTML.jpg

图 31-4。

汽车库存页面

虽然自定义标签助手显示在列表的右侧,但是字体 Awesome 图像没有显示,因为字体 Awesome 库还没有添加到应用中。

备用视图

ByMake视图类似于索引,但是将部分视图设置为除了在页面标题中不显示Make信息。在Views\Cars目录中创建一个名为ByMake.cshtml的新视图。清除所有生成的代码,并添加以下内容:

@model IEnumerable<Car>
@{
    ViewData["Title"] = "Index";
}
<h1>Vehicle Inventory for @ViewBag.MakeName</h1>
@{
    var mode = new ViewDataDictionary(ViewData) {{"ByMake", true}};
}
<partial name="Partials/_CarListPartial" model="Model" view-data="@mode"/>

有两个明显的区别。第一个是从ViewBag创建一个包含ByMake属性的新的ViewDataDictionary。然后将它和模型一起传递到 partial 中,这样就可以隐藏Make信息。

该视图的动作方法需要获取所有具有指定MakeId的车辆,并将ViewBag设置为MakeName,以便在 UI 中显示。这两个值都来自路由。在CarsController中添加一个名为ByMake()的新动作方法。

[HttpGet("/[controller]/[action]/{makeId}/{makeName}")]
public IActionResult ByMake(int makeId, string makeName)
{
  ViewBag.MakeName = makeName;
  return View(_repo.GetAllBy(makeId));
}

现在您已经有了Index视图,运行应用并导航到https://localhost:5001/Cars/1/VW,您将看到如图 31-5 所示的列表。

img/340876_10_En_31_Fig5_HTML.jpg

图 31-5。

特定品牌的汽车库存

详细视图

Views\Cars目录中创建一个名为Details.cshtml的新视图。清除所有生成的代码,并添加以下内容:

@model Car
@{
  ViewData["Title"] = "Details";
}
<h1>Details for @Model.PetName</h1>
@Html.DisplayForModel()
<div>
  <item-edit item-id="@Model.Id"></item-edit>
  <item-delete item-id="@Model.Id"></item-delete>
  <item-list></item-list>
</div>

@Html.DisplayForModel()使用本节前面构建的显示模板(Car.cshtml)来显示Car细节。

在更新Details()动作方法之前,添加一个名为GetOne()的助手方法,该方法将检索单个Car记录。

internal Car GetOneCar(int? id) => !id.HasValue ? null : _repo.Find(id.Value);

Details()动作方法更新如下:

[HttpGet("{id?}")]
public IActionResult Details(int? id)
{
  if (!id.HasValue)
  {
    return BadRequest();
  }
  var car = GetOneCar(id);
  if (car == null)
  {
    return NotFound();
  }
  return View(car);
}

Details()动作方法的 route 为Car id取一个可选的 route 参数,并将其设置为该方法的id参数。请注意,route 参数带有一个带标记的问号。这表明它是一个可选参数,就像int?类型上的问号使变量成为可空的int。如果没有提供该参数,或者如果服务包装器找不到具有所提供的 route 参数的车辆,则该方法返回一个NotFound结果。否则,该方法将定位的Car记录发送到细节视图。

运行应用并导航至https://localhost:5001/Cars/Details/1,您将看到如图 31-6 所示的屏幕。

img/340876_10_En_31_Fig6_HTML.jpg

图 31-6。

特定汽车的详细信息

创建视图

Create视图启动较早。以下是完整视图的完整列表:

@model Car

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

<h1>Create a New Car</h1>
<hr/>
<div class="row">
    <div class="col-md-4">
        <form asp-controller="Cars" asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            @Html.EditorForModel()
            <div class="form-group">
                <button type="submit" class="btn btn-success">Create <i class="fas fa-plus"></i></button>&nbsp;&nbsp;|&nbsp;&nbsp;
                <item-list></item-list>
            </div>
        </form>
    </div>
</div>
@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

@Html.EditorForModel()方法使用本节前面构建的编辑器模板(Car.cshtml)来显示Car细节。

该视图还在Scripts部分引入了_ValidationScriptsPartial。回想一下,在布局中,这个部分发生在加载 jQuery 之后的*。sections 模式有助于确保在节的内容之前加载适当的依赖项。*

创建操作方法

创建过程使用两个操作方法:一个(HTTP Get)返回新记录输入的空白视图,另一个(HTTP Put)提交新记录的值。

getnames 帮助器方法

GetMakes()助手方法将Make记录的列表返回到一个SelectList中。它将IMakeRepo的一个实例作为参数。

internal SelectList GetMakes(IMakeRepo makeRepo)
  => new SelectList(makeRepo.GetAll(), nameof(Make.Id), nameof(Make.Name));

Create Get 方法

Create() HTTP Get action 方法将Make记录的一个SelectList添加到ViewData字典中,然后由Id获取一辆汽车并发送给Create视图。

[HttpGet]
public IActionResult Create([FromServices] IMakeRepo makeRepo)
{
  ViewData["MakeId"] = GetMakes(makeRepo);
  return View();
}

创建的表单可在/Cars/Create查看,如图 31-7 所示。

img/340876_10_En_31_Fig7_HTML.jpg

图 31-7。

创建视图

创建帖子的方法

Create() HTTP Post 操作方法使用隐式模型绑定从表单值构建一个Car实体。此处列出了代码,并附有解释:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create([FromServices] IMakeRepo makeRepo, Car car)
{
  if (ModelState.IsValid)
  {
    _repo.Add(car);
    return RedirectToAction(nameof(Details),new {id = car.Id});
  }
  ViewData["MakeId"] = GetMakes(makeRepo);
  return View(car);
}

当请求是 post 时,HttpPost属性将其标记为Cars / Create路由的应用端点。ValidateAntiForgeryToken属性使用__RequestVerificationToken的隐藏输入值来帮助减少对站点的攻击。

IMakeRepo从依赖注入容器注入到方法中。因为这是方法注入,所以使用了FromServices属性。提醒一下,这通知绑定引擎不要尝试这种类型的绑定,并让 DI 容器知道创建该类的一个实例。

Car实体被隐式绑定到传入的请求数据。如果ModelState有效,实体被添加到数据库,然后用户被重定向到Detail()动作方法,使用新创建的Car Id作为路由参数。这是 Post-Redirect-Get 模式。用户提交了一个 Post 方法(Create()),然后被重定向到一个 Get 方法(Details())。这可以防止浏览器在用户刷新页面时重新提交帖子。

如果ModelState无效,则Makes SelectList被添加到ViewData,并且被提交的实体被发送回Create视图。ModelState也被隐式发送到视图中,因此任何错误都可以显示出来。

编辑视图

Views\Cars目录中创建一个名为Edit.cshtml的新视图。清除所有生成的代码,并添加以下内容:

@model Car
@{
    ViewData["Title"] = "Edit";
}
<h1>Edit @Model.PetName</h1>
<hr />
<div class="row">
  <div class="col-md-4">
    <form asp-area="" asp-controller="Cars" asp-action="Edit" asp-route-id="@Model.Id">
      @Html.EditorForModel()
      <input type="hidden" asp-for="Id" />
      <input type="hidden" asp-for="TimeStamp" />
      <div class="form-group">
        <button type="submit" class="btn btn-primary">
            Save <i class="fas fa-save"></i>
        </button>&nbsp;&nbsp;|&nbsp;&nbsp;
        <item-list></item-list>
      </div>
    </form>
  </div>
</div>
@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

这个视图也使用了@Html.EditorForModel()方法和_ValidationScriptsPartial。但它也包括两个隐藏的输入,分别用于IdTimeStamp。这些将与其余的表单数据一起发布,但用户不能编辑。没有IdTimeStamp,更新将无法保存。

编辑操作方法

编辑过程还使用两个操作方法:一个(HTTP Get)返回要编辑的实体,另一个(HTTP Put)提交更新记录的值。

Edit Get 方法

Create() HTTP Get action 方法通过Id使用服务包装器获取一辆汽车,并将其发送给Create视图。

[HttpGet("{id?}")]
public IActionResult Edit([FromServices] IMakeRepo makeRepo, int? id)
{
  var car = GetOneCar(id);
  if (car == null)
  {
    return NoContent();
  }
  ViewData["MakeId"] = GetMakes(makeRepo);
  return View(car);
}

该路线有一个选项id参数,然后使用id参数将其传递给该方法。一个IMakeRepo的实例被注入到该方法中,并用于为品牌下拉菜单创建SelectList。该方法使用GetOneCar()助手方法来获得一个Car记录。如果不能定位一个Car记录,该方法返回一个NoContent错误。否则,它会将Make SelectList添加到ViewData字典中,并呈现视图。

编辑表单可在/Cars/Edit/1处查看,如图 31-8 所示。

img/340876_10_En_31_Fig8_HTML.jpg

图 31-8。

编辑视图

编辑帖子的方法

Edit() HTTP Post 操作方法类似于Create() HTTP Post 方法,除了在代码清单后面注明的例外:

[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public IActionResult Edit([FromServices] IMakeRepo makeRepo, int id, Car car)
{
  if (id != car.Id)
  {
    return BadRequest();
  }
  if (ModelState.IsValid)
  {
    _repo.Update(car);
    return RedirectToAction(nameof(Details),new {id = car.Id});
  }
  ViewData["MakeId"] = GetMakes(makeRepo);
  return View(car);
}

Edit HTTP Post 方法采用一个必需的路由参数。如果这与重组的CarId不匹配,则向客户端发送一个BadRequest结果。如果ModelState有效,实体被更新,然后用户被重定向到Detail()动作方法,使用Car Id作为路由参数。这也使用了 Post-Redirect-Get 模式。

如果ModelState无效,则Makes SelectList被添加到ViewData,并且被提交的实体被发送回Edit视图。ModelState也被隐式发送到视图中,因此任何错误都可以显示出来。

删除视图

Views\Cars目录中创建一个名为Delete.cshtml的新视图。清除所有生成的代码,并添加以下内容:

@model Car
@{
  ViewData["Title"] = "Delete";
}
<h1>Delete @Model.PetName</h1>
<h3>Are you sure you want to delete this car?</h3>
<div>
  @Html.DisplayForModel()
  <form asp-action="Delete">
    <input type="hidden" asp-for="Id" />
    <input type="hidden" asp-for="TimeStamp" />
    <button type="submit" class="btn btn-danger">
      Delete <i class="fas fa-trash"></i>
    </button>&nbsp;&nbsp;|&nbsp;&nbsp;
    <item-list></item-list>
  </form>
</div>

这个视图也使用了@Html.DisplayForModel()方法和两个隐藏输入IdTimeStamp。这些将是作为表单数据发布的唯一字段。

删除操作方法

Delete流程还使用了两个动作方法:一个(HTTP Get)返回要删除的实体,一个(HTTP Put)提交要删除的值。

Delete Get 方法

Delete() HTTP Get 操作方法的功能与Details()操作方法相同。

[HttpGet("{id?}")]
public IActionResult Delete(int? id)
{
  var car = GetOneCar(id);
  if (car == null)
  {
    return NotFound();
  }
  return View(car);
}

删除表单可在/Cars/Delete/1查看,如图 31-9 所示。

img/340876_10_En_31_Fig9_HTML.jpg

图 31-9。

删除视图

删除帖子的方法

Delete() HTTP Post 动作方法只将IdTimeStamp发送给服务包装器。

[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public IActionResult Delete(int id, Car car)
{
  if (id != car.Id)
  {
    return BadRequest();
  }
  _repo.Delete(car);
  return RedirectToAction(nameof(Index));
}

HTTP Post 方法被简化为只发送 EF Core 删除记录所需的值。

这就完成了Car实体的视图和控制器。

查看组件

视图组件是 ASP.NET 核心中的另一个新特性。它们结合了部分视图和子动作的优点来呈现部分 UI。像局部视图一样,它们是从另一个视图调用的,但是与局部视图本身不同,视图组件也有一个服务器端组件。这种组合使它们非常适合创建动态菜单(如下所示)、登录面板、侧边栏内容或任何需要运行服务器端代码但不能作为独立视图的功能。

Note

经典 ASP.NET MVC 中的子动作是控制器上的动作方法,不能作为面向客户端的端点。他们不存在于 ASP.NET 核心。

对于 AutoLot,视图组件将根据数据库中的品牌动态创建菜单。菜单在每个页面上都是可见的,所以它的逻辑位置是在_Layout.cshtml中。但是_Layout.cshtml没有服务器端组件(不像视图),所以应用中的每个动作都必须向_Layout.cshtml提供数据。这可以在OnActionExecuting事件处理程序中和放置在ViewBag中的记录中完成,但是维护起来可能会很麻烦。服务器端功能和 UI 封装的结合使这个场景成为视图组件的完美用例。

服务器端代码

在 AutoLot 的根目录下创建一个名为ViewComponents的新文件夹。Mvc 项目。在这个文件夹中添加一个名为MenuViewComponent.cs的新类文件。约定是用ViewComponent后缀命名视图组件类,就像控制器一样。就像控制器一样,当调用视图组件时,ViewComponent后缀会被删除。

将以下using语句添加到文件顶部:

using System.Linq;
using AutoLot.Dal.Repos.Interfaces;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;

将类更改为public并从ViewComponent继承。视图组件不必从ViewComponent基类继承,但是像Controller基类一样,从ViewComponent继承简化了很多工作。创建一个接受IMakeRepo接口实例的构造函数,并将其赋给一个类级变量。此时的代码如下所示:

namespace AutoLot.Mvc.ViewComponents
{
  public class MenuViewComponent : ViewComponent
  {
    private readonly IMakeRepo _makeRepo;
    public MenuViewComponent(IMakeRepo makeRepo)
    {
      _makeRepo = makeRepo;
  }
}

视图组件有两种可用的方法,Invoke()InvokeAsync()。必须实现其中的一个,因为MakeRepo只进行同步调用,所以添加Invoke()方法,如下所示:

public async IViewComponentResult Invoke()
{
}

当从视图中呈现视图组件时,调用公共方法Invoke() / InvokeAsync()。这个方法返回一个IViewComponentResult,它在概念上类似于一个PartialViewResult,但是更加精简。在Invoke()方法中,从回购中获取Makes的列表,如果成功,则返回一个ViewViewComponentResult(不,那不是错别字;它实际上是类型的名称)使用 makes 列表作为视图模型。如果获取Make记录的调用失败,返回带有错误消息的ContentViewComponentResult。将方法更新为以下内容:

public IViewComponentResult Invoke()
{
  var makes = _makeRepo.GetAll().ToList();
  if (!makes.Any())
  {
    return new ContentViewComponentResult("Unable to get the makes");
  }
  return View("MenuView", makes);
}

基类ViewComponentView助手方法类似于同名的Controller类助手方法,有几个关键区别。第一个区别是默认的视图文件名是Default.cshtml而不是方法名。然而,像控制器视图助手方法一样,视图的名称可以是任何东西,只要名称被传递到方法调用中(没有.cshtml扩展名)。第二个区别是视图 的位置必须是以下三个目录之一:

Views/< controller>/Components/<view_component_name>/<view_name>
Views/Shared/Components/<view_component_name>/<view_name>
Pages/Shared/Components/<view_component_name>/<view_name>

Note

ASP.NET Core 2 . x 引入了 Razor 页面作为创建 web 应用的另一种机制。这本书不包括剃刀页。

C# 类可以存在于任何地方(甚至在另一个程序集中),但是<viewname>.cshtml必须在前面列出的目录中。

构建局部视图

MenuViewComponent呈现的局部视图将遍历Make记录,将每个记录添加为一个列表项,显示在引导菜单中。“全部”菜单项首先作为硬编码值添加。

Views\Shared文件夹下创建一个名为Components的新文件夹。在这个新文件夹中,创建另一个名为Menu的新文件夹。这个文件夹名必须与之前创建的视图组件类的名称相匹配,并去掉后缀ViewComponent。在这个文件夹中,创建一个名为MenuView.cshtml的局部视图。

清除现有代码并添加以下标记:

@model IEnumerable<Make>
<div class="dropdown-menu">
<a class="dropdown-item text-dark" asp-area="" asp-controller="Cars" asp-action="Index">All</a>

@foreach (var item in Model)
{
    <a class="dropdown-item text-dark" asp-controller="Cars" asp-action="ByMake" asp-route-makeId="@item.Id" asp-route-makeName="@item.Name">@item.Name</a>
}
</div>

调用视图组件

视图组件通常从视图中呈现(尽管它们也可以从控制器动作方法中呈现)。语法很简单:Component.Invoke(<view component name>)或者@await Component.InvokeAsync(<view component name>)。就像控制器一样,当调用视图组件时,必须删除ViewComponent后缀。

@await Component.InvokeAsync("Menu") //async version
@Component.Invoke("Menu") //non-async version

调用视图组件作为定制标记助手

在 ASP.NET 1.1 中引入了视图组件,可以使用标记助手语法来调用它。不要使用Component.InvokeAsync() / Component.Invoke(),只需像这样调用视图组件:

<vc:menu></vc:menu>

要使用这种调用视图组件的方法,您的应用必须选择使用它们。这是通过添加带有包含视图组件的程序集名称的@addTagHelper命令来完成的。必须将下面一行添加到_ViewImports.cshtml文件中,该文件已经为定制标记助手添加了:

@addTagHelper *, AutoLot.Mvc

更新菜单

打开_Menu.cshtml部分并导航到映射到Home/Index动作方法的<li></li>块之后。将以下标记复制到分部:

<li class="nav-item dropdown">
  <a class="nav-link dropdown-toggle text-dark" data-toggle="dropdown">Inventory <i class="fa fa-car"></i></a>
    <vc:menu />
</li>

粗体行将MenuViewComponent呈现到菜单中。周围的标记是引导格式。

现在,当您运行该应用时,您将看到清单菜单,子菜单项中列出了品牌,如图 31-10 所示。

img/340876_10_En_31_Fig10_HTML.jpg

图 31-10。

MenuViewComponent 提供的菜单

捆绑和缩小

使用客户端库构建 web 应用的另外两个考虑因素是捆绑和缩小以提高性能。

集束

Web 浏览器对允许从同一个端点同时下载的文件数量有一个限制。如果您对 JavaScript 和 CSS 文件使用可靠的开发技术,将相关的代码和样式分离成更小、更易维护的文件,这可能会有问题。这提供了更好的开发体验,但在文件等待下载时会降低应用的性能。捆绑只是将文件连接在一起,以防止它们在等待浏览器下载限制时被阻止。

缩小

此外,为了提高性能,缩小过程会更改 CSS 和 JavaScript 文件,使它们变得更小。删除了不必要的空格和行尾,并缩短了非关键字名称。虽然这使得文件对人来说几乎不可读,但是功能不受影响,并且大小可以显著减小。这反过来加快了下载过程,从而提高了应用的性能。

网络优化解决方案

作为构建过程的一部分,有许多开发工具可以捆绑和缩小文件。这些当然是有效的,但如果进程变得不同步,可能会有问题,因为对于原始文件以及打包和缩小的文件,确实没有一个好的比较器。

WebOptimizer 是一个开源包,它将捆绑、缩小和缓存作为 ASP.NET 核心管道的一部分。这确保了捆绑和缩小的文件准确地表示原始文件。它们不仅准确,而且被缓存,大大减少了页面请求的磁盘读取次数。在第二十九章中创建项目时,您已经添加了Libershark.WebOptimizer.Core包。现在是使用它的时候了。

更新 Startup.cs

第一步是将 WebOptimizer 添加到管道中。打开自动 Lot 中的Startup.cs文件。Mvc 项目,导航到Configure()方法,并添加下面一行代码(就在app.UseStaticFiles()调用之前):

app.UseWebOptimizer();

下一步是配置最小化和捆绑的内容。通常,在开发您的应用时,您希望看到文件的非绑定/非精简版本,但是对于登台和生产,绑定和精简才是您想要的。在ConfigureServices()方法中,添加以下代码块:

if (_env.IsDevelopment() || _env.IsEnvironment("Local"))
{
  services.AddWebOptimizer(false,false);
}
else
{
  services.AddWebOptimizer(options =>
  {
    options.MinifyCssFiles(); //Minifies all CSS files
    //options.MinifyJsFiles(); //Minifies all JS files
    options.MinifyJsFiles("js/site.js");
    options.MinifyJsFiles("lib/**/*.js");
  });
}

如果环境为Development,则禁用所有捆绑和缩小。如果不是,下面的代码将缩小所有 CSS 文件,缩小site.js,并缩小lib目录下的所有 JavaScript 文件(扩展名为.js)。注意,所有路径都从项目中的wwwroot文件夹开始。

WebOptimizer 也支持捆绑。第一个示例使用文件 globbing 创建一个包,第二个示例创建一个列出特定名称的包。

options.AddJavaScriptBundle("js/validations/validationCode.js", "js/validations/**/*.js");
options.AddJavaScriptBundle("js/validations/validationCode.js", "js/validations/validators.js", "js/validations/errorFormatting.js");

需要注意的是,缩小和打包的文件实际上并不在磁盘上,而是放在缓存中。同样需要注意的是,缩小后的文件保持相同的名称(site.js),并且名称中没有通常的 min(site.min.js)。

Note

当更新视图以添加捆绑文件的链接时,Visual Studio 会报错捆绑文件不存在。别担心,它仍然会从缓存中进行渲染。

Update _ViewImports.cshtml

最后一步是将 WebOptimizer 标记助手添加到系统中。这些功能与本章前面提到的asp-append-version标签助手的功能相同,但对所有捆绑和缩小的文件都是自动完成的。将下面一行添加到_ViewImports.cshtml文件的末尾:

@addTagHelper *, WebOptimizer.Core

ASP.NET 核心的期权模式

options 模式通过依赖注入提供了配置的设置类对其他类的访问。可以使用IOptions<T>的一个版本将配置类注入到另一个类中。该接口有多个版本,如表 31-6 所示。

表 31-6。

一些选项接口

|

连接

|

描述

| | --- | --- | | IOptionsMonitor<T> | 检索选项并支持以下功能:变更通知(使用OnChange)、配置重载、命名选项(使用GetCurrentValue)和选择性选项失效。 | | IOptionsMonitorCache<T> | 缓存T的实例,支持完全/部分失效/重新加载。 | | IOptionsSnaphot<T> | 对每个请求重新计算选项。 | | IOptionsFactory<T> | 创建 t 的新实例。 | | IOptions<T> | 根接口。不支持IOptionsMonitor<T>。保留以向后兼容。 |

添加经销商信息

一个汽车网站应该显示经销商信息,该信息应该是可定制的,而不必重新部署整个网站。这将通过使用 options 模式来完成。首先更新appsettings.json文件,添加经销商信息。

{
  "Logging": {
    "MSSqlServer": {
      "schema": "Logging",
      "tableName": "SeriLogs",
      "restrictedToMinimumLevel": "Warning"
    }
  },
  "ApplicationName": "AutoLot.MVC",
  "AllowedHosts": "*",
  "DealerInfo": {
    "DealerName": "Skimedic's Used Cars",
    "City": "West Chester",
    "State": "Ohio"
  }
}

接下来,我们需要创建一个视图模型来保存经销商信息。在自动车床的Models文件夹中。Mvc 项目,添加一个名为DealerInfo.cs的新类。将该类更新为以下内容:

namespace AutoLot.Mvc.Models
{
  public class DealerInfo
  {
    public string DealerName { get; set; }
    public string City { get; set; }
    public string State { get; set; }
  }
}

Note

要配置的类必须有一个公共的无参数构造函数,并且是非抽象的。可以在类属性上设置默认值。

IServiceCollectionConfigure()方法将配置文件的一部分映射到一个特定的类型。然后可以使用 options 模式将该类型注入到类和视图中。打开Startup.cs文件,添加以下using语句:

using AutoLot.Mvc.Models;

接下来,导航到ConfigureServices()方法,并添加以下代码行:

services.Configure<DealerInfo>(Configuration.GetSection(nameof(DealerInfo)));

打开HomeController并添加以下using语句:

using Microsoft.Extensions.Options;

接下来将Index()方法更新如下:

[Route("/")]
[Route("/[controller]")]
[Route("/[controller]/[action]")]
[HttpGet]
public IActionResult Index([FromServices] IOptionsMonitor<DealerInfo> dealerMonitor)
{
  var vm = dealerMonitor.CurrentValue;
  return View(vm);
}

当从服务集合中配置一个类并将其添加到 DI 容器中时,可以使用 options 模式检索它。在这个例子中,OptionsMonitor将读取配置文件来创建一个DealerInfo类的实例。CurrentValue属性获取从当前设置文件创建的DealerInfo的实例(即使该文件在应用启动后已经更改)。然后,DealerInfo实例被传递给Index.cshtml视图。

现在更新位于Views\Home目录中的Index.cshtml视图,将其强类型化为DealerInfo类,并显示模型的属性。

@model AutoLot.Mvc.Models.DealerInfo
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome to @Model.DealerName</h1>
    <p class="lead">Located in @Model.City, @Model.State</p>
</div>

Note

有关 ASP.NET 核心中选项模式的更多信息,请查阅文档: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-5.0

创建服务包装

到目前为止,自动 Lot。Mvc 应用一直直接使用数据访问层。另一种方法是使用 AutoLot。Api 服务,并让该服务处理所有数据访问。

更新应用配置

自动手枪。Api 应用端点会因环境而异。例如,在您的工作站上开发时,基本 URI 是https://localhost:5021。在您的集成环境中,URI 可能是 https://mytestserver.com 。环境意识,结合更新的配置系统(在第二十九章中介绍),将用于添加这些不同的值。

appsettings.Development.json文件将添加本地机器的服务信息。随着代码在不同的环境中移动,每个环境的特定文件中的设置都会更新,以匹配该环境的基本 URI 和终结点。在本例中,您只更新开发环境的设置。打开appsettings.Development.json文件,并将其更新为以下内容(更改内容以粗体显示):

{
  "Logging": {
    "MSSqlServer": {
      "schema": "Logging",
      "tableName": "SeriLogs",
      "restrictedToMinimumLevel": "Warning"
    }
  },
  "RebuildDataBase": false,
  "ApplicationName": "AutoLot.Mvc - Dev",
  "ConnectionStrings": {
    "AutoLot": "Server=.,5433;Database=AutoLot;User ID=sa;Password=P@ssw0rd;"
  },
  "ApiServiceSettings": {
    "Uri": "https://localhost:5021/",
    "CarBaseUri": "api/Cars",
    "MakeBaseUri": "api/Makes"
  }
}

Note

确保端口号与 AutoLot.Api 的配置相匹配。

通过利用 ASP.NET 核心配置系统并更新特定于环境的文件(例如,appsettings.staging.jsonappsettings.production.json),您的应用将具有适当的值,而无需更改任何代码。

创建 ServiceSettings 类

服务设置将以与经销商信息相同的方式从设置中填充。在 AutoLot 中创建一个名为ApiWrapper的新文件夹。服务项目。在这个文件夹中,添加一个名为ApiServiceSettings.cs的类。类的属性名需要匹配 JSON ApiServiceSettings部分中的属性名。此处列出了该类:

namespace AutoLot.Services.ApiWrapper
{
  public class ApiServiceSettings
  {
    public ApiServiceSettings() { }
    public string Uri { get; set; }
    public string CarBaseUri { get; set; }
    public string MakeBaseUri { get; set; }
  }
}

API 服务包装器

ASP.NET 核心 2.1 引入了IHTTPClientFactory,它允许配置强类型类来调用 RESTful 服务。创建强类型类允许将所有 API 调用封装在一个地方。这集中了与服务的通信、HTTP 客户机的配置、错误处理等等。然后可以将该类添加到依赖注入容器中,供以后在应用中使用。DI 容器和IHTTPClientFactory处理HTTPClient的创建和处理。

IApiServiceWrapper 接口

AutoLot 服务包装器接口包含调用 AutoLot 的方法。Api 服务。在ApiWrapper目录下创建一个名为IApiServiceWrapper.cs的新接口,并将using语句更新如下:

using System.Collections.Generic;
using System.Threading.Tasks;
using AutoLot.Models.Entities;

将界面更新为如下所示的代码:

namespace AutoLot.Services.ApiWrapper
{
  public interface IApiServiceWrapper
  {
    Task<IList<Car>> GetCarsAsync();
    Task<IList<Car>> GetCarsByMakeAsync(int id);
    Task<Car> GetCarAsync(int id);
    Task<Car> AddCarAsync(Car entity);
    Task<Car> UpdateCarAsync(int id, Car entity);
    Task DeleteCarAsync(int id, Car entity);
    Task<IList<Make>> GetMakesAsync();
  }
}

ApiServiceWrapper 类

在 AutoLot 的ApiWrapper目录中创建一个名为ApiServiceWrapper的新类。服务项目。将using声明更新如下:

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using AutoLot.Models.Entities;
using Microsoft.Extensions.Options;

创建类public并添加一个构造函数,该构造函数接受HttpClientIOptionsMonitor<ApiServiceSettings>的实例。创建一个ServiceSettings类型的私有变量,并使用IOptionsMonitor<ServiceSettings>参数的CurrentValue属性对其赋值。代码如下所示:

public class ApiServiceWrapper : IApiServiceWrapper
{
  private readonly HttpClient _client;
  private readonly ApiServiceSettings _settings;
  public ApiServiceWrapper(HttpClient client, IOptionsMonitor<ApiServiceSettings> settings)
  {
      _settings = settings.CurrentValue;
    _client = client;
    _client/BaseAddress = new Uri(_settins.Uri);
  }
}

Note

接下来的部分包含大量没有任何错误处理的代码。这真是个馊主意!在已经很长的一章中,为了节省空间,省略了错误处理。

内部支撑方法

该类包含四个由公共方法使用的支持方法。

Post 和 Put 辅助方法

这些方法包装了相关的HttpClient方法。

internal async Task<HttpResponseMessage> PostAsJson(string uri, string json)
{
  return await _client.PostAsync(uri, new StringContent(json, Encoding.UTF8, "application/json"));
}

internal async Task<HttpResponseMessage> PutAsJson(string uri, string json)
{
  return await _client.PutAsync(uri, new StringContent(json, Encoding.UTF8, "application/json"));
}

HTTP 删除助手方法调用

最后一个 helper 方法用于执行 HTTP delete。HTTP 1.1 规范(以及更高版本)允许在 delete 语句中传递主体,但是还没有一个扩展方法来实现这个功能。HttpRequestMessage必须从零开始建造。

第一步是创建一个请求消息,使用对象初始化来设置ContentMethodRequestUri。完成后,消息被发送,响应被返回给调用代码。该方法如下所示:

internal async Task<HttpResponseMessage> DeleteAsJson(string uri, string json)
{
  HttpRequestMessage request = new HttpRequestMessage
  {
    Content = new StringContent(json, Encoding.UTF8, "application/json"),
    Method = HttpMethod.Delete,
    RequestUri = new Uri(uri)
  };
  return await _client.SendAsync(request);
}

HTTP Get 调用

有四个 Get 调用:一个获取所有的Car记录,一个通过Make获取Car记录,一个获取单个的Car,一个获取所有的Make记录。它们都遵循相同的模式。调用GetAsync()方法返回一个HttpResponseMessage。使用EnsureSuccessStatusCode()方法检查调用的成功或失败,如果调用没有返回成功的状态代码,就会抛出异常。然后,响应的主体被序列化回属性类型(实体或实体列表)并返回给调用代码。这里显示了这些方法中的每一种:

public async Task<IList<Car>> GetCarsAsync()
{
  var response = await _client.GetAsync($"{_settings.Uri}{_settings.CarBaseUri}");
  response.EnsureSuccessStatusCode();
  var result = await response.Content.ReadFromJsonAsync<IList<Car>>();
  return result;
}

public async Task<IList<Car>> GetCarsByMakeAsync(int id)
{
  var response = await _client.GetAsync($"{_settings.Uri}{_settings.CarBaseUri}/bymake/{id}");
  response.EnsureSuccessStatusCode();
  var result = await response.Content.ReadFromJsonAsync<IList<Car>>();
  return result;
}

public async Task<Car> GetCarAsync(int id)
{
  var response = await _client.GetAsync($"{_settings.Uri}{_settings.CarBaseUri}/{id}");
  response.EnsureSuccessStatusCode();
  var result = await response.Content.ReadFromJsonAsync<Car>();
  return result;
}

public async Task<IList<Make>> GetMakesAsync()
{
  var response = await _client.GetAsync($"{_settings.Uri}{_settings.MakeBaseUri}");
  response.EnsureSuccessStatusCode();
  var result = await response.Content.ReadFromJsonAsync<IList<Make>>();
  return result;
}

HTTP Post 调用

添加Car记录的方法使用 HTTP Post 请求。它使用 helper 方法将实体发布为 JSON,然后从响应体返回Car记录。此处列出了方法:

public async Task<Car> AddCarAsync(Car entity)
{
  var response = await PostAsJson($"{_settings.Uri}{_settings.CarBaseUri}",
    JsonSerializer.Serialize(entity));
  if (response == null)
  {
    throw new Exception("Unable to communicate with the service");
  }

  return await response.Content.ReadFromJsonAsync<Car>();
}

HTTP Put 调用

更新Car记录的方法使用了 HTTP Put 请求。它还使用 helper 方法将Car记录作为 JSON,并从响应主体返回更新后的Car记录。

public async Task<Car> UpdateCarAsync(int id, Car entity)
{
  var response = await PutAsJson($"{_settings.Uri}{_settings.CarBaseUri}/{id}",
    JsonSerializer.Serialize(entity));
  response.EnsureSuccessStatusCode();
  return await response.Content.ReadFromJsonAsync<Car>();
}

HTTP 删除调用

要添加的最后一个方法是用于执行 HTTP Delete。该模式遵循其余的方法:使用 helper 方法并检查响应是否成功。因为实体被删除了,所以没有任何东西返回到调用代码。该方法如下所示:

public async Task DeleteCarAsync(int id, Car entity)
{
  var response = await DeleteAsJson($"{_settings.Uri}{_settings.CarBaseUri}/{id}",
    JsonSerializer.Serialize(entity));
  response.EnsureSuccessStatusCode();
}

配置服务

在 AutoLot 的ApiWrapper目录中创建一个名为ServiceConfiguration.cs的新类。服务项目。将using声明更新如下:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

创建类publicstatic,并为IServiceCollection添加一个公共静态扩展方法。

namespace AutoLot.Services.ApiWrapper
{
  public static class ServiceConfiguration
  {
    public static IServiceCollection ConfigureApiServiceWrapper(this IServiceCollection services, IConfiguration config)
    {
      return services;
    }
  }
}

扩展方法的第一行将ApiServiceSettings添加到 DI 容器中。第二行将IApiServiceWrapper添加到 DI 容器中,并向HTTPClient工厂注册该类。这使得IApiServiceWrapper能够被注入到其他类中,HTTPClient工厂将管理HTTPClient的注入和寿命。

public static IServiceCollection ConfigureApiServiceWrapper(this IServiceCollection services, IConfiguration config)
{
  services.Configure<ApiServiceSettings>(config.GetSection(nameof(ApiServiceSettings)));
  services.AddHttpClient<IApiServiceWrapper,ApiServiceWrapper>();
  return services;
}

打开Startup.cs类并添加以下using语句:

using AutoLot.Services.ApiWrapper;

最后,导航到ConfigureServices()方法并添加下面一行:

services.ConfigureApiServiceWrapper(Configuration);

构建 api cartcontroller

当前版本的CarsController与数据访问库中的 repos 紧密绑定。CarsController的下一次迭代将使用服务包装器与数据库通信。将CarsController重命名为CarsDalController(包括构造函数),并将一个名为CarsController的新类添加到Controllers目录中。该类几乎是CarsController的精确副本。我将它们分开,以帮助澄清使用回购和服务之间的区别。

Note

在访问同一个数据库时,很少会同时使用数据访问层和服务层。我展示了这两种选择,以便您可以决定哪种方法最适合您。

using语句更新如下:

using System.Threading.Tasks;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Models.Entities;
using AutoLot.Services.ApiWrapper;
using AutoLot.Services.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;

接下来,创建类public,从Controller继承,并添加Route属性。创建一个接受IAutoLotServiceWrapperIAppLogging实例的构造函数,并将这两个实例分配给类级变量。起始代码如下所示:

namespace AutoLot.Mvc.Controllers
{
[Route("[controller]/[action]")]
public class CarsController : Controller
{
  private readonly IApiServiceWrapper _serviceWrapper;
  private readonly IAppLogging<CarsController> _logging;
  public CarsController(IApiServiceWrapper serviceWrapper, IAppLogging<CarsController> logging)
  {
    _serviceWrapper = serviceWrapper;
    _logging = logging;
  }
}

getnames 帮助器方法

GetMakes()助手方法为数据库中的所有Makes构建一个SelectList。它使用Id作为值,使用Name作为显示文本。

internal async Task<SelectList> GetMakesAsync()=>
  new SelectList(
    await _serviceWrapper.GetMakesAsync(),
    nameof(Make.Id),
    nameof(Make.Name));

获得一辆汽车的方法

GetOne()帮助器方法获得一个单独的Car记录:

internal async Task<Car> GetOneCarAsync(int? id)
  => !id.HasValue ? null : await _serviceWrapper.GetCarAsync(id.Value);

公共行动方法

这个控制器中的公共动作方法与CarsController的唯一区别是数据访问,所有的动作方法都是异步的。既然您已经了解了每个操作的用途,下面是其余的方法,其中的更改以粗体和/或带注释的形式突出显示:

[Route("/[controller]")]
[Route("/[controller]/[action]")]
public async Task<IActionResult> Index()
  => View(await _serviceWrapper.GetCarsAsync());

[HttpGet("{makeId}/{makeName}")]
public async Task<IActionResult> ByMake(int makeId, string makeName)
{
  ViewBag.MakeName = makeName;
  return View(await _serviceWrapper.GetCarsByMakeAsync(makeId));
}

[HttpGet("{id?}")]
public async Task<IActionResult> Details(int? id)
{
  if (!id.HasValue)
  {
    return BadRequest();
  }
  var car = await GetOneCarAsync(id);
  if (car == null)
  {
    return NotFound();
  }
  return View(car);
}

[HttpGet]
public async Task<IActionResult> Create()
{
  ViewData["MakeId"] = await GetMakesAsync();
  return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Car car)
{
  if (ModelState.IsValid)
  {
    await _serviceWrapper.AddCarAsync(car);
    return RedirectToAction(nameof(Index));
  }
  ViewData["MakeId"] = await GetMakesAsync();
  return View(car);
}

[HttpGet("{id?}")]
public async Task<IActionResult> Edit(int? id)
{
  var car = await GetOneCarAsync(id);
  if (car == null)
  {
    return NotFound();
  }
  ViewData["MakeId"] = await GetMakesAsync();
  return View(car);
}

[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, Car car)
{
  if (id != car.Id)
  {

    return BadRequest();
  }
  if (ModelState.IsValid)
  {
    await _serviceWrapper.UpdateCarAsync(id,car);
    return RedirectToAction(nameof(Index));
  }
  ViewData["MakeId"] = await GetMakesAsync();
  return View(car);
}

[HttpGet("{id?}")]
public async Task<IActionResult> Delete(int? id)
{
  var car = await GetOneCarAsync(id);
  if (car == null)
  {
    return NotFound();
  }
  return View(car);
}

[HttpPost("{id}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, Car car)
{
  await _serviceWrapper.DeleteCarAsync(id,car);
  return RedirectToAction(nameof(Index));
}

更新视图组件

MenuViewComponent目前使用的是数据访问层和Invoke的非同步版本。对该类进行以下更改:

using System.Linq;
using System.Threading.Tasks;
using AutoLot.Dal.Repos.Interfaces;
using AutoLot.Services.ApiWrapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewComponents;

namespace AutoLot.Mvc.ViewComponents
{

  public class MenuViewComponent : ViewComponent
  {
    private readonly IApiServiceWrapper _serviceWrapper;
    public MenuViewComponent(IApiServiceWrapper serviceWrapper)
    {
        _serviceWrapper = serviceWrapper;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
      var makes = await _serviceWrapper.GetMakesAsync();
      if (makes == null)
      {
          return new ContentViewComponentResult("Unable to get the makes");
      }
      return View("MenuView", makes);
    }
  }
}

运行 AutoLot。Mvc 和 AutoLot。Api 在一起

AutoLot。Mvc 依赖于 AutoLot。要运行的 Api。这可以通过 Visual Studio、命令行或两者的组合来完成。

Note

记住两者都是自动的。Mvc 和 AutoLot。Api 被配置为在每次运行时重建数据库。确保至少关闭其中一个,否则它们会发生冲突。为了加快调试速度,在测试不会更改任何数据的功能时,请关闭这两个选项。

使用 Visual Studio

您可以将 Visual Studio 配置为同时运行多个项目。这是通过在解决方案资源管理器中右击该解决方案,选择“选择启动项目”,并设置 AutoLot 的操作来完成的。Api 和 AutoLot。Mvc 启动,如图 31-11 所示。

img/340876_10_En_31_Fig11_HTML.jpg

图 31-11。

在 Visual Studio 中设置多个启动项目

当您按 F5(或单击绿色运行箭头)时,两个项目都将启动。当你这样做的时候,有一些复杂的事情。首先,Visual Studio 会记住运行应用的最后一个配置文件。这意味着如果您使用 IIS Express 运行 AutoLot。Api,两者一起运行将运行 AutoLot。Api 使用 IIS Express,服务设置中的端口将会不正确。这很容易解决。在配置多个启动选项之前,更改appsettings.development.json文件中的端口或使用 Kestrel 运行应用。

第二个问题归结于时机。两个项目基本上同时开始。如果你有 AutoLot。Api 配置为在每次运行时重新创建数据库,它不会为自动装载做好准备。当执行ViewComponent来构建菜单时使用 Mvc。自动手枪的快速刷新。Mvc 浏览器(一旦你在 AutoLot 看到了 SwaggerUI。Api)将解决这个问题。

使用命令行

在每个项目目录中打开命令提示符,输入dotnet watch run。这允许你控制顺序和时间,也将确保应用使用 Kestrel 而不是 IIS 来执行。关于从命令行运行时的调试信息,请参考第二十九章。

摘要

这一章完成了我们对 ASP.NET 岩心的研究,完成了自动 Lot。Mvc 应用。它从深入研究视图、局部视图以及编辑器和显示模板开始。接下来是对标记助手的检查,将客户端标记与服务器端代码混合在一起。

下一组主题涵盖了客户端库,包括管理项目中的库以及捆绑和缩小。配置完成后,使用库的新路径更新布局,将布局分解成一组片段,并添加环境标记助手以进一步细化客户端库处理。

接下来是使用HTTPClientFactory和 ASP.NET 核心配置系统来创建与 AutoLot 通信的服务包装器。Api,它用于为动态菜单系统创建一个视图组件。在简要讨论了如何加载这两个应用(AutoLot。Api 和 AutoLot。Mvc)同时开发了应用的核心。

这一发展始于CarsController和所有动作方法的创建。然后,通过创建所有的Cars视图,添加并完成定制标记助手。当然,我们只构建了一个控制器及其视图,但是可以遵循该模式为所有的 AutoLot 实体提供控制器和视图。