Author Archives: user

Ultimate ASP.NET Core Web API 4 HANDLING GET REQUESTS

4 HANDLING GET REQUESTS

4 处理 GET 请求

We’re all set to add some business logic to our application. But before we do that, let’s talk a bit about controller classes and routing because they play an important part while working with HTTP requests.‌
我们已准备好向应用程序添加一些业务逻辑。但在我们这样做之前,让我们先谈谈控制器类和路由,因为它们在处理 HTTP 请求时起着重要的作用。

4.1 Controllers and Routing in WEB API

4.1 WEB API 中的控制器和路由

Controllers should only be responsible for handling requests, model validation, and returning responses to the frontend or some HTTP client. Keeping business logic away from controllers is a good way to keep them lightweight, and our code more readable and maintainable.‌
控制器应该只负责处理请求、模型验证以及将响应返回给前端或某些 HTTP 客户端。让业务逻辑远离控制器是保持它们轻量级的好方法,并且我们的代码更具可读性和可维护性。

If you want to create the controller in the main project, you would right- click on the Controllers folder and then Add=>Controller. Then from the menu, you would choose API Controller Class and give it a name:
如果要在主项目中创建控制器,请右键单击 Controllers 文件夹,然后单击 Add=>Controller。然后从菜单中选择 API Controller Class 并为其命名:

alt text

But, that’s not the thing we are going to do. We don’t want to create our controllers in the main project.
但是,这不是我们要做的事情。我们不想在主项目中创建控制器。

What we are going to do instead is create a presentation layer in our application.
相反,我们要做的是在我们的应用程序中创建一个表示层。

The purpose of the presentation layer is to provide the entry point to our system so that consumers can interact with the data. We can implement this layer in many ways, for example creating a REST API, gRPC, etc.
表示层的目的是提供我们系统的入口点,以便消费者可以与数据进行交互。我们可以通过多种方式实现此层,例如创建 REST API、gRPC 等。

However, we are going to do something different from what you are normally used to when creating Web APIs. By convention, controllers are defined in the Controllers folder inside the main project.
但是,我们将做一些与您通常习惯的创建 Web API 不同的事情。按照约定,控制器在主项目内的 Controllers 文件夹中定义。

Why is this a problem?
为什么这是一个问题?

Because ASP.NET Core uses Dependency Injection everywhere, we need to have a reference to all of the projects in the solution from the main project. This allows us to configure our services inside the Program class.
由于 ASP.NET Core 在任何地方都使用依赖关系注入,因此我们需要从主项目引用解决方案中的所有项目。这允许我们在 Program 类中配置我们的服务。

While this is exactly what we want to do, it introduces a big design flaw. What’s preventing our controllers from injecting anything they want inside the constructor?
虽然这正是我们想要做的,但它引入了一个很大的设计缺陷。是什么阻止了我们的控制器在构造函数中注入他们想要的任何东西?

So how can we impose some more strict rules about what controllers can do?
那么,我们如何对控制器可以做什么施加一些更严格的规则呢?

Do you remember how we split the Service layer into the Service.Contracts and Service projects? That was one piece of the puzzle.
你还记得我们是如何将 Service 层拆分为 Service.Contracts 和 Service 项目的吗?这是拼图的一部分。

Another part of the puzzle is the creation of a new class library project,CompanyEmployees.Presentation.
难题的另一部分是创建一个新的类库项目 CompanyEmployees.Presentation。

Inside that new project, we are going to install Microsoft.AspNetCore.Mvc.Core package so it has access to the ControllerBase class for our future controllers. Additionally, let’s create a single class inside the Presentation project:
在该新项目中,我们将安装 Microsoft.AspNetCore.Mvc.Core 包,以便它可以访问我们未来控制器的 ControllerBase 类。此外,让我们在 Presentation 项目中创建一个类:

namespace CompanyEmployees.Presentation
{
    public static class AssemblyReference { }
}

It's an empty static class that we are going to use for the assembly reference inside the main project, you will see that in a minute.
这是一个空的静态类,我们将用于主项目中的程序集引用,您稍后会看到它。

The one more thing, we have to do is to reference the Service.Contracts project inside the Presentation project.
我们还要做的另一件事是引用 Presentation 项目中的 Service.Contracts 项目。

Now, we are going to delete the Controllers folder and the WeatherForecast.cs file from the main project because we are not going to need them anymore.
现在,我们将从主项目中删除 Controllers 文件夹和 WeatherForecast.cs 文件,因为我们不再需要它们。

Next, we have to reference the Presentation project inside the main one. As you can see, our presentation layer depends only on the service contracts, thus imposing more strict rules on our controllers.
接下来,我们必须在主项目中引用 Presentation 项目。正如你所看到的,我们的表示层只依赖于服务契约,因此对我们的控制器施加了更严格的规则。

Then, we have to modify the Program.cs file:
然后,我们必须修改 Program.cs 文件:

//builder.Services.AddControllers();
builder.Services.AddControllers().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);

Without this code, our API wouldn’t work, and wouldn’t know where to route incoming requests. But now, our app will find all of the controllers inside of the Presentation project and configure them with the framework. They are going to be treated the same as if they were defined conventionally.
如果没有这些代码,我们的 API 将无法工作,并且不知道将传入请求路由到何处。但现在,我们的应用程序将在 Presentation 项目中找到所有控制器,并使用框架对其进行配置。它们将被当作按约定定义的一样对待。

But, we don’t have our controllers yet. So, let’s navigate to the Presentation project, create a new folder named Controllers, and then a new class named CompaniesController. Since this is a class library project, we don’t have an option to create a controller as we had in the main project. Therefore, we have to create a regular class and then modify it:
但是,我们还没有控制器。因此,让我们导航到 Presentation 项目,创建一个名为 Controllers 的新文件夹,然后创建一个名为 CompaniesController 的新类。由于这是一个类库项目,因此我们无法像在主项目中那样创建控制器。因此,我们必须创建一个常规类,然后对其进行修改:

using Microsoft.AspNetCore.Mvc;

namespace CompanyEmployees.Presentation.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CompaniesController : ControllerBase
    { }
}

We’ve created this controller in the same way the main project would.
我们以与主项目相同的方式创建此控制器。

Every web API controller class inherits from the ControllerBase abstract class, which provides all necessary behavior for the derived class.
每个 Web API 控制器类都继承自 ControllerBase 抽象类,该抽象类为派生类提供所有必要的行为。

Also, above the controller class we can see this part of the code:
此外,在 controller 类的上方,我们可以看到这部分代码:

[Route("api/[controller]")]

This attribute represents routing and we are going to talk more about routing inside Web APIs.
这个属性表示路由,我们将更多地讨论 Web API 中的路由。

Web API routing routes incoming HTTP requests to the particular action method inside the Web API controller. As soon as we send our HTTP request, the MVC framework parses that request and tries to match it to an action in the controller.
Web API 路由将传入的 HTTP 请求路由到 Web API 控制器内的特定作方法。一旦我们发送 HTTP 请求,MVC 框架就会解析该请求并尝试将其与控制器中的作匹配。

There are two ways to implement routing in the project:
有两种方法可以在项目中实现路由:

• Convention-based routing and
• 基于约定的路由和

• Attribute routing
• 属性路由

Convention-based routing is called such because it establishes a convention for the URL paths. The first part creates the mapping for the controller name, the second part creates the mapping for the action method, and the third part is used for the optional parameter. We can configure this type of routing in the Program class:
之所以称为基于约定的路由,是因为它为 URL 路径建立了约定。第一部分为控制器名称创建映射,第二部分为作方法创建映射,第三部分用于可选参数。我们可以在 Program 类中配置这种类型的路由:

alt text

Our Web API project doesn’t configure routes this way, but if you create an MVC project this will be the default route configuration. Of course, if you are using this type of route configuration, you have to use the app.UseRouting method to add the routing middleware in the application’s pipeline.
我们的 Web API 项目不会以这种方式配置路由,但如果您创建一个 MVC 项目,这将是默认路由配置。当然,如果您使用的是这种类型的路由配置,则必须使用该应用程序。UseRouting 方法在应用程序的管道中添加路由中间件。

If you inspect the Program class in our main project, you won’t find the UseRouting method because the routes are configured with the app.MapControllers method, which adds endpoints for controller actions without specifying any routes.
如果你检查主项目中的 Program 类,则不会找到 UseRouting 方法,因为路由是使用应用程序配置的app.MapControllers 方法,该方法为控制器作添加端点,而无需指定任何路由。

Attribute routing uses the attributes to map the routes directly to the action methods inside the controller. Usually, we place the base route above the controller class, as you can see in our Web API controller class. Similarly, for the specific action methods, we create their routes right above them.
属性路由 (Attribute routing) 使用属性将路由直接映射到控制器内的作方法。通常,我们将基路由放在 controller 类之上,正如您在 Web API controller 类中看到的那样。同样,对于特定的 action methods,我们在它们的正上方创建它们的 route。

While working with the Web API project, the ASP.NET Core team suggests that we shouldn’t use Convention-based Routing, but Attribute routing instead.
在使用 Web API 项目时,ASP.NET Core 团队建议我们不应使用基于约定的路由,而应使用属性路由。

Different actions can be executed on the resource with the same URI, but with different HTTP Methods. In the same manner for different actions, we can use the same HTTP Method, but different URIs. Let’s explain this quickly.
可以对具有相同 URI 但使用不同的 HTTP 方法的资源执行不同的作。以相同的方式用于不同的作,我们可以使用相同的 HTTP 方法,但使用不同的 URI。让我们快速解释一下。

For Get request, Post, or Delete, we use the same URI /api/companies but we use different HTTP Methods like GET, POST, or DELETE. But if we send a request for all companies or just one company, we are going to use the same GET method but different URIs (/api/companies for all companies and /api/companies/{companyId} for a single company).
对于 Get 请求、Post 或 Delete,我们使用相同的 URI /api/companies,但我们使用不同的 HTTP 方法,如 GET、POST 或 DELETE。但是,如果我们向所有公司或仅向一家公司发送请求,我们将使用相同的 GET 方法,但使用不同的 URI(所有公司的 /api/companies,单个公司的 /api/companies/{companyId})。

We are going to understand this even more once we start implementing different actions in our controller.
一旦我们开始在控制器中实现不同的作,我们将更加了解这一点。

4.2 Naming Our Resources

4.2 命名我们的资源

The resource name in the URI should always be a noun and not an action. That means if we want to create a route to get all companies, we should create this route: api/companies and not this one:‌/api/getCompanies.
URI 中的资源名称应始终是名词,而不是作。这意味着,如果我们想创建一个路由来获取所有公司,我们应该创建这个路由:api/companies,而不是这个:/api/getCompanies。

The noun used in URI represents the resource and helps the consumer to understand what type of resource we are working with. So, we shouldn’t choose the noun products or orders when we work with the companies resource; the noun should always be companies. Therefore, by following this convention if our resource is employees (and we are going to work with this type of resource), the noun should be employees.
URI 中使用的名词代表资源,并帮助使用者了解我们正在使用的资源类型。因此,当我们使用 companies 资源时,我们不应该选择名词 products 或 orders;名词应始终为 companies。因此,如果我们的资源是 employees(并且我们将使用这种类型的资源),则名词应该是 employees,请遵循此约定。

Another important part we need to pay attention to is the hierarchy between our resources. In our example, we have a Company as a principal entity and an Employee as a dependent entity. When we create a route for a dependent entity, we should follow a slightly different convention:/api/principalResource/{principalId}/dependentResource.
我们需要注意的另一个重要部分是我们资源之间的层次结构。在我们的示例中,我们将 Company 作为委托人实体,将 Employee 作为依赖实体。当我们为依赖实体创建路由时,我们应该遵循略有不同的约定:/api/principalResource/{principalId}/dependentResource。

Because our employees can’t exist without a company, the route for the employee's resource should be /api/companies/{companyId}/employees.
因为我们的员工没有公司就无法存在,所以员工资源的路由应该是 /api/companies/{companyId}/employees。

With all of this in mind, we can start with the Get requests.
考虑到所有这些,我们可以从 Get 请求开始。

4.3 Getting All Companies From the Database

4.3 从数据库中获取所有公司

So let’s start.‌
那么让我们开始吧。

The first thing we are going to do is to change the base route from [Route("api/[controller]")] to [Route("api/companies")]. Even though the first route will work just fine, with the second example we are more specific to show that this routing should point to the CompaniesController class.
我们要做的第一件事是更改 base 路由从 [Route(“api/[controller]”)] 到 [Route(“api/companies”)]。尽管第一个路由可以正常工作,但在第二个示例中,我们更具体地表明此路由应指向 CompaniesController 类。

Now it is time to create the first action method to return all the companies from the database. Let’s create a definition for the GetAllCompanies method in the ICompanyRepository interface:
现在,是时候创建第一个作方法以从数据库中返回所有公司了。让我们在 ICompanyRepository 接口中为 GetAllCompanies 方法创建一个定义:

using Entities.Models;

namespace Contract
{
    public interface ICompanyRepository
    {

            IEnumerable<Company> GetAllCompanies(bool trackChanges);

    }
}

For this to work, we need to add a reference from the Entities project to the Contracts project.
为此,我们需要添加从 Entities 项目到 Contracts 项目的引用。

Now, we can continue with the interface implementation in the CompanyRepository class:
现在,我们可以继续在 CompanyRepository 类中实现接口:

using Contract;
using Entities.Models;

namespace Repository
{
    //public class CompanyRepository : RepositoryBase<Company>, ICompanyRepository
    //{
    //    public CompanyRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
    //}

    internal sealed class CompanyRepository : RepositoryBase<Company>, ICompanyRepository
    {
        public CompanyRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
        public IEnumerable<Company> GetAllCompanies(bool trackChanges) => FindAll(trackChanges).OrderBy(c => c.Name).ToList();
    }
}

As you can see, we are calling the FindAll method from the RepositoryBase class, ordering the result with the OrderBy method, and then executing the query with the ToList method.
如您所见,我们从 RepositoryBase 类调用 FindAll 方法,使用 OrderBy 方法对结果进行排序,然后使用 ToList 方法执行查询。

After the repository implementation, we have to implement a service layer.
在仓库实现之后,我们必须实现一个服务层。

Let’s start with the ICompanyService interface modification:
让我们从 ICompanyService 接口修改开始:

using Entities.Models;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        IEnumerable<Company> GetAllCompanies(bool trackChanges);
    }
}

Since the Company model resides in the Entities project, we have to add the Entities reference to the Service.Contracts project. At least, we have for now.
由于 Company 模型驻留在 Entities 项目中,因此我们必须将 Entities 引用添加到 Service.Contracts 项目中。至少,我们现在有。

Let’s be clear right away before we proceed. Getting all the entities from the database is a bad idea. We’re going to start with the simplest method and change it later on.
在我们继续之前,让我们马上弄清楚。从数据库中获取所有实体是一个坏主意。我们将从最简单的方法开始,稍后再进行更改。

Then, let’s continue with the CompanyService modification:
然后,让我们继续进行 CompanyService 修改:

internal sealed class CompanyService : ICompanyService { private readonly IRepositoryManager _repository; private readonly ILoggerManager _logger; public CompanyService(IRepositoryManager repository, ILoggerManager logger) { _repository = repository; _logger = logger; } public IEnumerable<Company> GetAllCompanies(bool trackChanges) { try { var companies = _repository.Company.GetAllCompanies(trackChanges); return companies; } catch (Exception ex) { _logger.LogError($"Something went wrong in the {nameof(GetAllCompanies)} service method {ex}"); throw; } } }

We are using our repository manager to call the GetAllCompanies method from the CompanyRepository class and return all the companies from the database.
我们使用存储库管理器从 CompanyRepository 类调用 GetAllCompanies 方法,并返回数据库中的所有公司。

Finally, we have to return companies by using the GetAllCompanies method inside the Web API controller.
最后,我们必须在 Web API 控制器中使用 GetAllCompanies 方法返回公司。

The purpose of the action methods inside the Web API controllers is not only to return results. It is the main purpose, but not the only one. We need to pay attention to the status codes of our Web API responses as well. Additionally, we are going to decorate our actions with the HTTP attributes which will mark the type of the HTTP request to that action.
Web API 控制器中的作方法的目的不仅仅是返回结果。这是主要目的,但不是唯一目的。我们还需要注意 Web API 响应的状态代码。此外,我们将使用 HTTP 属性来装饰我们的作,这些属性将标记该作的 HTTP 请求的类型。

So, let’s modify the CompaniesController:
那么,让我们修改 CompaniesController:

using Microsoft.AspNetCore.Mvc;
using Service.Contracts;

namespace CompanyEmployees.Presentation.Controllers
{
    [Route("api/companies")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
        private readonly IServiceManager _service;
        public CompaniesController(IServiceManager service) => _service = service;
        [HttpGet]
        public IActionResult GetCompanies()
        {
            try
            {
                var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
                return Ok(companies);
            }
            catch
            {
                return StatusCode(500, "Internal server error");
            }
        }
    }
}

Let’s explain this code a bit.
让我们稍微解释一下这段代码。

First of all, we inject the IServiceManager interface inside the constructor. Then by decorating the GetCompanies action with the [HttpGet] attribute, we are mapping this action to the GET request. Then, we use an injected service to call the service method that gets the data from the repository class.
首先,我们在构造函数中注入 IServiceManager 接口。然后,通过使用 GetCompanies的[HttpGet] 属性,我们将此作映射到 GET 请求。然后,我们使用注入的服务来调用从 repository 类获取数据的 service 方法。

The IActionResult interface supports using a variety of methods, which return not only the result but also the status codes. In this situation,the OK method returns all the companies and also the status code 200 — which stands for OK. If an exception occurs, we are going to return the internal server error with the status code 500.
IActionResult 接口支持使用多种方法,这些方法不仅返回结果,还返回状态代码。在这种情况下,OK 方法返回所有公司以及状态代码 200 — 代表 OK。如果发生异常,我们将返回状态代码为 500 的内部服务器错误。

Because there is no route attribute right above the action, the route for the GetCompanies action will be api/companies which is the route placed on top of our controller.
由于作正上方没有 route 属性,因此 GetCompanies作的路由将是 api/companies,这是放置在控制器顶部的路由。

4.4 Testing the Result with Postman

4.4 使用 Postman 测试结果

To check the result, we are going to use a great tool named Postman, which helps a lot with sending requests and displaying responses. If you download our exercise files, you will find the file Bonus 2- CompanyEmployeesRequests.postman_collection.json, which contains a request collection divided for each chapter of this book. You can import them in Postman to save yourself the time of manually typing them:‌
为了检查结果,我们将使用一个名为 Postman 的出色工具,它对发送请求和显示响应有很大帮助。如果您下载我们的练习文件,您将找到文件 Bonus 2- CompanyEmployeesRequests.postman_collection.json,其中包含为本书的每一章划分的请求集合。您可以在 Postman 中导入它们,以节省手动输入它们的时间:

alt text

NOTE: Please note that some GUID values will be different for your project, so you have to change them according to those values.
注意:请注意,某些 GUID 值对于您的项目会有所不同,因此您必须根据这些值更改它们。

So let’s start the application by pressing the F5 button and check that it is now listening on the https://localhost:5001 address:
因此,让我们按 F5 按钮启动应用程序,并检查它现在是否正在侦听 https://localhost:5001 地址:

alt text

If this is not the case, you probably ran it in the IIS mode; so turn the application off and start it again, but in the CompanyEmployees mode:
如果不是这种情况,您可能在 IIS 模式下运行了它;因此,请关闭应用程序并再次启动它,但在 CompanyEmployees 模式下:

alt text

Now, we can use Postman to test the result:
现在,我们可以使用 Postman 来测试结果:
https://localhost:5001/api/companies

alt text

Excellent, everything is working as planned. But we are missing something. We are using the Company entity to map our requests to the database and then returning it as a result to the client, and this is not a good practice. So, in the next part, we are going to learn how to improve our code with DTO classes.
太好了,一切都按计划进行。但我们缺少一些东西。我们使用 Company 实体将请求映射到数据库,然后将其作为结果返回给客户端,这不是一个好的做法。因此,在下一部分中,我们将学习如何使用 DTO 类改进我们的代码。

4.5 DTO Classes vs. Entity Model Classes

4.5 DTO 类与实体模型类

A data transfer object (DTO) is an object that we use to transport data between the client and server applications.‌
数据传输对象 (DTO) 是我们用来在客户端和服务器应用程序之间传输数据的对象。

So, as we said in a previous section of this book, it is not a good practice to return entities in the Web API response; we should instead use data transfer objects. But why is that?
因此,正如我们在本书的上一节中所说,在 Web API 响应中返回实体不是一个好的做法;我们应该改用 Data Transfer 对象。但这是为什么呢?

Well, EF Core uses model classes to map them to the tables in the database and that is the main purpose of a model class. But as we saw, our models have navigational properties and sometimes we don’t want to map them in an API response. So, we can use DTO to remove any property or concatenate properties into a single property.
EF Core 使用模型类将它们映射到数据库中的表,这就是模型类的主要用途。但正如我们所看到的,我们的模型具有导航属性,有时我们不想在 API 响应中映射它们。因此,我们可以使用 DTO 删除任何属性或将属性连接成单个属性。

Moreover, there are situations where we want to map all the properties from a model class to the result — but still, we want to use DTO instead. The reason is if we change the database, we also have to change the properties in a model — but that doesn’t mean our clients want the result changed. So, by using DTO, the result will stay as it was before the model changes.
此外,在某些情况下,我们希望将模型类中的所有属性映射到结果——但我们仍然希望使用 DTO 来代替。原因是如果我们更改数据库,我们也必须更改模型中的属性 — 但这并不意味着我们的客户希望更改结果。因此,通过使用 DTO,结果将保持与模型更改之前相同。

As we can see, keeping these objects separate (the DTO and model classes) leads to a more robust and maintainable code in our application.
正如我们所看到的,将这些对象分开(DTO 和模型类)会导致我们的应用程序代码更加健壮和可维护。

Now, when we know why should we separate DTO from a model class in our code, let’s create a new project named Shared and then a new folder DataTransferObjects with the CompanyDto record inside:
现在,当我们知道为什么应该在代码中将 DTO 与模型类分开时,让我们创建一个名为 Shared 的新项目,然后创建一个新文件夹 DataTransferObjects,创建CompanyDto 类:

namespace Shared.DataTransferObjects
{
    public record CompanyDto(Guid Id, string Name, string FullAddress);
}

Instead of a regular class, we are using a record for DTO. This specific record type is known as a Positional record.
我们使用的不是常规类,而是 DTO 的记录。此特定记录类型称为 Positional record。

A Record type provides us an easier way to create an immutable reference type in .NET. This means that the Record’s instance property values cannot change after its initialization. The data are passed by value and the equality between two Records is verified by comparing the value of their properties.
Record 类型为我们提供了一种在 .NET 中创建不可变引用类型的更简单方法。这意味着 Record 的实例属性值在初始化后无法更改。数据按值传递,并通过比较两个 Record 的属性值来验证它们之间的相等性。

Records can be a valid alternative to classes when we have to send or receive data. The very purpose of a DTO is to transfer data from one part of the code to another, and immutability in many cases is useful. We use them to return data from a Web API or to represent events in our application.
当我们必须发送或接收数据时,记录可以是类的有效替代方案。DTO 的真正目的是将数据从代码的一部分传输到另一部分,在许多情况下,不变性很有用。我们使用它们从 Web API 返回数据或表示应用程序中的事件。

This is the exact reason why we are using records for our DTOs.
这就是我们为 DTO 使用记录的确切原因。

In our DTO, we have removed the Employees property and we are going to use the FullAddress property to concatenate the Address and Country properties from the Company class. Furthermore, we are not using validation attributes in this record, because we are going to use this record only to return a response to the client. Therefore, validation attributes are not required.
在我们的 DTO 中,我们删除了 Employees 属性,我们将使用 FullAddress 属性来连接 Company 类中的 Address 和 Country 属性。此外,我们没有在此记录中使用验证属性,因为我们仅将使用此记录将响应返回给客户端。因此,验证属性不是必需的。

