Chapter4:HANDING GET REQUESTS

4 HANDING GET REQUESTS

We’re all set to add some business logic to our application. But before 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

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 客户端。使业务逻辑远离控制器是使它们保持轻量级的好方法,并且我们的代码更具可读性和可维护性。

To create the controller, right-click on the Controllers folder inside the main project and then Add=>Controller. Then from the menu, choose API Controller Class and name it CompaniesController.cs.
若要创建控制器,请右键单击主项目中的“Controllers”文件夹,然后单击“Add=>Controller”。然后,从菜单中选择“API 控制器类”并将其命名为“CompaniesController.cs”。

alt

Our controller should be generated with the default code inside:
我们的控制器应该使用内部的默认代码生成:

using Microsoft.AspNetCore.Mvc;
namespace CompanyEmployees.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
    }
}

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:
另外,在控制器类的上方,我们可以看到这部分代码:

[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 Startup class in the Configure method:
之所以称为基于约定的路由,是因为它为 URL 路径建立了约定。 第一部分 为控制器名称创建映射,第二部分 为操作方法创建映射,第三部分 用于可选参数。我们可以在 Configure 方法的 Startup 类中配置这种类型的路由:

alt

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.
属性路由使用属性将路由直接映射到控制器内的操作方法。通常,我们将基本路由放在控制器类之上,如 Web API 控制器类所示。同样,对于特定的操作方法,我们在它们上方创建它们的路由。

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).
对于获取请求、发布或删除,我们使用相同的 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

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 中使用的名词表示资源,并帮助使用者了解我们正在使用的资源类型。因此,当我们与公司资源合作时,我们不应该选择名词产品或订单;名词应始终是公司。因此,如果我们的资源是雇员(我们将使用这种类型的资源),则遵循此约定,名词应该是雇员。

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.
我们需要注意的另一个重要部分是资源之间的层次结构。在我们的示例中,我们将公司作为主实体,将员工作为从属实体。当我们为依赖实体创建路由时,我们应该遵循一个稍微不同的约定:
/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.4 Getting All Companies From the Database

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.
我们要做的第一件事是改变基本路线从 [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;
using System.Collections.Generic;
namespace Contracts
{
    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. But we are going to stop here for a moment to draw your attention to one important thing.
为此,我们需要将“ Entities ”项目中的引用添加到“ Contracts ”项目。但是,我们将在这里停留片刻,提请你们注意一件重要的事情。

In our main project, we are referencing the LoggerService, Repository, and Entities projects. Since both the LoggerService and Repository projects have a reference for the Contracts project (which has a reference to the Entities project; we just added it) this means that the main project has a reference for the Entities project as well through the LoggerService or Repository projects. That said, we can remove the Entities reference from the main project:
在我们的主项目中,我们引用了 LoggerService、Repository 和 Entities 项目。由于 LoggerService 和 Repository 项目都有对 Contracts 项目的引用(该项目具有对 Entities 项目的引用;我们刚刚添加了它),这意味着主项目也通过 LoggerService 或 Repository 项目获得了实体项目的引用。也就是说,我们可以从主项目中删除实体引用:

alt

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

using Contracts;
using Entities.Models;
using Entities;
using System.Collections.Generic;
using System.Linq;
namespace Repository
{
    public class CompanyRepository : RepositoryBase<Company>, ICompanyRepository
    { 
        public CompanyRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
        public IEnumerable<Company> GetAllCompanies(bool trackChanges) => 
            FindAll(trackChanges).OrderBy(c => c.Name).ToList();
    }
}

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

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 Contracts;
using Microsoft.AspNetCore.Mvc;
using System;

namespace CompanyEmployees.Controllers
{
    [Route("api/companies")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
        private readonly IRepositoryManager _repository;
        private readonly ILoggerManager _logger;
        public CompaniesController(IRepositoryManager repository, ILoggerManager logger)
        {
            _repository = repository;
            _logger = logger;
        }

        [HttpGet]
        public IActionResult GetCompanies()
        {
            try
            {
                var companies = _repository.Company.GetAllCompanies(trackChanges: false);
                return Ok(companies);
            }
            catch (Exception ex) {
                _logger.LogError($"Something went wrong in the {nameof(GetCompanies)} action {ex}"); 
                return StatusCode(500, "Internal server error"); }
        }
    }

}

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

First of all, we inject the logger and repository services 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 both injected services to log the messages and to get the data from the repository class.
首先,我们将记录器和存储库服务注入构造函数中。然后通过装饰 GetCompanies 操作 [HttpGet] 属性,我们将此操作映射到 GET 请求。然后,我们使用两个注入的服务来记录消息并从存储库类中获取数据。

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.
由于操作正上方没有路由属性,因此 GetCompanies 操作的路由将是 api/companies,这是放置在控制器顶部的路由。

4.4 Testing the Result with 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

Please note that some GUID values will be different for your project, so you have to change them according to your 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

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模式下运行它;因此,关闭应用程序并重新启动它,但在公司员工模式下:

alt

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

alt
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.
非常好,一切都按计划工作。但是我们错过了一些东西。我们使用公司实体将我们的请求映射到数据库,然后将其作为结果返回给客户端,这不是一个好的做法。因此,在下一部分中,我们将学习如何使用 DTO 类改进代码。

4.5 DTO Classes vs. Entity Model Classes

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响应中返回实体不是一个好的做法;我们应该改用数据传输对象。但这是为什么呢?

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 the folder DataTransferObjects in the Entities project with the CompanyDto class inside:
现在,当我们知道为什么要在代码中将 DTO 与模型类分开时,让我们在 Entities 项目中创建文件夹 DataTransferObjects,其中包含 CompanyDto 类:

using System;
namespace Entities.DataTransferObjects
{
    public class CompanyDto { 
        public Guid Id { get; set; } 
        public string Name { get; set; } 
        public string FullAddress { get; set; } 
    }
}

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 class, because we are going to use this class only to return a response to the client. Therefore, validation attributes are not required.
我们删除了“ Employees ”属性,我们将使用“ FullAddress ”属性从“ Company ”类中连接“ Address 和“ Country ”属性。此外,我们不在此类中使用验证属性,因为我们将仅使用此类向客户端返回响应。因此,验证属性不是必需的。

So, let’s open and modify the GetCompanies action:
因此,让我们打开并修改 GetCompanies 操作:

using Contracts;
using Entities.DataTransferObjects;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq;
namespace CompanyEmployees.Controllers
{
    [Route("api/companies")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
        private readonly IRepositoryManager _repository;
        private readonly ILoggerManager _logger;
        public CompaniesController(IRepositoryManager repository, ILoggerManager logger)
        {
            _repository = repository;
            _logger = logger;
        }

        //[HttpGet]
        //public IActionResult GetCompanies()
        //{
        //    try
        //    {
        //        var companies = _repository.Company.GetAllCompanies(trackChanges: false);
        //        return Ok(companies);
        //    }
        //    catch (Exception ex) {
        //        _logger.LogError($"Something went wrong in the {nameof(GetCompanies)} action {ex}"); 
        //        return StatusCode(500, "Internal server error"); }
        //}

        [HttpGet]
        public IActionResult GetCompanies()
        {
            try
            {
                var companies = _repository.Company.GetAllCompanies(trackChanges: false);
                var companiesDto = companies.Select(c => new CompanyDto
                {
                    Id = c.Id,
                    Name = c.Name,
                    FullAddress = string.Join(' ', c.Address, c.Country)
                }).ToList();
                return Ok(companiesDto);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong in the {nameof(GetCompanies)} action {ex}");
                return StatusCode(500, "Internal server error");
            }
        }
    }
}

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

alt

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.
这次我们得到了我们的公司Dto结果,这是一种更可取的方式。但这也可以改进。如果我们看一下 GetCompanies 操作中的映射代码,我们可以看到我们手动映射所有属性。当然,对于一些领域来说没关系 – 但是如果我们有更多的领域呢?有一种更好、更简洁的方法来映射我们的类,那就是使用自动映射器。

4.6 Using AutoMapper in ASP.NET Core
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.
自动映射器是一个库,可帮助我们在应用程序中映射对象。通过使用这个库,我们将删除用于手动映射的代码 ,从而使操作可读且可维护。‌

So, to install AutoMapper, let’s open a Package Manager Console window and run the following command:
因此,要安装自动映射器,让我们打开程序包管理器控制台窗口并运行以下命令:

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

After installation, we are going to register this library in the ConfigureServices method:
安装完成后,我们将在配置服务方法:

services.AddAutoMapper(typeof(Startup));

As soon as our library is registered, we are going to create a profile class where we specify the source and destination objects for mapping:
注册库后,我们将创建一个配置文件类,在其中指定用于映射的源对象和目标对象:

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

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 class, which contains both the Address and the Country from the model class, we have to specify additional mapping rules with the ForMember method.
MappingProfile 类必须从自动映射器的 Profile 类继承。在构造函数中,我们使用 CreateMap 方法,在其中指定要映射到的源对象和目标对象。由于我们在 DTO 类中具有 FullAddress 属性,该属性同时包含模型类中的Address and the Country ,因此我们必须使用 ForMember 方法指定其他映射规则。

Now, we can use AutoMapper in our controller like any other service registered in IoC:
现在,我们可以在控制器中使用自动映射器,就像在 IoC 中注册的任何其他服务一样:

using AutoMapper;
using Contracts;
using Entities.DataTransferObjects;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;

namespace CompanyEmployees.Controllers
{
    [Route("api/companies")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
        private readonly IRepositoryManager _repository; 
        private readonly ILoggerManager _logger; 
        private readonly IMapper _mapper; 
        public CompaniesController(IRepositoryManager repository, ILoggerManager logger, IMapper mapper) 
        { 
            _repository = repository; 
            _logger = logger; 
            _mapper = mapper;
        }

        [HttpGet]
        public IActionResult GetCompanies()
        {
            try
            {
                var companies = _repository.Company.GetAllCompanies(trackChanges: false);
                var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
                return Ok(companiesDto);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Something went wrong in the {nameof(GetCompanies)} action {ex}");
                return StatusCode(500, "Internal server error");
            }
        }
    }
}

Excellent.Let’s use Postman again to send the request to test our app:
非常好。让我们再次使用Postman发送请求以测试我们的应用程序:
https://localhost:5001/api/companies

alt

我们可以看到一切都在按预期工作,但现在有了更好的代码。

发表评论