So, the first thing we have to do is to add the reference from the Shared project to the Service.Contracts project, and remove the Entities reference. At this moment the Service.Contracts project is only referencing the Shared project.
因此,我们要做的第一件事是将 Shared 项目中的引用添加到 Service.Contracts 项目中,并删除 Entities 引用。此时,Service.Contracts 项目仅引用 Shared 项目。

Then, we have to modify the ICompanyService interface:
然后,我们必须修改 ICompanyService 接口:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges);
    }
}

And the CompanyService class:
以及 CompanyService 类:

using Contract;
using Service.Contracts;
using Shared.DataTransferObjects;

namespace Service
{
    internal sealed class CompanyService : ICompanyService
    {
        private readonly IRepositoryManager _repository;
        private readonly ILoggerManager _logger;
        public CompanyService(IRepositoryManager repository, ILoggerManager logger)
        {
            _repository = repository;
            _logger = logger;
        }
        public IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges)
        {
            try
            {
                var companies = _repository.Company.GetAllCompanies(trackChanges);
                var companiesDto = companies.Select(c => new CompanyDto(c.Id, c.Name ?? "", string.Join(' ', c.Address, c.Country))).ToList();
                return companiesDto;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong in the {nameof(GetAllCompanies)} service method {ex}");
                throw;
            }
        }
    }
}

Let’s start our application and test it with the same request from Postman:https://localhost:5001/api/companies
让我们启动我们的应用程序,并使用来自 Postman:https://localhost:5001/api/companies 的相同请求对其进行测试

alt text

This time we get our CompanyDto result, which is a more preferred way. But this can be improved as well. If we take a look at our mapping code in the GetCompanies action, we can see that we manually map all the properties. Sure, it is okay for a few fields — but what if we have a lot more? There is a better and cleaner way to map our classes and that is by using the Automapper.
这次我们得到 CompanyDto 结果,这是一种更可取的方法。但这也可以改进。如果我们查看 GetCompanies作中的映射代码,我们可以看到我们手动映射了所有属性。当然,对于一些领域来说,这是可以的——但如果我们有更多的领域呢?有一种更好、更简洁的方法来映射我们的类,那就是使用 Automapper。

4.6 Using AutoMapper in ASP.NET Core

4.6 在 ASP.NET Core 中使用 AutoMapper

AutoMapper is a library that helps us with mapping objects in our applications. By using this library, we are going to remove the code for manual mapping — thus making the action readable and maintainable.‌
AutoMapper 是一个库,可帮助我们在应用程序中映射对象。通过使用这个库,我们将删除用于手动映射的代码 — 从而使作可读且可维护。

So, to install AutoMapper, let’s open a Package Manager Console window, choose the Service project as a default project from the drop-down list, and run the following command:
因此,要安装 AutoMapper,让我们打开一个 Package Manager Console 窗口,从下拉列表中选择 Service 项目作为默认项目,然后运行以下命令:

PM> Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

After installation, we are going to register this library in the Program class:
安装后,我们将在 Program 类中注册此库:

builder.Services.AddAutoMapper(typeof(Program));

As soon as our library is registered, we are going to create a profile class, also in the main project, where we specify the source and destination objects for mapping:
注册库后,我们将创建一个 profile 类,也是在主项目中,我们在其中指定要映射的源和目标对象:

using AutoMapper;
using Entities.Models;
using Shared.DataTransferObjects;

namespace CompanyEmployees
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<Company, CompanyDto>().ForMember(c => c.FullAddress, opt => opt.MapFrom(x => string.Join(' ', x.Address, x.Country)));
        }
    }
}

The MappingProfile class must inherit from the AutoMapper’s Profile class. In the constructor, we are using the CreateMap method where we specify the source object and the destination object to map to. Because we have the FullAddress property in our DTO record, which contains both the Address and the Country from the model class, we have to specify additional mapping rules with the ForMember method.
MappingProfile 类必须继承自 AutoMapper 的 Profile 类。在构造函数中,我们使用 CreateMap 方法,在其中指定要映射到的源对象和目标对象。由于我们的 DTO 记录中有 FullAddress 属性,其中包含模型类中的 Address 和 Country,因此我们必须使用 ForMember 方法指定其他映射规则。

Now, we have to modify the ServiceManager class to enable DI in our service classes:
现在,我们必须修改 ServiceManager 类以在我们的服务类中启用 DI:

using Contract;
using Service.Contracts;
using Service;
using AutoMapper;

//public sealed class ServiceManager : IServiceManager
//{
//    private readonly Lazy<ICompanyService> _companyService; private readonly Lazy<IEmployeeService> _employeeService;
//    public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger)
//    {
//        _companyService = new Lazy<ICompanyService>(() => new CompanyService(repositoryManager, logger));
//        _employeeService = new Lazy<IEmployeeService>(() => new EmployeeService(repositoryManager, logger));
//    }
//    public ICompanyService CompanyService => _companyService.Value;
//    public IEmployeeService EmployeeService => _employeeService.Value;
//}

public sealed class ServiceManager : IServiceManager
{
    private readonly Lazy<ICompanyService> _companyService;
    private readonly Lazy<IEmployeeService> _employeeService;
    public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger, IMapper mapper)
    {
        _companyService = new Lazy<ICompanyService>(() => new CompanyService(repositoryManager, logger, mapper));
        _employeeService = new Lazy<IEmployeeService>(() => new EmployeeService(repositoryManager, logger, mapper));
    }
    public ICompanyService CompanyService => _companyService.Value;
    public IEmployeeService EmployeeService => _employeeService.Value;
}

Of course, now we have two errors regarding our service constructors. So we need to fix that in both CompanyService and EmployeeService classes:
当然,现在我们有两个关于服务构造函数的错误。因此,我们需要在 CompanyService 和 EmployeeService 类中修复该问题:

using AutoMapper;
using Contract;
using Service.Contracts;
using Shared.DataTransferObjects;

namespace Service
{
    //internal sealed class CompanyService : ICompanyService
    //{
    //    private readonly IRepositoryManager _repository;
    //    private readonly ILoggerManager _logger;
    //    public CompanyService(IRepositoryManager repository, ILoggerManager logger)
    //    {
    //        _repository = repository;
    //        _logger = logger;
    //    }
    //}

    //internal sealed class CompanyService : ICompanyService
    //{
    //    private readonly IRepositoryManager _repository;
    //    private readonly ILoggerManager _logger;
    //    public CompanyService(IRepositoryManager repository, ILoggerManager logger)
    //    {
    //        _repository = repository; 
    //        _logger = logger;
    //    }
    //    public IEnumerable<Company> GetAllCompanies(bool trackChanges)
    //    {
    //        try
    //        {
    //            var companies = _repository.Company.GetAllCompanies(trackChanges);
    //            return companies;
    //        }
    //        catch (Exception ex)
    //        {
    //            _logger.LogError($"Something went wrong in the {nameof(GetAllCompanies)} service method {ex}");
    //            throw;
    //        }
    //    }
    //}

    internal sealed class CompanyService : ICompanyService
    {
        private readonly IRepositoryManager _repository;
        private readonly ILoggerManager _logger;
        private readonly IMapper _mapper;
        public CompanyService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)
        {
            _repository = repository;
            _logger = logger;
            _mapper = mapper;
        }
        public IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges)
        {
            try
            {
                var companies = _repository.Company.GetAllCompanies(trackChanges);
                var companiesDto = companies.Select(c => new CompanyDto(c.Id, c.Name ?? "", string.Join(' ', c.Address, c.Country))).ToList();
                return companiesDto;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong in the {nameof(GetAllCompanies)} service method {ex}");
                throw;
            }
        }
    }
}

We should do the same in the EmployeeService class:
我们应该在 EmployeeService 类中做同样的事情:

using AutoMapper;
using Contract;
using Service.Contracts;

namespace Service
{
    internal sealed class EmployeeService : IEmployeeService
    {
        private readonly IRepositoryManager _repository; 
        private readonly ILoggerManager _logger;
        private readonly IMapper _mapper;
        public EmployeeService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)
        {
            _repository = repository; 
            _logger = logger;
            _mapper = mapper;
        }
    }
}

Finally, we can modify the GetAllCompanies method in the CompanyService class:
最后,我们可以修改 CompanyService 类中的 GetAllCompanies 方法:

using AutoMapper;
using Contract;
using Service.Contracts;
using Shared.DataTransferObjects;

namespace Service
{
    internal sealed class CompanyService : ICompanyService
    {
        private readonly IRepositoryManager _repository;
        private readonly ILoggerManager _logger;
        private readonly IMapper _mapper;
        public CompanyService(IRepositoryManager repository, ILoggerManager logger, IMapper mapper)
        {
            _repository = repository;
            _logger = logger;
            _mapper = mapper;
        }

        public IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges)
        {
            try
            {
                var companies = _repository.Company.GetAllCompanies(trackChanges);
                var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
                return companiesDto;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong in the {nameof(GetAllCompanies)} service method {ex}");
                throw;
            }
        }
    }
}

We are using the Map method and specify the destination and then the source object.
我们使用的是 Map 方法,并指定目标对象,然后指定源对象。

Excellent.
非常好。

Now if we start our app and send the same request from Postman, we are going to get an error message:
现在,如果我们启动应用程序并从 Postman 发送相同的请求,我们将收到一条错误消息:

alt text

This happens because AutoMapper is not able to find the specific FullAddress property as we specified in the MappingProfile class. We are intentionally showing this error for you to know what to do if it happens to you in your projects.
发生这种情况是因为 AutoMapper 无法找到我们在 MappingProfile 类中指定的特定 FullAddress 属性。我们有意显示此错误,以便您知道如果在您的项目中发生该怎么做。

So to solve this, all we have to do is to modify the MappingProfile class:
因此,要解决此问题,我们所要做的就是修改 MappingProfile 类:

using AutoMapper;
using Entities.Models;
using Shared.DataTransferObjects;

namespace CompanyEmployees
{
    public class MappingProfile : Profile
    {
        //public MappingProfile()
        //{
        //    CreateMap<Company, CompanyDto>().ForMember(c => c.FullAddress, opt => opt.MapFrom(x => string.Join(' ', x.Address, x.Country)));
        //}

        public MappingProfile() { CreateMap<Company, CompanyDto>().ForCtorParam("FullAddress", opt => opt.MapFrom(x => string.Join(' ', x.Address, x.Country))); }
    }
}

This time, we are not using the ForMember method but the ForCtorParam method to specify the name of the parameter in the constructor that AutoMapper needs to map to.
这一次,我们不使用 ForMember 方法,而是使用 ForCtorParam 方法来指定 AutoMapper 需要映射到的构造函数中的参数名称。

Now, let’s use Postman again to send the request to test our app:
现在,让我们再次使用 Postman 发送请求来测试我们的 app:
https://localhost:5001/api/companies

alt text

We can see that everything is working as it is supposed to, but now with much better code.
我们可以看到一切都按预期工作,但现在代码要好得多。

Ultimate ASP.NET Core Web API 3 ONION ARCHITECTURE IMPLEMENTATION

3 Onion architecture implementation

3 洋葱架构实现

In this chapter, we are going to talk about the Onion architecture, its layers, and the advantages of using it. We will learn how to create different layers in our application to separate the different application parts and improve the application's maintainability and testability.‌
在本章中,我们将讨论 Onion 架构、它的层以及使用它的优势。我们将学习如何在应用程序中创建不同的层,以分离不同的应用程序部分并提高应用程序的可维护性和可测试性。

That said, we are going to create a database model and transfer it to the MSSQL database by using the code first approach. So, we are going to learn how to create entities (model classes), how to work with the DbContext class, and how to use migrations to transfer our created database model to the real database. Of course, it is not enough to just create a database model and transfer it to the database. We need to use it as well, and for that, we will create a Repository pattern as a data access layer.
也就是说,我们将创建一个数据库模型,并使用 Code First 方法将其传输到 MSSQL 数据库。因此,我们将学习如何创建实体(模型类),如何使用 DbContext 类,以及如何使用迁移将我们创建的数据库模型传输到真实数据库。当然,仅仅创建一个数据库模型并将其传输到数据库是不够的。我们也需要使用它,为此,我们将创建一个 Repository 模式作为数据访问层。

With the Repository pattern, we create an abstraction layer between the data access and the business logic layer of an application. By using it, we are promoting a more loosely coupled approach to access our data in the database.
使用 Repository 模式,我们在应用程序的数据访问和业务逻辑层之间创建一个抽象层。通过使用它,我们正在推广一种更松散耦合的方法来访问数据库中的数据。

Also, our code becomes cleaner, easier to maintain, and reusable. Data access logic is stored in a separate class, or sets of classes called a repository, with the responsibility of persisting the application’s business model.
此外,我们的代码变得更干净、更易于维护和可重用。数据访问逻辑存储在单独的类或称为存储库的类集中,负责持久化应用程序的业务模型。

Additionally, we are going to create a Service layer to extract all the business logic from our controllers, thus making the presentation layer and the controllers clean and easy to maintain.
此外,我们将创建一个 Service 层,从控制器中提取所有业务逻辑,从而使表示层和控制器干净且易于维护。

So, let’s start with the Onion architecture explanation.
那么,让我们从 Onion 架构解释开始。

3.1 About Onion Architecture

3.1 关于 Onion 架构

The Onion architecture is a form of layered architecture and we can visualize these layers as concentric circles. Hence the name Onion architecture. The Onion architecture was first introduced by Jeffrey Palermo, to overcome the issues of the traditional N-layered architecture approach.‌
洋葱架构是分层架构的一种形式,我们可以将这些层可视化为同心圆。因此得名 Onion 架构。Onion 架构最初由 Jeffrey Palermo 引入,以克服传统 N 层架构方法的问题。

There are multiple ways that we can split the onion, but we are going to choose the following approach where we are going to split the architecture into 4 layers:
我们可以通过多种方式拆分洋葱,但我们将选择以下方法,我们将架构拆分为 4 层:

• Domain Layer 域层
• Service Layer 服务层
• Infrastructure Layer 基础设施层
• Presentation Layer 表示层

Conceptually, we can consider that the Infrastructure and Presentation layers are on the same level of the hierarchy.
从概念上讲,我们可以认为 Infrastructure 和 Presentation 层位于层次结构的同一级别。

Now, let us go ahead and look at each layer with more detail to see why we are introducing it and what we are going to create inside of that layer:
现在,让我们继续更详细地了解每个层,以了解我们为什么要引入它以及我们将在该层中创建什么:

alt text

We can see all the different layers that we are going to build in our project.
我们可以看到要在项目中构建的所有不同层

3.1.1 Advantages of the Onion Architecture‌

3.1.1 Onion 架构的优势

Let us take a look at what are the advantages of Onion architecture, and why we would want to implement it in our projects.
让我们看一下 Onion 架构的优势是什么,以及为什么我们想在我们的项目中实现它。

All of the layers interact with each other strictly through the interfaces defined in the layers below. The flow of dependencies is towards the core of the Onion. We will explain why this is important in the next section.
所有层都严格通过下面层中定义的接口相互交互。依赖项的流向 Onion 的核心。我们将在下一节中解释为什么这很重要。

Using dependency inversion throughout the project, depending on abstractions (interfaces) and not the implementations, allows us to switch out the implementation at runtime transparently. We are depending on abstractions at compile-time, which gives us strict contracts to work with, and we are being provided with the implementation at runtime.
在整个项目中使用依赖反转,取决于抽象 (接口) 而不是实现,允许我们在运行时透明地切换实现。我们在编译时依赖于抽象,这为我们提供了严格的契约,并且我们在运行时获得了实现。

Testability is very high with the Onion architecture because everything depends on abstractions. The abstractions can be easily mocked with a mocking library such as Moq. We can write business logic without concern about any of the implementation details. If we need anything from an external system or service, we can just create an interface for it and consume it. We do not have to worry about how it will be implemented.The higher layers of the Onion will take care of implementing that interface transparently.
Onion 架构的可测试性非常高,因为一切都依赖于抽象。可以使用模拟库(如 Moq)轻松模拟抽象。我们可以编写业务逻辑,而无需担心任何实现细节。如果我们需要来自外部系统或服务的任何内容,我们只需为它创建一个接口并使用它。我们不必担心它将如何实施。Onion 的较高层将负责透明地实现该接口。

3.1.2 Flow of Dependencies‌

3.1.2 依赖流程

The main idea behind the Onion architecture is the flow of dependencies, or rather how the layers interact with each other. The deeper the layer resides inside the Onion, the fewer dependencies it has.
Onion 架构背后的主要思想是依赖关系的流动,或者更确切地说是各层如何相互交互。该层位于 Onion 内部的深度越深,它的依赖项就越少。

The Domain layer does not have any direct dependencies on the outside layers. It is isolated, in a way, from the outside world. The outer layers are all allowed to reference the layers that are directly below them in the hierarchy.
Domain 层对外部层没有任何直接的依赖关系。在某种程度上,它与外界隔绝。外部层都允许引用层次结构中位于其正下方的层。

We can conclude that all the dependencies in the Onion architecture flow inwards. But we should ask ourselves, why is this important?
我们可以得出结论,Onion 架构中的所有依赖项都是向内流动的。但我们应该问问自己,为什么这很重要?

The flow of dependencies dictates what a certain layer in the Onion architecture can do. Because it depends on the layers below it in the hierarchy, it can only call the methods that are exposed by the lower layers.
依赖项的流程决定了 Onion 架构中的某个层可以做什么。因为它依赖于层次结构中低于它的层,所以它只能调用由较低层公开的方法。

We can use lower layers of the Onion architecture to define contracts or interfaces. The outer layers of the architecture implement these interfaces. This means that in the Domain layer, we are not concerning ourselves with infrastructure details such as the database or external services.
我们可以使用 Onion 架构的较低层来定义合约或接口。体系结构的外部层实现这些接口。这意味着在域层,我们不关心基础设施细节,例如数据库或外部服务。

Using this approach, we can encapsulate all of the rich business logic in the Domain and Service layers without ever having to know any implementation details. In the Service layer, we are going to depend only on the interfaces that are defined by the layer below, which is the Domain layer.
使用这种方法,我们可以将所有丰富的业务逻辑封装在 Domain 和 Service 层中,而无需了解任何实现细节。在 Service 层中,我们将仅依赖于由下面的层定义的接口,即 Domain 层。

So, after all the theory, we can continue with our project implementation.
所以,在所有理论之后,我们可以继续我们的项目实施。

Let’s start with the models and the Entities project.
让我们从模型和实体项目开始。

3.2 Creating Models

3.2 创建模型

Using the example from the second chapter of this book, we are going to extract a new Class Library project named Entities.‌
使用本书第二章中的示例,我们将提取一个名为 Entities 的新类库项目。

Inside it, we are going to create a folder named Models, which will contain all the model classes (entities). Entities represent classes that Entity Framework Core uses to map our database model with the tables from the database. The properties from entity classes will be mapped to the database columns.

So, in the Models folder we are going to create two classes and modify them:

//Company.cs 

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Entities.Models
{
    public class Company
    {
        [Column("CompanyId")]
        public Guid Id { get; set; }

        [Required(ErrorMessage = "Company name is a required field.")]
        [MaxLength(60, ErrorMessage = "Maximum length for the Name is 60 characters.")]

        public string? Name { get; set; }
        [Required(ErrorMessage = "Company address is a required field.")]
        [MaxLength(60, ErrorMessage = "Maximum length for the Address is 60 characters")]
        public string? Address { get; set; }

        public string? Country { get; set; }

        public ICollection<Employee>? Employees { get; set; }
    }
}
//Employee.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Entities.Models
{
    public class Employee
    {
        [Column("EmployeeId")] 
        public Guid Id { get; set; }

        [Required(ErrorMessage = "Employee name is a required field.")]
        [MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters.")]
        public string? Name { get; set; }

        [Required(ErrorMessage = "Age is a required field.")]
        public int Age { get; set; }

        [Required(ErrorMessage = "Position is a required field.")]
        [MaxLength(20, ErrorMessage = "Maximum length for the Position is 20 characters.")]
        public string? Position { get; set; }

        [ForeignKey(nameof(Company))]
        public Guid CompanyId { get; set; }

        public Company? Company { get; set; }
    }
}

We have created two classes: the Company and Employee. Those classes contain the properties which Entity Framework Core is going to map to the columns in our tables in the database. But not all the properties will be mapped as columns. The last property of the Company class (Employees) and the last property of the Employee class (Company) are navigational properties; these properties serve the purpose of defining the relationship between our models.
我们创建了两个类:Company 和 Employee。这些类包含 Entity Framework Core 将映射到数据库中表中列的属性。但并非所有属性都将映射为列。Company 类的最后一个属性 (Employees) 和 Employee 类的最后一个属性 (Company) 是导航属性;这些属性用于定义模型之间的关系。

We can see several attributes in our entities. The [Column] attribute will specify that the Id property is going to be mapped with a different name in the database. The [Required] and [MaxLength] properties are here for validation purposes. The first one declares the property as mandatory and the second one defines its maximum length.
我们可以在实体中看到几个属性。[Column] 属性将指定 Id 属性将在数据库中使用不同的名称进行映射。此处的 [Required] 和 [MaxLength] 属性用于验证目的。第一个选项将属性声明为 mandatory 属性,第二个选项定义其最大长度。

Once we transfer our database model to the real database, we are going to see how all these validation attributes and navigational properties affect the column definitions.
一旦我们将数据库模型传输到真实数据库,我们将看到所有这些验证属性和导航属性如何影响列定义。

3.3 Context Class and the Database Connection

3.3 Context 类和数据库连接

Before we start with the context class creation, we have to create another‌ .NET Class Library and name it Repository. We are going to use this project for the database context and repository implementation.
在开始创建上下文类之前,我们必须创建另一个 .NET 类库并将其命名为 Repository。我们将使用此项目进行数据库上下文和存储库实现。

Now, let's create the context class, which will be a middleware component for communication with the database. It must inherit from the Entity Framework Core’s DbContext class and it consists of DbSet properties, which EF Core is going to use for the communication with the database.Because we are working with the DBContext class, we need to install the Microsoft.EntityFrameworkCore package in the Repository project. Also, we are going to reference the Entities project from the Repository project:
现在,让我们创建 context 类,它将是一个用于与数据库通信的中间件组件。它必须继承自 Entity Framework Core 的 DbContext 类,并且由 DbSet 属性组成,EF Core 将使用这些属性与数据库通信。由于我们正在使用 DBContext 类,因此需要在 Repository 项目中安装 Microsoft.EntityFrameworkCore 包。此外,我们还将从 Repository 项目中引用 Entities 项目:

alt text

Then, let’s navigate to the root of the Repository project and create the RepositoryContext class:
然后,让我们导航到 Repository 项目的根目录并创建 RepositoryContext 类:

// RepositoryContext.cs 
using Entities.Models;
using Microsoft.EntityFrameworkCore;

namespace Repository
{
    public class RepositoryContext : DbContext
    {
        public RepositoryContext(DbContextOptions options) : base(options) { }
        public DbSet<Company>? Companies { get; set; }
        public DbSet<Employee>? Employees { get; set; }
    }
}

After the class modification, let’s open the appsettings.json file, in the main project, and add the connection string named sqlconnection:
修改类后,让我们在主项目中打开 appsettings.json 文件,并添加名为 sqlconnection 的连接字符串:

{
    "Logging": {
        "LogLevel": { "Default": "Warning" }
    },
    "ConnectionStrings": {
        "sqlConnection": "server=.; database=CompanyEmployee; Integrated Security=true"
    },
    "AllowedHosts": "*"
}

It is quite important to have the JSON object with the ConnectionStrings name in our appsettings.json file, and soon you will see why.
在我们的 appsettings.json 文件中拥有具有 ConnectionStrings 名称的 JSON 对象非常重要,您很快就会明白原因。

But first, we have to add the Repository project’s reference into the main project.
但首先,我们必须将 Repository 项目的引用添加到主项目中。

Then, let’s create a new ContextFactory folder in the main project and inside it a new RepositoryContextFactory class. Since our RepositoryContext class is in a Repository project and not in the main one, this class will help our application create a derived DbContext instance during the design time which will help us with our migrations:
然后,让我们在主项目中创建一个新的 ContextFactory 文件夹,并在其中创建一个新的 RepositoryContextFactory 类。由于我们的 RepositoryContext 类位于 Repository 项目中,而不是在主项目中,因此此类将帮助我们的应用程序在设计时创建一个派生的 DbContext 实例,这将有助于我们进行迁移:

// RepositoryContextFactory.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Repository;

namespace CompanyEmployees.ContextFactory
{
    public class RepositoryContextFactory : IDesignTimeDbContextFactory<RepositoryContext>
    {
        public RepositoryContext CreateDbContext(string[] args)
        {
            var configuration = new ConfigurationBuilder().SetBasePath(
                Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build(); 
            var builder = new DbContextOptionsBuilder<RepositoryContext>().
                UseSqlServer(configuration.GetConnectionString("sqlConnection"));
            return new RepositoryContext(builder.Options);
        }
    }
}

We are using the IDesignTimeDbContextFactory<out TContext> interface that allows design-time services to discover implementations of this interface. Of course, the TContext parameter is our RepositoryContext class.
我们正在使用 IDesignTimeDbContextFactory<out TContext> 接口,该接口允许设计时服务发现此接口的实现。当然,TContext 参数是我们的 RepositoryContext 类。

For this, we need to add two using directives:
为此,我们需要添加两个 using 指令:

using Microsoft.EntityFrameworkCore.Design;
using Repository;

Then, we have to implement this interface with the CreateDbContext method. Inside it, we create the configuration variable of the IConfigurationRoot type and specify the appsettings file, we want to use. With its help, we can use the GetConnectionString method to access the connection string from the appsettings.json file. Moreover, to be able to use the UseSqlServer method, we need to install the Microsoft.EntityFrameworkCore.SqlServer package in the main project and add one more using directive:
然后,我们必须使用 CreateDbContext 方法实现此接口。在其中,我们创建 IConfigurationRoot 类型的配置变量,并指定我们要使用的 appsettings 文件。在它的帮助下,我们可以使用 GetConnectionString 方法从 appsettings.json 文件访问连接字符串。此外,为了能够使用 UseSqlServer 方法,我们需要在主项目中安装 Microsoft.EntityFrameworkCore.SqlServer 包,并再添加一个 using 指令:

using Microsoft.EntityFrameworkCore;

If we navigate to the GetConnectionString method definition, we will see that it is an extension method that uses the ConnectionStrings name from the appsettings.json file to fetch the connection string by the provided key:
如果我们导航到 GetConnectionString 方法定义,我们将看到它是一个扩展方法,它使用 appsettings.json 文件中的 ConnectionStrings 名称通过提供的键获取连接字符串:

alt text

Finally, in the CreateDbContext method, we return a new instance of our RepositoryContext class with provided options.
最后,在 CreateDbContext 方法中,我们返回 RepositoryContext 类的新实例,其中包含提供的选项。

3.4 Migration and Initial Data Seed

3.4 迁移和初始数据种子

Migration is a standard process of creating and updating the database from our application. Since we are finished with the database model creation, we can transfer that model to the real database. But we need to modify our CreateDbContext method first:‌
迁移是从我们的应用程序创建和更新数据库的标准过程。由于我们已经完成了数据库模型的创建,因此我们可以将该模型传输到真实数据库。但是我们需要先修改我们的 CreateDbContext 方法:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Repository;

namespace CompanyEmployees.ContextFactory
{
    public class RepositoryContextFactory : IDesignTimeDbContextFactory<RepositoryContext>
    {
        public RepositoryContext CreateDbContext(string[] args)
        {
            var configuration = new ConfigurationBuilder().SetBasePath(
                Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();

            //var builder = new DbContextOptionsBuilder<RepositoryContext>().
            //    UseSqlServer(configuration.GetConnectionString("sqlConnection"));

            var builder = new DbContextOptionsBuilder<RepositoryContext>().
                    UseSqlServer(configuration.GetConnectionString("sqlConnection"),
                    b => b.MigrationsAssembly("CompanyEmployees"));

            return new RepositoryContext(builder.Options);
        }
    }
}

We have to make this change because migration assembly is not in our main project, but in the Repository project. So, we’ve just changed the project for the migration assembly.
我们必须进行此更改,因为迁移程序集不在我们的主项目中,而是在 Repository 项目中。因此,我们刚刚更改了迁移程序集的项目。

Before we execute our migration commands, we have to install an additional ef core library: Microsoft.EntityFrameworkCore.Tools
在执行迁移命令之前,我们必须安装一个额外的 ef 核心库:Microsoft.EntityFrameworkCore.Tools

Now, let’s open the Package Manager Console window and create our first migration:
现在,让我们打开 Package Manager Console 窗口并创建我们的第一个迁移:

PM> Add-Migration DatabaseCreation

net8使用以下命令

PS C:\CompanyEmployees\CompanyEmployees> dotnet ef migrations add DatabaseCreation
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS 

With this command, we are creating migration files and we can find them in the Migrations folder in our main project:
使用此命令,我们正在创建迁移文件,并且可以在主项目的 Migrations 文件夹中找到它们:

alt text

With those files in place, we can apply migration:
这些文件就位后,我们可以应用迁移:

PM> Update-Database

net8使用以下命令

PS C:\CompanyEmployees\CompanyEmployees> dotnet ef database update
Build started...
Build succeeded.
Applying migration '20250503152559_DatabaseCreation'.
Done.

Excellent. We can inspect our database now:
非常好。我们现在可以检查我们的数据库:

alt text

Once we have the database and tables created, we should populate them with some initial data. To do that, we are going to create another folder in the Repository project called Configuration and add the CompanyConfiguration class:
创建数据库和表后,我们应该使用一些初始数据填充它们。为此,我们将在 Repository 项目中创建另一个名为 Configuration 的文件夹,并添加 CompanyConfiguration 类:

// CompanyConfiguration.cs

using Entities.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Repository.Configuration
{
    public class CompanyConfiguration : IEntityTypeConfiguration<Company>
    {
        public void Configure(EntityTypeBuilder<Company> builder)
        {
            builder.HasData(
                new Company
                {
                    Id = new Guid("c9d4c053-49b6-410c-bc78-2d54a9991870"),
                    Name = "IT_Solutions Ltd",
                    Address = "583 Wall Dr. Gwynn Oak, MD 21207",
                    Country = "USA"
                },
                new Company
                {
                    Id = new Guid("3d490a70-94ce-4d15-9494-5248280c2ce3"),
                    Name = "Admin_Solutions Ltd",
                    Address = "312 Forest Avenue, BF 923",
                    Country = "USA"
                });
        }
    }
}

Let’s do the same thing for the EmployeeConfiguration class:
让我们对 EmployeeConfiguration 类执行相同的作:

// EmployeeConfiguration.cs

using Entities.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Repository.Configuration
{
    public class EmployeeConfiguration : IEntityTypeConfiguration<Employee>
    {
        public void Configure(EntityTypeBuilder<Employee> builder)
        {
            builder.HasData(
                new Employee
                {
                    Id = new Guid("80abbca8-664d-4b20-b5de-024705497d4a"),
                    Name = "Sam Raiden",
                    Age = 26,
                    Position = "Software developer",
                    CompanyId = new Guid("c9d4c053-49b6-410c-bc78-2d54a9991870")
                },
                new Employee
                {
                    Id = new Guid("86dba8c0-d178-41e7-938c-ed49778fb52a"),
                    Name = "Jana McLeaf",
                    Age = 30,
                    Position = "Software developer",
                    CompanyId = new Guid("c9d4c053-49b6-410c-bc78-2d54a9991870")
                },
                new Employee
                {
                    Id = new Guid("021ca3c1-0deb-4afd-ae94-2159a8479811"),
                    Name = "Kane Miller",
                    Age = 35,
                    Position = "Administrator",
                    CompanyId = new Guid("3d490a70-94ce-4d15-9494-5248280c2ce3")
                });
        }
    }
}

To invoke this configuration, we have to change the RepositoryContext class:
要调用此配置,我们必须更改 RepositoryContext 类:

// /Repository/RepositoryContext.cs 
using Entities.Models;
using Microsoft.EntityFrameworkCore;
using Repository.Configuration;

namespace Repository
{
    //public class RepositoryContext : DbContext
    //{
    //    public RepositoryContext(DbContextOptions options) : base(options) { }
    //    public DbSet<Company>? Companies { get; set; }
    //    public DbSet<Employee>? Employees { get; set; }
    //}

    public class RepositoryContext : DbContext { 
        public RepositoryContext(DbContextOptions options) : base(options) { } 
        protected override void OnModelCreating(ModelBuilder modelBuilder) { 
            modelBuilder.ApplyConfiguration(new CompanyConfiguration()); 
            modelBuilder.ApplyConfiguration(new EmployeeConfiguration()); 
        }
        public DbSet<Company> Companies { get; set; } 
        public DbSet<Employee> Employees { get; set; } }
}

Now, we can create and apply another migration to seed these data to the database:
现在,我们可以创建并应用另一个迁移,以将这些数据播种到数据库:

dotnet ef migrations add InitialData
dotnet ef database update

This will transfer all the data from our configuration files to the respective tables.
这会将配置文件中的所有数据传输到相应的表中。

3.5 Repository Pattern Logic

3.5 存储库模式逻辑

After establishing a connection to the database and creating one, it's time to create a generic repository that will provide us with the CRUD methods. As a result, all the methods can be called upon any repository class in our project.‌
在建立与数据库的连接并创建一个连接后,是时候创建一个通用存储库了,它将为我们提供 CRUD 方法。因此,所有方法都可以在我们项目中的任何存储库类上调用。

Furthermore, creating the generic repository and repository classes that use that generic repository is not going to be the final step. We will go a step further and create a wrapper class around repository classes and inject it as a service in a dependency injection container.
此外,创建使用该泛型存储库的泛型存储库和存储库类不会是最后一步。我们将更进一步,围绕存储库类创建一个包装器类,并将其作为服务注入到依赖项注入容器中。

Consequently, we will be able to instantiate this class once and then call any repository class we need inside any of our controllers.
因此,我们将能够实例化这个类一次,然后在我们的任何控制器中调用我们需要的任何仓库类。

The advantages of this approach will become clearer once we use it in the project.
一旦我们在项目中使用它,这种方法的优势就会变得更加明显。

That said, let’s start by creating an interface for the repository inside the Contracts project:
也就是说,让我们从 Contracts 项目中的存储库创建一个接口开始:

// /Contract/IRepositoryBase.cs

using System.Linq.Expressions;

namespace Contract
{
    public interface IRepositoryBase<T>
    {
        IQueryable<T> FindAll(bool trackChanges);
        IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression, bool trackChanges);
        void Create(T entity);
        void Update(T entity);
        void Delete(T entity);
    }
}

Right after the interface creation, we are going to reference Contracts inside the Repository project. Also, in the Repository project, we are going to create an abstract class RepositoryBase — which is going to implement the IRepositoryBase interface:
在创建接口后,我们将引用 Repository 项目中的 Contracts。此外,在 Repository 项目中,我们将创建一个抽象类 RepositoryBase — 它将实现 IRepositoryBase 接口:

// /Repository/RepositoryBase.cs

using System.Linq.Expressions;
using Contract;
using Microsoft.EntityFrameworkCore;

namespace Repository
{
    public abstract class RepositoryBase<T> : IRepositoryBase<T> where T : class
    {
        protected RepositoryContext RepositoryContext;
        public RepositoryBase(RepositoryContext repositoryContext) => RepositoryContext = repositoryContext;
        public IQueryable<T> FindAll(bool trackChanges) => !trackChanges ? RepositoryContext.Set<T>().AsNoTracking() : RepositoryContext.Set<T>();
        public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression, bool trackChanges) => !trackChanges ? RepositoryContext.Set<T>().Where(expression).AsNoTracking() : RepositoryContext.Set<T>().Where(expression); 
        public void Create(T entity) => RepositoryContext.Set<T>().Add(entity); 
        public void Update(T entity) => RepositoryContext.Set<T>().Update(entity); 
        public void Delete(T entity) => RepositoryContext.Set<T>().Remove(entity);
    }
}

This abstract class as well as the IRepositoryBase interface work with the generic type T. This type T gives even more reusability to the RepositoryBase class. That means we don’t have to specify the exact model (class) right now for the RepositoryBase to work with. We can do that later on.
此抽象类以及 IRepositoryBase 接口使用泛型类型 T。此类型 T 为 RepositoryBase 类提供了更多的可重用性。这意味着我们现在不必为 RepositoryBase 指定确切的模型(类)。我们以后再做。

Moreover, we can see the trackChanges parameter. We are going to use it to improve our read-only query performance. When it’s set to false, we attach the AsNoTracking method to our query to inform EF Core that it doesn’t need to track changes for the required entities. This greatly improves the speed of a query.
此外,我们可以看到 trackChanges 参数。我们将使用它来提高只读查询性能。当它设置为 false 时,我们将 AsNoTracking 方法附加到我们的查询,以通知 EF Core 它不需要跟踪所需实体的更改。这大大提高了查询的速度。

3.6 Repository User Interfaces and Classes

3.6 存储库接口和类

Now that we have the RepositoryBase class, let’s create the user classes that will inherit this abstract class.‌
现在我们有了 RepositoryBase 类,让我们创建将继承此抽象类的用户类。

By inheriting from the RepositoryBase class, they will have access to all the methods from it. Furthermore, every user class will have its interface for additional model-specific methods.
通过继承 RepositoryBase 类,他们将可以访问该类中的所有方法。此外,每个 user class 都将具有其用于其他特定于模型的方法的接口。

This way, we are separating the logic that is common for all our repository user classes and also specific for every user class itself.
这样,我们将所有存储库用户类通用的逻辑以及每个用户类本身的特定逻辑分开。

Let’s create the interfaces in the Contracts project for the Company and Employee classes:
让我们在 Contracts 项目中为 Company 和 Employee 类创建接口:

// /Contract/ICompanyRepository.cs

namespace Contract
{
    public interface ICompanyRepository { }
}
// /Contract/IEmployeeRepository.cs

namespace Contract
{
    public interface IEmployeeRepository { }
}

After this, we can create repository user classes in the Repository project.
在此之后,我们可以在 Repository 项目中创建存储库用户类。

The first thing we are going to do is to create the CompanyRepository class:
我们要做的第一件事是创建 CompanyRepository 类:

// /Repository/CompanyRepository.cs

using Contract;
using Entities.Models;

namespace Repository
{
    public class CompanyRepository : RepositoryBase<Company>, ICompanyRepository
    {
        public CompanyRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
    }
}

And then, the EmployeeRepository class:
然后,EmployeeRepository 类:

// /Repository/EmployeeRepository.cs

using Contract;
using Entities.Models;

namespace Repository
{
    public class EmployeeRepository : RepositoryBase<Employee>, IEmployeeRepository
    {
        public EmployeeRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
    }
}

After these steps, we are finished creating the repository and repository- user classes. But there are still more things to do.
完成这些步骤后,我们完成了 repository 和 repository- user 类的创建。但还有更多的事情要做。

3.7 Creating a Repository Manager

3.7 创建仓库管理器

It is quite common for the API to return a response that consists of data from multiple resources; for example, all the companies and just some employees older than 30. In such a case, we would have to instantiate both of our repository classes and fetch data from their resources.‌
API 返回由来自多个资源的数据组成的响应是很常见的;例如,所有公司和一些 30 岁以上的员工。在这种情况下,我们将不得不实例化我们的两个存储库类并从它们的资源中获取数据。

Maybe it’s not a problem when we have only two classes, but what if we need the combined logic of five or even more different classes? It would just be too complicated to pull that off.
当我们只有两个类时,也许这不是问题,但如果我们需要五个甚至更多不同类的组合逻辑呢?要做到这一点太复杂了。

With that in mind, we are going to create a repository manager class, which will create instances of repository user classes for us and then register them inside the dependency injection container. After that, we can inject it inside our services with constructor injection (supported by ASP.NET Core). With the repository manager class in place, we may call any repository user class we need.
考虑到这一点,我们将创建一个存储库管理器类,它将为我们创建存储库用户类的实例,然后在依赖项注入容器中注册它们。之后,我们可以通过构造函数注入(由 ASP.NET Core 支持)将其注入我们的服务中。有了 repository manager 类,我们可以调用我们需要的任何 repository user 类。

But we are also missing one important part. We have the Create, Update, and Delete methods in the RepositoryBase class, but they won’t make any change in the database until we call the SaveChanges method. Our repository manager class will handle that as well.
但我们也缺少一个重要的部分。我们在 RepositoryBase 类中有 Create、Update 和 Delete 方法,但在调用 SaveChanges 方法之前,它们不会在数据库中进行任何更改。我们的仓库管理器类也会处理这个问题。

That said, let’s get to it and create a new interface in the Contract project:
也就是说,让我们开始在 Contract 项目中创建一个新界面:

// /Contract/IRepositoryManager.cs

namespace Contract
{
    public interface IRepositoryManager
    {
        ICompanyRepository Company { get; }
        IEmployeeRepository Employee { get; }
        void Save();
    }
}

And add a new class to the Repository project:
并将一个新类添加到 Repository 项目中:

// /Repository/RepositoryManager.cs

using Contract;

namespace Repository
{
    public sealed class RepositoryManager : IRepositoryManager
    {
        private readonly RepositoryContext _repositoryContext;
        private readonly Lazy<ICompanyRepository> _companyRepository;
        private readonly Lazy<IEmployeeRepository> _employeeRepository;
        public RepositoryManager(RepositoryContext repositoryContext)
        {
            _repositoryContext = repositoryContext;
            _companyRepository = new Lazy<ICompanyRepository>(() => new CompanyRepository(repositoryContext));
            _employeeRepository = new Lazy<IEmployeeRepository>(() => new EmployeeRepository(repositoryContext));
        }
        public ICompanyRepository Company => _companyRepository.Value;
        public IEmployeeRepository Employee => _employeeRepository.Value;
        public void Save() => _repositoryContext.SaveChanges();
    }
}

As you can see, we are creating properties that will expose the concrete repositories and also we have the Save() method to be used after all the modifications are finished on a certain object. This is a good practice because now we can, for example, add two companies, modify two employees, and delete one company — all in one action — and then just call the Save method once. All the changes will be applied or if something fails, all the changes will be reverted:
如您所见,我们正在创建将公开具体存储库的属性,并且我们还有 Save() 方法,可在完成对某个对象的所有修改后使用。这是一种很好的做法,因为现在我们可以添加两个公司,修改两个员工,删除一个公司 - 所有这些都在一个作中完成 - 然后只需调用 Save 方法一次。将应用所有更改,或者如果失败,则所有更改都将被还原:

_repository.Company.Create(company); 
_repository.Company.Create(anotherCompany); _repository.Employee.Update(employee); 
_repository.Employee.Update(anotherEmployee); _repository.Company.Delete(oldCompany); 
_repository.Save();

The interesting part with the RepositoryManager implementation is that we are leveraging the power of the Lazy class to ensure the lazy initialization of our repositories. This means that our repository instances are only going to be created when we access them for the first time, and not before that.
RepositoryManager 实现的有趣之处在于,我们利用 Lazy 类的强大功能来确保存储库的延迟初始化。这意味着我们的存储库实例只会在我们第一次访问它们时创建,而不是在此之前。

After these changes, we need to register our manager class in the main project. So, let’s first modify the ServiceExtensions class by adding this code:
完成这些更改后,我们需要在主项目中注册我们的 manager 类。因此,让我们首先通过添加以下代码来修改 ServiceExtensions 类:

// /CompanyEmployees.Extensions/ServiceExtensions.cs

using Contract;
using LoggerService;
using Repository;

namespace CompanyEmployees.Extensions
{
    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services) =>
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder =>
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader());
            });

        public static void ConfigureIISIntegration(this IServiceCollection services) =>
            services.Configure<IISOptions>(options =>
            {
            });

        public static void ConfigureLoggerService(this IServiceCollection services) => 
            services.AddSingleton<ILoggerManager, LoggerManager>();

        public static void ConfigureRepositoryManager(this IServiceCollection services) => 
            services.AddScoped<IRepositoryManager, RepositoryManager>();
    }
}

And in the Program class above the AddController() method, we have to add this code:

builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();
builder.Services.ConfigureRepositoryManager();

builder.Services.AddControllers();

Excellent.
非常好。

As soon as we add some methods to the specific repository classes, and add our service layer, we are going to be able to test this logic.
一旦我们将一些方法添加到特定的存储库类中,并添加我们的服务层,我们就可以测试这个逻辑。

So, we did an excellent job here. The repository layer is prepared and ready to be used to fetch data from the database.
所以,我们在这里做得很好。存储库层已准备就绪,可用于从数据库中获取数据。

Now, we can continue towards creating a service layer in our application.
现在,我们可以继续在我们的应用程序中创建一个服务层。

3.8 Adding a Service Layer

3.8 添加服务层

The Service layer sits right above the Domain layer (the Contracts project is the part of the Domain layer), which means that it has a reference to the Domain layer. The Service layer will be split into two‌ projects, Service.Contracts and Service.
Service 层位于 Domain 层的正上方(Contracts 项目是 Domain 层的一部分),这意味着它具有对 Domain 层的引用。Service 层将拆分为两个项目:Service.Contract 和 Service。

So, let’s start with the Service.Contracts project creation (.NET Core Class Library) where we will hold the definitions for the service interfaces that are going to encapsulate the main business logic. In the next section, we are going to create a presentation layer and then, we will see the full use of this project.
因此,让我们从 Service.Contracts 项目创建(.NET Core 类库)开始,我们将在其中保存将封装主业务逻辑的服务接口的定义。在下一节中,我们将创建一个表示层,然后,我们将看到此项目的完整使用。

Once the project is created, we are going to add three interfaces inside it.
创建项目后,我们将在其中添加三个接口。

ICompanyService:

// /Service.Contracts/ICompanyService.cs

namespace Service.Contracts
{
    public interface ICompanyService { }
}

IEmployeeService:

// /Service.Contracts/IEmployeeService.cs

namespace Service.Contracts
{
    public interface IEmployeeService { }
}

And IServiceManager:

// /Service.Contracts/IServiceManager.cs

namespace Service.Contracts
{
    public interface IServiceManager
    {
        ICompanyService CompanyService { get; }
        IEmployeeService EmployeeService { get; }
    }
}

As you can see, we are following the same pattern as with the repository contracts implementation.
如您所见,我们遵循与 repository contracts 实现相同的模式。

Now, we can create another project, name it Service, and reference the Service.Contracts and Contracts projects inside it:
现在,我们可以创建另一个项目,将其命名为 Service,并引用Service.Contracts 和 Contracts 项目:

alt text

After that, we are going to create classes that will inherit from the interfaces that reside in the Service.Contracts project.
之后,我们将创建将从驻留在 Service.Contracts 项目中的接口继承的类。

So, let’s start with the CompanyService class:
因此,让我们从 CompanyService 类开始:

// /Service/CompanyService.cs

using Contract;
using Service.Contracts;

namespace Service
{
    internal sealed class CompanyService : ICompanyService
    {
        private readonly IRepositoryManager _repository;
        private readonly ILoggerManager _logger;
        public CompanyService(IRepositoryManager repository, ILoggerManager logger)
        {
            _repository = repository; _logger = logger;
        }
    }
}

As you can see, our class inherits from the ICompanyService interface, and we are injecting the IRepositoryManager and ILoggerManager interfaces. We are going to use IRepositoryManager to access the repository methods from each user repository class (CompanyRepository or EmployeeRepository), and ILoggerManager to access the logging methods we’ve created in the second section of this book.
如您所见,我们的类继承自 ICompanyService 接口,并且我们正在注入 IRepositoryManager 和 ILoggerManager 接口。我们将使用 IRepositoryManager 访问每个用户存储库类(CompanyRepository 或 EmployeeRepository)中的存储库方法,并使用 ILoggerManager 访问我们在本书的第二部分中创建的日志记录方法。

To continue, let’s create a new EmployeeService class:
要继续,让我们创建一个新的 EmployeeService 类:

// /Service/EmployeeService.cs

using Contract;
using Service.Contracts;

namespace Service
{
    internal sealed class EmployeeService : IEmployeeService
    {
        private readonly IRepositoryManager _repository; 
        private readonly ILoggerManager _logger; 
        public EmployeeService(IRepositoryManager repository, ILoggerManager logger)
        {
            _repository = repository; _logger = logger;
        }
    }
}

Finally, we are going to create the ServiceManager class:
最后,我们将创建 ServiceManager 类:

// /Service/ServiceManager.cs

using Contract;
using Service.Contracts;
using Service;

public sealed class ServiceManager : IServiceManager
{
    private readonly Lazy<ICompanyService> _companyService; private readonly Lazy<IEmployeeService> _employeeService;
    public ServiceManager(IRepositoryManager repositoryManager, ILoggerManager logger)
    {
        _companyService = new Lazy<ICompanyService>(() => new CompanyService(repositoryManager, logger));
        _employeeService = new Lazy<IEmployeeService>(() => new EmployeeService(repositoryManager, logger));
    }
    public ICompanyService CompanyService => _companyService.Value;
    public IEmployeeService EmployeeService => _employeeService.Value;
}

Here, as we did with the RepositoryManager class, we are utilizing the Lazy class to ensure the lazy initialization of our services.
在这里,正如我们对 RepositoryManager 类所做的那样,我们利用 Lazy 类来确保服务的延迟初始化。

Now, with all these in place, we have to add the reference from the Service project inside the main project. Since Service is already referencing Service.Contracts, our main project will have the same reference as well.
现在,完成所有这些作后,我们必须在主项目中添加来自 Service 项目的引用。由于 Service 已经引用了 Service.Contracts,因此我们的主项目也将具有相同的引用。

Now, we have to modify the ServiceExtensions class:
现在,我们必须修改 ServiceExtensions 类:

using Contract;
using LoggerService;
using Repository;
using Service.Contracts;

namespace CompanyEmployees.Extensions
{
    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services) =>
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder =>
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader());
            });

        public static void ConfigureIISIntegration(this IServiceCollection services) =>
            services.Configure<IISOptions>(options =>
            {
            });

        public static void ConfigureLoggerService(this IServiceCollection services) => 
            services.AddSingleton<ILoggerManager, LoggerManager>();

        public static void ConfigureRepositoryManager(this IServiceCollection services) => 
            services.AddScoped<IRepositoryManager, RepositoryManager>();

        public static void ConfigureServiceManager(this IServiceCollection services) => 
            services.AddScoped<IServiceManager, ServiceManager>();
    }
}

And we have to add using directives:
我们必须添加 using 指令:

using Service; 
using Service.Contracts;

Then, all we have to do is to modify the Program class to call this extension method:
然后,我们所要做的就是修改 Program 类以调用此扩展方法:

builder.Services.ConfigureIISIntegration();
builder.Services.ConfigureRepositoryManager();
builder.Services.ConfigureServiceManager();

builder.Services.AddControllers();

3.9 Registering RepositoryContext at a Runtime

3.9 在运行时注册 RepositoryContext

With the RepositoryContextFactory class, which implements the IDesignTimeDbContextFactory interface, we have registered our RepositoryContext class at design time. This helps us find the RepositoryContext class in another project while executing migrations.‌
使用实现 IDesignTimeDbContextFactory 接口的 RepositoryContextFactory 类,我们在设计时注册了 RepositoryContext 类。这有助于我们在执行迁移时在另一个项目中找到 RepositoryContext 类。

But, as you could see, we have the RepositoryManager service registration, which happens at runtime, and during that registration, we must have RepositoryContext registered as well in the runtime, so we could inject it into other services (like RepositoryManager service). This might be a bit confusing, so let’s see what that means for us.
但是,正如你所看到的,我们有 RepositoryManager 服务注册,这发生在运行时,在注册期间,我们也必须在运行时注册 RepositoryContext,以便我们可以将其注入到其他服务(如 RepositoryManager 服务)中。这可能有点令人困惑,所以让我们看看这对我们意味着什么。

Let’s modify the ServiceExtensions class:
让我们修改 ServiceExtensions 类:

using Contract;
using LoggerService;
using Microsoft.EntityFrameworkCore;
using Repository;
using Service.Contracts;

namespace CompanyEmployees.Extensions
{
    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services) =>
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder =>
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader());
            });

        public static void ConfigureIISIntegration(this IServiceCollection services) =>
            services.Configure<IISOptions>(options =>
            {
            });

        public static void ConfigureLoggerService(this IServiceCollection services) => 
            services.AddSingleton<ILoggerManager, LoggerManager>();

        public static void ConfigureRepositoryManager(this IServiceCollection services) => 
            services.AddScoped<IRepositoryManager, RepositoryManager>();

        public static void ConfigureServiceManager(this IServiceCollection services) => 
            services.AddScoped<IServiceManager, ServiceManager>();

        public static void ConfigureSqlContext(this IServiceCollection services, IConfiguration configuration) => 
            services.AddDbContext<RepositoryContext>(opts => opts.UseSqlServer(configuration.GetConnectionString("sqlConnection")));
    }
}

We are not specifying the MigrationAssembly inside the UseSqlServer method. We don’t need it in this case.
我们没有在 UseSqlServer 方法中指定 MigrationAssembly。在这种情况下,我们不需要它。

As the final step, we have to call this method in the Program class:
作为最后一步,我们必须在 Program 类中调用此方法:

builder.Services.ConfigureRepositoryManager();
builder.Services.ConfigureServiceManager();
builder.Services.ConfigureSqlContext(builder.Configuration);

builder.Services.AddControllers();

With this, we have completed our implementation, and our service layer is ready to be used in our next chapter where we are going to learn about handling GET requests in ASP.NET Core Web API.
这样,我们已经完成了我们的实现,我们的服务层已准备好在下一章中使用,我们将在下一章中学习如何在 ASP.NET Core Web API 中处理 GET 请求。

One additional thing. From .NET 6 RC2, there is a shortcut method AddSqlServer, which can be used like this:
还有一件事。从 .NET 6 RC2 开始,有一个快捷方法 AddSqlServer,可以像这样使用:

public static void ConfigureSqlContext(this IServiceCollection services, IConfiguration configuration) => services.AddSqlServer<RepositoryContext>((configuration.GetConnectionString("sqlConnection")));

This method replaces both AddDbContext and UseSqlServer methods and allows an easier configuration. But it doesn’t provide all of the features the AddDbContext method provides. So for more advanced options, it is recommended to use AddDbContext. We will use it throughout the rest of the project.
此方法替换了 AddDbContext 和 UseSqlServer 方法,并允许更轻松地进行配置。但它不提供 AddDbContext 方法提供的所有功能。因此,对于更高级的选项,建议使用 AddDbContext。我们将在项目的其余部分使用它。

Ultimate ASP.NET Core Web API 2 Creating the Required Projects

2 Configuring a logging service

2 配置日志记录服务

Why do logging messages matter so much during application development? While our application is in the development stage, it's easy to debug the code and find out what happened. But debugging in a production environment is not that easy.‌
为什么在应用程序开发过程中,日志记录消息如此重要?虽然我们的应用程序处于开发阶段,但很容易调试代码并找出发生了什么。但是在生产环境中进行调试并不是那么容易。

That's why log messages are a great way to find out what went wrong and why and where the exceptions have been thrown in our code in the production environment. Logging also helps us more easily follow the flow of our application when we don’t have access to the debugger.
这就是为什么日志消息是找出问题所在以及生产环境中代码中引发异常的原因和位置的好方法。日志记录还有助于我们在无法访问调试器时更轻松地跟踪应用程序的流程。

.NET Core has its implementation of the logging mechanism, but in all our projects we prefer to create our custom logger service with the external logger library NLog.
.NET Core 具有日志记录机制的实现,但在我们所有的项目中,我们更喜欢使用外部记录器库 NLog 创建自定义记录器服务。

We are going to do that because having an abstraction will allow us to have any logger behind our interface. This means that we can start with NLog, and at some point, we can switch to any other logger and our interface will still work because of our abstraction.
我们之所以这样做,是因为有一个抽象将允许我们在接口后面拥有任何 Logger。这意味着我们可以从 NLog 开始,在某些时候,我们可以切换到任何其他 logger,并且由于我们的抽象,我们的接口仍然可以工作。

2.1 Creating the Required Projects

2.1 创建所需的项目

Let’s create two new projects. In the first one named Contracts, we are going to keep our interfaces. We will use this project later on too, to define our contracts for the whole application. The second one, LoggerService, we are going to use to write our logger logic in.‌
让我们创建两个新项目。在第一个名为 Contract,我们将保留我们的接口。我们稍后也将使用这个项目来定义整个应用程序的 Contract。第二个是 LoggerService,我们将使用它来编写我们的 Logger 逻辑。

To create a new project, right-click on the solution window, choose Add, and then NewProject. Choose the Class Library (C#) project template:
要创建新项目,请右键单击解决方案窗口,选择 Add(添加),然后选择 NewProject。选择 Class Library (C#) 项目模板:

alt text

Finally, name it Contracts, and choose the .NET 6.0 as a version. Do the same thing for the second project and name it LoggerService. Now that we have these projects in place, we need to reference them from our main project.
最后,将其命名为 Contracts,并选择 .NET 6.0 作为版本。对第二个项目执行相同的作,并将其命名为 LoggerService。现在我们已经准备好了这些项目,我们需要从主项目中引用它们。

To do that, navigate to the solution explorer. Then in the LoggerService project, right-click on Dependencies and choose the Add Project Reference option. Under Projects, click Solution and check the Contracts project.
为此,请导航到解决方案资源管理器。然后在 LoggerService 项目中,右键单击 Dependencies 并选择 Add Project Reference 选项。在 Projects 下,单击 Solution 并选中 Contracts 项目。

Now, in the main project right click on Dependencies and then click on Add Project Reference. Check the LoggerService checkbox to import it. Since we have referenced the Contracts project through the LoggerService, it will be available in the main project too.
现在,在主项目中,右键单击 Dependencies,然后单击 Add Project Reference。选中 LoggerService 复选框以导入它。由于我们已经通过 LoggerService 引用了 Contracts 项目,因此它也将在主项目中可用。

2.2 Creating the ILoggerManager Interface and Installing NLog

2.2 创建 ILoggerManager 接口并安装 NLog

Our logger service will contain four methods for logging our messages:‌
我们的 logger 服务将包含四种记录消息的方法:

• Info messages
• 信息消息

• Debug messages
• 调试消息

• Warning messages
• 警告消息

• Error messages
• 错误消息

To achieve this, we are going to create an interface named ILoggerManager inside the Contracts project containing those four method definitions.
为此,我们将在包含这四个方法定义的 Contracts 项目中创建一个名为 ILoggerManager 的接口。

So, let’s do that first by right-clicking on the Contracts project, choosing the Add -> New Item menu, and then selecting the Interface option where we have to specify the name ILoggerManager and click the Add button. After the file creation, we can add the code:
因此,让我们首先右键单击 Contracts 项目,选择 Add -> New Item 菜单,然后选择 Interface 选项,我们必须在其中指定名称 ILoggerManager 并单击 Add 按钮。创建文件后,我们可以添加代码:

namespace Contract
{
    public interface ILoggerManager
    {
            void LogInfo(string message); 
            void LogWarn(string message);
            void LogDebug(string message); 
            void LogError(string message); 
    }
}

Before we implement this interface inside the LoggerService project, we need to install the NLog library in our LoggerService project. NLog is a logging platform for .NET which will help us create and log our messages.
在 LoggerService 项目中实现此接口之前,我们需要在 LoggerService 项目中安装 NLog 库。NLog 是一个 .NET 的日志记录平台,它将帮助我们创建和记录我们的消息。

We are going to show two different ways of adding the NLog library to our project.
我们将展示将 NLog 库添加到项目中的两种不同方法。

  1. In the LoggerService project, right-click on the Dependencies and choose Manage NuGet Packages. After the NuGet Package Manager window appears, just follow these steps:
    在 LoggerService 项目中,右键单击 Dependencies 并选择 Manage NuGet Packages。出现 NuGet 包管理器窗口后,只需按照以下步骤作:

alt text

  1. From the View menu, choose Other Windows and then click on the Package Manager Console. After the console appears, type:
    从 View (视图) 菜单中,选择 Other Windows (其他窗口),然后单击 Package Manager 控制台。控制台出现后,键入:
Install-Package NLog.Extensions.Logging -Version 1.7.4

After a couple of seconds, NLog is up and running in our application.
几秒钟后,NLog 在我们的应用程序中启动并运行。

2.3 Implementing the Interface and Nlog.Config File

2.3 实现接口和 Nlog.Config 文件

In the LoggerService project, we are going to create a new‌ class: LoggerManager. We can do that by repeating the same steps for the interface creation just choosing the class option instead of an interface. Now let’s have it implement the ILoggerManager interface we previously defined:
在 LoggerService 项目中,我们将创建一个新类:LoggerManager。为此,只需选择 class 选项而不是 interface,即可对界面创建重复相同的步骤。现在让我们让它实现我们之前定义的 ILoggerManager 接口:

using Contract;
using NLog;

namespace LoggerService
{
    public class LoggerManager : ILoggerManager
    {
        private static ILogger logger = LogManager.GetCurrentClassLogger();
        public LoggerManager() { }
        public void LogDebug(string message) => logger.Debug(message);
        public void LogError(string message) => logger.Error(message);
        public void LogInfo(string message) => logger.Info(message);
        public void LogWarn(string message) => logger.Warn(message);
    }
}

As you can see, our methods are just wrappers around NLog’s methods. Both ILogger and LogManager are part of the NLog namespace. Now, we need to configure it and inject it into the Program class in the section related to the service configuration.
如你所见,我们的方法只是 NLog 方法的包装器。ILogger 和 LogManager 都是 NLog 命名空间的一部分。现在,我们需要配置它并将其注入到与服务配置相关的部分的 Program 类中。

NLog needs to have information about where to put log files on the file system, what the name of these files will be, and what is the minimum level of logging that we want.
NLog 需要了解将日志文件放在文件系统上的哪个位置、这些文件的名称以及我们所需的最低日志记录级别的信息。

We are going to define all these constants in a text file in the main project and name it nlog.config. So, let’s right-click on the main project, choose Add -> New Item, and then search for the Text File. Select the Text File, and add the name nlog.config.
我们将在主项目的文本文件中定义所有这些常量,并将其命名为 nlog.config。因此,让我们右键单击主项目,选择 Add -> New Item,然后搜索 Text File。选择文本文件,并添加名称 nlog.config。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      autoReload="true" 
      internalLogLevel="Trace" 
      internalLogFile=".\internal_logs\internallog.txt">
    <targets>
        <target name="logfile" 
                xsi:type="File" 
                fileName=".\logs\${shortdate}_logfile.txt" 
                layout="${longdate} ${level:uppercase=true} ${message}"/>
    </targets>
    <rules>
        <logger name="*" minlevel="Debug" writeTo="logfile" />
    </rules>
</nlog>

You can find the internal logs at the project root, and the logs folder in the bin\debug folder of the main project once we start the app. Once the application is published both folders will be created at the root of the output folder which is what we want.
您可以在项目根目录下找到内部日志,并在启动应用程序后在主项目的 bin\debug 文件夹中找到 logs 文件夹。应用程序发布后,将在输出文件夹的根目录下创建两个文件夹,这就是我们想要的。

NOTE: If you want to have more control over the log output, we suggest renaming the current file to nlog.development.config and creating another configuration file called nlog.production.config. Then you can do something like this in the code: env.ConfigureNLog($"nlog.{env.EnvironmentName}.config"); to get the different configuration files for different environments. From our experience production path is what matters, so this might be a bit redundant.
注意:如果您想对日志输出进行更多控制,我们建议将当前文件重命名为 nlog.development.config,并创建另一个名为 nlog.production.config 的配置文件。然后,您可以在代码中执行如下作:env.ConfigureNLog($"nlog.{env.EnvironmentName}.config");以获取不同环境的不同配置文件。根据我们的经验,生产路径才是最重要的,所以这可能有点多余。

2.4 Configuring Logger Service for Logging Messages

2.4 配置 Logger 服务以记录消息

Setting up the configuration for a logger service is quite easy. First, we need to update the Program class and include the path to the configuration file for the NLog configuration:‌
为 Logger 服务设置配置非常简单。首先,我们需要更新 Program 类并包含 NLog 配置的配置文件路径:

using CompanyEmployees.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
using NLog;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

// 这个方法在net8的nlog5.4中提示已过时
LogManager.LoadConfiguration(
    string.Concat(Directory.GetCurrentDirectory(), 
    "/nlog.config"));
builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();

We are using NLog’s LogManager static class with the LoadConfiguration method to provide a path to the configuration file.
我们将 NLog 的 LogManager 静态类与 LoadConfiguration 方法结合使用,以提供配置文件的路径。

NOTE: If VisualStudio asks you to install the NLog package in the main project, don’t do it. Just remove the LoggerService reference from the main project and add it again. We have already installed the required package in the LoggerService project and the main project should be able to reference it as well.
注意:如果 VisualStudio 要求您在主项目中安装 NLog 包,请不要这样做。只需从主项目中删除 LoggerService 引用,然后再次添加即可。我们已经在 LoggerService 项目中安装了所需的包,主项目也应该能够引用它。

The next thing we need to do is to add the logger service inside the .NET Core’s IOC container. There are three ways to do that:
接下来我们需要做的是在 .NET Core 的 IOC 容器中添加记录器服务。有三种方法可以做到这一点:

• By calling the services.AddSingleton method, we can create a service the first time we request it and then every subsequent request will call the same instance of the service. This means that all components share the same service every time they need it and the same instance will be used for every method call.
• 通过调用services.AddSingleton 方法,我们可以在第一次请求服务时创建一个服务,然后每个后续请求都将调用该服务的同一实例。这意味着所有组件每次需要时都共享相同的服务,并且每个方法调用都将使用相同的实例。

• By calling the services.AddScoped method, we can create a service once per request. That means whenever we send an HTTP request to the application, a new instance of the service will be created.
• 通过调用services.AddScoped 方法,我们可以为每个请求创建一次服务。这意味着每当我们向应用程序发送 HTTP 请求时,都会创建一个新的服务实例。

• By calling the services.AddTransient method, we can create a service each time the application requests it. This means that if multiple components need the service, it will be created again for every single component request.
• 通过调用services.AddTransient方法,我们可以在每次应用程序请求时创建一个服务。这意味着,如果多个组件需要该服务,将为每个组件请求再次创建该服务。

So, let’s add a new method in the ServiceExtensions class:
因此,让我们在 ServiceExtensions 类中添加一个新方法:

using Contract;
using LoggerService;

namespace CompanyEmployees.Extensions
{
    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services) =>
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder =>
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader());
            });

        public static void ConfigureIISIntegration(this IServiceCollection services) =>
            services.Configure<IISOptions>(options =>
            {
            });

        public static void ConfigureLoggerService(this IServiceCollection services) => 
            services.AddSingleton<ILoggerManager, LoggerManager>();
    }
}

And after that, we need to modify the Program class to include our newly created extension method:
之后,我们需要修改 Program 类以包含我们新创建的扩展方法:

builder.Services.AddControllers();
builder.Services.ConfigureLoggerService();
builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();

Every time we want to use a logger service, all we need to do is to inject it into the constructor of the class that needs it. .NET Core will resolve that service and the logging features will be available.
每次我们想使用 Logger 服务时,我们需要做的就是将其注入到需要它的类的构造函数中。.NET Core 将解析该服务,并且日志记录功能将可用。

This type of injecting a class is called Dependency Injection and it is built into .NET Core.
这种类型的注入类称为依赖项注入,它内置于 .NET Core 中。

Let’s learn a bit more about it.
让我们更多地了解它。

2.5 DI, IoC, and Logger Service Testing

2.5 DI、IoC 和 Logger 服务测试

What is Dependency Injection (DI) exactly and what is IoC (Inversion of Control)?‌
究竟什么是依赖注入 (DI),什么是 IoC(控制反转)?

Dependency injection is a technique we use to achieve the decoupling of objects and their dependencies. It means that rather than instantiating an object explicitly in a class every time we need it, we can instantiate it once and then send it to the class.
依赖注入是我们用来实现对象及其依赖的解耦的一种技术。这意味着,与其每次需要时都在类中显式实例化对象,不如实例化它一次,然后将其发送到类。

This is often done through a constructor. The specific approach we utilize is also known as the Constructor Injection.
这通常是通过构造函数完成的。我们使用的特定方法也称为 Constructor Injection。

In a system that is designed around DI, you may find many classes requesting their dependencies via their constructors. In this case, it is helpful to have a class that manages and provides dependencies to classes through the constructor.
在围绕 DI 设计的系统中,您可能会发现许多类通过其构造函数请求其依赖项。在这种情况下,拥有一个通过构造函数管理类并提供类依赖项的类会很有帮助。

These classes are referred to as containers or more specifically, Inversion of Control containers. An IoC container is essentially a factory that is responsible for providing instances of the types that are requested from it.
这些类称为容器,或者更具体地说,称为控制反转容器。IoC 容器本质上是一个工厂,负责提供从它请求的类型的实例。

To test our logger service, we are going to use the default WeatherForecastController. You can find it in the main project in the Controllers folder. It comes with the ASP.NET Core Web API template.
为了测试我们的 logger 服务,我们将使用默认的 WeatherForecastController。您可以在主项目的 Controllers 文件夹中找到它。它附带 ASP.NET Core Web API 模板。

In the Solution Explorer, we are going to open the Controllers folder and locate the WeatherForecastController class. Let’s modify it:
在解决方案资源管理器中,我们将打开 Controllers 文件夹并找到 WeatherForecastController 类。让我们修改一下:

using Contract;
using Microsoft.AspNetCore.Mvc;

[Route("[controller]")]
[ApiController]
public class WeatherForecastController : ControllerBase
{
    private ILoggerManager _logger;
    public WeatherForecastController(ILoggerManager logger) { _logger = logger; }
    [HttpGet]
    public IEnumerable<string> Get()
    {
        _logger.LogInfo("Here is info message from our values controller."); 
        _logger.LogDebug("Here is debug message from our values controller."); 
        _logger.LogWarn("Here is warn message from our values controller."); 
        _logger.LogError("Here is an error message from our values controller.");
        return new string[] { "value1", "value2" };
    }
}

Now let’s start the application and browse to :
现在,让我们启动应用程序并浏览到:
https://localhost:5001/weatherforecast.

As a result, you will see an array of two strings. Now go to the folder that you have specified in the nlog.config file, and check out the result. You should see two folders: the internal_logs folder and the logs folder. Inside the logs folder, you should find a file with the following logs:
结果,您将看到一个包含两个字符串的数组。现在转到您在 nlog.config 文件中指定的文件夹,并查看结果。您应该看到两个文件夹:internal_logs 文件夹和 logs 文件夹。在 logs 文件夹中,您应该会找到一个包含以下日志的文件:

alt text

That’s all we need to do to configure our logger for now. We’ll add some messages to our code along with the new features.
这就是我们现在需要做的配置 logger 的全部工作。我们将向代码中添加一些消息以及新功能。

Ultimate ASP.NET Core Web API 1 PROJECT CONFIGURATION

Ultimate ASP.NET Core Web API 2nd Premium Edition

Ultimate ASP.NET Core Web API 1 PROJECT CONFIGURATION
1 项目配置

Ultimate ASP.NET Core Web API 2 Creating the Required Projects
2 创建所需的项目

Ultimate ASP.NET Core Web API 3 ONION ARCHITECTURE IMPLEMENTATION
3 洋葱架构实现

Ultimate ASP.NET Core Web API 4 HANDLING GET REQUESTS
4 处理 GET 请求

Ultimate ASP.NET Core Web API 5 GLOBAL ERROR HANDLING
5 全局错误处理

Ultimate ASP.NET Core Web API 6 GETTING ADDITIONAL RESOURCES
6 获取资源

Ultimate ASP.NET Core Web API 7 CONTENT NEGOTIATION
7 内容协商

Ultimate ASP.NET Core Web API 8 METHOD SAFETY AND METHOD IDEMPOTENCY
8 方法安全和方法幂等性

Ultimate ASP.NET Core Web API 9 CREATING RESOURCES
9 创建资源

Ultimate ASP.NET Core Web API 10 WORKING WITH DELETE REQUESTS
10 使用 DELETE 请求

Ultimate ASP.NET Core Web API 11 WORKING WITH PUT REQUESTS
11 使用 PUT 请求

Ultimate ASP.NET Core Web API 12 WORKING WITH PATCH REQUESTS
12 使用PATCH请求

Ultimate ASP.NET Core Web API 13 VALIDATION
13 验证

Ultimate ASP.NET Core Web API 14 ASYNCHRONOUS CODE
14 异步代码

Ultimate ASP.NET Core Web API 15 ACTION FILTERS
15 动作过滤器

Ultimate ASP.NET Core Web API 16 PAGING
16 分页

Ultimate ASP.NET Core Web API 17 FILTERING
17 过滤

Ultimate ASP.NET Core Web API 18 SEARCHING
18 搜索

Ultimate ASP.NET Core Web API 19 SORTING
19 排序

Ultimate ASP.NET Core Web API 20 DATA SHAPING
20 数据整形

Ultimate ASP.NET Core Web API 21 SUPPORTING HATEOAS
21 支持 HATEOAS

Ultimate ASP.NET Core Web API 22 WORKING WITH OPTIONS AND HEAD REQUESTS
22 使用 OPTIONS 和 HEAD 请求

Ultimate ASP.NET Core Web API 23 ROOT DOCUMENT
23 根文档

Ultimate ASP.NET Core Web API 24 VERSIONING APIS
24 API版本控制

Ultimate ASP.NET Core Web API 25 CACHING
25 缓存

Ultimate ASP.NET Core Web API 26 RATE LIMITING AND THROTTLING
26 速率限制

Ultimate ASP.NET Core Web API 27 JWT, IDENTITY, AND REFRESH TOKEN
27 个 JWT、身份和刷新令牌

Ultimate ASP.NET Core Web API 28 REFRESH TOKEN
28 刷新令牌

Ultimate ASP.NET Core Web API 29 BINDING CONFIGURATION AND OPTIONS PATTERN
29 绑定配置和选项模式

Ultimate ASP.NET Core Web API 30 DOCUMENTING API WITH SWAGGER
30 使用 SWAGGER 编写 API 文档

Ultimate ASP.NET Core Web API 31 DEPLOYMENT TO IIS
31 部署到 IIS

Ultimate ASP.NET Core Web API 32 BONUS 1 - RESPONSE PERFORMANCE IMPROVEMENTS
32 赠送章节 1 - 响应性能改进

Ultimate ASP.NET Core Web API 33 BONUS 2 - INTRODUCTION TO CQRS AND MEDIATR WITH ASP.NET CORE WEB API
33 赠送章节 2 - 使用 ASP.NET 核心 WEB API 的 CQRS 和 MEDIATR 简介

1 Project configuration

1 项目配置

Configuration in .NET Core is very different from what we’re used to in‌ .NET Framework projects. We don’t use the web.config file anymore, but instead, use a built-in Configuration framework that comes out of the box in .NET Core.
配置.NET Core 与我们习惯的 .NET Framework 不同,不再使用 web.config 文件,而是使用开箱即用的内置配置框架。

To be able to develop good applications, we need to understand how to configure our application and its services first.
为了能够开发好的应用程序,需要首先了解如何配置应用程序及其服务。

In this section, we’ll learn about configuration in the Program class and set up our application. We will also learn how to register different services and how to use extension methods to achieve this.
在本节中,将了解 Program 类中的配置并设置应用程序。还将学习注册不同的服务,以及使用扩展方法来实现这一点。

Of course, the first thing we need to do is to create a new project, so,let’s dive right into it.
当然,需要做的第一件事是创建一个新项目,所以,直接开始吧。

1.1 Creating a New Project

1.1 创建新项目

Let's open Visual Studio, we are going to use VS 2022, and create a new ASP.NET Core Web API Application:‌
打开Visual Studio,我们将使用 VS 2022,创建一个新的 ASP.NET Core Web API 应用程序:

alt text

Now let’s choose a name and location for our project:
给项目选择一个名称和位置:

alt text

Next, we want to choose a .NET 6.0 from the dropdown list. Also, we don’t want to enable OpenAPI support right now. We’ll do that later in the book on our own. Now we can proceed by clicking the Create button and the project will start initializing:
接下来,我们要从下拉列表中选择 .NET 6.0。此外,我们现在不想启用 OpenAPI 支持。我们将在本书的后面自己做这件事。现在,我们可以通过单击 Create 按钮继续,项目将开始初始化:

alt text

1.2 launchSettings.json File Configuration

1.2 launchSettings.json 文件配置

After the project has been created, we are going to modify the launchSettings.json file, which can be found in the Properties section of the Solution Explorer window.‌
创建项目后,我们将修改 launchSettings.json 文件,该文件可以在 Solution Explorer 窗口的 Properties 部分找到。

This configuration determines the launch behavior of the ASP.NET Core applications. As we can see, it contains both configurations to launch settings for IIS and self-hosted applications (Kestrel).
此配置决定了 ASP.NET Core 应用程序的启动行为。正如我们所看到的,它包含用于启动 IIS 设置的配置和自托管应用程序 (Kestrel)。

For now, let’s change the launchBrowser property to false to prevent the web browser from launching on application start.
现在,让我们将 launchBrowser 属性更改为 false,以防止 Web 浏览器在应用程序启动时启动。

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:1629",
      "sslPort": 44370
    }
  },
  "profiles": {
    "CompanyEmployees": {
        "commandName": "Project",
        "dotnetRunMessages": true,
        "launchBrowser": false,
        "launchUrl": "weatherforecast",
        "applicationUrl": "https://localhost:5001;http://localhost:5000",
        "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
        }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "weatherforecast",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

This is convenient since we are developing a Web API project and we don’t need a browser to check our API out. We will use Postman (described later) for this purpose.
这很方便,因为我们正在开发一个 Web API 项目,我们不需要浏览器来检查我们的 API。为此,我们将使用 Postman(稍后介绍)。

If you’ve checked Configure for HTTPS checkbox earlier in the setup phase, you will end up with two URLs in the applicationUrl section — one for HTTPS (localhost:5001), and one for HTTP (localhost:5000).
如果您在创建项目时选中了 Configure for HTTPS 复选框,则 applicationUrl 部分最终会有两个 URL——一个用于 HTTPS (localhost:5001),一个用于 HTTP (localhost:5000)。

You’ll also notice the sslPort property which indicates that our application, when running in IISExpress, will be configured for HTTPS (port 44370), too.
您还会注意到 sslPort 属性,该属性指示我们的应用程序在 IISExpress 中运行时,也将配置为 HTTPS(端口 44370)。

NOTE: This HTTPS configuration is only valid in the local environment. You will have to configure a valid certificate and HTTPS redirection once you deploy the application.
注意:此 HTTPS 配置仅在本地环境中有效。部署应用程序后,您必须配置有效的证书和 HTTPS 重定向。

There is one more useful property for developing applications locally and that’s the launchUrl property. This property determines which URL will the application navigate to initially. For launchUrl property to work, we need to set the launchBrowser property to true. So, for example, if we set the launchUrl property to weatherforecast, we will be redirected to https://localhost:5001/weatherforecast when we launch our application.
还有一个用于在本地开发应用程序的有用属性,即 launchUrl 属性。此属性确定应用程序最初将导航到哪个 URL。要使 launchUrl 属性正常工作,我们需要将 launchBrowser 属性设置为 true。因此,例如,如果我们将 launchUrl 属性设置为 weatherforecast,则当我们启动应用程序时,我们将重定向到 https://localhost:5001/weatherforecast

1.3 Program.cs Class Explanations

1.3 Program.cs 类说明

Program.cs is the entry point to our application and it looks like this:‌
Program.cs 是我们应用程序的入口点,它看起来像这样:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Compared to the Program.cs class from .NET 5, there are some major changes. Some of the most obvious are:
与 .NET 5 中的 Program.cs 类相比,有一些重大变化。一些最明显的是:

• Top-level statements
• 顶级语句

• Implicit using directives
• 隐式 using 指令

• No Startup class (on the project level)
• 无 Startup 类(在项目级别)

“Top-level statements” means the compiler generates the namespace, class, and method elements for the main program in our application. We can see that we don’t have the class block in the code nor the Main method. All of that is generated for us by the compiler. Of course, we can add other functions to the Program class and those will be created as the local functions nested inside the generated Main method. Top-level statements are meant to simplify the entry point to the application and remove the extra “fluff” so we can focus on the important stuff instead.
“顶级语句”是指编译器为应用程序中的主程序生成命名空间、类和方法元素。我们可以看到,代码中没有 class 块,也没有 Main 方法。所有这些都是由编译器为我们生成的。当然,我们可以向 Program 类添加其他函数,这些函数将创建为嵌套在生成的 Main 方法中的本地函数。顶级语句旨在简化应用程序的入口点并删除额外的 “绒毛”,以便我们可以专注于重要的东西。

“Implicit using directives” mean the compiler automatically adds a different set of using directives based on a project type, so we don’t have to do that manually. These using directives are stored in the obj/Debug/net6.0 folder of our project under the name CompanyEmployees.GlobalUsings.g.cs:
“隐式 using 指令”意味着编译器会根据项目类型自动添加一组不同的 using 指令,因此我们不必手动执行此作。这些 using 指令存储在项目的 obj/Debug/net6.0 文件夹中,名称为 CompanyEmployees.GlobalUsings.g.cs:

// <auto-generated/>
global using global::Microsoft.AspNetCore.Builder;
global using global::Microsoft.AspNetCore.Hosting;
global using global::Microsoft.AspNetCore.Http;
global using global::Microsoft.AspNetCore.Routing;
global using global::Microsoft.Extensions.Configuration;
global using global::Microsoft.Extensions.DependencyInjection;
global using global::Microsoft.Extensions.Hosting;
global using global::Microsoft.Extensions.Logging;
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Net.Http.Json;
global using global::System.Threading;
global using global::System.Threading.Tasks;

This means that we can use different classes from these namespaces in our project without adding using directives explicitly in our project files. Of course, if you don’t want this type of behavior, you can turn it off by visiting the project file and disabling the ImplicitUsings tag:
这意味着我们可以在项目中使用来自这些命名空间的不同类,而无需在项目文件中显式添加 using 指令。当然,如果您不希望出现此类行为,可以通过访问项目文件并禁用 ImplicitUsings 标签来关闭它:

<ImplicitUsings>disable</ImplicitUsings>

By default, this is enabled in the .csproj file, and we are going to keep it like that.
默认情况下,这在 .csproj 文件中处于启用状态,我们将保持这种状态(不要去修改它)。

Now, let’s take a look at the code inside the Program class. With this line of code:
现在,让我们看看 Program 类中的代码。使用这行代码:

var builder = WebApplication.CreateBuilder(args);

The application creates a builder variable of the type WebApplicationBuilder. The WebApplicationBuilder class is responsible for four main things:
应用程序将创建一个 WebApplicationBuilder 类型的 builder 变量。WebApplicationBuilder 类负责四个主要任务:

• Adding Configuration to the project by using the builder.Configuration property
• 使用builder.Configuration构建器将配置添加到项目中

• Registering services in our app with the builder.Services property
• 使用builder.Services构建器在我们的应用程序中注册服务

• Logging configuration with the builder.Logging property
• 使用builder.Logging生成器进行日志记录配置

• Other IHostBuilder and IWebHostBuilder configuration
• 其他 IHostBuilder 和 IWebHostBuilder 配置

Compared to .NET 5 where we had a static CreateDefaultBuilder class, which returned the IHostBuilder type, now we have the static CreateBuilder method, which returns WebApplicationBuilder type.
与 .NET 5 相比,我们有一个静态 CreateDefaultBuilder 类,它返回 IHostBuilder 类型,现在我们有静态 CreateBuilder 方法,它返回 WebApplicationBuilder 类型。

Of course, as we see it, we don’t have the Startup class with two familiar methods: ConfigureServices and Configure. Now, all this is replaced by the code inside the Program.cs file.
当然,正如我们所看到的,我们没有包含两个熟悉方法的 Startup 类:ConfigureServices 和 Configure。现在,所有这些都被 Program.cs 文件中的代码替换了。

Since we don’t have the ConfigureServices method to configure our services, we can do that right below the builder variable declaration. In the new template, there’s even a comment section suggesting where we should start with service registration. A service is a reusable part of the code that adds some functionality to our application, but we’ll talk about services more later on.
由于我们没有 ConfigureServices 方法来配置我们的服务,因此我们可以在 builder 变量声明的正下方执行此作。在新模板中,甚至还有一个注释部分,建议我们应该从哪里开始服务注册。服务是代码的可重用部分,它为应用程序添加了一些功能,但我们稍后将更多地讨论服务。

In .NET 5, we would use the Configure method to add different middleware components to the application’s request pipeline. But since we don’t have that method anymore, we can use the section below the var app = builder.Build(); part to do that. Again, this is marked with the comment section as well:
在 .NET 5 中,我们将使用 Configure 方法将不同的中间件组件添加到应用程序的请求管道中。但是由于我们不再有该方法,因此我们可以使用 var app = builder.Build(); 部分来做到这一点。同样,这也用评论部分标记:

alt text

NOTE: If you still want to create your application using the .NET 5 way, with Program and Startup classes, you can do that, .NET 6 supports it as well. The easiest way is to create a .NET 5 project, copy the Startup and Program classes and paste it into the .NET 6 project.
注意:如果您仍希望使用 .NET 5 方式创建应用程序,使用 Program 和 Startup 类,您可以这样做,.NET 6 也支持它。最简单的方法是创建一个 .NET 5 项目,复制 Startup 和 Program 类并将其粘贴到 .NET 6 项目中。

Since larger applications could potentially contain a lot of different services, we can end up with a lot of clutter and unreadable code in the Program class. To make it more readable for the next person and ourselves, we can structure the code into logical blocks and separate those blocks into extension methods.
由于较大的应用程序可能包含许多不同的服务,因此我们最终会在 Program 类中得到很多杂乱和不可读的代码。为了让下一个人和我们自己更具可读性,我们可以将代码构建成逻辑块,并将这些块分离到扩展方法中。

1.4 Extension Methods and CORS Configuration

1.4 扩展方法和 CORS 配置

An extension method is inherently a static method. What makes it different from other static methods is that it accepts this as the first parameter, and this represents the data type of the object which will be using that extension method. We’ll see what that means in a moment.‌
扩展方法本质上是一种静态方法。它与其他静态方法的不同之处在于,它接受 this 作为第一个参数,这表示将使用该扩展方法的对象的数据类型。我们稍后会看看这意味着什么。

An extension method must be defined inside a static class. This kind of method extends the behavior of a type in .NET. Once we define an extension method, it can be chained multiple times on the same type of object.
必须在 static 类中定义扩展方法。此方法扩展了 .NET 中类型的行为。一旦我们定义了一个扩展方法,它就可以在同一类型的对象上被多次链接。

So, let’s start writing some code to see how it all adds up.
那么,让我们开始编写一些代码,看看这一切是如何加起来的。

We are going to create a new folder Extensions in the project and create a new class inside that folder named ServiceExtensions. The ServiceExtensions class should be static.
我们将在项目中创建一个新文件夹 Extensions,并在该文件夹中创建一个名为 ServiceExtensions 的新类。ServiceExtensions 类应该是 static 类。

namespace CompanyEmployees.Extensions
{
    public static class ServiceExtensions
    {
    }
}

Let’s start by implementing something we need for our project immediately so we can see how extensions work.
让我们立即开始实现项目所需的内容,以便了解扩展的工作原理。

The first thing we are going to do is to configure CORS in our application. CORS (Cross-Origin Resource Sharing) is a mechanism to give or restrict access rights to applications from different domains.
我们要做的第一件事是在我们的应用程序中配置 CORS。CORS(跨域资源共享)是一种机制,用于授予或限制来自不同域的应用程序的访问权限。

If we want to send requests from a different domain to our application, configuring CORS is mandatory. So, to start, we’ll add a code that allows all requests from all origins to be sent to our API:
如果我们想将请求从其他域发送到我们的应用程序,则必须配置 CORS。因此,首先,我们将添加一个代码,允许来自所有来源的所有请求都发送到我们的 API:

namespace CompanyEmployees.Extensions
{
    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services) =>
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder =>
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader());
            });
    }
}

We are using basic CORS policy settings because allowing any origin, method, and header is okay for now. But we should be more restrictive with those settings in the production environment. More precisely, as restrictive as possible.
我们使用的是基本的 CORS 策略设置,因为允许任何源、方法和标头目前是可以的。但是我们应该在生产环境中对这些设置进行更多限制。更准确地说,尽可能严格。

Instead of the AllowAnyOrigin() method which allows requests from any source, we can use the WithOrigins("https://example.com") which will allow requests only from that concrete source. Also, instead of AllowAnyMethod() that allows all HTTP methods, we can use WithMethods("POST", "GET") that will allow only specific HTTP methods. Furthermore, you can make the same changes for the AllowAnyHeader() method by using, for example, the WithHeaders("accept", "content-type") method to allow only specific headers.
我们可以使用 WithOrigins("https://example.com") 方法,而不是允许来自任何源的请求的 AllowAnyOrigin() 方法,它将仅允许来自该具体源的请求。此外,我们可以使用 WithMethods("POST", "GET") 来只允许特定的 HTTP 方法,而不是允许所有 HTTP 方法的 AllowAnyMethod()。此外,您可以对 AllowAnyHeader() 方法进行相同的更改,例如,使用 WithHeaders("accept", "content-type")方法仅允许特定标头。

1.5 IIS Configuration

1.5 IIS 配置

ASP.NET Core applications are by default self-hosted, and if we want to host our application on IIS, we need to configure an IIS integration which will eventually help us with the deployment to IIS. To do that, we need to add the following code to the ServiceExtensions class:‌
默认情况下,ASP.NET Core 应用程序是自托管的,如果我们想在 IIS 上托管应用程序,则需要配置 IIS 集成,这最终将帮助我们部署到 IIS。为此,我们需要将以下代码添加到 ServiceExtensions 类中:

namespace CompanyEmployees.Extensions
{
    public static class ServiceExtensions
    {
        public static void ConfigureCors(this IServiceCollection services) =>
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder =>
                builder.AllowAnyOrigin()
                       .AllowAnyMethod()
                       .AllowAnyHeader());
            });

        public static void ConfigureIISIntegration(this IServiceCollection services) =>
            services.Configure<IISOptions>(options =>
            {

            });
    }
}

We do not initialize any of the properties inside the options because we are fine with the default values for now. But if you need to fine-tune the configuration right away, you might want to take a look at the possible options:
我们没有初始化选项中的任何属性,因为我们现在对默认值没有问题。但是,如果您需要立即微调配置,则可能需要查看可能的选项:

Option Default Setting
AutomaticAuthentication true if true,the authentication middleware sets the HttpContext.User and responds to generic challenges,if false,the authentication middleware only provides an identity(HttpContext.User ) and responds to challenges when explicitly requested by the AuthenticationScheme . Windows Authentication must be enabled in IIS for AutomaticAuthentication to function.
如果为 true,则身份验证中间件设置 HttpContext.User 并响应一般质询,如果为 false,则身份验证中间件仅提供 identity(HttpContext.User ),并在 AuthenticationScheme 显式请求时响应质询。必须在 IIS 中启用 Windows 身份验证,AutomaticAuthentication 才能正常工作。
AuthenticationDisplayName null Sets the display name shown to users on login pages
设置在登录页面上向用户显示的显示名称
ForwardClientCertificate true if true and the MS-ASPNETCORE-CLIENTCERT request header is present,the HttpContext.Connection.ClientCertificate is populated.
如果 true 且存在 MS-ASPNETCORE-CLIENTCERT 请求标头,则填充 HttpContext.Connection.ClientCertificate。

Now, we mentioned extension methods are great for organizing your code and extending functionalities. Let’s go back to our Program class and modify it to support CORS and IIS integration now that we’ve written extension methods for those functionalities. We are going to remove the first comment and write our code over it:
现在,我们提到了扩展方法非常适合组织代码和扩展功能。让我们回到我们的 Program 类并对其进行修改以支持 CORS 和 IIS 集成,因为我们已经为这些功能编写了扩展方法。我们将删除第一个注释并在其上编写我们的代码:

using CompanyEmployees.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

And let's add a few mandatory methods to the second part of the Program class (the one for the request pipeline configuration):
让我们向 Program 类的第二部分(用于请求管道配置的那个)添加一些强制性方法:

using CompanyEmployees.Extensions;
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.All
});

app.UseCors("CorsPolicy");

app.UseAuthorization();

app.MapControllers();

app.Run();

We’ve added CORS and IIS configuration to the section where we need to configure our services. Furthermore, CORS configuration has been added to the application’s pipeline inside the second part of the Program class.But as you can see, there are some additional methods unrelated to IIS configuration. Let’s go through those and learn what they do.
我们已将 CORS 和 IIS 配置添加到需要配置服务的部分。此外,CORS 配置已添加到 Program 类的第二部分内的应用程序管道中。但正如你所看到的,还有一些与 IIS 配置无关的其他方法。让我们来了解一下这些并了解它们的作用。

• app.UseForwardedHeaders() will forward proxy headers to the current request. This will help us during application deployment. Pay attention that we require Microsoft.AspNetCore.HttpOverrides using directive to introduce the ForwardedHeaders enumeration
•app.UseForwardedHeaders()会将代理标头转发到当前请求。这将在应用程序部署期间帮助我们。请注意,我们需要 Microsoft.AspNetCore.HttpOverrides using 指令来引入 ForwardedHeaders 枚举

• app.UseStaticFiles() enables using static files for the request. If we don’t set a path to the static files directory, it will use a wwwroot folder in our project by default.
•app.UseStaticFiles() 允许对请求使用静态文件。如果我们没有设置静态文件目录的路径,它将默认使用我们项目中的 wwwroot 文件夹。

• app.UseHsts() will add middleware for using HSTS, which adds the Strict-Transport-Security header.
• app.UseHsts() 将添加用于使用 HSTS 的中间件,从而添加 Strict-Transport-Security 标头。

1.6 Additional Code in the Program Class

1.6 Program 类中的其他代码

We have to pay attention to the AddControllers() method. This method registers only the controllers in IServiceCollection and not Views or Pages because they are not required in the Web API project which we are building.‌
我们必须注意 AddControllers() 方法。此方法仅在 IServiceCollection 中注册控制器,而不在 Views 或 Pages 中注册控制器,因为我们正在构建的 Web API 项目中不需要它们。

Right below the controller registration, we have this line of code:
在控制器注册的正下方,我们有这行代码:

var app = builder.Build();

With the Build method, we are creating the app variable of the type WebApplication. This class (WebApplication) is very important since it implements multiple interfaces like IHost that we can use to start and stop the host, IApplicationBuilder that we use to build the middleware pipeline (as you could’ve seen from our previous custom code), and IEndpointRouteBuilder used to add endpoints in our app.
使用 Build 方法,我们将创建 WebApplication 类型的 app 变量。这个类 (WebApplication) 非常重要,因为它实现了多个接口,例如我们可用于启动和停止主机的 IHost、用于构建中间件管道的 IApplicationBuilder(正如您从之前的自定义代码中所看到的那样),以及用于在应用程序中添加端点的 IEndpointRouteBuilder。

The UseHttpRedirection method is used to add the middleware for the redirection from HTTP to HTTPS. Also, we can see the UseAuthorization method that adds the authorization middleware to the specified IApplicationBuilder to enable authorization capabilities.
UseHttpRedirection 方法用于添加中间件,以便从 HTTP 重定向到 HTTPS。此外,我们还可以看到 UseAuthorization 方法,该方法将授权中间件添加到指定的 IApplicationBuilder 以启用授权功能。

Finally, we can see the MapControllers method that adds the endpoints from controller actions to the IEndpointRouteBuilder and the Run method that runs the application and block the calling thread until the host shutdown.
最后,我们可以看到 MapControllers 方法,该方法将控制器中的终结点添加到 IEndpointRouteBuilder,以及 Run 方法,该方法运行应用程序并阻止调用线程,直到主机关闭。

Microsoft advises that the order of adding different middlewares to the application builder is very important, and we are going to talk about that in the middleware section of this book.
Microsoft 建议,向应用程序构建器添加不同中间件的顺序非常重要,我们将在本书的中间件部分讨论这一点。

1.7 Environment-Based Settings

1.7 基于环境的设置

While we develop our application, we use the “development” environment. But as soon as we publish our application, it goes to the “production” environment. Development and production environments should have different URLs, ports, connection strings, passwords, and other sensitive information.‌
当我们开发应用程序时,我们使用 “development” 环境。但是,一旦我们发布应用程序,它就会进入 “production” 环境(生产环境)。开发和生产环境应具有不同的 URL、端口、连接字符串、密码和其他敏感信息。

Therefore, we need to have a separate configuration for each environment and that’s easy to accomplish by using .NET Core-provided mechanisms.
因此,我们需要为每个环境提供单独的配置,这可以通过使用 .NET Core 提供的机制轻松实现。

As soon as we create a project, we are going to see the appsettings.json file in the root, which is our main settings file, and when we expand it we are going to see the appsetings.Development.json file by default. These files are separate on the file system, but Visual Studio makes it obvious that they are connected somehow:
一旦我们创建了一个项目,我们将在根目录中看到 appsettings.json 文件,这是我们的主要设置文件,当我们展开它时,我们将看到 appsetings。Development.json 文件。这些文件在文件系统上是独立的,但 Visual Studio 清楚地表明它们以某种方式连接在一起:

alt text

The apsettings.{EnvironmentSuffix}.json files are used to override the main appsettings.json file. When we use a key-value pair from the original file, we override it. We can also define environment-specific values too.
apsettings.{EnvironmentSuffix}.json 文件用于覆盖主 appsettings.json 文件。当我们使用原始文件中的键值对时,我们会覆盖它。我们也可以定义特定于环境的值。

For the production environment, we should add another file: appsettings.Production.json:
对于生产环境,我们应该添加另一个文件:appsettings.Production.json:

alt text

The appsettings.Production.json file should contain the configuration for the production environment.
appsettings.Production.json文件应包含生产环境的配置。

To set which environment our application runs on, we need to set up the ASPNETCORE_ENVIRONMENT environment variable. For example, to run the application in production, we need to set it to the Production value on the machine we do the deployment to.
要设置应用程序在哪个环境上运行,我们需要设置 ASPNETCORE_ENVIRONMENT 环境变量。例如,要在生产环境中运行应用程序,我们需要在执行部署的机器上将其设置为 Production 值。

We can set the variable through the command prompt by typing set ASPNETCORE_ENVIRONMENT=Production in Windows or export ASPNET_CORE_ENVIRONMENT=Production in Linux.
我们可以通过命令提示符设置变量,方法是在 Windows 中键入 set ASPNETCORE_ENVIRONMENT=Production 或在 Linux 中键入 export ASPNET_CORE_ENVIRONMENT=Production。

ASP.NET Core applications use the value of that environment variable to decide which appsettings file to use accordingly. In this case, that will be appsettings.Production.json.
ASP.NET Core 应用程序使用该环境变量的值来决定相应地使用哪个 appsettings 文件。在本例中,这将是 appsettings.Production.json。

If we take a look at our launchSettings.json file, we are going to see that this variable is currently set to Development.
如果我们查看 launchSettings.json 文件,我们将看到此变量当前设置为 Development。

Now, let’s talk a bit more about the middleware in ASP.NET Core applications.
现在,让我们更多地讨论一下 ASP.NET Core 应用程序中的中间件。

1.8 ASP.NET Core Middleware

1.8 ASP.NET Core 中间件

As we already used some middleware code to modify the application’s pipeline (CORS, Authorization...), and we are going to use the middleware throughout the rest of the book, we should be more familiar with the ASP.NET Core middleware.‌
由于我们已经使用了一些中间件代码来修改应用程序的管道(CORS、Authorization...),并且我们将在本书的其余部分使用中间件,因此我们应该更熟悉 ASP.NET Core 中间件。

ASP.NET Core middleware is a piece of code integrated inside the application’s pipeline that we can use to handle requests and responses. When we talk about the ASP.NET Core middleware, we can think of it as a code section that executes with every request.
ASP.NET Core 中间件是一段集成在应用程序管道中的代码,我们可以使用它来处理请求和响应。当我们谈论 ASP.NET Core 中间件时,我们可以将其视为随每个请求一起执行的代码部分。

Usually, we have more than a single middleware component in our application. Each component can:
通常,我们的应用程序中有多个中间件组件。每个组件都可以:

• Pass the request to the next middleware component in the pipeline and also
• 将请求传递给管道中的下一个中间件组件,以及

• It can execute some work before and after the next component in the pipeline
• 它可以在管道中的下一个元件之前和之后执行一些工作

To build a pipeline, we are using request delegates, which handle each HTTP request. To configure request delegates, we use the Run, Map, and Use extension methods. Inside the request pipeline, an application executes each component in the same order they are placed in the code- top to bottom:
为了构建管道,我们使用请求委托来处理每个 HTTP 请求。要配置请求委托,我们使用 Run、Map 和 Use 扩展方法。在请求管道中,应用程序按照它们在代码中的放置顺序(从上到下)执行每个组件:

alt text

Additionally, we can see that each component can execute custom logic before using the next delegate to pass the execution to another component. The last middleware component doesn’t call the next delegate, which means that this component is short-circuiting the pipeline. This is a terminal middleware because it stops further middleware from processing the request. It executes the additional logic and then returns the execution to the previous middleware components.
此外,我们可以看到,在使用 next()将执行传递给另一个组件之前,每个组件都可以执行自定义 logic。最后一个中间件组件不调用下一个委托,这意味着该组件正在使管道短路。这是一个终端中间件,因为它会阻止进一步的中间件处理请求。它执行额外的 logic,然后将执行返回给前面的中间件组件。

Before we start with examples, it is quite important to know about the order in which we should register our middleware components. The order is important for the security, performance, and functionality of our applications:
在我们开始示例之前,了解我们应该注册中间件组件的顺序非常重要。该顺序对于我们应用程序的安全性、性能和功能非常重要:

alt text

As we can see, we should register the exception handler in the early stage of the pipeline flow so it could catch all the exceptions that can happen in the later stages of the pipeline. When we create a new ASP.NET Core app, many of the middleware components are already registered in the order from the diagram. We have to pay attention when registering additional existing components or the custom ones to fit this recommendation.
正如我们所看到的,我们应该在管道流的早期阶段注册异常处理程序,以便它可以捕获管道后期可能发生的所有异常。当我们创建新的 ASP.NET Core 应用程序时,许多中间件组件已经按照图中的顺序注册了。在注册其他现有组件或自定义组件以适应此建议时,我们必须注意。

For example, when adding CORS to the pipeline, the app in the development environment will work just fine if you don’t add it in this order. But we’ve received several questions from our readers stating that they face the CORS problem once they deploy the app. But once we suggested moving the CORS registration to the required place, the problem disappeared.
例如,在将 CORS 添加到管道时,如果不按此顺序添加,开发环境中的应用程序将正常工作。但是我们收到了读者的几个问题,他们指出,一旦部署了应用程序,他们就会面临 CORS 问题。但是,一旦我们建议将 CORS 注册移动到所需位置,问题就消失了。

Now, we can use some examples to see how we can manipulate the application’s pipeline. For this section’s purpose, we are going to create a separate application that will be dedicated only to this section of the book. The later sections will continue from the previous project, that we’ve already created.
现在,我们可以使用一些示例来了解如何作应用程序的管道。出于本节的目的,我们将创建一个单独的应用程序,该应用程序将专门用于本书的这一部分。后面的部分将从我们已经创建的上一个项目继续。

1.8.1 Creating a First Middleware Component‌

1.8.1 创建第一个 Middleware 组件

Let’s start by creating a new ASP.NET Core Web API project, and name it MiddlewareExample.
让我们首先创建一个新的 ASP.NET Core Web API 项目,并将其命名为 MiddlewareExample。

In the launchSettings.json file, we are going to add some changes regarding the launch profiles:
在 launchSettings.json 文件中,我们将添加一些有关启动配置文件的更改:

{
    "profiles": {
        "MiddlewareExample": {
            "commandName": "Project",
            "dotnetRunMessages": true,
            "launchBrowser": true,
            "launchUrl": "weatherforecast",
            "applicationUrl": "https://localhost:5001;http://localhost:5000",
            "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }
        }
    }
}

Now, inside the Program class, right below the UseAuthorization part, we are going to use an anonymous method to create a first middleware component:
现在,在 Program 类中,在 UseAuthorization 部分的正下方,我们将使用匿名方法创建第一个中间件组件:

app.UseAuthorization();
app.Run(async context => { await context.Response.WriteAsync("Hello from the middleware component."); });
app.MapControllers();

We use the Run method, which adds a terminal component to the app pipeline. We can see we are not using the next delegate because the Run method is always terminal and terminates the pipeline. This method accepts a single parameter of the RequestDelegate type. If we inspect this delegate we are going to see that it accepts a single HttpContext parameter:
我们使用 Run 方法,该方法将终端组件添加到应用程序管道中。我们可以看到我们没有使用 next() 委托,因为 Run 方法始终是 terminal 并终止管道。此方法接受 RequestDelegate 类型的单个参数。如果我们检查这个委托,我们将看到它接受一个 HttpContext 参数:

namespace Microsoft.AspNetCore.Http { 
    public delegate Task RequestDelegate(HttpContext context); 
}

So, we are using that context parameter to modify our requests and responses inside the middleware component. In this specific example, we are modifying the response by using the WriteAsync method. For this method, we need Microsoft.AspNetCore.Http namespace.
因此,我们使用该 context 参数来修改中间件组件内的请求和响应。在此特定示例中,我们将使用 WriteAsync 方法修改响应。对于此方法,我们需要 Microsoft.AspNetCore.Http 命名空间。

Let’s start the app, and inspect the result:
让我们启动应用程序,并检查结果:

alt text

There we go. We can see a result from our middleware.
好了。我们可以看到中间件的结果。

1.8.2 Working with the Use Method‌

1.8.2 使用 Use 方法

To chain multiple request delegates in our code, we can use the Use method. This method accepts a Func delegate as a parameter and returns a Task as a result:
要在代码中链接多个请求委托,我们可以使用 Use 方法。此方法接受 Func 委托作为参数,并返回 Task 作为结果:

public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware);

So, this means when we use it, we can make use of two parameters, context and next:
所以,这意味着当我们使用它时,我们可以使用两个参数,context 和 next:

app.UseAuthorization();
app.Use(async (context, next) =>
{
    Console.WriteLine($"Logic before executing the next delegate in the Use method");
    await next.Invoke();
    Console.WriteLine($"Logic after executing the next delegate in the Use method");
});
app.Run(async context =>
{
    Console.WriteLine($"Writing the response to the client in the Run method");
    await context.Response.WriteAsync("Hello from the middleware component.");
});
app.MapControllers();

As you can see, we add several logging messages to be sure what the order of executions inside middleware components is. First, we write to a console window, then we invoke the next delegate passing the execution to another component in the pipeline. In the Run method, we write a second message to the console window and write a response to the client. After that, the execution is returned to the Use method and we write the third message (the one below the next delegate invocation) to the console window.
如你所见,我们添加了几个日志记录消息,以确保中间件组件内的执行顺序是什么。首先,我们写入控制台窗口,然后调用下一个委托,将执行传递给管道中的另一个组件。在 Run 方法中,我们将第二条消息写入控制台窗口,并将响应写入客户端。之后,执行将返回到 Use 方法,我们将第三条消息(下一个委托调用下面的消息)写入控制台窗口。

The Run method doesn’t accept the next delegate as a parameter, so without it to send the execution to another component, this component short-circuits the request pipeline.
Run 方法不接受下一个委托作为参数,因此如果没有它将执行发送到另一个组件,此组件会使请求管道短路。

Now, let’s start the app and inspect the result, which proves our execution order:
现在,让我们启动应用程序并检查结果,它证明了我们的执行顺序:

alt text

Maybe you will see two sets of messages but don’t worry, that’s because the browser sends two sets of requests, one for the /weatherforecast and another for the favicon.ico. If you, for example, use Postman to test this, you will see only one set of messages.
也许你会看到两组消息,但不要担心,那是因为浏览器发送了两组请求,一组用于 /weatherforecast,另一组用于 favicon.ico。例如,如果您使用 Postman 对此进行测试,您将只看到一组消息。

One more thing to mention. We shouldn’t call the next.Invoke after we send the response to the client. This can cause exceptions if we try to set the status code or modify the headers of the response.
还有一件事要提。我们不应该调用下一个。在将响应发送到客户端后调用。如果我们尝试设置状态代码或修改响应的标头,这可能会导致异常。

For example:
例如:

app.Use(async (context, next) =>
{
    await context.Response.WriteAsync("Hello from the middleware component."); 
    await next.Invoke();
    Console.WriteLine($"Logic after executing the next delegate in the Use method");
});
app.Run(async context =>
{
    Console.WriteLine($"Writing the response to the client in the Run method");
    context.Response.StatusCode = 200; 
    await context.Response.WriteAsync("Hello from the middleware component.");
});

Here we write a response to the client and then call next.Invoke. Of course, this passes the execution to the next component in the pipeline. There, we try to set the status code of the response and write another one. But let’s inspect the result:
在这里,我们向客户端写入响应,然后调用 next。调用。当然,这会将执行传递给管道中的下一个组件。在那里,我们尝试设置响应的状态代码并编写另一个状态代码。但让我们检查一下结果:

alt text

We can see the error message, which is pretty self-explanatory.
我们可以看到错误消息,这是不言自明的。

1.8.3 Using the Map and MapWhen Methods‌

1.8.3 使用 Map 和 MapWhen 方法

To branch the middleware pipeline, we can use both Map and MapWhen methods. The Map method is an extension method that accepts a path string as one of the parameters:
要对中间件管道进行分支,我们可以同时使用 Map 和 MapWhen 方法。该方法 Map 是一种扩展方法,它接受路径字符串作为参数之一:

public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action<IApplicationBuilder> configuration)

When we provide the pathMatch string, the Map method will compare it to the start of the request path. If they match, the app will execute the branch.
当我们提供 pathMatch 字符串时,Map 方法会将其与请求路径的开头进行比较。如果它们匹配,应用程序将执行分支。

So, let’s see how we can use this method by modifying the Program class:
那么,让我们看看如何通过修改 Program 类来使用此方法:

app.Use(async (context, next) =>
{
    Console.WriteLine($"Logic before executing the next delegate in the Use method");
    await next.Invoke();
    Console.WriteLine($"Logic after executing the next delegate in the Use method");
}); 
app.Map("/usingmapbranch", builder =>
{
    builder.Use(async (context, next) =>
    {
        Console.WriteLine("Map branch logic in the Use method before the next delegate"); 
        await next.Invoke();
        Console.WriteLine("Map branch logic in the Use method after the next delegate");
    });
    builder.Run(async context =>
    {
        Console.WriteLine($"Map branch response to the client in the Run method");
        await context.Response.WriteAsync("Hello from the map branch.");
    });
});
app.Run(async context =>
{
    Console.WriteLine($"Writing the response to the client in the Run method");
    await context.Response.WriteAsync("Hello from the middleware component.");
});

By using the Map method, we provide the path match, and then in the delegate, we use our well-known Use and Run methods to execute middleware components.
通过使用 Map 方法,我们提供路径匹配,然后在委托中,我们使用我们著名的 Use 和 Run 方法来执行中间件组件。

Now, if we start the app and navigate to /usingmapbranch, we are going to see the response in the browser:
现在,如果我们启动应用程序并导航到 /usingmapbranch,我们将在浏览器中看到响应:

alt text

But also, if we inspect console logs, we are going to see our new messages:
但是,如果我们检查控制台日志,我们将看到我们的新消息:

alt text

Here, we can see the messages from the Use method before the branch, and the messages from the Use and Run methods inside the Map branch. We are not seeing any message from the Run method outside the branch. It is important to know that any middleware component that we add after the Map method in the pipeline won’t be executed. This is true even if we don’t use the Run middleware inside the branch.
在这里,我们可以看到分支之前来自 Use 方法的消息,以及来自 Map 分支内的 Use 和 Run 方法的消息。我们没有看到来自分支外部的 Run 方法的任何消息。请务必知道,我们在管道中的 Map 方法之后添加的任何中间件组件都不会被执行。即使我们不在分支中使用 Run 中间件,也是如此。

1.8.4 Using MapWhen Method‌

1.8.4 使用 MapWhen 方法

If we inspect the MapWhen method, we are going to see that it accepts two parameters:
如果我们检查 MapWhen 方法,我们将看到它接受两个参数:

public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Func<HttpContext, bool> predicate, Action<IApplicationBuilder> configuration)

This method uses the result of the given predicate to branch the request pipeline.
此方法使用给定谓词的结果对请求管道进行分支。

So, let’s see it in action:
那么,让我们看看它的实际效果:

app.Map("/usingmapbranch", builder =>
{
    ...
});
app.MapWhen(context => context.Request.Query.ContainsKey("testquerystring"), builder =>
{
    builder.Run(async context =>
    {
        await context.Response.WriteAsync("Hello from the MapWhen branch.");
    });
});
app.Run(async context =>
{
    ...
});

Here, if our request contains the provided query string, we execute the Run method by writing the response to the client. So, as we said, based on the predicate’s result the MapWhen method branch the request pipeline.
在这里,如果我们的请求包含提供的查询字符串,我们将通过将响应写入客户端来执行 Run 方法。因此,正如我们所说,根据谓词的结果,MapWhen 方法对请求管道进行分支。

Now, we can start the app and navigate to:
现在,我们可以启动应用程序并导航到:
https://localhost:5001?testquerystring=test

alt text

And there we go. We can see our expected message. Of course, we can chain multiple middleware components inside this method as well.
好了。我们可以看到预期的消息。当然,我们也可以在此方法中链接多个中间件组件。

So, now we have a good understanding of using middleware and its order of invocation in the ASP.NET Core application. This knowledge is going to be very useful to us once we start working on a custom error handling middleware (a few sections later).
因此,现在我们已经很好地了解了中间件的使用及其在 ASP.NET Core 应用程序中的调用顺序。一旦我们开始开发自定义错误处理中间件,这些知识将对我们非常有用(稍后将介绍几节)。

In the next chapter, we’ll learn how to configure a Logger service because it’s really important to have it configured as early in the project as possible. We can close this app, and continue with the CompanyEmployees app.
在下一章中,我们将学习如何配置 Logger 服务,因为在项目的早期配置它非常重要。我们可以关闭此应用程序,并继续使用 CompanyEmployees 应用程序。

C# 扩展方法

C# 扩展方法

简介

在 C# 中,扩展方法是一种特殊的静态方法,可以像实例方法一样调用,但实际上是静态的。这些方法可以扩展现有类型的功能,而无需修改类型的定义。

定义扩展方法的步骤

  1. 静态类:扩展方法必须定义在一个静态类中。
  2. 静态方法:扩展方法本身必须是静态的。
  3. this 参数:扩展方法的第一个参数前加上 this 关键字,指定要扩展的类型。

示例

扩展字符串类型

扩展 string 类型,添加一个方法 ToReverse 来返回字符串的反转。

public static class StringExtensions
{
    public static string ToReverse(this string str)
    {
        if (string.IsNullOrEmpty(str))
            return str;

        char[] charArray = str.ToCharArray();
        Array.Reverse(charArray);
        return new string(charArray);
    }
}

// 使用扩展方法
class Program
{
    static void Main()
    {
        string text = "hello";
        string reversed = text.ToReverse(); // 调用扩展方法
        Console.WriteLine(reversed); // 输出:olleh
    }
}

扩展集合类型

扩展 IEnumerable,添加一个方法 ToFormattedString,将集合转换为逗号分隔的字符串。

public static class CollectionExtensions
{
    public static string ToFormattedString<T>(this IEnumerable<T> collection)
    {
        return string.Join(", ", collection);
    }
}

// 使用扩展方法
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5 };
        string result = numbers.ToFormattedString(); // 调用扩展方法
        Console.WriteLine(result); // 输出:1, 2, 3, 4, 5
    }
}

常用场景

  1. 扩展框架类型:为系统类型(如 string, int, DateTime)添加新方法。
  2. 简化代码:提供更简洁的调用方式。
  3. 增强 LINQ 功能:许多 LINQ 方法(如 Where, Select)本质上是扩展方法。
  4. 代码复用:为通用操作创建工具方法,提高可读性和复用性。

注意事项

  1. 优先级:如果实例方法和扩展方法同名,实例方法会优先调用。
  2. 命名空间:使用扩展方法时,必须引入包含扩展方法的命名空间。
  3. 泛型支持:扩展方法可以是泛型的,用于更加通用的操作。

扩展方法的局限性

• 无法重写现有方法。
• 无法添加新属性。
• 不能改变类型的行为,只能提供额外的方法。

扩展方法的优势

扩展方法的优势在于代码的可读性、可维护性、和开发体验,虽然其本质与调用静态类中的静态方法类似,但扩展方法带来了显著的便利和代码语义上的改善。

  1. 更直观的调用方式

扩展方法可以像实例方法一样调用,符合自然语言习惯,提升代码的可读性。

// 静态方法调用:
var reversed = StringHelper.Reverse("hello");
// 扩展方法调用:
var reversed = "hello".ToReverse();
  1. 更符合面向对象的设计理念

扩展方法的调用方式让外部操作看起来像是类的实例行为,符合面向对象编程的习惯。这样无需修改类的定义即可扩展其功能。

简化链式调用

扩展方法支持链式调用,简化了操作步骤,让代码更加紧凑。

// 使用扩展方法:
var result = "hello".ToReverse().ToUpper();

// 使用静态方法:
var reversed = StringHelper.Reverse("hello");
var result = StringHelper.ToUpper(reversed);

与 LINQ 紧密集成

许多 LINQ 方法(如 Where, Select, OrderBy)都是扩展方法,调用方式非常流畅。

// 扩展方法:
var result = numbers.Where(x => x > 10).OrderBy(x => x).ToList();

// 静态方法:
var filtered = LinqHelper.Where(numbers, x => x > 10);
var ordered = LinqHelper.OrderBy(filtered, x => x);
var result = LinqHelper.ToList(ordered);
  1. 提高开发体验

扩展方法在调用时支持智能提示,能直接显示可用的扩展方法,让开发更高效。
• 静态方法: 开发者需要记住静态类名称。
• 扩展方法: 开发者只需聚焦于对象,IDE 自动提示可用的方法。

C#中的Lambda表达式

什么是Lambda?

在C#中,一个Lambda表达式就是一个匿名函数。

Lambda的语法结构如下:

(Input Params) => Expression

• 中间的"=>" 是Lambda的操作符,一般读作"goes to"

• 左边的部分"Input Params"是Lambda表达式的输入参数,当且仅当只有一个参数的时候,括号可以忽略,其他情况哪怕参数个数是0个,也不能忽略.

• 右边的部分"Expression"是语句(Expression)或者代码块(Statement),当且仅当只有一行代码时,大括号可以忽略,其他情况均不可忽略,参考以下样例:

参考以下样例:

() => Console.WriteLine("No Params")  // 0 个参数,左边的圆括号不能省略
x => x * x   // 1个参数,左边的圆括号可加可不加
(x, y) => x - y     // 2个参数,左边的圆括号不能省略
(x, y) => { x += y; Console.WriteLine(x); }   // 大括号不能省略

阿隆佐·邱奇(Alonzo Church),美国数学家和逻辑学家,对计算机科学领域做出了重要贡献。邱奇在20世纪30年代提出了lambda演算,这是一种形式化的计算理论,用于研究函数的定义、应用和等价性。他在提出lambda演算时,定义了一种匿名函数的表达方式,即lambda表达式。这种表达方式允许函数没有名字,直接通过参数和表达式来描述,因此得名lambda表达式。邱奇的这一贡献为后来的函数式编程语言奠定了基础。

ASP.NET Core Razor Pages in Action 11 客户端技术和 AJAX

ASP.NET Core Razor Pages in Action 11 客户端技术和 AJAX

本章涵盖

• 选择您的客户端技术
• 从客户端代码调用页面处理程序
• 在 Razor Pages 中使用 JSON
• 基于每个页面隔离 CSS 样式

我们使用的所有代码示例都依赖于在服务器上完整生成的页面的 HTML,除了一个领域:客户端验证。验证错误消息是使用客户端库生成的,特别是 Microsoft 的 jQuery Unobtrusive Validation 库。作为一项功能,客户端验证增强了网站的可用性。它为用户提供有关表单验证错误的即时反馈,而无需等待将整个表单提交到服务器进行处理。

如果您想让用户满意,可用性至关重要,而使用客户端技术的服务器端应用程序的可用性可以显著提高。在本章中,我们将介绍如何在 Razor Pages 应用程序中实现一些使用客户端技术的常见模式。由于它作为默认项目模板的一部分包含在内,因此我们将查看 jQuery 的 DOM作和发出由浏览器发起的异步 HTTP 请求。我们还将探索使用纯 JavaScript 的替代方案,并考虑 jQuery 的未来。

异步 HTTP 请求(通常称为 AJAX)使您能够从服务器获取数据,而无需整页回发,因此用户看不到这些数据。您可以对这些数据执行任何作。您可以使用它来执行计算或更新页面的某些部分。或者,您可以将数据作为文件下载提供。数据本身可以有多种形式。它可以是 HTML,也可以是 XML 结构,或者更常见的是 JSON。在本章中,您将了解如何使用页面处理程序方法从 AJAX 请求生成 HTML 和 JSON 数据,并了解此方法的局限性。您还将了解如何将数据成功发布到页面处理程序。

如果您想广泛使用 JSON,建议的方法是将 Web API 控制器构建到您的应用程序中,这为 RESTful HTTP 服务提供了基础。在本书中,我不介绍 API 控制器,但我们将探索 .NET 6 中引入的新简化的请求处理功能(基于最小的 API),该功能以相当少的仪式提供类似的收益。我们还将介绍 .NET 6 中的另一项新功能,该功能使您能够将 CSS 样式隔离到单个页面的范围,而无需增加 HTTP 请求的数量。在介绍一些代码示例之前,我们将讨论如何进行客户端开发。

11.1 选择客户端技术

毫无疑问,jQuery 库是有史以来使用最多的 JavaScript 库。它于 2006 年推出,很快成为作 DOM、处理事件、管理 CSS 转换和执行异步 HTTP 请求 (AJAX) 的实际方式。当 jQuery 启动时,这些 API 的实现在不同浏览器中差异很大。jQuery 充当适配器,提供一组在所有支持的浏览器中以相同方式工作的 API。

许多其他客户端库都依赖于 jQuery,包括领先的 UI 框架库 Bootstrap,它捆绑到默认的 ASP.NET Core Web 应用程序模板中。从 .NET 6 开始的新模板中包含的最新版本的 Bootstrap(版本 5)消除了对 jQuery 的依赖。如今,大多数浏览器都比以前更严格地遵守标准。他们中的许多人共享相同的底层技术。jQuery 旨在解决的问题已基本消失。

尽管如此,jQuery 仍包含在默认的 Razor Pages 应用程序模板中,以提供对客户端验证的支持。从长远来看,这种情况很可能会改变,因为 GitHub 上有一个未解决的问题,它讨论了客户端验证的潜在替代,以便它不再依赖于 jQuery (https://github.com/dotnet/aspnetcore/issues/8573)。尽管如此,jQuery 仍然被广泛使用,因此在本章中,我们将研究使用 jQuery 和纯 JavaScript 实现客户端解决方案。

存在其他客户端库。其中使用最广泛的可能是 React 和 Angular。Angular 更准确地称为应用程序框架,但两者都主要用于构建单页应用程序 (SPA),其中工作流逻辑在浏览器中执行,而不是在服务器上执行。它们可以用作 Razor Pages 应用程序的一部分,但如果您只想添加客户端功能的隔离实例,那么它们就有点矫枉过正了。

11.2 从 JavaScript 调用页面处理程序

本节重点介绍如何从客户端代码调用命名页面处理程序方法。我们将介绍如何使用部分页面返回可用于更新页面部分的 HTML 片段。然后,我们将探索如何将标准表单发布到页面处理程序和独立数据。执行此作时,我们将特别注意请求验证,请记住,默认情况下,它已融入 Razor Pages 框架中。最后,我们将介绍在与客户端代码中的页面处理程序交互时如何使用 JSON。

11.2.1 使用部分页面返回 HTML

在第一个示例中,您将了解如何将 HTML 代码片段异步加载到页面中。具体来说,当用户单击列表中的属性名称时,您会将属性的详细信息加载到 Bootstrap 模式中。在本练习中,您将使用部分页面。一个部分将包含模态框的大纲 HTML,即 head 和 foot,它们将包含在现有的 City 页面中。当用户单击 city 页面上的属性列表时,将从服务器加载模态主体。您将添加客户端代码,用于标识单击了哪个属性,然后向返回 PartialResult 的页面处理程序发出请求,如果您还记得第 3 章,则表示对部分页面的处理。它非常适合生成 HTML 片段,例如可能用于更新页面区域的片段。

您将创建要开始的模态框的轮廓。将新的 cshtml 文件添加到 Pages\Shared 文件夹。请记住,没有用于分部视图的模板,因此,如果您使用的是 Visual Studio,则可以选择 Razor View > Empty 模板,然后删除默认内容。将部分文件命名为 _PropertyModalPartial.cshtml,并将任何现有内容替换为以下代码。

列表 11.1 基于 Bootstrap 5 模态的 PropertyModalPartial 内容

<div class="modal fade" id="property-modal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Property Details</h5>
        <button type="button" class="btn-close" 
         ➥ data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">

      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" 
         ➥ data-bs-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

这段代码或多或少直接摘自 Bootstrap 5 的模态框文档。它是一个标准模态,其正文中没有内容。您已更改标题以使其与您的应用程序更相关,并添加了 id 属性,以便您可以从客户端代码中识别模式。您还向模态框添加了 fade 类,因此它在出现和消失时会发出动画效果。

现在,您需要为模态框提供一些内容。这将放置在另一个名为 _PropertyDetailsPartial.cshtml 的部件中,该部件也位于 Pages\Shared 文件夹中。它的内容很简单。将 Property 作为 Razor 文件的模型传入,并按如下方式呈现属性的详细信息。

清单 11.2 包含将要加载的属性详细信息的 partial

@model Property 
<h3>@Model.Name</h3>
<address>
    @Model.Address
</address>
<p>Availability: @(Model.AvailableFrom < DateTime.Now ? 
➥ "Immediate" : Model.AvailableFrom.ToShortDateString())</p>
<p>Day Rate: @Model.DayRate.ToString("c")</p>
<p>Max Guests: @Model.MaxNumberOfGuests</p>
<p>Smoking permitted? @(Model.SmokingPermitted ? "Yes" : "No")</p>

接下来,您需要一个使用此部分生成 HTML 的页面处理程序。将以下处理程序方法添加到现有 OnGetAsync 方法之后的 City.cshtml.cs 文件中。

清单 11.3 将 HTML 生成为 PartialViewResult 的命名处理程序

public async Task<PartialViewResult> OnGetPropertyDetails(int id)
{
    var property = await _propertyService.FindAsync(id);
    return Partial("_PropertyDetailsPartial", property);
}

这是一个命名的处理程序方法。它采用 int 作为参数,表示所选属性的标识。它使用属性服务从数据库中获取属性详细信息,然后将其传递给部分文件,返回呈现的结果。您需要将属性服务注入到 PageModel 的构造函数中。

清单 11.4 将属性服务注入 CityModel 构造函数

private readonly ICityService _cityService;
private readonly IPropertyService _propertyService;

public CityModel(ICityService cityService, IPropertyService propertyService)
{
    _cityService = cityService;
    _propertyService = propertyService;
}

最后的步骤涉及对 City.cshtml 文件的一些更改。迭代所选城市中的属性集合,并在此文件中呈现其详细信息。属性的名称显示在 h5 标题中,该标题分配了 role=“button”,因此当用户将鼠标悬停在标题上时,光标将变为指针。您需要修改标题以添加一些数据属性。一个将帮助您识别特定属性,其他用于触发模态。在下面的清单中,我用几行中断了 h5 元素的结果声明,因此更容易注释代码。

清单 11.5 修改后的 h5 元素旨在触发模态框

<h5 role="button"
data-id="@property.Id"                ❶
data-bs-toggle="modal"                ❷
data-bs-target="#property-modal"      ❸
>@property.Name</h5>

❶ data-id 属性具有分配给它的属性 Id 。您将检索此值并将其传递给您刚刚添加的命名页面处理程序。
❷ 这个 Bootstrap 自定义属性使元素成为模态框的触发器。
❸ 此属性采用目标模态框的 ID。

最后,在页面中包含模态部分(清单 11.6),并使用 scripts 部分添加一个 JavaScript 块。客户端代码使用 jQuery 向所有 h5 元素添加 click 事件处理程序。在单击处理程序中,从 data-id 属性中检索指定属性的 ID。构建一个 URL,其中查询字符串参数表示页面处理程序方法的名称及其 id 参数。这用于 jQuery 加载方法,该方法使用 GET 方法发出 HTTP 请求,然后将响应加载到指定的元素中。

清单 11.6 向页面添加 partial 和 script 块

<partial name="_PropertyModalPartial" />                             ❶
@section scripts{
<script>
$(function(){
    $('h5').on('click', function() {                                 ❷
        const id = $(this).data('id');                               ❸
        $('.modal-body').load(`?handler=propertydetails&id=${id}`);  ❹
    });
});
</script>
}

❶ 使用 partial 标签辅助函数来包含模态 partial。
❷ 为所有 h5 元素添加 click 事件处理程序。
❸ 在处理程序中,从 data-id 属性中检索所选属性的 ID。
❹ jQuery 加载方法使用 GET 方法调用指定的 URL,并将响应插入选择器中指定的元素中。在本例中,这就是模态体。

运行应用程序,单击主页上的一个城市,然后在结果列表中单击该属性的名称。详细信息应显示在模式中。
消除对 jQuery 的依赖,您可以改用 Fetch API,所有现代浏览器都支持它。您唯一需要做的更改是 scripts 部分。

清单 11.7 使用 Fetch API 调用命名处理程序

@section scripts{
<script>
document.querySelectorAll('h5').forEach(el => {                          ❶
    el.addEventListener('click', (event) => {                            ❶
        const id = event.target.dataset.id;                              ❷
        fetch(`?handler=propertydetails&id=${id}`)                       ❸
        .then((response) => {
            return response.text();                                      ❹
        })
        .then((result) => {
            document.querySelector('.modal-body').innerHTML = result;    ❺
        });
    });
});
</script>
}

❶ 为所有 h5 元素添加 click 事件处理程序。
❷ 在处理程序中,从 data-id 属性中检索所选属性的 ID。
❸ 像以前一样向指定的 URL 发出请求。
❹ 在 Response 对象上调用 text() 方法,以字符串形式获取返回的内容。
❺ 将其分配给模态体。

如果您进行这些更改并运行应用程序,则应该不会看到行为有任何差异。Fetch 示例比 jQuery 示例更冗长一些,但一旦您了解了代码中发生的事情,它就没有那么复杂了。

Fetch 与 Promise 一起使用,这类似于 .NET 任务。它们表示异步作的未来结果。Fetch HTTP 调用以流的形式返回响应。使用 text() 方法将流读入字符串,然后使用结果字符串更新模态体。如果你不熟悉它并想了解更多关于 Fetch 的信息,我推荐 Mozilla Developer Network 文档作为一个很好的起点:http://mng.bz/vXO4

在我们继续之前,必须了解进行这些异步调用时幕后发生的事情。您将使用浏览器开发人员工具查看正在发出的实际请求并检查其响应。每当您进行客户端工作时,浏览器开发人员工具都是必不可少的帮助,因为它们揭示了浏览器中发生的情况。您应该参考浏览器的文档以获取有关如何访问和使用它们的更多信息,但对于 Chrome 和 Edge 用户,您只需在打开浏览器时按 F12 键即可。显示工具后,单击 Network 选项卡,其中显示网络流量(请求)的详细信息,如图 11.1 所示。然后导航到其中一个城市页面,并单击属性名称。您应该会看到请求的详细信息出现。

图 11.1 该请求记录在 Network (网络) 选项卡中。

在此示例中,请求 URL 为 berlin?handler=properydetails&id=39。HTTP 响应状态代码为 200,类型指定为 Fetch,因为我的屏幕截图是使用 Fetch 示例获取的。如果我使用 jQuery 代码进行抓取,则类型将记录为 xhr,表示 jQuery 进行 AJAX 调用所依赖的浏览器的 XmlHttpRequest 对象。根据您的配置,您可能有不同的标题,但您可以右键单击任何标题来自定义您的视图。如果点击 Network 选项卡中的请求名称,则可以看到实际请求的更多详细信息(图 11.2),包括请求和响应标头以及请求中传递的任何数据的详细信息。

图 11.2 单击请求可查看更多详细信息。

您可以使用 Response (响应) 选项卡在右侧查看从服务器返回的实际响应。在图 11.3 所示的示例中,您可以立即从 Unicode 字符的存在中看出,除了我在第 3.1.5 节中介绍的默认基本拉丁字符集之外,我没有配置任何其他编码。

图 11.3 在 Network (网络) 选项卡中捕获原始响应。

因此,这是一个成功完成的简单 GET 请求。在下一个练习中,您将尝试将表单的内容发布到页面处理程序。

11.2.2 发布到页面处理程序

在模窗中拥有所选住宿的详细信息后,选择抵达和离开日期、指定同行宾客人数并获取住宿总费用将非常有用。在本节中,您将向模式添加一个表单,以便您执行此作,然后将表单内容发布到另一个处理程序方法,该方法将返回住宿的总成本。

首先,您需要一个输入模型来包装 Property 实例和表单值。将以下清单中的类声明添加到 CityModel 类中,使其嵌套在其中。

列表 11.8 将 BookingInputModel 作为嵌套类添加到 CityModel 中

public class BookingInputModel
{
    public Property Property { get; set; }
    [Display(Name = "No. of guests")]
    public int NumberOfGuests { get; set; }
    [DataType(DataType.Date), Display(Name = "Arrival")]
    public DateTime? StartDate { get; set; }
    [DataType(DataType.Date), Display(Name = "Departure")]
    public DateTime? EndDate { get; set; }
}

除了 Property 之外,此类还包括您将添加到模态框的表单字段的属性。BookingInputModel 将替换 Property 作为模态框的模型。您尚未包含任何 BindProperty 属性,因为您将 BookingInputModel 作为参数传递给处理请求的处理程序方法。handler 方法的代码如清单 11.9 所示。它根据天数、日房价和客人数量计算住宿总费用。这也添加到 CityModel 中。

清单 11.9 OnPostBooking 处理程序方法

public ContentResult OnPostBooking(BookingInputModel model)
{
    Var numberOfDays = (int)(model.EndDate.Value - 
    ➥ model.StartDate.Value).TotalDays;
    var totalCost = numberOfDays * model.Property.DayRate * 
    ➥ model.NumberOfGuests;
    return Content(totalCost.ToString("c"));
}

此处理程序方法返回 ContentResult,这是一种将字符串作为响应返回的方法。它不是您在实际应用程序中可能经常使用的东西。但是,它有助于简化此示例。此外,计算成本的基本逻辑通常属于业务逻辑层中的服务。但同样,我想让这个例子保持简单。下一个列表显示了添加到 _PropertyDetailsPartial.cshtml 文件的表单。同样,为了保持示例简单,我没有包括 validation。

清单 11.10 添加到物业详情部分的预订表单

<form id="booking-form">
    <input type="hidden" asp-for="Property.DayRate" />                ❶
    <div class="form-group">
        <label asp-for="StartDate" class="control-label"></label>
        <input asp-for="StartDate" class="form-control" />            ❷
    </div>
    <div class="form-group">
        <label asp-for="EndDate" class="control-label"></label>
        <input asp-for="EndDate" class="form-control" />              ❷
    </div>
    <div class="form-group">
        <label asp-for="NumberOfGuests" class="control-label"></label>
        <input asp-for="NumberOfGuests" class="form-control" max="@Model.Property.MaxNumberOfGuests" />                       ❸
    </div>
</form>

❶ 包括一个包含所选属性的日价的隐藏字段。
❷ 添加到达日期和离开日期的输入。
❸ 使用 max 属性将数字输入限制为所选住宿可以容纳的最大客人数。

您还需要更改部分的模型类型。目前它是一个 Property。您将将其更改为 BookingInputModel,因此请将 _PropertyDetailsPartial.cshtml 的第一行替换为以下内容:

@model CityModel.BookingInputModel

接下来,向 modal partial 属性添加一个新按钮;充当动态加载属性详细信息的框架的 Partial。将以下清单中的粗线添加到 footer 元素中。

Listing 11.11 在模态框部分添加了 Book 按钮

<div class="modal-footer">
    <button type="button" class="btn btn-secondary" 
     ➥ data-bs-dismiss="modal">Close</button>
    <button type="button" class="btn btn-success" 
     ➥ data-bs-dismiss="modal">Book</button>
</div>

最后,您需要将 new 按钮连接到一个 click 处理程序,该处理程序将表单发布到页面处理程序方法。jQuery 版本如下面的清单所示。

清单 11.12 使用 jQuery 向新按钮添加新的处理程序

$('.btn-success').on('click', function(){
    const data = $('#booking-form').serialize();             ❶
    $.post('?handler=booking', data, function(totalCost){    ❷
        alert(`Your stay will cost ${totalCost}`);           ❸
    });
});

❶ 使用 jQuery serialize 方法将表单字段值转换为 URL 编码的字符串以进行发布。
❷ 发布到页面处理程序并定义一个将响应作为参数的回调函数。
❸ 将响应合并到浏览器警报中显示的字符串中。

您已准备好对此进行测试。在浏览器中获得新版本的 City 页面后,请确保在 network 选项卡上打开开发人员工具。然后点击住宿,输入预订的开始日期和结束日期,然后指定客人人数。单击 Book 按钮。现在,您应该在 Network (网络) 选项卡中看到 400 错误代码(图 11.4)。

图 11.4 请求失败,出现 400 错误码。

400 HTTP 状态代码表示格式错误的客户端请求。在 Razor Pages 中,此错误最常见的原因是 POST 请求缺少请求验证令牌。如果您回想一下第 6 章,当 form 标记帮助程序的方法设置为 post 时,标记将生成为隐藏字段。如果检查在清单 11.10 中添加的代码,则 form 元素上没有 method 属性;因此,未生成隐藏的输入。

在这种情况下,修复方法很简单。您只需将 method=“post” 添加到表单元素中,然后重新运行应用程序。将生成隐藏字段,并将其包含在发布到服务器的序列化值中。但是,当您使用 AJAX 发布值时,完全没有 form 元素的情况并不少见。例如,您可能希望发布计算结果,而不是表单字段的内容。那么在这些情况下,您有什么选择呢?

首先,您可以考虑禁用请求验证。这必须在 PageModel 级别通过在处理程序类上添加 IgnoreAntiforgeryToken 属性来完成。您不能在页面处理程序级别禁用请求验证(与 MVC作方法不同),因此,如果您的页面上有其他处理程序处理 POST 请求,您也将禁用这些处理程序的请求验证。这可能是不可接受的,而且禁用此安全功能通常是不可取的。这样,您就需要生成一个令牌值并将其包含在 AJAX 请求中。

可以使用 Razor 页面的 Html 帮助程序属性上的 AntiForgeryToken 方法呈现具有令牌值的隐藏输入:

@Html.AntiForgeryToken()

或者,您可以使用 @inject 指令将 IAntiforgery 服务注入页面,并使用其 GetAndStoreTokens 方法生成令牌集并访问生成的 RequestToken 属性。

清单 11.13 从 IAntiforgery 服务生成请求验证令牌

@using Microsoft.AspNetCore.Antiforgery
@inject IAntiforgery antiforgery
@{
    var token = antiforgery.GetAndStoreTokens(HttpContext).RequestToken;
}

仅当 JavaScript 代码嵌入到 Razor 页面中时,此方法才适用,因为需要在脚本中呈现 C# 令牌变量。您不能在外部 JavaScript 文件中包含 C# 代码,因此,如果您希望将脚本排除在页面之外,则必须使用 AntiForgeryToken 方法来呈现隐藏字段。

接下来,在请求中包含令牌,作为表单字段(发布表单值)或作为请求标头值。表单字段的默认名称为 __RequestVerificationToken(带有两个前导下划线),请求标头的默认名称为 RequestVerificationToken(没有前导下划线)。重做此示例,而不使用 form 元素进行说明。首先,从属性详细信息中部分删除 form 标记,以便输入不再包含在表单中。接下来,将对 Html.AntiForgeryToken 方法的调用添加到 City Razor 页面。我将我的放在 partial 标签帮助程序的正上方:

@Html.AntiForgeryToken()
<partial name="_PropertyModalPartial" />
@section scripts{

您只需更改在脚本块中分配数据的方式。由于没有表单,因此无法再序列化表单,因此请创建一个对象,其属性将镜像页面处理程序期望作为参数的输入模型。您只需指定页面处理程序所需的属性值。下面的清单显示了 changed 的 button click 事件处理程序。

清单 11.14 更改 button click 事件处理程序以使用对象

$('.btn-success').on('click', function(){
    const data = {                                       ❶
            startdate: $('#StartDate').val(),            ❷
            enddate: $('#EndDate').val(),                ❷
            numberofguests: $('#NumberOfGuests').val(),  ❷
            __requestverificationtoken: $('[name="__RequestVerificationToken"]').val(),          ❸
            property:{                                   ❹
                dayrate: $('#Property_DayRate').val()    ❹
            }                                            ❹
        }
    $.post('?handler=booking', data, function(totalCost){
        alert(`Your stay will cost ${totalCost}`);
    });
});

❶ 创建一个对象。
❷ 使用 jQuery val() 方法获取表单字段值并将其分配给属性。
❸ 令牌的 hidden 字段没有 id 属性,因此您可以使用其名称作为 jQuery 选择器。
❹ 输入模型的 Property 属性是嵌套的。

jQuery 库负责将 data 变量表示的 JavaScript 对象转换为 URL 编码的字符串,以便发布和为请求分配正确的内容类型标头 (x-www-form-urlencoded)。在按钮单击处理程序的纯 JavaScript 版本(在章节下载中提供)中,您可以通过包含顶级 DayRate 属性来拼合输入模型。这样,您就可以使用浏览器的 URLSearchParams 接口 (http://mng.bz/49vj) 生成适合轻松发布的 URL 编码值字符串。此接口无法序列化具有嵌套属性的对象。

11.2.3 使用 JsonResult

到目前为止,您已将 simple values 和 JavaScript 对象传递给页面处理程序。本节将开始研究如何使用 JSON,JSON 已成为 Web 应用程序中客户端和服务器之间交换数据的实际数据格式。在此示例中,您将使用纯 JavaScript,并将您从表单字段构建的 JavaScript 对象序列化为 JSON,然后再将其发布到页面处理程序。然后,您将转换页面处理程序方法以返回 JsonResult 而不是 ContentResult。

当您指定 x-www-form-urlencoded 作为请求的内容类型时,框架知道它应该解析请求正文中构成您发送到服务器的数据的已发布名称-值对。您可以通过将内容类型设置为 application/json 来让框架知道您何时发布 JSON。但你还要告诉页面处理程序从何处获取数据。您可以通过将 FromBody 属性应用于页面处理程序参数来实现此目的。清单 11.15 显示了页面处理程序方法在处理 JSON 所需的更改后的外观。在代码中,您创建一个匿名类型来表示返回的数据。虽然这适用于特殊情况,但更可靠的方法将涉及为返回类型声明类或记录,以便它们是可测试的。

清单 11.15 使用 JsonResult 和 FromBody 属性

public JsonResult OnPostBooking([FromBody]BookingInputModel model)    ❶
{       
    var numberOfDays = (int)(model.EndDate.Value - 
    ➥ model.StartDate.Value).TotalDays;
    var totalCost = numberOfDays * model.Property.DayRate *
    ➥ model.NumberOfGuests;
    var result = new  { TotalCost = totalCost };                      ❷
    return new JsonResult(result);                                    ❸
}

❶ 将返回类型更改为 JsonResult,并将 [FromBody] 属性添加到处理程序参数,以告知框架在何处查找 JSON 数据。
❷ 创建一个对象来表示返回的数据。
❸ 将对象传递给 JsonResult 构造函数,并对其进行适当的序列化。

清单 11.16 显示了按钮点击事件处理程序的纯 JavaScript 版本,该处理程序生成 JSON,将其发布到服务器,并处理结果。有一些要点需要注意。这次必须在标头中传递请求验证令牌。您不能将其包含在请求正文的 JSON 中,因为请求验证是在框架解析 JSON 之前进行的。此外,您必须将请求的内容类型指定为 application/json。最后,当您使用 Fetch API 时,您对响应使用 json() 方法(而不是您之前使用的 text() 方法)来反序列化响应,以便您可以使用它。默认的 JSON 序列化程序使用驼峰式大小写生成属性名称,因此您传递给 JsonResult 构造函数的匿名对象的 TotalCost 属性将变为 totalCost。

清单 11.16 使用 Fetch API 发布 JSON 的按钮点击处理程序

document.querySelector('.btn-success')
    .addEventListener('click', () => {
        const token = document.querySelector(
        ➥ '[name="__RequestVerificationToken"]').value;
        const data = {
                startdate: document.getElementById('StartDate').value,
                enddate: document.getElementById('EndDate').value,
                numberofguests: document.getElementById(
                ➥ 'NumberOfGuests').value,
                property: {
                    dayrate: document.getElementById(
                    ➥ 'Property_DayRate').value
                }
            };
            fetch('?handler=booking', {
                method: 'post',
                headers: {
                    "Content-type": "application/json",              ❶
                    "RequestVerificationToken" : token               ❷
                },
                body: JSON.stringify(data)                           ❸
            }).then((response) => {
                return response.json();                              ❹
            }).then((result) => {
                alert(`Your stay will cost ${result.totalCost}`);    ❺
            });
        });

❶ 您必须将内容类型指定为 application/json。
❷ 您必须将请求验证令牌作为请求标头传递。
❸ 使用 JSON.stringify 方法将 JavaScript 对象序列化为 JSON 格式。
❹ 在响应中使用 json 方法,该方法将返回的 JSON 解析为对象。
❺ 访问结果对象的 totalCost 属性。

请注意,令牌的标头名称没有前导下划线。如果您在使用 JSON 时收到 400 错误代码,请在检查标头是否存在后检查标头的名称。如果您未将内容类型指定为 application/json,它将默认为 text/plain,并且您的处理程序将出错,因为它不会尝试解析请求正文。

jQuery 版本(清单 11.17)使用 ajax 方法而不是速记 post 方法来请求,因为 ajax 方法使您能够设置标头。请求的内容类型是使用设置中的一个 contentType 选项指定的,而不是显式设置标头值。

清单 11.17 使用 jQuery 和页面处理程序发布和处理 JSON

$('.btn-success').on('click', function(){
    const token = $('[name="__RequestVerificationToken"]').val();   ❶
    const data = {
            startdate: $('#StartDate').val(),
            enddate: $('#EndDate').val(),
            numberofguests: $('#NumberOfGuests').val(),
            property:{
                dayrate: $('#Property_DayRate').val()
            }
        }
    $.ajax({
        url: '?handler=booking',
        method: "post",
        contentType: "application/json",                            ❷
        headers: {
            "RequestVerificationToken" : token                      ❸
        },
        data: JSON.stringify(data)                                  ❹
    })
    .done(function(response){
        alert(`Your stay will cost ${response.totalCost}`);
    });
});

❶ 获取 token 值。
❷ 通过 contentType 选项设置内容类型。
❸ 在 header 中传递令牌。
❹ 使用 JSON.stringify 方法将 JavaScript 对象序列化为 JSON 格式。

使用页面处理程序和 JSON 的要点如下:

• 请记住将 FromBody 属性应用于处理程序参数。
• 将请求的内容类型设置为 application/JSON。
• 在请求标头中传递请求验证令牌。
• 使用浏览器开发人员工具中的 Network (网络) 选项卡来诊断问题。

在我看来,使用页面处理程序方法处理和返回 JSON 是可以接受的。请记住,每次执行页面处理程序时,都会实例化 PageModel 的一个实例,并解析其所有依赖项。如果您发现您正在注入仅 JSON 处理页面处理程序所需的其他依赖项,则表明有一点代码异味。此时,或者如果你的需求更广泛,你应该考虑使用最少的请求处理 API,这是 .NET 6 中引入的一项新功能。

11.3 最小请求处理 API

在 .NET 6 之前,在 ASP.NET Core 中通过 HTTP 处理大量服务(例如使用和生成 JSON 的服务)的推荐方法是使用构成 MVC 框架一部分的 Web API 框架。为此,您需要创建从 ApiController 派生的类,向它们添加处理请求所需的作方法,将相关服务添加到您的应用程序,并将控制器作方法映射为终端节点。在您的应用程序中合并 Web API 控制器涉及一定程度的仪式。

如果您还记得第 1 章,引入 Razor Pages 的很大一部分动机是减少现有 MVC 框架在服务器上生成 HTML 所需的仪式。减少仪式的努力在 .NET 6 中继续进行。在第 2 章中,您已经看到了应用程序引导和配置是如何基于新的最小托管 API 简化为一个文件的。作为整体最小 API 功能的一部分,还引入了一种精简的处理请求的方法,允许您将路由映射到函数,并且它确实有效。

使用 WebApplication 上的 Map[HttpMethod] 方法(MapPost、MapGet、MapPut 等)注册最小 API 请求处理程序,其约定与在 PageModel 类中注册页面处理程序的约定相同。回想一下,WebApplication 类型的实例是从生成器返回的。Program.cs 中的 Build 方法调用。传入路由模板和路由处理程序,即在路由匹配时执行的标准 .NET 委托。这可以是命名函数或可以接受参数的 lambda 表达式(图 11.5)。

图 11.5 最小 API 请求处理程序剖析。这是您将构建的实际请求处理程序的非作性、精简版本,纯粹是为了说明移动部件而设计的。

路由处理程序可以配置为返回许多内置响应类型之一,包括 JSON、文本和文件。内置返回类型中明显遗漏的是 HTML。这就是 Razor Pages 的用途。

11.3.1 最小 API 示例

在下一个练习中,您会将当前预订页面处理程序迁移到最小 API 方法。您将在 /api/property/booking 中定义一个响应 POST 请求的终端节点。它将 BookingInputModel 作为参数并返回 JSON 响应。打开 Program.cs 文件,就在 app 之前。Run()中,添加以下清单中的代码行。

清单 11.18 将页面处理程序迁移到最小的 API

app.MapPost("/api/property/booking", (BookingInputModel model) =>    ❶
{
    var numberOfDays = (int)(model.EndDate.Value -
    ➥ model.StartDate.Value).TotalDays;
    var totalCost = numberOfDays * model.Property.DayRate * 
    ➥ model.NumberOfGuests;
    return Results.Ok(new { TotalCost = totalCost });                ❷
});

❶ 使用 MapPost 方法将 POST 请求映射到指定的路由,并接受 BookingInputModel 作为参数。
❷ 使用 Results.Ok 方法将数据序列化为 JSON 并返回。

接下来,您将修改客户端脚本以调用此新终端节点。默认情况下,浏览器不允许脚本向网页中的其他域发出 HTTP 请求。此安全功能旨在减少跨站点请求伪造,因此不需要请求验证,因此不会为 API 启用。清单 11.19 显示了相应地修改的客户端代码。它只包括 Fetch request 部分,其余部分与前面的示例相同。修改请求的 URL 以反映传递给 MapPost 方法的模板,并删除请求验证标头。

清单 11.19 对最小 API 的 Fetch 请求

fetch('/api/property/booking', {               ❶
    method: 'post',
    headers: {
        "Content-type": "application/json",    ❷
    },
    body: JSON.stringify(data)

❶ 将 URL 更改为指向 API 注册中指定的 URL。
❷ 无需包含请求验证令牌

路由处理程序本身的代码与您之前使用的 OnPostBooking 页面处理程序没有太大区别。但是,路由处理程序本身的性能更好,尤其是因为不需要实例化 PageModel。另请注意,您不必告诉请求处理程序在何处查找 BookingInputModel 参数的数据。我们将在下一节中更详细地介绍这些参数。

您传递给请求处理程序的路由模板类似于第 4 章中讨论的路由模板。您可以以相同的方式指定路由参数和使用约束。生成的模板将与您的页面一起添加到终端节点集合中。

11.3.2 最小 API 中的参数

最少的 API 参数来自多个来源。他们是 (按顺序)

• Route values
• Query string
• Headers
• Body
• Services
• Custom

您可以使用其中一个 From* 属性让框架明确知道要以哪个源为目标:

• FromRoute
• FromQuery
• FromHeader
• FromBody
• FromServices

当从正文绑定参数时,最小的 API 期望参数格式为 JSON,并尝试对其进行反序列化。.NET 6 不支持从表单进行绑定。如果您使用的是 .NET 的更高版本,则应查看文档以查看此情况是否已更改,尽管正如您所看到的,在将表单传递给 API 之前,在客户端上将表单序列化为 JSON 非常简单。

在以下示例中,您将把预订计算迁移到您将向依赖关系注入系统注册的服务,然后作为参数添加到请求处理程序中。这是分离应用程序 logic 的推荐方法,因为它可以更轻松地进行调试和测试。首先,向 Models 文件夹添加一个新类来表示预订。该类名为 Booking,其属性表示计算预订所需的数据。

清单 11.20 Booking 类

namespace CityBreaks.Models
{
    public class Booking
    {
        public DateTime StartDate { get; set; }
        public DateTime EndDate { get; set; }
        public int NumberOfGuests { get; set; }
        public decimal DayRate { get; set; }
    }
}

将以下界面添加到 Services 文件夹。它指定一个返回 decimal 并采用 Booking 类实例的方法。

Listing 11.21 IBooking 接口

using CityBreaks.Models;

namespace CityBreaks.Services
{
    public interface IBookingService
    {
        decimal Calculate(Booking booking);
    }
}

将 IBooking 接口的以下实现添加到 Services 文件夹中。Calculate 方法的代码与当前请求处理程序中的代码基本相同。

Listing 11.22 BookingService类

using CityBreaks.Models;

namespace CityBreaks.Services
{
    public class BookingService : IBookingService
    {
        public decimal Calculate(Booking booking)
        {
            var numberOfDays = (int)(booking.EndDate -        
            ➥ booking.StartDate).TotalDays;
            var totalCost = numberOfDays * booking.DayRate * 
            ➥ booking.NumberOfGuests;
            return totalCost;
        }
    }
}

现在,您在 Program.cs 中注册服务:

builder.Services.AddSingleton<IBookingService, BookingService>();

最后,您修改请求处理程序以将服务作为参数,并修改路由处理程序以利用它来执行计算。

Listing 11.23 将BookingService作为参数的请求处理程序

 app.MapPost("/api/property/booking", 
    (BookingInputModel model, IBookingService bookingService) =>   ❶
{
    var booking = new Booking {                                    ❷
        StartDate = model.StartDate.Value,                         ❷
        EndDate = model.EndDate.Value,                             ❷
        NumberOfGuests = model.NumberOfGuests,                     ❷
        DayRate = model.Property.DayRate                           ❷
    };                                                             ❷
    var totalCost = bookingService.Calculate(booking);             ❸
    return Results.Ok(new { TotalCost = totalCost });
});

❶ 处理程序将 IBookingService 作为参数。
❷ 从输入模型实例化 Booking 类的实例。
❸ 它作为参数传递给服务,返回的值被发送回客户端。

在此示例中,将推断绑定源。框架将从所有来源中搜索它们,直到找到匹配的来源。要使它们明确,从而缩小搜索范围,请相应地添加 From* 属性:

app.MapPost("/api/property/booking", 
    ([FromBody]BookingInputModel model, 
     [FromServices]IBookingService bookingService)

11.3.3 最小 API 返回类型

在到目前为止的示例中,您已使用 Results.Ok 方法返回序列化为 JSON 且状态代码为 200 的数据。这是您可以与最小 API 请求处理程序一起使用的几种返回类型之一。在 .NET 6 中引入的静态 Results 类包括用于不同返回类型的其他方法,这些方法都实现 IResult。表 11.1 中详细介绍了 Razor Pages 应用程序中最常使用的 Razor Pages。

表 11.1 Results 类中的常用方法

Method

Response type

Description

Results.Ok

application/json

Produces JSON

Results.Json

application/json

Produces JSON and enables you to set various options including serializer options

Results.Text

text/plain (default)

Produces a text response, with a configurable content type

Results.File

application/octet-stream (default)

Writes a file to the response, with a configurable content type

Results.Bytes

application/octet-stream (default)

Writes a byte array to the response, with a configurable content type

除了这些选项之外,您还可以返回纯字符串或更复杂的类型。例如,您的处理程序可以从服务获取总成本,并简单地返回:

return bookingService.Calculate(booking);

响应将仅包含值,内容类型设置为 text/plain。请注意,您需要在客户端上调整 Fetch 代码以使用 text() 方法而不是 json() 方法,并且您将向警报中的插值字符串提供整个响应:

}).then((response) => {
    return response.text();    
}).then((result) => {
    alert(`Your stay will cost ${result}`);    
});

如果返回复杂类型,则值将序列化为 JSON,并且内容类型将设置为 application/json:

var totalCost = bookingService.Calculate(booking);
return new { TotalCost = totalCost };

这些选项在某些情况下可能很方便,但与使用 IResult 选项相比,它们只能节省几次击键,这些选项是强类型并且适合进行测试。

路由处理程序授权

路由处理程序可以与您的页面一起参与授权。您可以在任何参数之前传入 Authorize 属性。例如

app.MapPost("/api/property/booking", [Authorize](BookingInputModel model) => 

或者,您可以将 RequireAuthorization 方法链接到处理程序:

app.MapPost("/api/property/booking", (BookingInputModel model) => {
    ...
}).RequireAuthorization();

RequireAuthorization 方法将 params string[] 作为参数,使您能够传入应应用于路由处理程序的任何授权策略的名称。

11.4 Razor Pages 中的 CSS 隔离

在第 2 章中,我简要提到了 Shared 文件夹中的 Layout.cshtml.css 文件,其中包含应用于 _Layout.cshtml 文件的 CSS 样式声明。我提到过,样式表的稍微奇怪的名称是 CSS 隔离使用的约定的一部分,该约定已引入 .Net 6 中的 Razor Pages。本节讨论什么是 CSS 隔离及其工作原理。

首先,看看 CSS 隔离旨在缓解的问题类型。在构建 Web 应用程序时,通常会将 CSS 样式声明放在主布局文件中引用的全局样式表中。这样,样式表中的声明对所有使用该布局的页面都可用,无论它们在特定页面中是否需要。随着您继续开发应用程序,将添加与特定页面甚至部分相关的新样式。例如,您可能希望更改单个页面的默认字体,以便向样式表中添加新的 CSS 选择器,该选择器可用于仅定位该页面上的元素,并相应地更新目标元素的类属性。你的全局样式表不断增长。您发现自己越来越依赖编辑器的搜索功能来查找您可能想要更新的特定样式的声明。随着时间的推移,您会忘记哪些样式声明实际上正在使用,哪些可以安全地删除。

例如,假设您希望将一些 CSS 应用于 City 页面上的 h5 标题。它们当前由事件处理程序定位,该事件处理程序侦听 click 事件并使用属性详细信息填充模式对话框。通常,当用户将鼠标悬停在网页上的可点击元素上时,他们希望光标从箭头变为指向手指。因此,将 cursor:pointer 规则应用于页面上的所有 h5 元素是有意义的。您不希望将此更改应用于应用程序中的所有其他 h5 元素,因此您需要缩小 CSS 规则的适用范围。你可以向 h5 元素添加一个 CSS 类,然后使用它有选择地定位标题:

h5.clickable{
    cursor: pointer;
}

将此添加到全局样式表中,您就可以开始工作了。显然,您必须记住 clickable 类的用途,并且必须记住将其应用于页面上的所有 h5 元素。可能是你想修改不同页面上的样式。您可以通过添加更多 CSS 类来充当过滤器来实现此目的。或者,您可以使用 Razor 中的部分来包含特定于页面的样式表:

@section styles{
<link href="/css/city.css" rel="stylesheet">
}

这种方法的缺点是会增加站点的 HTTP 请求数,尤其是在为多个页面添加特定于页面的样式表时。你不能真的使用 bundle 来组合所有这些特定于页面的样式表,因为这会破坏练习的目标。

Razor Pages 中的 CSS 隔离有助于创建特定于页面的样式表,这些样式表不依赖于部分,而是捆绑到一个文件中。该功能在 Razor Pages 中默认启用,因此无需添加其他包或配置任何服务或中间件。你所要做的就是在它要影响的页面旁边放置一个样式表。您只需遵循特定的命名约定:末尾带有 .css 的 Razor 页面文件名。

以上面的示例为例,将名为 City.cshtml.css 的文件添加到 Pages 文件夹,并在其中放置样式声明以影响可点击的 h5 元素:

h5{
    cursor: pointer;
}

对样式表的引用位于布局页面上。您在引用中使用的文件名采用以下格式:[name_of_application].styles.css。您的应用程序的名称是 CityBreaks,您应该已经在布局页面上找到了该引用。它作为项目模板的一部分被放置在那里:

<link href="~/CityBreaks.styles.css" rel="stylesheet" />

当您运行应用程序并导航到 City 页面时,当您将鼠标悬停在属性名称上时,您可以看到光标变为指针(图 11.6)。如果导航到 /claims-manager,则可以看到用于显示用户名的 h5 元素不受同一 CSS 规则的影响,尽管您刚刚添加的样式表在布局文件中被全局引用。

图 11.6 独立的 CSS 样式声明仅适用于此页面上的 3 级标题。

那么它是如何工作的呢?如果查看 City 页面的呈现源代码,可以看到一个附加属性 (b-jbmzjjkv6t) 已注入到 City.cshtml 模板中的每个 HTML 元素中(图 11.7)。

图 11.7 将附加属性 (b-jbmzjjkv6t) 注入到“城市”页面中的每个 HTML 元素中。

此属性(范围标识符)唯一标识 City.cshtml 中的元素。请注意,带有容器 CSS 类的 main 和 div 元素注入了不同的范围标识符。它们属于 _Layout.cshtml 文件。每个具有附带独立样式表的 Razor 文件都会获得其标识符。

在构建时,所有隔离的样式表都合并为一个,并且它们的样式声明被更改,因此它们仅适用于具有与它们所定位的页面关联的标识符的元素。下面是一个代码段,它显示了为我的示例生成的样式表的前几行,其中包括 City.cshtml.css 文件内容以及 _Layout.cshtml.css 内容。

Listing 11.24 在构建时将所有隔离的样式表合并为一个

/* _content/CityBreaks/Pages/City.cshtml.rz.scp.css */               ❶
h5[b-jbmzjjkv6t]{                                                    ❷
    cursor:pointer;
}
/* _content/CityBreaks/Pages/Shared/_Layout.cshtml.rz.scp.css */     ❶

a.navbar-brand[b-wjjjhz4rtp] {                                       ❷
  white-space: normal;
  text-align: center;
  word-break: break-all;
}

❶ 注入一条评论,显示以下样式适用于哪个页面。
❷ 样式与注入了 specific 属性的元素隔离。

范围标识符由框架生成,格式为 b-{10 character string},默认情况下,每个文件都会获得其唯一的字符串,从而保证样式的隔离。但是,您可能希望在少量页面之间共享样式。您可以通过自定义生成的标识符来实现此目的,以便多个页面获得相同的值。这是在项目文件中完成的。以下示例导致 Layout 和 City 页面共享相同的标识符:shared-style。

清单 11.25 自定义所选页面的隔离标识符

<ItemGroup>
    <None Update="Pages/Shared/_Layout.cshtml.css" CssScope="shared-style" />
    <None Update="Pages/City.cshtml.css" CssScope="shared-style" />
</ItemGroup>

鉴于 CSS 隔离是一项构建时功能,因此标记帮助程序不支持它,因为它们在运行时生成输出。

本章总结了我们对 Razor Pages 中的应用程序开发的了解。最后几章更侧重于配置和发布您的应用程序,以及确保它免受恶意活动的影响。我们将在下一章开始介绍减少应用程序中错误的方法,如何在错误发生时妥善处理它们,以及如何使用日志记录来了解应用程序上线后发生的情况。

总结

客户端技术可以帮助您提高应用程序的可用性。
虽然它仍然是一个很棒的库,但随着越来越多的浏览器与标准保持一致,使用 jQuery 来作 DOM 和发出异步 (AJAX) 请求的情况正在减少。
您可以使用 PartialResult 将 HTML 块返回到 AJAX 调用。
在通过 AJAX 将表单值发布到页面处理程序方法时,必须确保包含请求验证令牌。
当请求内容类型为 x-www-form-urlenencoded 时,请求验证令牌可以包含在标头或请求正文中。
在请求正文中发布 JSON 时,必须将令牌作为标头包含在内。
将 JSON 发布到页面处理程序时,必须使用 FromBody 属性标记处理程序参数,以告知框架要使用哪个绑定源。
请求处理程序是 .NET 6 中的一项新功能,是最小 API 的一部分。
请求处理程序将路由映射到函数,并且可以采用参数。
请求处理程序参数绑定源可以是隐式的,也可以使用 From 属性显式地表示源,其中通配符 表示源 — Body、Services、Route 等。
请求处理程序返回序列化为 JSON 的 IResult、字符串或其他类型。
请求处理程序可以通过 Authorize 属性或 RequireAuthorization 方法参与授权。
Razor Pages 的 CSS 隔离是 .NET 6 中的新增功能。
CSS 隔离使您能够在单独的文件中维护范围限定为页面的样式,以便于维护,而不会增加整个应用程序中的 HTTP 请求数。
CSS 隔离在构建时将属性注入 HTML 元素,并使用它们将 CSS 声明的范围限定为页面。
只生成一个样式表,其中包括所有范围的样式,从而减少了 HTTP 请求。
范围样式仅影响 HTML 元素,而不影响标记帮助程序。