Author Archives: user

Ultimate ASP.NET Core Web API 14 ASYNCHRONOUS CODE

14 ASYNCHRONOUS CODE
14 异步代码

In this chapter, we are going to convert synchronous code to asynchronous inside ASP.NET Core. First, we are going to learn a bit about asynchronous programming and why should we write async code. Then we are going to use our code from the previous chapters and rewrite it in an async manner.‌
在本章中,我们将在 ASP.NET Core 中将同步代码转换为异步代码。首先,我们将学习一些关于异步编程的知识,以及为什么我们应该编写异步代码。然后,我们将使用前几章中的代码,并以异步方式重写它。

We are going to modify the code, step by step, to show you how easy is to convert synchronous code to asynchronous code. Hopefully, this will help you understand how asynchronous code works and how to write it from scratch in your applications.
我们将逐步修改代码,向您展示将同步代码转换为异步代码是多么容易。希望这将帮助您了解异步代码的工作原理,以及如何在应用程序中从头开始编写异步代码。

14.1 What is Asynchronous Programming?

14.1 什么是异步编程?

Async programming is a parallel programming technique that allows the working process to run separately from the main application thread.‌
异步编程是一种并行编程技术,它允许工作进程独立于主应用程序线程运行。

By using async programming, we can avoid performance bottlenecks and enhance the responsiveness of our application.
通过使用异步编程,我们可以避免性能瓶颈并提高应用程序的响应能力。

How so?
怎么会这样呢?

Because we are not sending requests to the server and blocking it while waiting for the responses anymore (as long as it takes). Now, when we send a request to the server, the thread pool delegates a thread to that request. Eventually, that thread finishes its job and returns to the thread pool freeing itself for the next request. At some point, the data will be fetched from the database and the result needs to be sent to the requester. At that time, the thread pool provides another thread to handle that work. Once the work is done, a thread is going back to the thread pool.
因为我们不再向服务器发送请求并在等待响应时阻止它(只要需要)。现在,当我们向服务器发送请求时,线程池会将线程委托给该请求。最终,该线程完成其作业并返回到线程池,从而为下一个请求释放自身。在某些时候,将从数据库中获取数据,并且需要将结果发送给请求者。此时,线程池会提供另一个线程来处理该工作。工作完成后,线程将返回到线程池。

It is very important to understand that if we send a request to an endpoint and it takes the application three or more seconds to process that request, we probably won’t be able to execute this request any faster in async mode. It is going to take the same amount of time as the sync request.
请务必了解,如果我们向终端节点发送请求,并且应用程序需要 3 秒或更长时间来处理该请求,则我们可能无法在异步模式下更快地执行此请求。它将花费与同步请求相同的时间。

Let’s imagine that our thread pool has two threads and we have used one thread with a first request. Now, the second request arrives and we have to use the second thread from a thread pool. At this point, our thread pool is out of threads. If a third request arrives now it has to wait for any of the first two requests to complete and return assigned threads to a thread pool. Only then the thread pool can assign that returned thread to a new request:
假设我们的线程池有两个线程,并且我们在第一个请求中使用了一个线程。现在,第二个请求到达,我们必须使用线程池中的第二个线程。此时,我们的线程池已用完线程。如果现在收到第三个请求,则必须等待前两个请求中的任何一个完成并将分配的线程返回到线程池。只有这样,线程池才能将返回的线程分配给新请求:

alt text

As a result of a request waiting for an available thread, our client experiences a slow down for sure. Additionally, if the client has to wait too long, they will receive an error response usually the service is unavailable (503). But this is not the only problem. Since the client expects the list of entities from the database, we know that it is an I/O operation. So, if we have a lot of records in the database and it takes three seconds for the database to return a result to the API, our thread is doing nothing except waiting for the task to complete. So basically, we are blocking that thread and making it three seconds unavailable for any additional requests that arrive at our API.
由于请求等待可用线程,我们的客户肯定会遇到速度变慢的情况。此外,如果客户端必须等待太长时间,他们将收到错误响应,通常是服务不可用 (503)。但这并不是唯一的问题。由于客户端需要数据库中的实体列表,因此我们知道这是一个 I/O作。因此,如果我们在数据库中有很多记录,并且数据库需要 3 秒钟才能将结果返回给 API,那么我们的线程除了等待任务完成外,什么都不做。所以基本上,我们阻止了该线程,并使其在 3 秒内无法用于到达我们 API 的任何其他请求。

With asynchronous requests, the situation is completely different.
对于异步请求,情况完全不同。

When a request arrives at our API, we still need a thread from a thread pool. So, that leaves us with only one thread left. But because this action is now asynchronous, as soon as our request reaches the I/O point where the database has to process the result for three seconds, the thread is returned to a thread pool. Now we again have two available threads and we can use them for any additional request. After the three seconds when the database returns the result to the API, the thread pool assigns the thread again to handle that response:
当请求到达我们的 API 时,我们仍然需要来自线程池的线程。所以,我们只剩下一条线索了。但是,由于此作现在是异步的,因此一旦我们的请求到达数据库必须处理结果三秒钟的 I/O 点,线程就会返回到线程池。现在我们又有两个可用的线程,我们可以将它们用于任何其他请求。在数据库将结果返回给 API 的三秒后,线程池会再次分配线程来处理该响应:

alt text

Now that we've cleared that out, we can learn how to implement asynchronous code in .NET Core and .NET 5+.
现在我们已经解决了这个问题,我们可以学习如何在 .NET Core 和 .NET 5+ 中实现异步代码。

14.2 Async, Await Keywords and Return Types

14.2 async、await 关键字和返回类型

The async and await keywords play a crucial part in asynchronous programming. We use the async keyword in the method declaration and its purpose is to enable the await keyword within that method. So yes,‌we can’t use the await keyword without previously adding the async keyword in the method declaration. Also, using only the async keyword doesn’t make your method asynchronous, just the opposite, that method is still synchronous.
async 和 await 关键字在异步编程中起着至关重要的作用。我们在方法声明中使用 async 关键字,其目的是在该方法中启用 await 关键字。所以,是的,如果不事先在方法声明中添加 async 关键字,我们就不能使用 await 关键字。此外,仅使用 async 关键字不会使您的方法异步,恰恰相反,该方法仍然是同步的。

The await keyword performs an asynchronous wait on its argument. It does that in several steps. The first thing it does is to check whether the operation is already complete. If it is, it will continue the method execution synchronously. Otherwise, the await keyword is going to pause the async method execution and return an incomplete task. Once the operation completes, a few seconds later, the async method can continue with the execution.
await 关键字对其参数执行异步等待。它分几个步骤来实现。它做的第一件事是检查作是否已经完成。如果是,它将同步继续方法执行。否则,await 关键字将暂停异步方法的执行并返回未完成的任务。作完成后,几秒钟后,async 方法可以继续执行。

Let’s see this with a simple example:
让我们通过一个简单的例子来了解这一点:

public async Task<IEnumerable<Company>> GetCompanies()
{
    _logger.LogInfo("Inside the GetCompanies method.");
    var companies = await _repoContext.Companies.ToListAsync();
    return companies;
}

So, even though our method is marked with the async keyword, it will start its execution synchronously. Once we log the required information synchronously, we continue to the next code line. We extract all the companies from the database and to do that, we use the await keyword. If our database requires some time to process the result and return it, the await keyword is going to pause the GetCompanies method execution and return an incomplete task. During that time the tread will be returned to a thread pool making itself available for another request. After the database operation completes the async method will resume executing and will return the list of companies.
因此,即使我们的方法标有 async 关键字,它也会同步开始执行。同步记录所需信息后,我们将继续下一个代码行。我们从数据库中提取所有公司,为此,我们使用 await 关键字。如果我们的数据库需要一些时间来处理结果并返回它,则 await 关键字将暂停 GetCompanies 方法的执行并返回一个未完成的任务。在此期间,tread 将返回到线程池,使其可用于另一个请求。数据库作完成后,异步方法将继续执行并返回公司列表。

From this example, we see the async method execution flow. But the question is how the await keyword knows if the operation is completed or not. Well, this is where Task comes into play.
在此示例中,我们可以看到异步方法执行流程。但问题是 await 关键字如何知道作是否完成。嗯,这就是 Task 发挥作用的地方。

14.2.1 Return Types of the Asynchronous Methods‌

14.2.1 异步方法的返回类型

In asynchronous programming, we have three return types:
在异步编程中,我们有三种返回类型:

Task<TResult>, for an async method that returns a value.
Task<TResult>,有返回值的异步方法。

• Task, for an async method that does not return a value.
Task 没有返回值的异步方法。

• void, which we can use for an event handler.
void,我们可以将其用于事件处理程序。

What does this mean?
这是什么意思?

Well, we can look at this through synchronous programming glasses. If our sync method returns an int, then in the async mode it should return Task<int> — or if the sync method returns IEnumerable<string>, then the async method should return Task<IEnumerable<string>>.
好吧,我们可以通过同步编程眼镜来看待这个问题。如果我们的 sync 方法返回一个 int,那么在异步模式下它应该返回 Task<int> — 或者如果 sync 方法返回IEnumerable<string> ,则异步方法应返回Task<IEnumerable<string>>

But if our sync method returns no value (has a void for the return type), then our async method should return Task. This means that we can use the await keyword inside that method, but without the return keyword.
但是,如果我们的 sync 方法没有返回任何值(返回类型为 void),则我们的 async 方法应返回 Task。这意味着我们可以在该方法中使用 await 关键字,但不需要 return 关键字。

You may wonder now, why not return Task all the time? Well, we should use void only for the asynchronous event handlers which require a void return type. Other than that, we should always return a Task.
您现在可能想知道,为什么不一直返回 Task 呢?好吧,我们应该只对需要 void 返回类型的异步事件处理程序使用 void。除此之外,我们应该始终返回一个 Task。

From C# 7.0 onward, we can specify any other return type if that type includes a GetAwaiter method.
从 C# 7.0 开始,我们可以指定任何其他返回类型(如果该类型包含 GetAwaiter 方法)。

It is very important to understand that the Task represents an execution of the asynchronous method and not the result. The Task has several properties that indicate whether the operation was completed successfully or not (Status, IsCompleted, IsCanceled, IsFaulted). With these properties, we can track the flow of our async operations. So, this is the answer to our question. With Task, we can track whether the operation is completed or not. This is also called TAP (Task-based Asynchronous Pattern).
了解 Task 表示异步方法的执行而不是结果,这一点非常重要。Task 具有多个属性,用于指示作是否已成功完成(Status、IsCompleted、IsCanceled、IsFaulted)。通过这些属性,我们可以跟踪异步作的流程。所以,这就是我们问题的答案。使用 Task,我们可以跟踪作是否完成。这也称为 TAP (基于任务的异步模式)。

Now, when we have all the information, let’s do some refactoring in our completely synchronous code.
现在,当我们获得所有信息时,让我们在完全同步的代码中进行一些重构。

14.2.2 The IRepositoryBase Interface and the RepositoryBase Class Explanation‌

14.2.2 IRepositoryBase 接口和 RepositoryBase 类说明

We won’t be changing the mentioned interface and class. That’s because we want to leave a possibility for the repository user classes to have either sync or async method execution. Sometimes, the async code could become slower than the sync one because EF Core’s async commands take slightly longer to execute (due to extra code for handling the threading), so leaving this option is always a good choice.
我们不会更改上述接口和类。这是因为我们希望为存储库用户类保留执行 sync 或 async 方法的可能性。有时,异步代码可能会变得比同步代码慢,因为 EF Core 的异步命令的执行时间略长(由于处理线程的额外代码),因此保留此选项始终是一个不错的选择。

It is general advice to use async code wherever it is possible, but if we notice that our async code runes slower, we should switch back to the sync one.
一般建议尽可能使用异步代码,但如果我们注意到异步代码符文速度较慢,我们应该切换回同步代码。

14.3 Modifying the ICompanyRepository Interface and the CompanyRepository Class

14.3 修改 ICompanyRepository 接口和 CompanyRepository 类

In the Contracts project, we can find the ICompanyRepository interface with all the synchronous method signatures which we should change.‌
在 Contracts 项目中,我们可以找到 ICompanyRepository 接口,其中包含我们应该更改的所有同步方法签名。

So, let’s do that:
所以,让我们这样做:

using Entities.Models;

namespace Contracts
{
    public interface ICompanyRepository
    {
    //    IEnumerable<Company> GetAllCompanies(bool trackChanges); 
    //    Company GetCompany(Guid companyId, bool trackChanges);
    //    void CreateCompany(Company company);
    //    IEnumerable<Company> GetByIds(IEnumerable<Guid> ids, bool trackChanges);
    //    void DeleteCompany(Company company);

        Task<IEnumerable<Company>> GetAllCompaniesAsync(bool trackChanges);
        Task<Company> GetCompanyAsync(Guid companyId, bool trackChanges);
        void CreateCompany(Company company);
        Task<IEnumerable<Company>> GetByIdsAsync(IEnumerable<Guid> ids, bool trackChanges);
        void DeleteCompany(Company company);
    }
}

The Create and Delete method signatures are left synchronous. That’s because, in these methods, we are not making any changes in the database. All we're doing is changing the state of the entity to Added and Deleted.
Create 和 Delete 方法签名保持同步。这是因为,在这些方法中,我们不会对数据库进行任何更改。我们所做的只是将实体的状态更改为 Added 和 Deleted。

So, in accordance with the interface changes, let’s modify our CompanyRepository.cs class, which we can find in the Repository project:
因此,根据接口的变化,让我们修改CompanyRepository.cs类,我们可以在 Repository 项目中找到它:

using Contracts;
using Entities.Models;
using Microsoft.EntityFrameworkCore;

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();
        //public Company GetCompany(Guid companyId, bool trackChanges) => FindByCondition(c => c.Id.Equals(companyId), trackChanges).SingleOrDefault();
        //public void CreateCompany(Company company) => Create(company);
        //public IEnumerable<Company> GetByIds(IEnumerable<Guid> ids, bool trackChanges) => FindByCondition(x => ids.Contains(x.Id), trackChanges).ToList();
        //public void DeleteCompany(Company company) => Delete(company);

        public async Task<IEnumerable<Company>> GetAllCompaniesAsync(bool trackChanges) => await FindAll(trackChanges).OrderBy(c => c.Name).ToListAsync();
        public async Task<Company> GetCompanyAsync(Guid companyId, bool trackChanges) => await FindByCondition(c => c.Id.Equals(companyId), trackChanges).SingleOrDefaultAsync();
        public void CreateCompany(Company company) => Create(company);
        public async Task<IEnumerable<Company>> GetByIdsAsync(IEnumerable<Guid> ids, bool trackChanges) => await FindByCondition(x => ids.Contains(x.Id), trackChanges).ToListAsync();
        public void DeleteCompany(Company company) => Delete(company);
    }
}

We only have to change these methods in our repository class.
我们只需要在我们的 repository 类中更改这些方法。

14.4 IRepositoryManager and RepositoryManager Changes

14.4 IRepositoryManager 和 RepositoryManager 更改

If we inspect the mentioned interface and the class, we will see the Save method, which calls the EF Core’s SaveChanges method. We have to change that as well:‌
如果我们检查上述接口和类,我们将看到 Save 方法,该方法调用 EF Core 的 SaveChanges 方法。我们也必须改变这一点:

namespace Contracts
{
    public interface IRepositoryManager
    {
        ICompanyRepository Company { get; }
        IEmployeeRepository Employee { get; }

        // void Save();
        Task SaveAsync();
    }
}

And the RepositoryManager class modification:
以及 RepositoryManager 类的修改:

// public void Save() => _repositoryContext.SaveChanges();
public async Task SaveAsync() => await _repositoryContext.SaveChangesAsync();

Because the SaveAsync(), ToListAsync()... methods are awaitable, we may use the await keyword; thus, our methods need to have the async keyword and Task as a return type.
由于 SaveAsync()、ToListAsync()...methods 是 awaitable 的,我们可以使用 await 关键字;因此,我们的方法需要将 async 关键字和 Task 作为返回类型。

Using the await keyword is not mandatory, though. Of course, if we don’t use it, our SaveAsync() method will execute synchronously — and that is not our goal here.
但是,使用 await 关键字不是必需的。当然,如果我们不使用它,我们的 SaveAsync() 方法将同步执行 — 这不是我们的目标。

14.5 Updating the Service layer

14.5 更新 Service 层

Again, we have to start with the interface modification:‌
同样,我们必须从接口修改开始:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        //IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges);
        //CompanyDto GetCompany(Guid companyId, bool trackChanges);
        //CompanyDto CreateCompany(CompanyForCreationDto company);
        //IEnumerable<CompanyDto> GetByIds(IEnumerable<Guid> ids, bool trackChanges);
        //(IEnumerable<CompanyDto> companies, string ids) CreateCompanyCollection(IEnumerable<CompanyForCreationDto> companyCollection);
        //void DeleteCompany(Guid companyId, bool trackChanges);
        //void UpdateCompany(Guid companyid, CompanyForUpdateDto companyForUpdate, bool trackChanges);

        Task<IEnumerable<CompanyDto>> GetAllCompaniesAsync(bool trackChanges); 
        Task<CompanyDto> GetCompanyAsync(Guid companyId, bool trackChanges); 
        Task<CompanyDto> CreateCompanyAsync(CompanyForCreationDto company); 
        Task<IEnumerable<CompanyDto>> GetByIdsAsync(IEnumerable<Guid> ids, bool trackChanges); 
        Task<(IEnumerable<CompanyDto> companies, string ids)> CreateCompanyCollectionAsync(IEnumerable<CompanyForCreationDto> companyCollection); 
        Task DeleteCompanyAsync(Guid companyId, bool trackChanges); 
        Task UpdateCompanyAsync(Guid companyid, CompanyForUpdateDto companyForUpdate, bool trackChanges);
    }
}

And then, let’s modify the class methods one by one.
然后,让我们逐个修改类方法。

GetAllCompanies:

//public IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges)
//{
//    var companies = _repository.Company.GetAllCompanies(trackChanges);
//    var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
//    return companiesDto;
//}

public async Task<IEnumerable<CompanyDto>> GetAllCompaniesAsync(bool trackChanges)
{
    var companies = await _repository.Company.GetAllCompaniesAsync(trackChanges);
    var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
    return companiesDto;
}

GetCompany:

//public CompanyDto GetCompany(Guid id, bool trackChanges)
//{
//    var company = _repository.Company.GetCompany(id, trackChanges);
//    if (company is null) throw new CompanyNotFoundException(id);
//    var companyDto = _mapper.Map<CompanyDto>(company);
//    return companyDto;
//}

public async Task<CompanyDto> GetCompanyAsync(Guid id, bool trackChanges)
{
    var company = await _repository.Company.GetCompanyAsync(id, trackChanges);
    if (company is null)
        throw new CompanyNotFoundException(id);
    var companyDto = _mapper.Map<CompanyDto>(company);
    return companyDto;
}

CreateCompany:

//public CompanyDto CreateCompany(CompanyForCreationDto company)
//{
//    var companyEntity = _mapper.Map<Company>(company);
//    _repository.Company.CreateCompany(companyEntity);
//    _repository.Save();
//    var companyToReturn = _mapper.Map<CompanyDto>(companyEntity);
//    return companyToReturn;
//}

public async Task<CompanyDto> CreateCompanyAsync(CompanyForCreationDto company)
{
    var companyEntity = _mapper.Map<Company>(company);
    _repository.Company.CreateCompany(companyEntity);
    await _repository.SaveAsync();
    var companyToReturn = _mapper.Map<CompanyDto>(companyEntity);
    return companyToReturn;
}

GetByIds:

//public IEnumerable<CompanyDto> GetByIds(IEnumerable<Guid> ids, bool trackChanges)
//{
//    if (ids is null)
//        throw new IdParametersBadRequestException();
//    var companyEntities = _repository.Company.GetByIds(ids, trackChanges);
//    if (ids.Count() != companyEntities.Count())
//        throw new CollectionByIdsBadRequestException();
//    var companiesToReturn = _mapper.Map<IEnumerable<CompanyDto>>(companyEntities);
//    return companiesToReturn;
//}

public async Task<IEnumerable<CompanyDto>> GetByIdsAsync(IEnumerable<Guid> ids, bool trackChanges)
{
    if (ids is null) throw new IdParametersBadRequestException();
    var companyEntities = await _repository.Company.GetByIdsAsync(ids, trackChanges);
    if (ids.Count() != companyEntities.Count())
        throw new CollectionByIdsBadRequestException();
    var companiesToReturn = _mapper.Map<IEnumerable<CompanyDto>>(companyEntities);
    return companiesToReturn;
}

CreateCompanyCollection:

//public (IEnumerable<CompanyDto> companies, string ids) CreateCompanyCollection(IEnumerable<CompanyForCreationDto> companyCollection)
//{
//    if (companyCollection is null)
//        throw new CompanyCollectionBadRequest();
//    var companyEntities = _mapper.Map<IEnumerable<Company>>(companyCollection);
//    foreach (var company in companyEntities)
//    {
//        _repository.Company.CreateCompany(company);
//    }
//    _repository.Save();
//    var companyCollectionToReturn = _mapper.Map<IEnumerable<CompanyDto>>(companyEntities);
//    var ids = string.Join(",", companyCollectionToReturn.Select(c => c.Id));
//    return (companies: companyCollectionToReturn, ids: ids);
//}

public async Task<(IEnumerable<CompanyDto> companies, string ids)> CreateCompanyCollectionAsync(IEnumerable<CompanyForCreationDto> companyCollection)
{
    if (companyCollection is null) 
        throw new CompanyCollectionBadRequest();
    var companyEntities = _mapper.Map<IEnumerable<Company>>(companyCollection);
    foreach (var company in companyEntities)
    {
        _repository.Company.CreateCompany(company);
    }
    await _repository.SaveAsync();
    var companyCollectionToReturn = _mapper.Map<IEnumerable<CompanyDto>>(companyEntities);
    var ids = string.Join(",", companyCollectionToReturn.Select(c => c.Id));
    return (companies: companyCollectionToReturn, ids: ids);
}

DeleteCompany:

//public void DeleteCompany(Guid companyId, bool trackChanges)
//{
//    var company = _repository.Company.GetCompany(companyId, trackChanges);
//    if (company is null)
//        throw new CompanyNotFoundException(companyId);
//    _repository.Company.DeleteCompany(company);
//    _repository.Save();
//}

public async Task DeleteCompanyAsync(Guid companyId, bool trackChanges)
{
    var company = await _repository.Company.GetCompanyAsync(companyId, trackChanges);
    if (company is null)
        throw new CompanyNotFoundException(companyId);
    _repository.Company.DeleteCompany(company);
    await _repository.SaveAsync();
}

UpdateCompany:

//public void UpdateCompany(Guid companyId, CompanyForUpdateDto companyForUpdate, bool trackChanges)
//{
//    var companyEntity = _repository.Company.GetCompany(companyId, trackChanges);
//    if (companyEntity is null)
//        throw new CompanyNotFoundException(companyId);
//    _mapper.Map(companyForUpdate, companyEntity);
//    _repository.Save();
//}

public async Task UpdateCompanyAsync(Guid companyId, CompanyForUpdateDto companyForUpdate, bool trackChanges)
{
    var companyEntity = await _repository.Company.GetCompanyAsync(companyId, trackChanges);
    if (companyEntity is null)
        throw new CompanyNotFoundException(companyId);
    _mapper.Map(companyForUpdate, companyEntity);
    await _repository.SaveAsync();
}}

That’s all the changes we have to make in the CompanyService class.
这就是我们在 CompanyService 类中必须进行的所有更改。

Now we can move on to the controller modification.
现在我们可以继续进行控制器修改。

14.6 Controller Modification

14.6 控制器修改

Finally, we need to modify all of our actions in‌ the CompaniesController to work asynchronously.
最后,我们需要修改CompaniesController 异步工作。

So, let’s first start with the GetCompanies method:
因此,让我们首先从 GetCompanies 方法开始:

//[HttpGet]
//public IActionResult GetCompanies()
//{
//    // throw new Exception("Exception");
//    var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
//    return Ok(companies);
//}

[HttpGet]
public async Task<IActionResult> GetCompanies()
{
    var companies = await _service.CompanyService.GetAllCompaniesAsync(trackChanges: false);
    return Ok(companies);
}

We haven’t changed much in this action. We’ve just changed the return type and added the async keyword to the method signature. In the method body, we can now await the GetAllCompaniesAsync() method. And that is pretty much what we should do in all the actions in our controller.
我们在这次行动中没有太大变化。我们刚刚更改了返回类型,并将 async 关键字添加到方法签名中。在方法主体中,我们现在可以等待 GetAllCompaniesAsync() 方法。这几乎就是我们在控制器中的所有作中应该做的事情。

NOTE: We’ve changed all the method names in the repository and service layers by adding the Async suffix. But, we didn’t do that in the controller’s action. The main reason for that is when a user calls a method from your service or repository layers they can see right-away from the method name whether the method is synchronous or asynchronous. Also, your layers are not limited only to sync or async methods, you can have two methods that do the same thing but one in a sync manner and another in an async manner. In that case, you want to have a name distinction between those methods. For the controller’s actions this is not the case. We are not targeting our actions by their names but by their routes. So, the name of the action doesn’t really add any value as it does for the method names.
注意:我们通过添加 Async 后缀更改了存储库和服务层中的所有方法名称。但是,我们没有在控制器的作中执行此作。主要原因是,当用户从您的服务或存储库层调用方法时,他们可以立即从方法名称中看到该方法是同步的还是异步的。此外,您的图层不仅限于同步或异步方法,您还可以有两个方法执行相同的作,但一个以同步方式,另一个以异步方式。在这种情况下,您希望在这些方法之间进行名称区分。对于控制器的作,情况并非如此。我们不是根据他们的名字来定位我们的行动,而是根据他们的路线来定位我们的行动。因此,作的名称并不像方法名称那样真正增加任何值。

So to continue, let’s modify all the other actions.
因此,要继续,让我们修改所有其他作。

GetCompany:

//[HttpGet("{id:guid}", Name = "CompanyById")]
//public IActionResult GetCompany(Guid id)
//{
//    var company = _service.CompanyService.GetCompany(id, trackChanges: false);
//    return Ok(company);
//}

[HttpGet("{id:guid}", Name = "CompanyById")]
public async Task<IActionResult> GetCompany(Guid id)
{
    var company = await _service.CompanyService.GetCompanyAsync(id, trackChanges: false);
    return Ok(company);
}

GetCompanyCollection:

//[HttpGet("collection/({ids})", Name = "CompanyCollection")]
//public IActionResult GetCompanyCollection([ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<Guid> ids)
//{
//    var companies = _service.CompanyService.GetByIds(ids, trackChanges: false);
//    return Ok(companies);
//}

[HttpGet("collection/({ids})", Name = "CompanyCollection")]
public async Task<IActionResult> GetCompanyCollection([ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<Guid> ids)
{
    var companies = await _service.CompanyService.GetByIdsAsync(ids, trackChanges: false);
    return Ok(companies);
}

CreateCompany:

//[HttpPost]
//public IActionResult CreateCompany([FromBody] CompanyForCreationDto company)
//{
//    if (company is null)
//        return BadRequest("CompanyForCreationDto object is null");
//    var createdCompany = _service.CompanyService.CreateCompany(company);

//    return CreatedAtRoute("CompanyById", new { id = createdCompany.Id }, createdCompany);
//}

[HttpPost]
public async Task<IActionResult> CreateCompany([FromBody] CompanyForCreationDto company)
{
    if (company is null)
        return BadRequest("CompanyForCreationDto object is null");
    if (!ModelState.IsValid)
        return UnprocessableEntity(ModelState);
    var createdCompany = await _service.CompanyService.CreateCompanyAsync(company);
    return CreatedAtRoute("CompanyById", new { id = createdCompany.Id }, createdCompany);
}

CreateCompanyCollection:

//[HttpPost("collection")]
//public IActionResult CreateCompanyCollection([FromBody] IEnumerable<CompanyForCreationDto> companyCollection)
//{
//    var result = _service.CompanyService.CreateCompanyCollection(companyCollection);
//    return CreatedAtRoute("CompanyCollection", new { result.ids }, result.companies);
//}

[HttpPost("collection")]
public async Task<IActionResult> CreateCompanyCollection([FromBody] IEnumerable<CompanyForCreationDto> companyCollection)
{
    var result = await _service.CompanyService.CreateCompanyCollectionAsync(companyCollection);
    return CreatedAtRoute("CompanyCollection", new { result.ids }, result.companies);
}

DeleteCompany:

//[HttpDelete("{id:guid}")]
//public IActionResult DeleteCompany(Guid id)
//{
//    _service.CompanyService.DeleteCompany(id, trackChanges: false);
//    return NoContent();
//}

[HttpDelete("{id:guid}")]
public async Task<IActionResult> DeleteCompany(Guid id)
{
    await _service.CompanyService.DeleteCompanyAsync(id, trackChanges: false);
    return NoContent();
}

UpdateCompany:

//[HttpPut("{id:guid}")]
//public IActionResult UpdateCompany(Guid id, [FromBody] CompanyForUpdateDto company)
//{
//    if (company is null)
//        return BadRequest("CompanyForUpdateDto object is null");
//    _service.CompanyService.UpdateCompany(id, company, trackChanges: true);
//    return NoContent();
//}

[HttpPut("{id:guid}")]
public async Task<IActionResult> UpdateCompany(Guid id, [FromBody] CompanyForUpdateDto company)
{
    if (company is null)
        return BadRequest("CompanyForUpdateDto object is null");
    await _service.CompanyService.UpdateCompanyAsync(id, company, trackChanges: true);
    return NoContent();
}

Excellent. Now we are talking async.
非常好。现在我们谈论的是异步。

Of course, we have the Employee entity as well and all of these steps have to be implemented for the EmployeeRepository class, IEmployeeRepository interface, and EmployeesController.
当然,我们也有 Employee 实体,所有这些步骤都必须为 EmployeeRepository 类、IEmployeeRepository 接口和 EmployeesController 实现。

You can always refer to the source code for this chapter if you have any trouble implementing the async code for the Employee entity.
如果您在实现 Employee 实体的异步代码时遇到任何问题,您始终可以参考本章的源代码。

After the async implementation in the Employee classes, you can try to send different requests (from any chapter) to test your async actions. All of them should work as before, without errors, but this time in an asynchronous manner.
在 Employee 类中异步实现之后,您可以尝试发送不同的请求(来自任何章节)来测试您的异步作。它们都应该像以前一样工作,没有错误,但这次以异步方式。

14.7 Continuation in Asynchronous Programming

14.7 异步编程中的延续

The await keyword does three things:‌
await 关键字执行三项作:

• It helps us extract the result from the async operation – we already learned about that
它帮助我们从异步作中提取结果——我们已经了解了这一点

• Validates the success of the operation
验证作是否成功

• Provides the Continuation for executing the rest of the code in the async method
提供 Continuation,用于在 async 方法中执行其余代码

So, in our GetCompanyAsync service method, all the code after awaiting an async operation is executed inside the continuation if the async operation was successful.
因此,在我们的 GetCompanyAsync 服务方法中,如果异步作成功,则等待异步作后的所有代码都将在延续内执行。

When we talk about continuation, it can be confusing because you can read in multiple resources about the SynchronizationContext and capturing the current context to enable this continuation. When we await a task, a request context is captured when await decides to pause the method execution. Once the method is ready to resume its execution, the application takes a thread from a thread pool, assigns it to the context (SynchonizationContext), and resumes the execution. But this is the case for ASP.NET applications.
当我们谈论 continuation 时,可能会令人困惑,因为您可以读取有关 SynchronizationContext 的多个资源并捕获当前上下文以启用此 continuation。当我们等待任务时,当 await 决定暂停方法执行时,会捕获请求上下文。一旦方法准备好恢复执行,应用程序就会从线程池中获取一个线程,将其分配给上下文 (SynchonizationContext),然后恢复执行。但 ASP.NET 应用程序就是这种情况。

We don’t have the SynchronizationContext in ASP.NET Core applications. ASP.NET Core avoids capturing and queuing the context, all it does is take the thread from a thread pool and assign it to the request. So, a lot less background works for the application to do.
我们在 ASP.NET Core 应用程序中没有 SynchronizationContext。ASP.NET Core 避免了捕获和排队上下文,但它所做的只是从线程池中获取线程并将其分配给请求。因此,应用程序需要完成的后台工作要少得多。

One more thing. We are not limited to a single continuation. This means that in a single method, we can use multiple await keywords.
还有一件事。我们不限于单一的延续。这意味着在单个方法中,我们可以使用多个 await 关键字。

14.8 Common Pitfalls

14.8 常见陷阱

In our GetAllCompaniesAsync repository method if we didn’t know any better, we could’ve been tempted to use the Result property instead of the await keyword:‌
在我们的 GetAllCompaniesAsync 存储库方法中,如果我们不知道更多,我们可能会想使用 Result 属性而不是 await 关键字:

public async Task<IEnumerable<Company>> GetAllCompaniesAsync(bool trackChanges) => 
    FindAll(trackChanges) 
    .OrderBy(c => c.Name) 
    .ToListAsync() 
    .Result;

We can see that the Result property returns the result we require:
我们可以看到 Result 属性返回我们需要的结果:

// Summary: 
// Gets the result value of this System.Threading.Tasks.Task`1. 
// 
// Returns: 
// The result value of this System.Threading.Tasks.Task`1, which 
// is of the same type as the task's type parameter. 
public TResult Result 
{ 
    get... 
}

But don’t use the Result property.
但不要使用 Result 属性。

With this code, we are going to block the thread and potentially cause a deadlock in the application, which is the exact thing we are trying to avoid using the async and await keywords. It applies the same to the Wait method that we can call on a Task.
使用此代码,我们将阻止线程并可能导致应用程序中的死锁,这正是我们试图避免使用 async 和 await 关键字的事情。它同样适用于我们可以在 Task 上调用的 Wait 方法。

So, that’s it regarding the asynchronous implementation in our project. We’ve learned a lot of useful things from this section and we can move on to the next one – Action filters.
所以,这就是我们项目中的异步实现。我们从本节中学到了很多有用的东西,我们可以继续下一个 –作过滤器。

Ultimate ASP.NET Core Web API 13 VALIDATION

13 VALIDATION

13 验证

While writing API actions, we have a set of rules that we need to check. If we take a look at the Company class, we can see different data annotation attributes above our properties:‌
在编写 API动作时,我们有一组规则需要检查。如果我们看一下 Company 类,我们可以在属性上方看到不同的数据注释属性:

alt text

Those attributes serve the purpose to validate our model object while creating or updating resources in the database. But we are not making use of them yet.
这些属性用于在数据库中创建或更新资源时验证我们的模型对象。但我们还没有利用它们。

In this chapter, we are going to show you how to validate our model objects and how to return an appropriate response to the client if the model is not valid. So, we need to validate the input and not the output of our controller actions. This means that we are going to apply this validation to the POST, PUT, and PATCH requests, but not for the GET request.
在本章中,我们将向您展示如何验证我们的模型对象,以及如何在模型无效时向客户端返回适当的响应。因此,我们需要验证控制器作的输入,而不是输出。这意味着我们将将此验证应用于 POST、PUT 和 PATCH 请求,但不应用于 GET 请求。

13.1 ModelState, Rerun Validation, and Built-in Attributes

13.1 ModelState、rerun 验证和内置属性

To validate against validation rules applied by Data Annotation attributes, we are going to use the concept of ModelState. It is a dictionary containing the state of the model and model binding validation.‌
为了根据 Data Annotation 属性应用的验证规则进行验证,我们将使用 ModelState 的概念。它是一个包含模型状态和模型绑定验证的字典。

It is important to know that model validation occurs after model binding and reports errors where the data, sent from the client, doesn’t meet our validation criteria. Both model validation and data binding occur before our request reaches an action inside a controller. We are going to use the ModelState.IsValid expression to check for those validation rules.
请务必了解,模型验证发生在模型绑定之后,并报告从客户端发送的数据不符合验证条件的错误。模型验证和数据绑定都发生在我们的请求到达控制器内的 action 之前。我们将使用 ModelState.IsValid 表达式来检查这些验证规则。

By default, we don’t have to use the ModelState.IsValid expression in Web API projects since, as we explained in section 9.2.1, controllers are decorated with the [ApiController] attribute. But, as we could’ve seen, it defaults all the model state errors to 400 – BadRequest and doesn’t allow us to return our custom error messages with a different status code. So, we suppressed it in the Program class.
默认情况下,我们不必在 Web API 项目中使用 ModelState.IsValid 表达式,因为正如我们在第 9.2.1 节中所解释的那样,控制器是使用 [ApiController] 属性修饰的。但是,正如我们所看到的,它默认所有模型状态错误为 400 – BadRequest,并且不允许我们返回具有不同状态代码的自定义错误消息。因此,我们在 Program 类中抑制了它。

The response status code, when validation fails, should be 422 Unprocessable Entity. That means that the server understood the content type of the request and the syntax of the request entity is correct, but it was unable to process validation rules applied on the entity inside the request body. If we didn’t suppress the model validation from the [ApiController] attribute, we wouldn’t be able to return this status code (422) since, as we said, it would default to 400.
验证失败时,响应状态代码应为 422 Unprocessable Entity。这意味着服务器理解请求的内容类型,并且请求实体的语法是正确的,但它无法处理应用于请求正文内的实体的验证规则。如果我们没有从 [ApiController] 属性中禁止显示模型验证,我们将无法返回此状态代码 (422),因为正如我们所说,它将默认为 400。

13.1.1 Rerun Validation‌

13.1.1 重新运行验证

In some cases, we want to repeat our validation. This can happen if, after the initial validation, we compute a value in our code, and assign it to the property of an already validated object.
在某些情况下,我们希望重复验证。如果在初始验证之后,我们在代码中计算一个值,并将其分配给已验证对象的属性,则可能会发生这种情况。

If this is the case, and we want to run the validation again, we can use the ModelStateDictionary.ClearValidationState method to clear the validation specific to the model that we’ve already validated, and then use the TryValidateModel method:
如果是这种情况,并且我们想要再次运行验证,我们可以使用 ModelStateDictionary.ClearValidationState 方法来清除特定于我们已经验证的模型的验证,然后使用 TryValidateModel 方法:

[HttpPost]
public IActionResult POST([FromBody] Book book)
{
    if (!ModelState.IsValid)
        return UnprocessableEntity(ModelState);
    var newPrice = book.Price - 10;
    book.Price = newPrice;
    ModelState.ClearValidationState(nameof(Book));
    if (!TryValidateModel(book, nameof(Book)))
        return UnprocessableEntity(ModelState);
    _service.CreateBook(book);
    return CreatedAtRoute("BookById", new { id = book.Id }, book);
}

This is just a simple example but it explains how we can revalidate our model object.
这只是一个简单的示例,但它解释了我们如何重新验证我们的模型对象。

13.1.2 Built-in Attributes‌

13.1.2 内置属性

Validation attributes let us specify validation rules for model properties. At the beginning of this chapter, we have marked some validation attributes. Those attributes (Required and MaxLength) are part of built-in attributes. And of course, there are more than two built-in attributes. These are the most used ones:
验证属性允许我们为模型属性指定验证规则。在本章的开头,我们标记了一些验证属性。这些属性 (Required 和 MaxLength) 是内置属性的一部分。当然,还有不止两个内置属性。这些是最常用的:

ATTRIBUTE USAGE
[ValidateNever] Indicates that property or parameter should be excluded from validation.
[Compare] We use it for the properties comparison.
[EmailAddress] Validates the email format of the property.
[Phone] Validates the phone format of the property.
[Range] Validates that the property falls within a specified range.
[RegularExpression] Validates that the property value matches a specified regular expression.
[Required] We use it to prevent a null value for the property.
[StringLength] Validates that a string property value doesn't exceed a specified length limit.

If you want to see a complete list of built-in attributes, you can visit this page. https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=net-6.0
如果您想查看内置属性的完整列表,可以访问此页面。https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=net-6.0

13.2 Custom Attributes and IValidatableObject

13.2 自定义属性和 IValidatableObject

There are scenarios where built-in attributes are not enough and we have to provide some custom logic. For that, we can create a custom attribute by using the ValidationAttribute class, or we can use the IValidatableObject interface.‌
在某些情况下,内置属性是不够的,我们必须提供一些自定义逻辑。为此,我们可以使用 ValidationAttribute 类创建自定义属性,也可以使用 IValidatableObject 接口。

So, let’s see an example of how we can create a custom attribute:
那么,让我们看看如何创建自定义属性的示例:

public class ScienceBookAttribute : ValidationAttribute
{
    public BookGenre Genre { get; set; }
    public string Error => $"The genre of the book must be {BookGenre.Science}";
    public ScienceBookAttribute(BookGenre genre) { Genre = genre; }
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        var book = (Book)validationContext.ObjectInstance;
        if (!book.Genre.Equals(Genre.ToString()))
            return new ValidationResult(Error);
        return ValidationResult.Success;
    }
}

Once this attribute is called, we are going to pass the genre parameter inside the constructor. Then, we have to override the IsValid method. There we extract the object we want to validate and inspect if the Genre property matches our value sent through the constructor. If it’s not we return the Error property as a validation result. Otherwise, we return success.
调用此属性后,我们将在构造函数中传递 genre 参数。然后,我们必须重写 IsValid 方法。在那里,我们提取要验证的对象,并检查 Genre 属性是否与通过构造函数发送的值匹配。如果不是,我们将返回 Error 属性作为验证结果。否则,我们将返回 success。

To call this custom attribute, we can do something like this:
要调用这个 custom attribute,我们可以做这样的事情:

public class Book
{
    public int Id { get; set; }
    [Required] public string? Name { get; set; }
    [Range(10, int.MaxValue)] public int Price { get; set; }
    [ScienceBook(BookGenre.Science)] public string? Genre { get; set; }
}

Now we can use the IValidatableObject interface:
现在我们可以使用 IValidatableObject 接口:

public class Book : IValidatableObject
{
    public int Id { get; set; }
    [Required] public string? Name { get; set; }
    [Range(10, int.MaxValue)] public int Price { get; set; }
    public string? Genre { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var errorMessage = $"The genre of the book must be {BookGenre.Science}";
        if (!Genre.Equals(BookGenre.Science.ToString()))
            yield return new ValidationResult(errorMessage, new[] { nameof(Genre) });
    }
}

This validation happens in the model class, where we have to implement the Validate method. The code inside that method is pretty straightforward. Also, pay attention that we don’t have to apply any validation attribute on top of the Genre property.
此验证发生在 model 类中,我们必须在其中实现 Validate 方法。该方法中的代码非常简单。此外,请注意,我们不必在 Genre 属性之上应用任何 validation 属性。

As we’ve seen from the previous examples, we can create a custom attribute in a separate class and even make it generic so it could be reused for other model objects. This is not the case with the IValidatableObject interface. It is used inside the model class and of course, the validation logic can’t be reused.
正如我们从前面的示例中看到的,我们可以在单独的类中创建自定义属性,甚至可以将其设置为泛型,以便它可以被其他模型对象重用。IValidatableObject 接口并非如此。它在 model 类中使用,当然,验证逻辑不能重用。

So, this could be something you can think about when deciding which one to use.
因此,这可能是您在决定使用哪一个时可以考虑的事情。

After all of this theory and code samples, we are ready to implement model validation in our code.
在所有这些理论和代码示例之后,我们准备好在代码中实现模型验证。

13.3 Validation while Creating Resource

13.3 创建资源时进行验证

Let’s send another request for the CreateEmployee action, but this time with the invalid request body:‌
让我们为 CreateEmployee作发送另一个请求,但这次的请求正文无效:
https://localhost:5001/api/companies/582ea192-6fb7-44ff-a2a1-08d988ca3ca9/employees

alt text

And we get the 500 Internal Server Error, which is a generic message when something unhandled happens in our code. But this is not good. This means that the server made an error, which is not the case. In this case, we, as a consumer, sent the wrong model to the API — thus the error message should be different.
我们得到 500 Internal Server Error,这是我们的代码中发生未处理的事情时的一般消息。但这并不好。这意味着服务器出错,但事实并非如此。在这种情况下,我们作为消费者向 API 发送了错误的模型——因此错误消息应该不同。

To fix this, let’s modify our EmployeeForCreationDto record because that’s what we deserialize the request body to:
为了解决这个问题,让我们修改我们的 EmployeeForCreationDto 记录,因为这是我们将请求正文反序列化为:

using System.ComponentModel.DataAnnotations;

namespace Shared.DataTransferObjects
{
    public record EmployeeForCreationDto(
        [Required(ErrorMessage = "Employee name is a required field.")]
        [MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters.")]
        string Name,

        [Required(ErrorMessage = "Age is a required field.")]
        int Age,

        [Required(ErrorMessage = "Position is a required field.")]
        [MaxLength(20, ErrorMessage = "Maximum length for the Position is 20 characters.")]

        string Position
        );
}

This is how we can apply validation attributes in our positional records. But, in our opinion, positional records start losing readability once the attributes are applied, and for that reason, we like using init setters if we have to apply validation attributes. So, we are going to do exactly that and modify this position record:
这就是我们在位置记录中应用验证属性的方法。但是,在我们看来,一旦应用了属性,位置记录就会开始失去可读性,因此,如果必须应用验证属性,我们喜欢使用 init setter。所以,我们将这样做并修改这个位置记录:

using System.ComponentModel.DataAnnotations;

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

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

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

Now, we have to modify our action:
现在,我们必须修改我们的作:

//[HttpPost]
//public IActionResult CreateEmployeeForCompany(Guid companyId, [FromBody] EmployeeForCreationDto employee)
//{
//    if (employee is null)
//        return BadRequest("EmployeeForCreationDto object is null");
//    var employeeToReturn = _service.EmployeeService.CreateEmployeeForCompany(companyId, employee, trackChanges: false);
//    return CreatedAtRoute("GetEmployeeForCompany", new { companyId, id = employeeToReturn.Id }, employeeToReturn);
//}

[HttpPost]
public IActionResult CreateEmployeeForCompany(Guid companyId, [FromBody] EmployeeForCreationDto employee)
{
    if (employee is null) return BadRequest("EmployeeForCreationDto object is null");
    if (!ModelState.IsValid) return UnprocessableEntity(ModelState);
    var employeeToReturn = _service.EmployeeService.CreateEmployeeForCompany(companyId, employee, trackChanges: false);
    return CreatedAtRoute("GetEmployeeForCompany", new { companyId, id = employeeToReturn.Id }, employeeToReturn);
}

As mentioned before in the part about the ModelState dictionary, all we have to do is to call the IsValid method and return the UnprocessableEntity response by providing our ModelState.
如前所述,在前面关于 ModelState 字典的部分,我们所要做的就是调用 IsValid 方法,并通过提供 ModelState 返回 UnprocessableEntity 响应。

And that is all.
就这样。

Let’s send our request one more time:
让我们再发送一次我们的请求:

https://localhost:5001/api/companies/582ea192-6fb7-44ff-a2a1-08d988ca3ca9/employees

alt text

Let’s send an additional request to test the max length rule:
让我们发送一个额外的请求来测试 max length 规则:

https://localhost:5001/api/companies/582ea192-6fb7-44ff-a2a1-08d988ca3ca9/employees

alt text

Excellent. It works as expected.
非常好。它按预期工作。

The same actions can be applied for the CreateCompany action and CompanyForCreationDto class — and if you check the source code for this chapter, you will find it implemented.
相同的作可以应用于 CreateCompany作和 CompanyForCreationDto 类 — 如果您查看本章的源代码,您会发现它已实现。

13.3.1 Validating Int Type‌

13.3.1 验证 int 类型

Let’s create one more request with the request body without the age property:
让我们再创建一个请求,请求正文不带 age 属性:

https://localhost:5001/api/companies/582ea192-6fb7-44ff-a2a1-08d988ca3ca9/employees

alt text

We can see that the age property hasn’t been sent, but in the response body, we don’t see the error message for the age property next to other error messages. That is because the age is of type int and if we don’t send that property, it would be set to a default value, which is 0.
我们可以看到 age 属性尚未发送,但在响应正文中,我们没有在其他错误消息旁边看到 age 属性的错误消息。这是因为 age 是 int 类型,如果我们不发送该属性,它将被设置为默认值,即 0。

So, on the server-side, validation for the Age property will pass, because it is not null.
因此,在服务器端,对 Age 属性的验证将通过,因为它不是 null。

To prevent this type of behavior, we have to modify the data annotation attribute on top of the Age property in the EmployeeForCreationDto class:
为了防止此类行为,我们必须修改 EmployeeForCreationDto 类中 Age 属性顶部的数据注释属性:

[Range(18, int.MaxValue, ErrorMessage = "Age is required and it can't be lower than 18")]
public int Age { get; init; }

Now, let’s try to send the same request one more time:
现在,让我们尝试再次发送相同的请求:

https://localhost:5001/api/companies/582ea192-6fb7-44ff-a2a1-08d988ca3ca9/employees

alt text

Now, we have the Age error message in our response.
现在,我们的响应中有 Age 错误消息。

If we want, we can add the custom error messages in our action: ModelState.AddModelError(string key, string errorMessage)
如果需要,我们可以在作中添加自定义错误消息:ModelState.AddModelError(string key, string errorMessage)

With this expression, the additional error message will be included with all the other messages.
使用此表达式,其他错误消息将包含在所有其他消息中。

13.4 Validation for PUT Requests

13.4 PUT 请求的验证

The validation for PUT requests shouldn’t be different from POST requests (except in some cases), but there are still things we have to do to at least optimize our code.‌
PUT 请求的验证不应与 POST 请求不同(在某些情况下除外),但我们仍然需要做一些事情来至少优化我们的代码。

But let’s go step by step.
但是,让我们一步一步来。

First, let’s add Data Annotation Attributes to the EmployeeForUpdateDto record:
首先,让我们将 Data Annotation Attributes 添加到 EmployeeForUpdateDto 记录中:

using System.ComponentModel.DataAnnotations;

namespace Shared.DataTransferObjects
{
    public record EmployeeForUpdateDto
    {
        [Required(ErrorMessage = "Employee name is a required field.")]
        [MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters.")]

        public string? Name { get; init; }

        [Range(18, int.MaxValue, ErrorMessage = "Age is required and it can't be lower than 18")]
        public int Age { get; init; }

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

Once we have done this, we realize we have a small problem. If we compare this class with the DTO class for creation, we are going to see that they are the same. Of course, we don’t want to repeat ourselves, thus we are going to add some modifications.
一旦我们完成了这些,我们就会意识到我们有一个小问题。如果我们将这个类与用于创建的 DTO 类进行比较,我们将发现它们是相同的。当然,我们不想重复自己,因此我们将添加一些修改。

Let’s create a new record in the DataTransferObjects folder:
让我们在 DataTransferObjects 文件夹中创建一个新记录:

using System.ComponentModel.DataAnnotations;

namespace Shared.DataTransferObjects
{
    public abstract record EmployeeForManipulationDto
    {
        [Required(ErrorMessage = "Employee name is a required field.")]
        [MaxLength(30, ErrorMessage = "Maximum length for the Name is 30 characters.")]
        public string? Name { get; init; }

        [Range(18, int.MaxValue, ErrorMessage = "Age is required and it can't be lower than 18")]
        public int Age { get; init; }

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

We create this record as an abstract record because we want our creation and update DTO records to inherit from it:
我们将此记录创建为抽象记录,因为我们希望创建和更新 DTO 记录继承自它:

public record EmployeeForCreationDto : EmployeeForManipulationDto; 
public record EmployeeForUpdateDto : EmployeeForManipulationDto;

Now, we can modify the UpdateEmployeeForCompany action by adding the model validation right after the null check:
现在,我们可以通过在 null 检查后立即添加模型验证来修改 UpdateEmployeeForCompany作:

if (employee is null) return BadRequest("EmployeeForUpdateDto object is null"); 
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);

The same process can be applied to the Company DTO records and actions. You can find it implemented in the source code for this chapter.
相同的过程可以应用于公司 DTO 记录和作。您可以在本章的源代码中找到它实现的。

Let’s test this:
让我们测试一下:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

alt text

Great.Everything works well.
一切都很顺利。

13.5 Validation for PATCH Requests

13.5 PATCH 请求的验证

The validation for PATCH requests is a bit different from the previous ones. We are using the ModelState concept again, but this time we have to place it in the ApplyTo method first:‌
PATCH 请求的验证与前面的验证略有不同。我们再次使用 ModelState 概念,但这次我们必须首先将其放在 ApplyTo 方法中:

patchDoc.ApplyTo(employeeToPatch, ModelState);

But once we do this, we are going to get an error. That’s because the current ApplyTo method comes from the JsonPatch namespace, and we need the method with the same name but from the NewtonsoftJson namespace.
但是一旦我们这样做了,就会得到一个错误。这是因为当前的 ApplyTo 方法来自 JsonPatch 命名空间,我们需要具有相同名称但来自 NewtonsoftJson 命名空间的方法。

Since we have the Microsoft.AspNetCore.Mvc.NewtonsoftJson package installed in the main project, we are going to remove it from there and install it in the Presentation project.
由于我们在主项目中安装了 Microsoft.AspNetCore.Mvc.NewtonsoftJson 包,因此我们将从主项目中删除它并将其安装在 Presentation 项目中。

If we navigate to the ApplyTo method declaration we can find two extension methods:
如果我们导航到 ApplyTo 方法声明,我们可以找到两个扩展方法:

public static class JsonPatchExtensions 
{ 
    public static void ApplyTo<T>(this JsonPatchDocument<T> patchDoc, T objectToApplyTo, ModelStateDictionary modelState) where T : class... 
    public static void ApplyTo<T>(this JsonPatchDocument<T> patchDoc, T objectToApplyTo, ModelStateDictionary modelState, string prefix) where T : class... 
}

We are using the first one.
我们正在使用第一个。

After the package installation, the error in the action will disappear.
安装包后,作中的错误将消失。

Now, right below thee ApplyTo method, we can add our familiar validation logic:
现在,在 ApplyTo 方法的正下方,我们可以添加熟悉的验证逻辑:

patchDoc.ApplyTo(result.employeeToPatch, ModelState); 
if (!ModelState.IsValid) return UnprocessableEntity(ModelState); 
_service.EmployeeService.SaveChangesForPatch(...);

Let’s test this now:
现在让我们测试一下:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

alt text

You can see that it works as it is supposed to.
您可以看到它按预期工作。

But, we have a small problem now. What if we try to send a remove operation, but for the valid path:
但是,我们现在有一个小问题。如果我们尝试发送一个 remove作,但对于有效路径,该怎么办:

alt text

We can see it passes, but this is not good. If you can remember, we said that the remove operation will set the value for the included property to its default value, which is 0. But in the EmployeeForUpdateDto class, we have a Range attribute that doesn’t allow that value to be below 18. So, where is the problem?
我们可以看到它通过,但这并不好。如果您还记得,我们说过 remove作会将 included 属性的值设置为其默认值,即 0。但在 EmployeeForUpdateDto 类中,我们有一个 Range 属性,它不允许该值低于 18。那么,问题出在哪里呢?

Let’s illustrate this for you:
让我们为您说明一下:

alt text

As you can see, we are validating patchDoc which is completely valid at this moment, but we save employeeEntity to the database. So, we need some additional validation to prevent an invalid employeeEntity from being saved to the database:
如你所见,我们正在验证 patchDoc,它目前是完全有效的,但我们把 employeeEntity 保存到数据库中。因此,我们需要一些额外的验证来防止将无效的 employeeEntity 保存到数据库中:

patchDoc.ApplyTo(result.employeeToPatch, ModelState); 
TryValidateModel(result.employeeToPatch); 
if (!ModelState.IsValid) return UnprocessableEntity(ModelState);

We can use the TryValidateModel method to validate the already patched employeeToPatch instance. This will trigger validation and every error will make ModelState invalid. After that, we execute a familiar validation check.
我们可以使用 TryValidateModel 方法来验证已修补的 employeeToPatch 实例。这将触发验证,并且每个错误都会使 ModelState 无效。之后,我们执行熟悉的验证检查。

Now, we can test this again:
现在,我们可以再次测试一下:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D- 4B20-B5DE-024705497D4A

alt text

And we get 422, which is the expected status code.
我们得到 422,这是预期的状态代码。

Ultimate ASP.NET Core Web API 12 WORKING WITH PATCH REQUESTS

12 WORKING WITH PATCH REQUESTS

12 使用PATCH请求

In the previous chapter, we worked with the PUT request to fully update our resource. But if we want to update our resource only partially, we should use PATCH.‌
在上一章中,我们使用了 PUT 请求来完全更新我们的资源。但是,如果我们想只部分更新我们的资源,我们应该使用 PATCH。

The partial update isn’t the only difference between PATCH and PUT. The request body is different as well. For the Company PATCH request, for example, we should use [FromBody]JsonPatchDocument<Company> and not [FromBody]Company as we did with the PUT requests.
部分更新并不是 PATCH 和 PUT 之间的唯一区别。请求正文也不同。例如,对于 Company PATCH 请求,我们应该使用 [FromBody]JsonPatchDocument<Company>,而不是 [FromBody]Company,就像我们对 PUT 请求所做的那样。

Additionally, for the PUT request’s media type, we have used application/json — but for the PATCH request’s media type, we should use application/json-patch+json. Even though the first one would be accepted in ASP.NET Core for the PATCH request, the recommendation by REST standards is to use the second one.
此外,对于 PUT 请求的媒体类型,我们使用了 application/json,但对于 PATCH 请求的媒体类型,我们应该使用 application/json-patch+json。尽管 PATCH 请求的 ASP.NET Core 会接受第一个选项,但 REST 标准建议使用第二个选项。

Let’s see what the PATCH request body looks like:
让我们看看 PATCH 请求正文是什么样子的:

[ { "op": "replace", "path": "/name", "value": "new name" }, { "op": "remove", "path": "/name" } ]

The square brackets represent an array of operations. Every operation is placed between curly brackets. So, in this specific example, we have two operations: Replace and Remove represented by the op property. The path property represents the object’s property that we want to modify and the value property represents a new value.
方括号表示一组作。每个作都放在大括号之间。因此,在这个特定示例中,我们有两个作:由 op 属性表示的 Replace 和 Remove。path 属性表示我们要修改的对象属性,value 属性表示新值。

In this specific example, for the first operation, we replace the value of the name property with a new name. In the second example, we remove the name property, thus setting its value to default.
在这个特定示例中,对于第一个作,我们将 name 属性的值替换为新名称。在第二个示例中,我们删除了 name 属性,从而将其值设置为 default。

There are six different operations for a PATCH request:
PATCH 请求有 6 种不同的操作作:

OPERATION REQUEST BODY EXPLANATION
Add { "op": "add", "path": "/name", "value": "new value" } Assigns a new value to a required property.
Remove { "op": "remove","path": "/name"} Sets a default value to a required property.
Replace { "op": "replace", "path": "/name", "value": "new value" } Replaces a value of a required property to a new value.
Copy {"op": "copy","from": "/name","path": "/title"} Copies the value from a property in the “from” part to the property in the “path” part.
Move { "op": "move", "from": "/name", "path": "/title" } Moves the value from a property in the “from” part to a property in the “path” part.
Test {"op": "test","path": "/name","value": "new value"} Tests if a property has a specified value.

After all this theory, we are ready to dive into the coding part.
在所有这些理论之后,我们准备深入研究编码部分。

12.1 Applying PATCH to the Employee Entity

12.1 将 PATCH 应用于 Employee 实体

Before we start with the code modification, we have to install two required libraries:‌
在我们开始修改代码之前,我们必须安装两个必需的库:

• The Microsoft.AspNetCore.JsonPatch library, in the Presentation project, to support the usage of JsonPatchDocument in our controller and
Presentation项目中的 Microsoft.AspNetCore.JsonPatch 库,用于支持在控制器中使用 JsonPatchDocument 和

• The Microsoft.AspNetCore.Mvc.NewtonsoftJson library, in the main project, to support request body conversion to a PatchDocument once we send our request.
主项目中的 Microsoft.AspNetCore.Mvc.NewtonsoftJson 库,用于支持在发送请求后将请求正文转换为 PatchDocument。

As you can see, we are still using the NewtonsoftJson library to support the PatchDocument conversion. The official statement from Microsoft is that they are not going to replace it with System.Text.Json: “The main reason is that this will require a huge investment from us, with not a very high value-add for the majority of our customers.”.
如您所见,我们仍在使用 NewtonsoftJson 库来支持 PatchDocument 转换。Microsoft 的官方声明是,他们不会用 System.Text.Json 替换它:“主要原因是这需要我们进行大量投资,对于我们的大多数客户来说,附加值不是很高。

By using AddNewtonsoftJson, we are replacing the System.Text.Json formatters for all JSON content. We don’t want to do that so, we are going ton add a simple workaround in the Program class:
通过使用 AddNewtonsoftJson,我们将替换所有 JSON 内容的 System.Text.Json 格式化程序。我们不想这样做,因此,我们将在 Program 类中添加一个简单的解决方法:

NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter() => 
    new ServiceCollection()
    .AddLogging()
    .AddMvc()
    .AddNewtonsoftJson()
    .Services.BuildServiceProvider()
    .GetRequiredService<IOptions<MvcOptions>>()
    .Value.InputFormatters.OfType<NewtonsoftJsonPatchInputFormatter>()
    .First();

By adding a method like this in the Program class, we are creating a local function. This function configures support for JSON Patch using Newtonsoft.Json while leaving the other formatters unchanged.
通过在 Program 类中添加这样的方法,我们正在创建一个本地函数。此函数使用 Newtonsoft.Json 配置对 JSON Patch 的支持,同时保持其他格式化程序不变。

For this to work, we have to include two more namespaces in the class:
为此,我们必须在 class 中再包含两个命名空间:

using Microsoft.AspNetCore.Mvc.Formatters; 
using Microsoft.Extensions.Options;

After that, we have to modify the AddControllers method:
之后,我们必须修改 AddControllers 方法:

builder.Services.AddControllers(config => { 
    config.RespectBrowserAcceptHeader = true; 
    config.ReturnHttpNotAcceptable = true; 
    config.InputFormatters.Insert(0, GetJsonPatchInputFormatter()); 
}).AddXmlDataContractSerializerFormatters()
.AddCustomCSVFormatter()
.AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference)
.Assembly);

// config.InputFormatters.Insert(0, GetJsonPatchInputFormatter());

We are placing our JsonPatchInputFormatter at the index 0 in the InputFormatters list.
我们将 JsonPatchInputFormatter 放在 InputFormatters 列表中的索引 0 处。

We will require a mapping from the Employee type to the EmployeeForUpdateDto type. Therefore, we have to create a mapping rule for that.
我们需要从 Employee 类型到 EmployeeForUpdateDto 类型的映射。因此,我们必须为此创建一个映射规则。

If we take a look at the MappingProfile class, we will see that we have a mapping from the EmployeeForUpdateDto to the Employee type:
如果我们看一下 MappingProfile 类,我们将看到我们有一个从 EmployeeForUpdateDto 到 Employee 类型的映射:

CreateMap<EmployeeForUpdateDto, Employee>();

But we need it another way. To do so, we are not going to create an additional rule; we can just use the ReverseMap method to help us in the process:
但我们需要另一种方式。为此,我们不会创建其他规则;我们可以使用 ReverseMap 方法来在此过程中帮助我们:

CreateMap<EmployeeForUpdateDto, Employee>().ReverseMap();

The ReverseMap method is also going to configure this rule to execute reverse mapping if we ask for it.
ReverseMap 方法还将配置此规则,以便在我们要求时执行反向映射。

After that, we are going to add two new method contracts to the IEmployeeService interface:
之后,我们将向 IEmployeeService 接口添加两个新的方法协定:

using Entities.Models;
using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface IEmployeeService
    {
        IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges);
        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);
        EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges);
        void DeleteEmployeeForCompany(Guid companyId, Guid id, bool trackChanges);
        void UpdateEmployeeForCompany(Guid companyId, Guid id, EmployeeForUpdateDto employeeForUpdate, bool compTrackChanges, bool empTrackChanges);
        (EmployeeForUpdateDto employeeToPatch, Employee employeeEntity) GetEmployeeForPatch(Guid companyId, Guid id, bool compTrackChanges, bool empTrackChanges); 
        void SaveChangesForPatch(EmployeeForUpdateDto employeeToPatch, Employee employeeEntity);
    }
}

Of course, for this to work, we have to add the reference to the Entities project.
当然,要使其正常工作,我们必须添加对 Entities 项目的引用。

Then, we have to implement these two methods in the EmployeeService class:
然后,我们必须在 EmployeeService 类中实现这两个方法:

public (EmployeeForUpdateDto employeeToPatch, Employee employeeEntity) GetEmployeeForPatch(Guid companyId, Guid id, bool compTrackChanges, bool empTrackChanges)
{
    var company = _repository.Company.GetCompany(companyId, compTrackChanges);
    if (company is null)
        throw new CompanyNotFoundException(companyId);
    var employeeEntity = _repository.Employee.GetEmployee(companyId, id, empTrackChanges);
    if (employeeEntity is null)
        throw new EmployeeNotFoundException(companyId);
    var employeeToPatch = _mapper.Map<EmployeeForUpdateDto>(employeeEntity);
    return (employeeToPatch, employeeEntity);
}
public void SaveChangesForPatch(EmployeeForUpdateDto employeeToPatch, Employee employeeEntity)
{
    _mapper.Map(employeeToPatch, employeeEntity);
    _repository.Save();
}

In the first method, we are trying to fetch both the company and employee from the database and if we can’t find either of them, we stop the execution flow and return the NotFound response to the client. Then, we map the employee entity to the EmployeeForUpdateDto type and return both objects (employeeToPatch and employeeEntity) inside the Tuple to the controller.
在第一种方法中,我们尝试从数据库中获取公司和员工,如果找不到他们中的任何一个,我们将停止执行流并将 NotFound 响应返回给客户端。然后,我们将 employee 实体映射到 EmployeeForUpdateDto 类型,并将 Tuple 中的两个对象(employeeToPatch 和 employeeEntity)返回给控制器。

The second method just maps from emplyeeToPatch to employeeEntity and calls the repository's Save method.
第二个方法只是从 emplyeeToPatch 映射到 employeeEntity,并调用存储库的 Save 方法。

Now, we can modify our controller:
现在,我们可以修改我们的控制器:

[HttpPatch("{id:guid}")]
public IActionResult PartiallyUpdateEmployeeForCompany(Guid companyId, Guid id, [FromBody] JsonPatchDocument<EmployeeForUpdateDto> patchDoc)
{
    if (patchDoc is null)
        return BadRequest("patchDoc object sent from client is null.");
    var result = _service.EmployeeService.GetEmployeeForPatch(companyId, id, compTrackChanges: false, empTrackChanges: true);
    patchDoc.ApplyTo(result.employeeToPatch);
    _service.EmployeeService.SaveChangesForPatch(result.employeeToPatch, result.employeeEntity);
    return NoContent();
}

You can see that our action signature is different from the PUT actions. We are accepting the JsonPatchDocument from the request body. After that, we have a familiar code where we check the patchDoc for null value and if it is, we return a BadRequest. Then we call the service method where we map from the Employee type to the EmployeeForUpdateDto type; we need to do that because the patchDoc variable can apply only to the EmployeeForUpdateDto type. After apply is executed, we call another service method to map again to the Employee type (from employeeToPatch to employeeEntity) and save changes in the database. In the end, we return NoContent.
您可以看到我们的作签名与 PUT作不同。我们正在接受来自请求正文的 JsonPatchDocument。之后,我们有一个熟悉的代码,我们在其中检查 patchDoc 是否为 null 值,如果为空,则返回一个 BadRequest。然后,我们调用服务方法,从其中 Employee 类型映射到 EmployeeForUpdateDto 类型;我们需要这样做,因为 patchDoc 变量只能应用于 EmployeeForUpdateDto 类型。执行 apply 后,我们调用另一个服务方法再次映射到 Employee 类型(从 employeeToPatch 到 employeeEntity)并将更改保存在数据库中。最后,我们返回 NoContent。

Don’t forget to include an additional namespace:
不要忘记包含额外的命名空间:

using Microsoft.AspNetCore.JsonPatch;

Now, we can send a couple of requests to test this code:
现在,我们可以发送几个请求来测试此代码:

Let’s first send the replace operation:
让我们首先发送 replace作:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

alt text

It works; we get the 204 No Content message. Let’s check the same employee:
它有效;我们收到 204 No Content 消息。让我们检查同一名员工:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

alt text

And we see the Age property has been changed.
我们看到 Age 属性已更改。

Let’s send a remove operation in a request:
让我们在请求中发送一个 remove作:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

alt text

This works as well. Now, if we check our employee, its age is going to be set to 0 (the default value for the int type):
这也有效。现在,如果我们检查我们的员工,它的 age 将被设置为 0(int 类型的默认值):

alt text

Finally, let’s return a value of 28 for the Age property:
最后,让我们为 Age 属性返回值 28:

https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

alt text

Let’s check the employee now:
现在让我们检查一下员工:

alt text

Excellent.
非常好。

Everything works as expected.
一切都按预期进行。

Ultimate ASP.NET Core Web API 11 WORKING WITH PUT REQUESTS

11 WORKING WITH PUT REQUESTS
11 使用 PUT 请求

In this section, we are going to show you how to update a resource using the PUT request. We are going to update a child resource first and then we are going to show you how to execute insert while updating a parent resource.‌
在本节中,我们将向您展示如何使用 PUT 请求更新资源。我们将首先更新子资源,然后我们将向您展示如何在更新父资源时执行 insert。

11.1 Updating Employee

11.1 更新员工

In the previous sections, we first changed our interface, then the repository/service classes, and finally the controller. But for the update, this doesn’t have to be the case.‌
在前面的部分中,我们首先更改了接口,然后更改了存储库/服务类,最后更改了控制器。但对于更新,情况并非必须如此。

Let’s go step by step.
让我们一步一步来。

The first thing we are going to do is to create another DTO record for update purposes:
我们要做的第一件事是创建另一个 DTO 记录以进行更新:

public record EmployeeForUpdateDto(string Name, int Age, string Position);

We do not require the Id property because it will be accepted through the URI, like with the DELETE requests. Additionally, this DTO contains the same properties as the DTO for creation, but there is a conceptual difference between those two DTO classes. One is for updating and the other is for creating. Furthermore, once we get to the validation part, we will understand the additional difference between those two.
我们不需要 Id 属性,因为它将通过 URI 接受,就像 DELETE 请求一样。此外,此 DTO 包含与用于创建的 DTO 相同的属性,但这两个 DTO 类之间存在概念差异。一个用于更新,另一个用于创建。此外,一旦我们进入验证部分,我们将了解这两者之间的额外区别。

Because we have an additional DTO record, we require an additional mapping rule:
因为我们有额外的 DTO 记录,所以我们需要额外的映射规则:

CreateMap<EmployeeForUpdateDto, Employee>();

After adding the mapping rule, we can modify the IEmployeeService interface:
添加映射规则后,我们可以修改 IEmployeeService 接口:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface IEmployeeService
    {
        IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges);
        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);
        EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges);
        void DeleteEmployeeForCompany(Guid companyId, Guid id, bool trackChanges);
        void UpdateEmployeeForCompany(Guid companyId, Guid id, EmployeeForUpdateDto employeeForUpdate, bool compTrackChanges, bool empTrackChanges);
    }
}

We are declaring a method that contains both id parameters – one for the company and one for employee, the employeeForUpdate object sent from the client, and two track changes parameters, again, one for the company and one for the employee. We are doing that because we won't track changes while fetching the company entity, but we will track changes while fetching the employee.
我们声明了一个包含两个 id 参数的方法 – 一个用于公司,一个用于员工,从客户端发送的 employeeForUpdate 对象,以及两个跟踪更改参数,同样,一个用于公司,一个用于员工。我们这样做是因为我们不会在获取公司实体时跟踪更改,但我们会在获取员工时跟踪更改。

That said, let’s modify the EmployeeService class:
也就是说,让我们修改 EmployeeService 类:

public void UpdateEmployeeForCompany(Guid companyId, Guid id, EmployeeForUpdateDto employeeForUpdate, bool compTrackChanges, bool empTrackChanges)
{
    var company = _repository.Company.GetCompany(companyId, compTrackChanges);
    if (company is null)
        throw new CompanyNotFoundException(companyId);
    var employeeEntity = _repository.Employee.GetEmployee(companyId, id, empTrackChanges);
    if (employeeEntity is null)
        throw new EmployeeNotFoundException(id);
    _mapper.Map(employeeForUpdate, employeeEntity);
    _repository.Save();
}

So first, we fetch the company from the database. If it doesn’t exist, we interrupt the flow and send the response to the client. After that, we do the same thing for the employee. But there is one difference here. Pay attention to the way we fetch the company and the way we fetch the employeeEntity. Do you see the difference?
因此,首先,我们从数据库中获取公司。如果不存在,我们将中断流并将响应发送到客户端。之后,我们为员工做同样的事情。但这里有一个区别。注意我们获取 company 的方式以及我们获取 employeeEntity 的方式。您看到区别了吗?

As we’ve already said: the trackChanges parameter will be set to true for the employeeEntity. That’s because we want EF Core to track changes on this entity. This means that as soon as we change any property in this entity, EF Core will set the state of that entity to Modified.
正如我们已经说过的:employeeEntity 的 trackChanges 参数将设置为 true。这是因为我们希望 EF Core 跟踪此实体上的更改。这意味着,一旦我们更改此实体中的任何属性,EF Core 就会将该实体的状态设置为 Modified。

As you can see, we are mapping from the employeeForUpdate object (we will change just the age property in a request) to the employeeEntity — thus changing the state of the employeeEntity object to Modified.
如您所见,我们正在从 employeeForUpdate 对象(我们只更改请求中的 age 属性)映射到 employeeEntity,从而将 employeeEntity 对象的状态更改为 Modified。

Because our entity has a modified state, it is enough to call the Save method without any additional update actions. As soon as we call the Save method, our entity is going to be updated in the database.
由于我们的实体具有已修改的状态,因此调用 Save 方法就足够了,无需任何其他更新作。调用 Save 方法后,我们的实体将在数据库中更新。

Now, when we have all of these, let’s modify the EmployeesController:
现在,当我们拥有所有这些时,让我们修改 EmployeesController:

[HttpPut("{id:guid}")]
public IActionResult UpdateEmployeeForCompany(Guid companyId, Guid id, [FromBody] EmployeeForUpdateDto employee)
{
    if (employee is null) return BadRequest("EmployeeForUpdateDto object is null");
    _service.EmployeeService.UpdateEmployeeForCompany(companyId, id, employee, compTrackChanges: false, empTrackChanges: true);
    return NoContent();
}

We are using the PUT attribute with the id parameter to annotate this action. That means that our route for this action is going to be: api/companies/{companyId}/employees/{id}.
我们使用带有 id 参数的 PUT 属性来注释此作。这意味着此作的路由将为:api/companies/{companyId}/employees/{id}。

Then, we check if the employee object is null, and if it is, we return a BadRequest response.
然后,我们检查 employee 对象是否为 null,如果为 null,则返回 BadRequest 响应。

After that, we just call the update method from the service layer and pass false for the company track changes and true for the employee track changes.
之后,我们只需从服务层调用 update 方法,并为公司跟踪变化传递 false,为员工跟踪变化传递 true。

Finally, we return the 204 NoContent status.
最后,我们返回 204 NoContent 状态。

We can test our action:
我们可以测试我们的操作:
https://localhost:5001/api/companies/C9D4C053-49B6-410C-BC78-2D54A9991870/employees/80ABBCA8-664D-4B20-B5DE-024705497D4A

alt text

And it works; we get the 204 No Content status.
它奏效了;我们得到 204 No Content 状态。

We can check our executed query through EF Core to confirm that only the Age column is updated:
我们可以通过 EF Core 检查已执行的查询,以确认仅更新了 Age 列:

alt text

Excellent.
非常好。

You can send the same request with the invalid company id or employee id. In both cases, you should get a 404 response, which is a valid response to this kind of situation.
您可以使用无效的公司 ID 或员工 ID 发送相同的请求。在这两种情况下,您都应该得到 404 响应,这是对这种情况的有效响应。

NOTE: We’ve changed only the Age property, but we have sent all the other properties with unchanged values as well. Therefore, Age is only updated in the database. But if we send the object with just the Age property, other properties will be set to their default values and the whole object will be updated — not just the Age column. That’s because the PUT is a request for a full update. This is very important to know.
注意:我们只更改了 Age 属性,但我们也发送了值未更改的所有其他属性。因此,仅在数据库中更新 Age。但是,如果我们只发送具有 Age 属性的对象,则其他属性将设置为其默认值,并且整个对象将更新,而不仅仅是 Age 列。这是因为 PUT 是完全更新的请求。了解这一点非常重要。

11.1.1 About the Update Method from the RepositoryBase Class‌

11.1.1 关于 RepositoryBase 类中的 Update 方法

Right now, you might be asking: “Why do we have the Update method in the RepositoryBase class if we are not using it?”
现在,您可能会问:“如果我们不使用 Update 方法,为什么我们在 RepositoryBase 类中有它?

The update action we just executed is a connected update (an update where we use the same context object to fetch the entity and to update it). But sometimes we can work with disconnected updates. This kind of update action uses different context objects to execute fetch and update actions or sometimes we can receive an object from a client with the Id property set as well, so we don’t have to fetch it from the database. In that situation, all we have to do is to inform EF Core to track changes on that entity and to set its state to modified. We can do both actions with the Update method from our RepositoryBase class. So, you see, having that method is crucial as well.
我们刚刚执行的更新作是连接更新(我们使用相同的上下文对象来获取实体并更新它的更新)。但有时我们可以使用断开连接的更新。这种更新作使用不同的上下文对象来执行 fetch 和 update作,或者有时我们也可以从设置了 Id 属性的客户端接收对象,因此我们不必从数据库中获取它。在这种情况下,我们只需通知 EF Core 跟踪该实体的更改,并将其状态设置为 modified。我们可以使用 RepositoryBase 类中的 Update 方法执行这两个作。所以,你看,拥有这种方法也很重要。

One note, though. If we use the Update method from our repository, even if we change just the Age property, all properties will be updated in the database.
不过,有一点需要注意。如果我们使用存储库中的 Update 方法,即使我们只更改 Age 属性,所有属性都将在数据库中更新。

11.2 Inserting Resources while Updating One

11.2 在更新资源时插入资源

While updating a parent resource, we can create child resources as well without too much effort. EF Core helps us a lot with that process. Let’s see how.‌
在更新父资源时,我们也可以创建子资源,而无需太多工作。EF Core 在此过程中为我们提供了很大帮助。让我们看看如何作。

The first thing we are going to do is to create a DTO record for update:
我们要做的第一件事是创建一个用于更新的 DTO 记录:

public record CompanyForUpdateDto(string Name, string Address, string Country, IEnumerable<EmployeeForCreationDto> Employees);

After this, let’s create a new mapping rule:
在此之后,让我们创建一个新的映射规则:

CreateMap<CompanyForUpdateDto, Company>();

Then, let’s move on to the interface modification:
然后,让我们继续进行接口修改:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges);
        CompanyDto GetCompany(Guid companyId, bool trackChanges);
        CompanyDto CreateCompany(CompanyForCreationDto company);
        IEnumerable<CompanyDto> GetByIds(IEnumerable<Guid> ids, bool trackChanges);
        (IEnumerable<CompanyDto> companies, string ids) CreateCompanyCollection(IEnumerable<CompanyForCreationDto> companyCollection);
        void DeleteCompany(Guid companyId, bool trackChanges);
        void UpdateCompany(Guid companyid, CompanyForUpdateDto companyForUpdate, bool trackChanges);
    }
}

And of course, the service class modification:
当然,服务类修改:

public void UpdateCompany(Guid companyId, CompanyForUpdateDto companyForUpdate, bool trackChanges)
{
    var companyEntity = _repository.Company.GetCompany(companyId, trackChanges);
    if (companyEntity is null)
        throw new CompanyNotFoundException(companyId);
    _mapper.Map(companyForUpdate, companyEntity);
    _repository.Save();
}

So again, we fetch our company entity from the database, and if it is null, we just return the NotFound response. But if it’s not null, we map the companyForUpdate DTO to companyEntity and call the Save method.
因此,我们从数据库中获取我们的公司实体,如果它是 null,我们只返回 NotFound 响应。但如果它不为 null,我们将 companyForUpdate DTO 映射到 companyEntity 并调用 Save 方法。

Right now, we can modify our controller:
现在,我们可以修改我们的控制器:

[HttpPut("{id:guid}")]
public IActionResult UpdateCompany(Guid id, [FromBody] CompanyForUpdateDto company)
{
    if (company is null)
        return BadRequest("CompanyForUpdateDto object is null");
    _service.CompanyService.UpdateCompany(id, company, trackChanges: true);
    return NoContent();
}

That’s it. You can see that this action is almost the same as the employee update action.
就是这样。您可以看到,此作与 employee update作几乎相同。

Let’s test this now:
现在让我们测试一下:
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3i

alt text

We modify the name of the company and attach an employee as well. As a result, we can see 204, which means that the entity has been updated. But what about that new employee?
我们修改公司名称并附加员工。结果,我们可以看到 204,这意味着该实体已更新。但是那位新员工呢?

Let’s inspect our query:
我们来检查一下我们的查询:

alt text

You can see that we have created the employee entity in the database. So, EF Core does that job for us because we track the company entity. As soon as mapping occurs, EF Core sets the state for the company entity to modified and for all the employees to added. After we call the Save method, the Name property is going to be modified and the employee entity is going to be created in the database.
您可以看到我们已经在数据库中创建了 employee 实体。因此,EF Core 为我们完成了这项工作,因为我们跟踪公司实体。映射发生后,EF Core 会将公司实体的状态设置为 modified,并将所有员工的状态设置为 added。调用 Save 方法后,将修改 Name 属性,并在数据库中创建 employee 实体。

We are finished with the PUT requests, so let’s continue with PATCH.
我们已经完成了 PUT 请求,所以让我们继续 PATCH。

Ultimate ASP.NET Core Web API 10 WORKING WITH DELETE REQUESTS

10 WORKING WITH DELETE REQUESTS

10 使用 DELETE 请求

Let’s start this section by deleting a child resource first. So, let’s modify the IEmployeeRepository interface:‌
让我们先删除子资源来开始本节。因此,让我们修改 IEmployeeRepository 接口:

using Entities.Models;

namespace Contracts
{
    public interface IEmployeeRepository
    {
        IEnumerable<Employee> GetEmployees(Guid companyId, bool trackChanges);
        Employee GetEmployee(Guid companyId, Guid id, bool trackChanges);
        void CreateEmployeeForCompany(Guid companyId, Employee employee);
        void DeleteEmployee(Employee employee);
    }
}

The next step for us is to modify the EmployeeRepository class:
下一步是修改 EmployeeRepository 类:

public void DeleteEmployee(Employee employee) => Delete(employee);

After that, we have to modify the IEmployeeService interface:
之后,我们必须修改 IEmployeeService 接口:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface IEmployeeService
    {
        IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges);
        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);
        EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges);
        void DeleteEmployeeForCompany(Guid companyId, Guid id, bool trackChanges);
    }
}

And of course, the EmployeeService class:
当然,还有 EmployeeService 类:

public void DeleteEmployeeForCompany(Guid companyId, Guid id, bool trackChanges)
{
    var company = _repository.Company.GetCompany(companyId, trackChanges);
    if (company is null) throw new CompanyNotFoundException(companyId);
    var employeeForCompany = _repository.Employee.GetEmployee(companyId, id, trackChanges);
    if (employeeForCompany is null)
throw new EmployeeNotFoundException(id);
    _repository.Employee.DeleteEmployee(employeeForCompany);
    _repository.Save();
}

Pretty straightforward method implementation where we fetch the company and if it doesn’t exist, we return the Not Found response. If it exists, we fetch the employee for that company and execute the same check, where if it’s true, we return another not found response. Lastly, we delete the employee from the database.
非常简单的方法实现,我们获取公司,如果它不存在,我们返回 Not Found 响应。如果存在,我们获取该公司的员工并执行相同的检查,如果为 true,则返回另一个 not found 响应。最后,我们从数据库中删除该员工。

Finally, we can add a delete action to the controller class:
最后,我们可以向 controller 类添加一个 delete作:

[HttpDelete("{id:guid}")]
public IActionResult DeleteEmployeeForCompany(Guid companyId, Guid id)
{
    _service.EmployeeService.DeleteEmployeeForCompany(companyId, id, trackChanges: false);
    return NoContent();
}

There is nothing new with this action. We collect the companyId from the root route and the employee’s id from the passed argument. Call the service method and return the NoContent() method, which returns the status code 204 No Content.
此作没有什么新内容。我们从根路由中收集 companyId,从传递的参数中收集员工的 ID。调用 service 方法并返回 NoContent() 方法,该方法返回状态代码 204 No Content。

Let’s test this:
我们来测试一下
https://localhost:5001/api/companies/14759d51-e9c1-4afc-f9bf-08d98898c9c3/employees/e06cfcc6-e353-4bd8-0870-08d988af0956

alt text

Excellent. It works great.
非常好。它效果很好。

You can try to get that employee from the database, but you will get 404 for sure:
你可以尝试从数据库中获取该员工,但你肯定会得到 404:
https://localhost:5001/api/companies/14759d51-e9c1-4afc-f9bf-08d98898c9c3/employees/e06cfcc6-e353-4bd8-0870-08d988af0956

alt text

We can see that the DELETE request isn’t safe because it deletes the resource, thus changing the resource representation. But if we try to send this delete request one or even more times, we would get the same 404 result because the resource doesn’t exist anymore. That’s what makes the DELETE request idempotent.
我们可以看到 DELETE 请求不安全,因为它会删除资源,从而更改资源表示形式。但是,如果我们尝试发送一次甚至多次此删除请求,我们将得到相同的 404 结果,因为资源不再存在。这就是 DELETE 请求具有幂等性的原因。

10.1 Deleting a Parent Resource with its Children

10.1 删除父资源及其子项

With Entity Framework Core, this action is pretty simple. With the basic configuration, cascade deleting is enabled, which means deleting a parent resource will automatically delete all of its children. We can confirm that from the migration file:‌
使用 Entity Framework Core,此作非常简单。使用基本配置时,启用了级联删除,这意味着删除父资源将自动删除其所有子资源。我们可以从迁移文件中确认:

alt text

So, all we have to do is to create a logic for deleting the parent resource.
因此,我们所要做的就是创建一个用于删除父资源的逻辑。

Well, let’s do that following the same steps as in a previous example:
好吧,让我们按照与上一个示例相同的步骤来执行此作:

using Entities.Models;

namespace Contracts
{
    public interface ICompanyRepository
    {
        IEnumerable<Company> GetAllCompanies(bool trackChanges); 
        Company GetCompany(Guid companyId, bool trackChanges);
        void CreateCompany(Company company);

        IEnumerable<Company> GetByIds(IEnumerable<Guid> ids, bool trackChanges);
        void DeleteCompany(Company company);
    }
}

Then let’s modify the repository class:
然后让我们修改 repository 类:

public void DeleteCompany(Company company) => Delete(company);

Then we have to modify the service interface:
然后我们就得修改服务接口了:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges);
        CompanyDto GetCompany(Guid companyId, bool trackChanges);
        CompanyDto CreateCompany(CompanyForCreationDto company);
        IEnumerable<CompanyDto> GetByIds(IEnumerable<Guid> ids, bool trackChanges);
        (IEnumerable<CompanyDto> companies, string ids) CreateCompanyCollection(IEnumerable<CompanyForCreationDto> companyCollection);
        void DeleteCompany(Guid companyId, bool trackChanges);
    }
}

And the service class:
而 service 类:

public void DeleteCompany(Guid companyId, bool trackChanges)
{
    var company = _repository.Company.GetCompany(companyId, trackChanges);
    if (company is null)
        throw new CompanyNotFoundException(companyId);
    _repository.Company.DeleteCompany(company);
    _repository.Save();
}

Finally, let’s modify our controller:
最后,让我们修改我们的控制器:

[HttpDelete("{id:guid}")]
public IActionResult DeleteCompany(Guid id)
{
    _service.CompanyService.DeleteCompany(id, trackChanges: false);
    return NoContent();
}

And let’s test our action:
让我们测试一下我们的操作:
https://localhost:5001/api/companies/0AD5B971-FF51-414D-AF01-34187E407557

alt text

It works.
它有效。

You can check in your database that this company alongside its children doesn’t exist anymore.
您可以在数据库中检查这家公司及其子公司是否不再存在。

There we go. We have finished working with DELETE requests and we are ready to continue to the PUT requests.
好了。我们已经完成了 DELETE 请求的处理,并准备继续处理 PUT 请求。

Ultimate ASP.NET Core Web API 9 CREATING RESOURCES

9 CREATING RESOURCES
9 创建资源

In this section, we are going to show you how to use the POST HTTP method to create resources in the database.‌
在本节中,我们将向您展示如何使用 POST HTTP 方法在数据库中创建资源。

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

9.1 Handling POST Requests

9.1 处理 POST 请求

Firstly, let’s modify the decoration attribute for the GetCompany action in the Companies controller:‌
首先,让我们在 Companies 控制器中修改 GetCompany作的 decoration 属性:

[HttpGet("{id:guid}", Name = "CompanyById")]

With this modification, we are setting the name for the action. This name will come in handy in the action method for creating a new company.
通过此修改,我们将设置作的名称。这个名字在创建新公司的作方法中会派上用场。

We have a DTO class for the output (the GET methods), but right now we need the one for the input as well. So, let’s create a new record in the Shared/DataTransferObjects folder:
我们有一个用于输出的 DTO 类(GET 方法),但现在我们也需要一个用于输入的 DTO 类。因此,让我们在 Shared/DataTransferObjects 文件夹中创建一个新记录:

public record CompanyForCreationDto(string Name, string Address, string Country);

We can see that this DTO record is almost the same as the Company record but without the Id property. We don’t need that property when we create an entity.
我们可以看到,此 DTO 记录与 Company 记录几乎相同,但没有 Id 属性。在创建实体时,我们不需要该属性。

We should pay attention to one more thing. In some projects, the input and output DTO classes are the same, but we still recommend separating them for easier maintenance and refactoring of our code. Furthermore, when we start talking about validation, we don’t want to validate the output objects — but we definitely want to validate the input ones.
我们还应该注意一件事。在某些项目中,输入和输出 DTO 类是相同的,但我们仍然建议将它们分开,以便于维护和重构代码。此外,当我们开始讨论验证时,我们不想验证输出对象 — 但我们肯定想要验证输入对象。

With all of that said and done, let’s continue by modifying the ICompanyRepository interface:
完成所有这些作后,让我们继续修改 ICompanyRepository 接口:

using Entities.Models;

namespace Contracts
{
    public interface ICompanyRepository
    {
        IEnumerable<Company> GetAllCompanies(bool trackChanges); 
        Company GetCompany(Guid companyId, bool trackChanges);
        void CreateCompany(Company company);
    }
}

After the interface modification, we are going to implement that interface:
修改接口后,我们将实现该接口:

public void CreateCompany(Company company) => Create(company);

We don’t explicitly generate a new Id for our company; this would be done by EF Core. All we do is to set the state of the company to Added.
我们不会为公司显式生成新 ID;这将由 EF Core 完成。我们所做的只是将公司的状态设置为 Added。

Next, we want to modify the ICompanyService interface:
接下来,我们要修改 ICompanyService 接口:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges);
        CompanyDto GetCompany(Guid companyId, bool trackChanges);
        CompanyDto CreateCompany(CompanyForCreationDto company);
    }
}

And of course, we have to implement this method in the CompanyService class:
当然,我们必须在 CompanyService 类中实现此方法:

public CompanyDto CreateCompany(CompanyForCreationDto company)
{
    var companyEntity = _mapper.Map<Company>(company);
    _repository.Company.CreateCompany(companyEntity);
    _repository.Save();
    var companyToReturn = _mapper.Map<CompanyDto>(companyEntity);
    return companyToReturn;
}

Here, we map the company for creation to the company entity, call the repository method for creation, and call the Save() method to save the entity to the database. After that, we map the company entity to the company DTO object to return it to the controller.
在这里,我们将要创建的公司映射到公司实体,调用用于创建的存储库方法,并调用 Save() 方法将实体保存到数据库。之后,我们将公司实体映射到公司 DTO 对象,以将其返回给控制器。

But we don’t have the mapping rule for this so we have to create another mapping rule for the Company and CompanyForCreationDto objects.Let’s do this in the MappingProfile class:
但是我们没有用于此目的的映射规则,因此我们必须为 Company 和 CompanyForCreationDto 对象创建另一个映射规则。让我们在 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)));

            CreateMap<Employee, EmployeeDto>();

            CreateMap<CompanyForCreationDto, Company>();
        }
    }
}

Our POST action will accept a parameter of the type CompanyForCreationDto, and as you can see our service method accepts the parameter of the same type as well, but we need the Company object to send it to the repository layer for creation. Therefore, we have to create this mapping rule.
我们的 POST作将接受 CompanyForCreationDto 类型的参数,如您所见,我们的服务方法也接受相同类型的参数,但我们需要 Company 对象将其发送到存储库层进行创建。因此,我们必须创建此映射规则。

Last, let’s modify the controller:
最后,我们修改控制器:

[HttpPost]
public IActionResult CreateCompany([FromBody] CompanyForCreationDto company)
{
    if (company is null)
        return BadRequest("CompanyForCreationDto object is null");
    var createdCompany = _service.CompanyService.CreateCompany(company);

    return CreatedAtRoute("CompanyById", new { id = createdCompany.Id }, createdCompany);
}

Let’s use Postman to send the request and examine the result:
让我们使用 Postman 发送请求并检查结果:
https://localhost:5001/api/companies

alt text

9.2 Code Explanation

9.2 代码说明

Let’s talk a little bit about this code. The interface and the repository parts are pretty clear, so we won’t talk about that. We have already explained the code in the service method. But the code in the controller contains several things worth mentioning.‌
我们来谈谈这段代码。界面和存储库部分非常清晰,因此我们不会讨论这些。我们已经在 service 方法中解释了代码。但是控制器中的代码包含几个值得一提的内容。

If you take a look at the request URI, you’ll see that we use the same one as for the GetCompanies action: api/companies — but this time we are using the POST request.
如果您查看请求 URI,您会发现我们使用与 GetCompanies作相同的 URI:api/companies,但这次我们使用的是 POST 请求。

The CreateCompany method has its own [HttpPost] decoration attribute, which restricts it to POST requests. Furthermore, notice the company parameter which comes from the client. We are not collecting it from the URI but the request body. Thus the usage of the [FromBody] attribute. Also, the company object is a complex type; therefore, we have to use [FromBody].
CreateCompany 方法具有自己的 [HttpPost] 修饰属性,该属性将其限制为 POST 请求。此外,请注意来自客户端的 company 参数。我们不是从 URI 中收集数据,而是从请求正文中收集数据。因此,使用[FromBody] 属性。此外,company 对象是一个复杂类型;因此,我们必须使用 [FromBody]。

If we wanted to, we could explicitly mark the action to take this parameter from the URI by decorating it with the [FromUri] attribute, though we wouldn’t recommend that at all because of security reasons and the complexity of the request.
如果需要,我们可以通过使用 [FromUri] 属性修饰作来显式标记从 URI 中获取此参数的作,尽管出于安全原因和请求的复杂性,我们根本不建议这样做。

Because the company parameter comes from the client, it could happen that it can’t be deserialized. As a result, we have to validate it against the reference type’s default value, which is null.
由于 company 参数来自客户端,因此可能无法对其进行反序列化。因此,我们必须根据引用类型的默认值 (null) 对其进行验证。

The last thing to mention is this part of the code:
最后要提到的是这部分代码:

CreatedAtRoute("CompanyById", new { id = companyToReturn.Id }, companyToReturn);

CreatedAtRoute will return a status code 201, which stands for Created. Also, it will populate the body of the response with the new company object as well as the Location attribute within the response header with the address to retrieve that company. We need to provide the name of the action, where we can retrieve the created entity.
CreatedAtRoute 将返回状态代码 201,代表 Created。此外,它还将使用新的 company 对象以及响应标头中的 Location 属性填充响应正文,其中包含用于检索该公司的地址。我们需要提供作的名称,以便我们可以在其中检索创建的实体。

If we take a look at the headers part of our response, we are going to see a link to retrieve the created company:
如果我们查看响应的 headers 部分,我们将看到一个链接来检索创建的公司:

alt text

Finally, from the previous example, we can confirm that the POST method is neither safe nor idempotent. We saw that when we send the POST request, it is going to create a new resource in the database — thus changing the resource representation. Furthermore, if we try to send this request a couple of times, we will get a new object for every request (it will have a different Id for sure).
最后,从前面的示例中,我们可以确认 POST 方法既不安全也不幂等。我们看到,当我们发送 POST 请求时,它将在数据库中创建一个新资源——从而改变资源表示形式。此外,如果我们尝试多次发送此请求,我们将为每个请求获取一个新对象(它肯定会具有不同的 Id)。

Excellent.
非常好。

There is still one more thing we need to explain.
还有一件事我们需要解释。

9.2.1 Validation from the ApiController Attribute‌

9.2.1 从 ApiController 属性进行验证

In this section, we are going to talk about the [ApiController] attribute that we can find right below the [Route] attribute in our controller:
在本节中,我们将讨论 [ApiController] 属性,我们可以在控制器中的 [Route] 属性正下方找到该属性:

[Route("api/companies")] [ApiController] public class CompaniesController : ControllerBase {

But, before we start with the explanation, let’s place a breakpoint in the CreateCompany action, right on the if (company is null) check.Then, let’s use Postman to send an invalid POST request:
但是,在开始解释之前,让我们在 CreateCompany作中放置一个断点,就在 if (company is null) 检查上。然后,让我们使用 Postman 发送一个无效的 POST 请求:
https://localhost:5001/api/companies

alt text

We are going to talk about Validation in chapter 13, but for now, we have to explain a couple of things.
我们将在第 13 章讨论验证,但现在,我们必须解释几件事。

First of all, we have our response - a Bad Request in Postman, and we have error messages that state what’s wrong with our request. But, we never hit that breakpoint that we’ve placed inside the CreateCompany action.
首先,我们有我们的响应 - Postman 中的 Bad Request,并且我们有错误消息说明我们的请求出了什么问题。但是,我们从未遇到放置在 CreateCompany作中的断点。

Why is that?
为什么?

Well, the [ApiController] attribute is applied to a controller class to enable the following opinionated, API-specific behaviors:
嗯,[ApiController] 属性应用于控制器类,以启用以下特定于 API 的固执己见的行为:

• Attribute routing requirement
属性路由要求

• Automatic HTTP 400 responses
自动 HTTP 400 响应

• Binding source parameter inference
绑定源参数推理

• Multipart/form-data request inference
部分/表单数据请求推理

• Problem details for error status codes
错误状态代码的问题详细信息

As you can see, it handles the HTTP 400 responses, and in our case, since the request’s body is null, the [ApiController] attribute handles that and returns the 400 (BadReqeust) response before the request even hits the CreateCompany action.
如您所见,它处理 HTTP 400 响应,在我们的示例中,由于请求正文为 null,因此 [ApiController] 属性会处理该响应,并在请求到达 CreateCompany作之前返回 400 (BadReqeust) 响应。

This is useful behavior, but it prevents us from sending our custom responses with different messages and status codes to the client. This will be very important once we get to the Validation.
这是有用的行为,但它会阻止我们将具有不同消息和状态代码的自定义响应发送到客户端。一旦我们进入验证,这将非常重要。

So to enable our custom responses from the actions, we are going to add this code into the Program class right above the AddControllers method:
因此,为了启用来自作的自定义响应,我们将以下代码添加到 AddControllers 方法正上方的 Program 类中:

builder.Services.Configure<ApiBehaviorOptions>(options => { options.SuppressModelStateInvalidFilter = true; });

With this, we are suppressing a default model state validation that is implemented due to the existence of the [ApiController] attribute in all API controllers. So this means that we can solve the same problem differently, by commenting out or removing the [ApiController] attribute only, without additional code for suppressing validation. It's all up to you. But we like keeping it in our controllers because, as you could’ve seen, it provides additional functionalities other than just 400 – Bad Request responses.
这样,我们将禁止默认模型状态验证,该验证是由于所有 API 控制器中存在 [ApiController] 属性而实现的。因此,这意味着我们可以以不同的方式解决相同的问题,只需注释掉或删除 [ApiController] 属性,而无需额外的代码来抑制验证。这一切都取决于您。但是我们喜欢将它保存在我们的控制器中,因为正如你所看到的,它提供了额外的功能,而不仅仅是 400 – Bad Request 响应。

Now, once we start the app and send the same request, we will hit that breakpoint and see our response in Postman.
现在,一旦我们启动应用程序并发送相同的请求,我们将命中该断点并在 Postman 中看到我们的响应。

Nicely done.
干得漂亮。

Now, we can remove that breakpoint and continue with learning about the creation of child resources.
现在,我们可以删除该断点并继续了解子资源的创建。

9.3 Creating a Child Resource

9.3 创建子资源

While creating our company, we created the DTO object required for the CreateCompany action. So, for employee creation, we are going to do the same thing:‌
在创建公司时,我们创建了 CreateCompany作所需的 DTO 对象。所以,对于员工创建,我们将做同样的事情:

public record EmployeeForCreationDto(string Name, int Age, string Position);

We don’t have the Id property because we are going to create that Id on the server-side. But additionally, we don’t have the CompanyId because we accept that parameter through the route:[Route("api/companies/{companyId}/employees")]
我们没有 Id 属性,因为我们要在服务器端创建该 Id。但此外,我们没有 CompanyId,因为我们通过路由接受该参数:[Route(“api/companies/{companyId}/employees”)]

The next step is to modify the IEmployeeRepository interface:
下一步是修改 IEmployeeRepository 接口:

using Entities.Models;

namespace Contracts
{
    public interface IEmployeeRepository
    {
        IEnumerable<Employee> GetEmployees(Guid companyId, bool trackChanges);
        Employee GetEmployee(Guid companyId, Guid id, bool trackChanges);
        void CreateEmployeeForCompany(Guid companyId, Employee employee);
    }
}

Of course, we have to implement this interface:
当然,我们必须实现这个接口:

using Contracts;
using Entities.Models;

namespace Repository
{
    public class EmployeeRepository : RepositoryBase<Employee>, IEmployeeRepository
    {
        public EmployeeRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
        public IEnumerable<Employee> GetEmployees(Guid companyId, bool trackChanges) => FindByCondition(e => e.CompanyId.Equals(companyId), trackChanges).OrderBy(e => e.Name).ToList();
        public Employee GetEmployee(Guid companyId, Guid id, bool trackChanges) => FindByCondition(e => e.CompanyId.Equals(companyId) && e.Id.Equals(id), trackChanges).SingleOrDefault();
        public void CreateEmployeeForCompany(Guid companyId, Employee employee) { employee.CompanyId = companyId; Create(employee); }

    }
}

Because we are going to accept the employee DTO object in our action and send it to a service method, but we also have to send an employee object to this repository method, we have to create an additional mapping rule in the MappingProfile class:
因为我们要在作中接受 employee DTO 对象并将其发送到 service 方法,但我们还必须将 employee 对象发送到此存储库方法,因此我们必须在 MappingProfile 类中创建额外的映射规则:

CreateMap<EmployeeForCreationDto, Employee>();

The next thing we have to do is IEmployeeService modification:
接下来我们要做的是修改 IEmployeeService:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface IEmployeeService
    {
        IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges);
        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);
        EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges);
    }
}

And implement this new method in EmployeeService:
并在 EmployeeService 中实现这个新方法:

public EmployeeDto CreateEmployeeForCompany(Guid companyId, EmployeeForCreationDto employeeForCreation, bool trackChanges)
{
    var company = _repository.Company.GetCompany(companyId, trackChanges);
    if (company is null) throw new CompanyNotFoundException(companyId);
    var employeeEntity = _mapper.Map<Employee>(employeeForCreation);
    _repository.Employee.CreateEmployeeForCompany(companyId, employeeEntity);
    _repository.Save();
    var employeeToReturn = _mapper.Map<EmployeeDto>(employeeEntity);
    return employeeToReturn;
}

We have to check whether that company exists in the database because there is no point in creating an employee for a company that does not exist. After that, we map the DTO to an entity, call the repository methods to create a new employee, map back the entity to the DTO, and return it to the caller.
我们必须检查数据库中是否存在该公司,因为为不存在的公司创建员工是没有意义的。之后,我们将 DTO 映射到实体,调用存储库方法以创建新员工,将实体映射回 DTO,并将其返回给调用方。

Now, we can add a new action in the EmployeesController:
现在,我们可以在 EmployeesController 中添加一个新动作:

[HttpPost]
public IActionResult CreateEmployeeForCompany(Guid companyId, [FromBody] EmployeeForCreationDto employee)
{
    if (employee is null) 
        return BadRequest("EmployeeForCreationDto object is null");
    var employeeToReturn = _service.EmployeeService.CreateEmployeeForCompany(companyId, employee, trackChanges: false);
    return CreatedAtRoute("GetEmployeeForCompany", new { companyId, id = employeeToReturn.Id }, employeeToReturn);
}

As we can see, the main difference between this action and the CreateCompany action (if we exclude the fact that we are working with different DTOs) is the return statement, which now has two parameters for the anonymous object.
正如我们所看到的,此作与 CreateCompany作(如果排除我们使用的是不同 DTO 的事实)之间的主要区别在于 return 语句,该语句现在有两个用于匿名对象的参数。

For this to work, we have to modify the HTTP attribute above the GetEmployeeForCompany action:
为此,我们必须修改 GetEmployeeForCompany作上方的 HTTP 属性:

[HttpGet("{id:guid}", Name = "GetEmployeeForCompany")]
public IActionResult GetEmployeeForCompany(Guid companyId, Guid id)
{
    var employee = _service.EmployeeService.GetEmployee(companyId, id, trackChanges: false);
    return Ok(employee);
}

Let’s give this a try:
让我们试一试:
https://localhost:5001/api/companies/14759d51-e9c1-4afc-f9bf-08d98898c9c3/employees

alt text

Excellent. A new employee was created.
非常好。创建了一个新员工。

If we take a look at the Headers tab, we'll see a link to fetch our newly created employee. If you copy that link and send another request with it, you will get this employee for sure:
如果我们查看 Headers 选项卡,我们将看到一个链接,用于获取我们新创建的员工。如果您复制该链接并发送另一个请求,您肯定会得到这个员工:

alt text

9.4 Creating Children Resources Together with a Parent

9.4 与父资源一起创建子资源

There are situations where we want to create a parent resource with its children. Rather than using multiple requests for every single child, we want to do this in the same request with the parent resource.‌
在某些情况下,我们希望创建一个包含其子资源的父资源。我们希望在具有父资源的同一请求中执行此作,而不是对每个子项使用多个请求。

We are going to show you how to do this.
我们将向您展示如何执行此作。

The first thing we are going to do is extend the CompanyForCreationDto class:
我们要做的第一件事是扩展 CompanyForCreationDto 类:

public record CompanyForCreationDto(string Name, string Address, string Country, IEnumerable<EmployeeForCreationDto> Employees);

We are not going to change the action logic inside the controller nor the repository/service logic; everything is great there. That’s all. Let’s test it:
我们不会更改控制器内部的动作逻辑,也不会更改存储库/服务逻辑;那里的一切都很棒。就这样。让我们测试一下:
https://localhost:5001/api/companies

alt text

You can see that this company was created successfully.
您可以看到此公司已成功创建。

Now we can copy the location link from the Headers tab, paste it in another Postman tab, and just add the /employees part:
现在我们可以从 Headers 选项卡复制位置链接,将其粘贴到另一个 Postman 选项卡中,然后添加 /employees 部分:

alt text

We have confirmed that the employees were created as well.
我们已经确认员工也被创建出来了。

9.5 Creating a Collection of Resources
9.5 创建资源集合

Until now, we have been creating a single resource whether it was Company or Employee. But it is quite normal to create a collection of resources, and in this section that is something we are going to work with.‌
到目前为止,我们一直在创建单个资源,无论是 Company 还是 Employee。但是创建资源集合是很正常的,在本节中,我们将要处理这一点。

If we take a look at the CreateCompany action, for example, we can see that the return part points to the CompanyById route (the GetCompany action). That said, we don’t have the GET action for the collection creating action to point to. So, before we start with the POST collection action, we are going to create the GetCompanyCollection action in the Companies controller.
例如,如果我们看一下 CreateCompany作,我们可以看到返回部分指向 CompanyById 路由(GetCompany作)。也就是说,我们没有要指向的集合创建作的 GET作。因此,在开始执行 POST 集合作之前,我们将在 Companies 控制器中创建 GetCompanyCollection作。

But first, let's modify the ICompanyRepository interface:
但首先,让我们修改 ICompanyRepository 接口:

IEnumerable<Company> GetByIds(IEnumerable<Guid> ids, bool trackChanges);

Then we have to change the CompanyRepository class:
然后我们必须更改 CompanyRepository 类:

public IEnumerable<Company> GetByIds(IEnumerable<Guid> ids, bool trackChanges) => FindByCondition(x => ids.Contains(x.Id), trackChanges) .ToList();

After that, we are going to modify ICompanyService:
之后,我们将修改 ICompanyService:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges);
        CompanyDto GetCompany(Guid companyId, bool trackChanges);
        CompanyDto CreateCompany(CompanyForCreationDto company);
        IEnumerable<CompanyDto> GetByIds(IEnumerable<Guid> ids, bool trackChanges);
    }
}

And implement this in CompanyService:
并在 CompanyService 中实现这一点:

public IEnumerable<CompanyDto> GetByIds(IEnumerable<Guid> ids, bool trackChanges)
{
    if (ids is null)
        throw new IdParametersBadRequestException();
    var companyEntities = _repository.Company.GetByIds(ids, trackChanges);
    if (ids.Count() != companyEntities.Count())
        throw new CollectionByIdsBadRequestException();
    var companiesToReturn = _mapper.Map<IEnumerable<CompanyDto>>(companyEntities);
    return companiesToReturn;
}

Here, we check if ids parameter is null and if it is we stop the execution flow and return a bad request response to the client. If it’s not null, we fetch all the companies for each id in the ids collection. If the count of ids and companies mismatch, we return another bad request response to the client. Finally, we are executing the mapping action and returning the result to the caller.
在这里,我们检查 ids 参数是否为 null,如果为 null,则停止执行流并向客户端返回错误的请求响应。如果它不为 null,我们将获取 ids 集合中每个 id 的所有公司。如果 ids 和 companies 的计数不匹配,我们将向客户端返回另一个错误的请求响应。最后,我们执行 mapping作并将结果返回给调用者。

Of course, we don’t have these two exception classes yet, so let’s create them.
当然,我们还没有这两个异常类,所以让我们创建它们。

Since we are returning a bad request result, we are going to create a new abstract class in the Entities/Exceptions folder:
由于我们返回了一个错误的请求结果,因此我们将在 Entities/Exceptions 文件夹中创建一个新的抽象类:

namespace Entities.Exceptions
{
    public abstract class BadRequestException : Exception
    {
        protected BadRequestException(string message) : base(message) { }
    }
}

Then, in the same folder, let’s create two new specific exception classes:
然后,在同一个文件夹中,让我们创建两个新的特定异常类:

namespace Entities.Exceptions
{
    public sealed class IdParametersBadRequestException : BadRequestException
    {
        public IdParametersBadRequestException() : base("Parameter ids is null") { }
    }

}
namespace Entities.Exceptions
{
    public sealed class CollectionByIdsBadRequestException : BadRequestException
    {
        public CollectionByIdsBadRequestException() : base("Collection count mismatch comparing to ids.") { }
    }
}

At this point, we’ve removed two errors from the GetByIds method. But, to show the correct response to the client, we have to modify the ConfigureExceptionHandler class – the part where we populate the StatusCode property:
此时,我们已经从 GetByIds 方法中删除了两个错误。但是,为了向客户端显示正确的响应,我们必须修改 ConfigureExceptionHandler 类 – 我们填充 StatusCode 属性的部分:

using Contracts;
using Entities.ErrorModel;
using Entities.Exceptions;
using Microsoft.AspNetCore.Diagnostics;

namespace CompanyEmployees.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this WebApplication app, ILoggerManager logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.ContentType = "application/json";
                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    if (contextFeature != null)
                    {
                        //context.Response.StatusCode = contextFeature.Error
                        //switch
                        //{
                        //    NotFoundException => StatusCodes.Status404NotFound,
                        //    _ => StatusCodes.Status500InternalServerError
                        //};

                        context.Response.StatusCode = contextFeature.Error switch
                        {
                            NotFoundException => StatusCodes.Status404NotFound,
                            BadRequestException => StatusCodes.Status400BadRequest,
                            _ => StatusCodes.Status500InternalServerError
                        };

                        logger.LogError($"Something went wrong: {contextFeature.Error}");
                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = contextFeature.Error.Message,
                        }.ToString());
                    }
                });
            });
        }
    }
}

After that, we can add a new action in the controller:
之后,我们可以在控制器中添加一个新的动作:

[HttpGet("collection/({ids})", Name = "CompanyCollection")]
public IActionResult GetCompanyCollection(IEnumerable<Guid> ids)
{
    var companies = _service.CompanyService.GetByIds(ids, trackChanges: false);
    return Ok(companies);
}

And that's it. This action is pretty straightforward, so let's continue towards POST implementation.
就是这样。此作非常简单,因此让我们继续进行 POST 实现。

Let’s modify the ICompanyService interface first:
让我们先修改 ICompanyService 接口:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface ICompanyService
    {
        IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges);
        CompanyDto GetCompany(Guid companyId, bool trackChanges);
        CompanyDto CreateCompany(CompanyForCreationDto company);
        IEnumerable<CompanyDto> GetByIds(IEnumerable<Guid> ids, bool trackChanges);
        (IEnumerable<CompanyDto> companies, string ids) CreateCompanyCollection(IEnumerable<CompanyForCreationDto> companyCollection);
    }
}

So, this new method will accept a collection of the CompanyForCreationDto type as a parameter, and return a Tuple with two fields (companies and ids) as a result.
因此,这个新方法将接受 CompanyForCreationDto 类型的集合作为参数,并返回一个包含两个字段(companies 和 ids)的 Tuples。

That said, let’s implement it in the CompanyService class:
也就是说,让我们在 CompanyService 类中实现它:

public (IEnumerable<CompanyDto> companies, string ids) CreateCompanyCollection(IEnumerable<CompanyForCreationDto> companyCollection)
{
    if (companyCollection is null)
        throw new CompanyCollectionBadRequest();
    var companyEntities = _mapper.Map<IEnumerable<Company>>(companyCollection);
    foreach (var company in companyEntities)
    {
        _repository.Company.CreateCompany(company);
    }
    _repository.Save(); 
    var companyCollectionToReturn = _mapper.Map<IEnumerable<CompanyDto>>(companyEntities);
    var ids = string.Join(",", companyCollectionToReturn.Select(c => c.Id));
    return (companies: companyCollectionToReturn, ids: ids);
}

So, we check if our collection is null and if it is, we return a bad request. If it isn’t, then we map that collection and save all the collection elements to the database. Finally, we map the company collection back, take all the ids as a comma-separated string, and return the Tuple with these two fields as a result to the caller.
因此,我们检查我们的集合是否为 null,如果为 null,则返回一个错误的请求。如果不是,那么我们映射该集合并将所有集合元素保存到数据库中。最后,我们将 company 集合映射回来,将所有 id 作为逗号分隔的字符串,并将包含这两个字段的 Tuple 作为结果返回给调用者。

Again, we can see that we don’t have the exception class, so let’s just create it:
同样,我们可以看到我们没有 exception 类,所以让我们创建它:

namespace Entities.Exceptions
{
    public sealed class CompanyCollectionBadRequest : BadRequestException
    {
        public CompanyCollectionBadRequest() : base("Company collection sent from a client is null.")
        { }
    }
}

Finally, we can add a new action in the CompaniesController:
最后,我们可以在 CompaniesController 中添加一个新作:

[HttpPost("collection")]
public IActionResult CreateCompanyCollection([FromBody] IEnumerable<CompanyForCreationDto> companyCollection)
{
    var result = _service.CompanyService.CreateCompanyCollection(companyCollection);
    return CreatedAtRoute("CompanyCollection", new { result.ids }, result.companies);
}

We receive the companyCollection parameter from the client, send it to the service method, and return a result with a comma-separated string and our newly created companies.
我们从客户端接收 companyCollection 参数,将其发送到 service 方法,并返回一个带有逗号分隔字符串的结果和我们新创建的公司。

Now you may ask, why are we sending a comma-separated string when we expect a collection of ids in the GetCompanyCollection action?
现在您可能会问,当我们期望在 GetCompanyCollection作中有一个 id 集合时,为什么还要发送一个逗号分隔的字符串?

Well, we can’t just pass a list of ids in the CreatedAtRoute method because there is no support for the Header Location creation with the list. You may try it, but we're pretty sure you would get the location like this:
我们不能只在 CreatedAtRoute 方法中传递 id 列表,因为不支持使用该列表创建 Header Location。您可以尝试一下,但我们非常确定您会得到这样的位置:

alt text

We can test our create action now with a bad request:
我们现在可以使用错误的请求来测试我们的 create操作:
https://localhost:5001/api/companies/collection

alt text

We can see that the request is handled properly and we have a correct response.
我们可以看到请求得到了正确的处理,并且我们得到了正确的响应。

Now, let’s send a valid request:
现在,让我们发送一个有效的请求:
https://localhost:5001/api/companies/collection

alt text

Excellent. Let’s check the header tab:
非常好。让我们检查一下 header 选项卡:

alt text

We can see a valid location link. So, we can copy it and try to fetch our newly created companies:
我们可以看到有效的位置链接。因此,我们可以复制它并尝试获取我们新创建的公司:

alt text

But we are getting the 415 Unsupported Media Type message. This is because our API can’t bind the string type parameter to the IEnumerable argument in the GetCompanyCollection action.
但是我们收到了 415 Unsupported Media Type 消息。这是因为我们的 API 无法将字符串类型参数绑定到 GetCompanyCollection作中的 IEnumerable 参数。

Well, we can solve this with a custom model binding.
好吧,我们可以通过自定义模型绑定来解决这个问题。

9.6 Model Binding in API

9.6 API 中的模型绑定

Let’s create the new folder ModelBinders in the Presentation project and inside the new class ArrayModelBinder:‌
让我们在 Presentation 项目和新类 ArrayModelBinder 中创建新文件夹 ModelBinders:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.ComponentModel;
using System.Reflection;

namespace CompanyEmployees.Presentation.ModelBinders
{
    public class ArrayModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (!bindingContext.ModelMetadata.IsEnumerableType)
            {
                bindingContext.Result = ModelBindingResult.Failed();
                return Task.CompletedTask;
            }
            var providedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
            if (string.IsNullOrEmpty(providedValue))
            {
                bindingContext.Result = ModelBindingResult.Success(null);
                return Task.CompletedTask;
            }
            var genericType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
            var converter = TypeDescriptor.GetConverter(genericType);
            var objectArray = providedValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(x => converter.ConvertFromString(x.Trim())).ToArray();
            var guidArray = Array.CreateInstance(genericType, objectArray.Length);
            objectArray.CopyTo(guidArray, 0);
            bindingContext.Model = guidArray;
            bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
            return Task.CompletedTask;
        }
    }
}

At first glance, this code might be hard to comprehend, but once we explain it, it will be easier to understand.
乍一看,这段代码可能很难理解,但是一旦我们解释了它,就会更容易理解。

We are creating a model binder for the IEnumerable type. Therefore, we have to check if our parameter is the same type.
我们正在为 IEnumerable 类型创建模型绑定器。因此,我们必须检查我们的参数是否为相同类型。

Next, we extract the value (a comma-separated string of GUIDs) with the ValueProvider.GetValue() expression. Because it is a type string, we just check whether it is null or empty. If it is, we return null as a result because we have a null check in our action in the controller. If it is not, we move on.
接下来,我们使用 ValueProvider.GetValue() 表达式提取值(以逗号分隔的 GUID 字符串)。因为它是一个类型字符串,所以我们只检查它是 null 还是空。如果是,我们将返回 null,因为我们在控制器中的作中有 null 检查。如果不是,我们继续前进。

In the genericType variable, with the reflection help, we store the type the IEnumerable consists of. In our case, it is GUID. With the converter variable, we create a converter to a GUID type. As you can see, we didn’t just force the GUID type in this model binder; instead, we inspected what is the nested type of the IEnumerable parameter and then created a converter for that exact type, thus making this binder generic.
在 genericType 变量中,在反射帮助下,我们存储 IEnumerable 包含的类型。在我们的例子中,它是 GUID。使用 converter 变量,我们创建到 GUID 类型的转换器。如你所见,我们不仅在此模型绑定器中强制使用 GUID 类型;相反,我们检查了 IEnumerable 参数的嵌套类型是什么,然后为该确切类型创建了一个转换器,从而使此 Binder 成为通用的。

After that, we create an array of type object (objectArray) that consist of all the GUID values we sent to the API and then create an array of GUID types (guidArray), copy all the values from the objectArray to the guidArray, and assign it to the bindingContext.
之后,我们创建一个对象类型的数组 (objectArray),其中包含我们发送到 API 的所有 GUID 值,然后创建一个 GUID 类型数组 (guidArray),将所有值从 objectArray 复制到 guidArray,并将其分配给 bindingContext。

These are the required using directives:
这些是必需的 using 指令:

using Microsoft.AspNetCore.Mvc.ModelBinding; 
using System.ComponentModel; 
using System.Reflection;

And that is it. Now, we have just to make a slight modification in the GetCompanyCollection action:
就是这样。现在,我们只需要在 GetCompanyCollection作中进行轻微修改:

[HttpGet("collection/({ids})", Name = "CompanyCollection")]
// public IActionResult GetCompanyCollection(IEnumerable<Guid> ids)
public IActionResult GetCompanyCollection([ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<Guid> ids)
{
    var companies = _service.CompanyService.GetByIds(ids, trackChanges: false);
    return Ok(companies);
}

This is the required namespace:
这是必需的命名空间:

using CompanyEmployees.Presentation.ModelBinders;

Visual Studio will provide two different namespaces to resolve the error, so be sure to pick the right one.
Visual Studio 将提供两个不同的命名空间来解决错误,因此请务必选择正确的命名空间。

Excellent.
非常好。

Our ArrayModelBinder will be triggered before an action executes. It will convert the sent string parameter to the IEnumerable type, and then the action will be executed:
我们的 ArrayModelBinder 将在 action 执行之前触发。它会将发送的字符串参数转换为 IEnumerable 类型,然后执行作:

https://localhost:5001/api/companies/collection/(582ea192-6fb7-44ff-a2a1-08d988ca3ca9,a216fbbe-ebbd-4e09-a2a2-08d988ca3ca9)

alt text

Well done.
干的好。

We are ready to continue towards DELETE actions.
我们已准备好继续执行 DELETE作。

Ultimate ASP.NET Core Web API 8 METHOD SAFETY AND METHOD IDEMPOTENCY

8 METHOD SAFETY AND METHOD IDEMPOTENCY
8 方法安全和方法幂等性

Before we start with the Create, Update, and Delete actions, we should explain two important principles in the HTTP standard. Those standards are Method Safety and Method Idempotency.‌
在开始执行 Create、Update 和 Delete作之前,我们应该解释 HTTP 标准中的两个重要原则。这些标准是方法安全和方法幂等性。

We can consider a method a safe one if it doesn’t change the resource representation. So, in other words, the resource shouldn’t be changed after our method is executed.
如果一个方法不改变资源表示,我们可以认为它是安全的。所以,换句话说,在我们的方法执行后,资源不应该被改变。

If we can call a method multiple times with the same result, we can consider that method idempotent. So in other words, the side effects of calling it once are the same as calling it multiple times.
如果我们可以多次调用一个方法并获得相同的结果,则可以认为该方法是幂等的。所以换句话说,调用一次的副作用与多次调用它的副作用相同。

Let’s see how this applies to HTTP methods:
让我们看看这如何应用于 HTTP 方法:

HTTP Method Is it Safe? Is it Idempotent?
GET Yes Yes
OPTIONS Yes Yes
HEAD Yes Yes
POST No No
DELETE No Yes
PUT No Yes
PATCH No No

As you can see, the GET, OPTIONS, and HEAD methods are both safe and idempotent, because when we call those methods they will not change the resource representation. Furthermore, we can call these methods multiple times, but they will return the same result every time.
如您所见,GET、OPTIONS 和 HEAD 方法既安全又幂等,因为当我们调用这些方法时,它们不会更改资源表示形式。此外,我们可以多次调用这些方法,但它们每次都会返回相同的结果。

The POST method is neither safe nor idempotent. It causes changes in the resource representation because it creates them. Also, if we call the POST method multiple times, it will create a new resource every time.
POST 方法既不安全也不幂等。它会导致资源表示形式发生变化,因为它会创建资源表示形式。此外,如果我们多次调用 POST 方法,它每次都会创建一个新资源。

The DELETE method is not safe because it removes the resource, but it is idempotent because if we delete the same resource multiple times, we will get the same result as if we have deleted it only once.
DELETE 方法不安全,因为它会删除资源,但它是幂等的,因为如果我们多次删除同一资源,我们将得到与只删除一次相同的结果。

PUT is not safe either. When we update our resource, it changes. But it is idempotent because no matter how many times we update the same resource with the same request it will have the same representation as if we have updated it only once.
PUT 也不安全。当我们更新资源时,它会发生变化。但它是幂等的,因为无论我们使用相同的请求更新同一资源多少次,它都将具有相同的表示形式,就好像我们只更新了一次一样。

Finally, the PATCH method is neither safe nor idempotent.
最后,PATCH 方法既不安全也不幂等。

Now that we’ve learned about these principles, we can continue with our application by implementing the rest of the HTTP methods (we have already implemented GET). We can always use this table to decide which method to use for which use case.
现在我们已经了解了这些原则,我们可以通过实现其余的 HTTP 方法(我们已经实现了 GET)来继续我们的应用程序。我们始终可以使用此表来决定将哪种方法用于哪个用例。

Ultimate ASP.NET Core Web API 7 CONTENT NEGOTIATION

7 CONTENT NEGOTIATION

7 内容协商

Content negotiation is one of the quality-of-life improvements we can add to our REST API to make it more user-friendly and flexible. And when we design an API, isn’t that what we want to achieve in the first place?‌
内容协商是我们可以添加到 REST API 中的生活质量改进之一,以使其更加用户友好和灵活。当我们设计 API 时,这不正是我们最初想要实现的目标吗?

Content negotiation is an HTTP feature that has been around for a while, but for one reason or another, it is often a bit underused.
内容协商是一项已经存在了一段时间的 HTTP 功能,但出于某种原因,它通常没有得到充分利用。

In short, content negotiation lets you choose or rather “negotiate” the content you want to get in a response to the REST API request.
简而言之,内容协商允许您选择或更确切地说是“协商”您希望在响应 REST API 请求时获得的内容。

7.1 What Do We Get Out of the Box?

7.1 我们开箱即用什么?

By default, ASP.NET Core Web API returns a JSON formatted result.‌
默认情况下,ASP.NET Core Web API 返回 JSON 格式的结果。

We can confirm that by looking at the response from the GetCompanies action:
我们可以通过查看 GetCompanies action的回复来确认这一点:
https://localhost:5001/api/companies

alt text

We can clearly see that the default result when calling GET on /api/companies returns the JSON result. We have also used the Accept header (as you can see in the picture above) to try forcing the server to return other media types like plain text and XML.
我们可以清楚地看到,在 /api/companies 上调用 GET 时,默认结果返回的是 JSON 结果。我们还使用了 Accept 标头(如上图所示)来尝试强制服务器返回其他媒体类型,如纯文本和 XML。

But that doesn’t work. Why?
但这行不通。为什么?

Because we need to configure server formatters to format a response the way we want it.
因为我们需要配置服务器格式化程序以按照我们想要的方式格式化响应。

Let’s see how to do that.
让我们看看如何做到这一点。

7.2 Changing the Default Configuration of Our Project

7.2 更改我们项目的默认配置

A server does not explicitly specify where it formats a response to JSON.‌ But you can override it by changing configuration options through the AddControllers method.
服务器没有明确指定它对 JSON 的响应的格式。但是,您可以通过 AddControllers 方法更改配置选项来覆盖它。

We can add the following options to enable the server to format the XML response when the client tries negotiating for it:
我们可以添加以下选项,使服务器能够在客户端尝试协商 XML 响应时格式化 XML 响应:

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

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
LogManager.LoadConfiguration(
    string.Concat(Directory.GetCurrentDirectory(),
    "/nlog.config"));
builder.Services.ConfigureLoggerService();

builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();
builder.Services.ConfigureRepositoryManager();
builder.Services.ConfigureServiceManager();
builder.Services.ConfigureSqlContext(builder.Configuration);
builder.Services.AddAutoMapper(typeof(Program));
// builder.Services.AddControllers().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);
builder.Services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; }).AddXmlDataContractSerializerFormatters().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);

First things first, we must tell a server to respect the Accept header. After that, we just add the AddXmlDataContractSerializerFormatters method to support XML formatters.
首先,我们必须告诉服务器遵守 Accept 标头。之后,我们只需添加 AddXmlDataContractSerializerFormatters 方法来支持 XML 格式化程序。

Now that we have our server configured, let’s test the content negotiation once more.
现在我们已经配置了服务器,让我们再次测试内容协商。

7.3 Testing Content Negotiation

7.3 测试 Content Negotiation

Let’s see what happens now if we fire the same request through Postman:‌
让我们看看如果我们通过 Postman 触发相同的请求,现在会发生什么:
https://localhost:5001/api/companies

alt text

We get an error because XmlSerializer cannot easily serialize our positional record type. There are two solutions to this. The first one is marking our CompanyDto record with the [Serializable] attribute:
我们收到一个错误,因为 XmlSerializer 无法轻松地序列化我们的位置记录类型。有两种解决方案。第一个是使用 [Serializable] 属性标记我们的 CompanyDto 记录:

[Serializable] 
public record CompanyDto(Guid Id, string Name, string FullAddress);

Now, we can send the same request again:
现在,我们可以再次发送相同的请求:

alt text

This time, we are getting our XML response but, as you can see,properties have some strange names. That’s because the compiler behind the scenes generates the record as a class with fields named like that (name_BackingField) and the XML serializer just serializes those fields with the same names.
这一次,我们收到了 XML 响应,但正如你所看到的,属性有一些奇怪的名称。这是因为后台编译器将记录生成为一个类,其中包含类似 (name_BackingField) 的字段,而 XML 序列化程序只是序列化具有相同名称的这些字段。

If we don’t want these property names in our response, but the regular ones, we can implement a second solution. Let’s modify our record with the init only property setters:
如果我们不希望响应中包含这些属性名称,而是常规属性名称,则可以实现第二个解决方案。让我们使用 init only 属性 setter 修改我们的记录:

namespace Shared.DataTransferObjects
{
    [Serializable]
    // public record CompanyDto(Guid Id, string Name, string FullAddress);
    public record CompanyDto
    {
        public Guid Id { get; init; }
        public string? Name { get; init; }
        public string? FullAddress { get; init; }
    }
}

This object is still immutable and init-only properties protect the state of the object from mutation once initialization is finished.
此对象仍然是不可变的,并且仅 init-only 属性可在初始化完成后保护对象的状态免受更改。

Additionally, we have to make one more change in the MappingProfile class:
此外,我们还必须在 MappingProfile 类中再进行一项更改:

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

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

            CreateMap<Employee, EmployeeDto>();
        }
    }
}

We are returning this mapping rule to a previous state since now, we do have properties in our object.
我们将此映射规则返回到以前的状态,因为从现在开始,我们的对象中确实有属性。

Now, we can send the same request again:
现在,我们可以再次发送相同的请求:

alt text

There is our XML response.
下面是我们的 XML 响应。

Now by changing the Accept header from text/xml to text/json, we can get differently formatted responses — and that is quite awesome, wouldn’t you agree?
现在,通过将 Accept 标头从 text/xml 更改为 text/json,我们可以获得不同格式的响应 — 这真是太棒了,你不同意吗?

Okay, that was nice and easy.
好吧,这很好,很容易。

But what if despite all this flexibility a client requests a media type that a server doesn’t know how to format?
但是,如果尽管有所有这些灵活性,但客户端请求的媒体类型服务器不知道如何格式化,该怎么办?

7.4 Restricting Media Types

7.4 限制媒体类型

Currently, it – the server - will default to a JSON type.‌
目前,它(服务器)将默认为 JSON 类型。

But we can restrict this behavior by adding one line to the configuration:
但是我们可以通过在配置中添加一行来限制这种行为:

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

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
LogManager.LoadConfiguration(
    string.Concat(Directory.GetCurrentDirectory(),
    "/nlog.config"));
builder.Services.ConfigureLoggerService();

builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();
builder.Services.ConfigureRepositoryManager();
builder.Services.ConfigureServiceManager();
builder.Services.ConfigureSqlContext(builder.Configuration);
builder.Services.AddAutoMapper(typeof(Program));
// builder.Services.AddControllers().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);
// builder.Services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; }).AddXmlDataContractSerializerFormatters().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);
builder.Services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; }).AddXmlDataContractSerializerFormatters().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);
var app = builder.Build();

We added the ReturnHttpNotAcceptable = true option, which tells the server that if the client tries to negotiate for the media type the server doesn’t support, it should return the 406 Not Acceptable status code.
我们添加了 ReturnHttpNotAcceptable = true 选项,该选项告诉服务器,如果客户端尝试协商服务器不支持的媒体类型,它应返回 406 Not Acceptable 状态代码。

This will make our application more restrictive and force the API consumer to request only the types the server supports. The 406 status code is created for this purpose.
这将使我们的应用程序更具限制性,并强制 API 使用者仅请求服务器支持的类型。406 状态代码就是为此目的而创建的。

Now, let’s try fetching the text/css media type using Postman to see what happens:
现在,让我们尝试使用 Postman 获取 text/css 媒体类型,看看会发生什么:
https://localhost:5001/api/companies

alt text

And as expected, there is no response body and all we get is a nice 406 Not Acceptable status code.
正如预期的那样,没有响应正文,我们得到的只是一个很好的 406 Not Acceptable 状态代码。

So far so good.
目前为止,一切都好。

7.5 More About Formatters

7.5 更多关于 Formatters

If we want our API to support content negotiation for a type that is not “in‌ the box,” we need to have a mechanism to do this.
如果我们希望我们的 API 支持非“开箱即用”的类型的内容协商,我们需要有一种机制来做到这一点。

So, how can we do that?
那么,我们该怎么做呢?

ASP.NET Core supports the creation of custom formatters. Their purpose is to give us the flexibility to create our formatter for any media types we need to support.
ASP.NET Core 支持创建自定义格式化程序。它们的目的是让我们能够灵活地为需要支持的任何媒体类型创建格式化程序。

We can make the custom formatter by using the following method:
我们可以使用以下方法制作自定义格式化程序:

• Create an output formatter class that inherits the TextOutputFormatter class.
创建继承 TextOutputFormatter 类的输出格式化程序类。

• Create an input formatter class that inherits the TextInputformatter class.
创建继承 TextInputformatter 类的输入格式化程序类。

• Add input and output classes to the InputFormatters and OutputFormatters collections the same way we did for the XML formatter.
向 InputFormatters 和 OutputFormatters 集合添加输入和输出类,方法与我们对 XML 格式化程序执行的作相同。

Now let’s have some fun and implement a custom CSV formatter for our example.
现在,让我们来找点乐子,为我们的示例实现一个自定义的 CSV 格式化程序。

7.6 Implementing a Custom Formatter

7.6 实现自定义格式化程序

Since we are only interested in formatting responses, we need to implement only an output formatter. We would need an input formatter only if a request body contained a corresponding type.‌
由于我们只对格式化响应感兴趣,因此我们只需要实现一个输出格式化程序。只有当请求正文包含相应的类型时,我们才需要 input 格式化程序。

The idea is to format a response to return the list of companies in a CSV format.
这个想法是设置响应的格式,以 CSV 格式返回公司列表。

Let’s add a CsvOutputFormatter class to our main project:
让我们将 CsvOutputFormatter 类添加到我们的主项目中:

using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using Shared.DataTransferObjects;
using System.Text;

namespace CompanyEmployees
{
    public class CsvOutputFormatter : TextOutputFormatter
    {
        public CsvOutputFormatter()
        {
            SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/csv"));
            SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode);
        }
        protected override bool CanWriteType(Type? type)
        {
            if (typeof(CompanyDto).IsAssignableFrom(type) || typeof(IEnumerable<CompanyDto>).IsAssignableFrom(type))
            {
                return base.CanWriteType(type);
            }
            return false;
        }
        public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
        {
            var response = context.HttpContext.Response; var buffer = new StringBuilder();
            if (context.Object is IEnumerable<CompanyDto>)
            {
                foreach (var company in (IEnumerable<CompanyDto>)context.Object)
                {
                    FormatCsv(buffer, company);
                }
            }
            else
            {
                FormatCsv(buffer, (CompanyDto)context.Object);
            }
            await response.WriteAsync(buffer.ToString());
        }
        private static void FormatCsv(StringBuilder buffer, CompanyDto company)
        {
            buffer.AppendLine($"{company.Id},\"{company.Name},\"{company.FullAddress}\"");
        }
    }
}

There are a few things to note here:
这里有几点需要注意:

• In the constructor, we define which media type this formatter should parse as well as encodings.
在构造函数中,我们定义此格式化程序应解析的媒体类型以及编码。

• The CanWriteType method is overridden, and it indicates whether or not the CompanyDto type can be written by this serializer.
CanWriteType 方法被覆盖,它指示此序列化程序是否可以写入 CompanyDto 类型。

• The WriteResponseBodyAsync method constructs the response.
WriteResponseBodyAsync 方法构造响应。

• And finally, we have the FormatCsv method that formats a response the way we want it.
最后,我们有 FormatCsv 方法,它可以按照我们想要的方式格式化响应。

The class is pretty straightforward to implement, and the main thing that you should focus on is the FormatCsv method logic.
该类的实现非常简单,您应该关注的主要内容是 FormatCsv 方法逻辑。

Now we just need to add the newly made formatter to the list of OutputFormatters in the ServicesExtensions class:
现在,我们只需将新创建的格式化程序添加到 ServicesExtensions 类中的 OutputFormatters 列表中:

public static IMvcBuilder AddCustomCSVFormatter(this IMvcBuilder builder) => builder.AddMvcOptions(config => config.OutputFormatters.Add(new CsvOutputFormatter()));

And to call it in the AddControllers:
要在 AddController 中调用它:

builder.Services.AddControllers(config => { config.RespectBrowserAcceptHeader = true; config.ReturnHttpNotAcceptable = true; }).AddXmlDataContractSerializerFormatters() .AddCustomCSVFormatter() .AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);

Let’s run this and see if it works. This time we will put text/csv as the value for the Accept header:
让我们运行它,看看它是否有效。这次我们将 text/csv 作为 Accept 标头的值:

https://localhost:5001/api/companies

alt text

Well, what do you know, it works!
嗯,你知道什么,它有效!

In this chapter, we finished working with GET requests in our project and we are ready to move on to the POST PUT and DELETE requests. We have a lot more ground to cover, so let’s get down to business.
在本章中,我们完成了项目中 GET 请求的处理,并准备继续处理 POST PUT 和 DELETE 请求。我们还有很多领域要涵盖,所以让我们开始谈正事。

Ultimate ASP.NET Core Web API 6 GETTING ADDITIONAL RESOURCES

6 GETTING ADDITIONAL RESOURCES
获取额外资源

As of now, we can continue with GET requests by adding additional actions to our controller. Moreover, we are going to create one more controller for the Employee resource and implement an additional action in it.‌
截至目前,我们可以通过向控制器添加其他作来继续处理 GET 请求。此外,我们将为 Employee 资源再创建一个控制器,并在其中实施一个额外的作。

6.1 Getting a Single Resource From the Database

6.1 从数据库中获取单个资源

Let’s start by modifying the ICompanyRepository interface:‌
让我们从修改 ICompanyRepository 接口开始:

using Entities.Models;

namespace Contracts
{
    public interface ICompanyRepository
    {
        IEnumerable<Company> GetAllCompanies(bool trackChanges); 
        Company GetCompany(Guid companyId, bool trackChanges);
    }
}

Then, we are going to implement this interface in the CompanyRepository.cs file:
然后,我们将在 CompanyRepository.cs 文件中实现这个接口:

using Contracts;
using Entities.Models;

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();

        public Company GetCompany(Guid companyId, bool trackChanges) => FindByCondition(c => c.Id.Equals(companyId), trackChanges).SingleOrDefault();
    }
}

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

using Shared;

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

And of course, we have to implement this interface in the CompanyService class:
当然,我们必须在 CompanyService 类中实现此接口:

using AutoMapper;
using Contracts;
using Service.Contracts;
using Shared;

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)
        {
            var companies = _repository.Company.GetAllCompanies(trackChanges);
            var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
            return companiesDto;
        }

        public CompanyDto GetCompany(Guid id, bool trackChanges)
        {
            var company = _repository.Company.GetCompany(id, trackChanges);
            //Check if the company is null
            var companyDto = _mapper.Map<CompanyDto>(company);
            return companyDto;
        }
    }
}

So, we are calling the repository method that fetches a single company from the database, maps the result to companyDto, and returns it. You can also see the comment about the null checks, which we are going to solve just in a minute.
因此,我们调用了 repository 方法,该方法从数据库中获取单个公司,将结果映射到 companyDto,然后返回它。您还可以查看有关 null 检查的注释,我们稍后将解决该问题。

Finally, let’s change the CompanyController class:
最后,让我们更改 CompanyController 类:

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

namespace CompanyEmployees.Presentation.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CompaniesController : ControllerBase
    {
        private readonly IServiceManager _service;
        public CompaniesController(IServiceManager service) => _service = service;

        [HttpGet]
        public IActionResult GetCompanies()
        {
            // throw new Exception("Exception");
            var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
            return Ok(companies);
        }

        [HttpGet("{id:guid}")]
        public IActionResult GetCompany(Guid id)
        {
            var company = _service.CompanyService.GetCompany(id, trackChanges: false);
            return Ok(company);
        }

    }
}

The route for this action is /api/companies/id and that’s because the /api/companies part applies from the root route (on top of the controller) and the id part is applied from the action attribute [HttpGet(“{id:guid}“)]. You can also see that we are using a route constraint (:guid part) where we explicitly state that our id parameter is of the GUID type. We can use many different constraints like int, double, long, float, datetime, bool, length, minlength, maxlength, and many others.
此作的路由是 /api/companies/id,这是因为 /api/companies 部分从根路由(在控制器的顶部)应用,而 id 部分从作属性 [HttpGet(“{id:guid}”)] 应用。您还可以看到,我们正在使用路由约束(:guid 部分),其中我们显式声明我们的 id 参数是 GUID 类型。我们可以使用许多不同的约束,如 int、double、long、float、datetime、bool、length、minlength、maxlength 等。

Let’s use Postman to send a valid request towards our API:
让我们使用 Postman 向我们的 API 发送一个有效的请求:
https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce3

alt text

Great. This works as expected. But, what if someone uses an invalid id parameter?
伟大。这按预期工作。但是,如果有人使用了无效的 id 参数怎么办?

6.1.1 Handling Invalid Requests in a Service Layer‌

6.1.1 处理服务层中的无效请求

As you can see, in our service method, we have a comment stating that the result returned from the repository could be null, and this is something we have to handle. We want to return the NotFound response to the client but without involving our controller’s actions. We are going to keep them nice and clean as they already are.
如你所见,在我们的 service 方法中,我们有一条注释,指出从存储库返回的结果可能是 null,这是我们必须处理的事情。我们希望将 NotFound 响应返回给客户端,但不涉及控制器的作。我们将保持它们已经的良好和干净。

So, what we are going to do is to create custom exceptions that we can call from the service methods and interrupt the flow. Then our error handling middleware can catch the exception, process the response, and return it to the client. This is a great way of handling invalid requests inside a service layer without having additional checks in our controllers.
因此,我们要做的是创建自定义异常,我们可以从服务方法中调用这些异常并中断流。然后我们的错误处理中间件可以捕获异常,处理响应,并将其返回给客户端。这是在服务层内处理无效请求的好方法,而无需在我们的控制器中进行额外的检查。

That said, let’s start with a new Exceptions folder creation inside the Entities project. Since, in this case, we are going to create a not found response, let’s create a new NotFoundException class inside that folder:
也就是说,让我们从 Entities 项目中创建新的 Exceptions 文件夹开始。由于在本例中,我们将创建一个未找到的响应,因此让我们在该文件夹中创建一个新的 NotFoundException 类:

namespace Entities.Exceptions
{
    public abstract class NotFoundException : Exception
    {
        protected NotFoundException(string message) : base(message) { }
    }
}

This is an abstract class, which will be a base class for all the individual not found exception classes. It inherits from the Exception class to represent the errors that happen during application execution. Since in our current case, we are handling the situation where we can’t find the company in the database, we are going to create a new CompanyNotFoundException class in the same Exceptions folder:
这是一个抽象类,它将成为所有单个 not found 异常类的基类。它继承自 Exception 类,以表示应用程序执行期间发生的错误。由于在当前情况下,我们正在处理在数据库中找不到公司的情况,因此我们将在同一个 Exceptions 文件夹中创建一个新的 CompanyNotFoundException 类:

namespace Entities.Exceptions
{
    public sealed class CompanyNotFoundException : NotFoundException
    {
        public CompanyNotFoundException(Guid companyId) : base($"The company with id: {companyId} doesn't exist in the database.") { }
    }
}

Right after that, we can remove the comment in the GetCompany method and throw this exception:
紧接着,我们可以删除 GetCompany 方法中的注释并引发以下异常:

using AutoMapper;
using Contracts;
using Entities.Exceptions;
using Service.Contracts;
using Shared;

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)
        {
            var companies = _repository.Company.GetAllCompanies(trackChanges);
            var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
            return companiesDto;
        }

        //public CompanyDto GetCompany(Guid id, bool trackChanges)
        //{
        //    var company = _repository.Company.GetCompany(id, trackChanges);
        //    //Check if the company is null
        //    var companyDto = _mapper.Map<CompanyDto>(company);
        //    return companyDto;
        //}

        public CompanyDto GetCompany(Guid id, bool trackChanges)
        {
            var company = _repository.Company.GetCompany(id, trackChanges);
            if (company is null) throw new CompanyNotFoundException(id);
            var companyDto = _mapper.Map<CompanyDto>(company);
            return companyDto;
        }
    }
}

Finally, we have to modify our error middleware because we don’t want to return the 500 error message to our clients for every custom error we throw from the service layer.
最后,我们必须修改我们的错误中间件,因为我们不想为我们从服务层抛出的每个自定义错误返回 500 错误消息给客户端。

So, let’s modify the ExceptionMiddlewareExtensions class in the main project:
因此,让我们修改主项目中的 ExceptionMiddlewareExtensions 类:

using Contracts;
using Entities.ErrorModel;
using Entities.Exceptions;
using Microsoft.AspNetCore.Diagnostics;

namespace CompanyEmployees.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this WebApplication app, ILoggerManager logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.ContentType = "application/json";
                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    if (contextFeature != null)
                    {
                        context.Response.StatusCode = contextFeature.Error
                        switch
                        {
                            NotFoundException => StatusCodes.Status404NotFound,
                            _ => StatusCodes.Status500InternalServerError
                        };
                        logger.LogError($"Something went wrong: {contextFeature.Error}");
                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = contextFeature.Error.Message,
                        }.ToString());
                    }
                });
            });
        }
    }
}

We remove the hardcoded StatusCode setup and add the part where we populate it based on the type of exception we throw in our service layer. We are also dynamically populating the Message property of the ErrorDetails object that we return as the response.
我们删除硬编码的 StatusCode 设置,并添加部分,根据我们在服务层中抛出的异常类型来填充它。我们还动态填充作为响应返回的 ErrorDetails 对象的 Message 属性。

Additionally, you can see the advantage of using the base abstract exception class here (NotFoundException in this case). We are not checking for the specific class implementation but the base type. This allows us to have multiple not found classes that inherit from the NotFoundException class and this middleware will know that we want to return the NotFound response to the client.
此外,您可以在此处看到使用基抽象异常类的优势(在本例中为 NotFoundException)。我们检查的不是特定的类实现,而是基类型。这允许我们拥有多个从 NotFoundException 类继承的未找到的类,并且此中间件将知道我们想要将 NotFound 响应返回给客户端。

Excellent. Now, we can start the app and send the invalid request:
非常好。现在,我们可以启动应用程序并发送无效请求:

https://localhost:5001/api/companies/3d490a70-94ce-4d15-9494-5248280c2ce2

alt text

We can see the status code we require and also the response object with proper StatusCode and Message properties. Also, if you inspect the log message, you will see that we are logging a correct message.
我们可以看到所需的状态代码,还可以看到具有适当 StatusCode 和 Message 属性的响应对象。此外,如果您检查日志消息,您将看到我们记录的消息正确无误。

With this approach, we have perfect control of all the exceptional cases in our app. We have that control due to global error handler implementation. For now, we only handle the invalid id sent from the client, but we will handle more exceptional cases in the rest of the project.
通过这种方法,我们可以完美地控制应用程序中的所有特殊情况。由于全局错误处理程序的实现,我们拥有了这种控制权。目前,我们只处理客户端发送的无效 ID,但我们将在项目的其余部分处理更多异常情况。

In our tests for a published app, the regular request sent from Postman took 7ms and the exceptional one took 14ms. So you can see how fast the response is.
在我们对已发布应用程序的测试中,从 Postman 发送的常规请求需要 7 毫秒,特殊请求需要 14 毫秒。所以你可以看到响应有多快。

Of course, we are using exceptions only for these exceptional cases (Company not found, Employee not found...) and not throwing them all over the application. So, if you follow the same strategy, you will not face any performance issues.
当然,我们只对这些特殊情况(未找到公司、未找到员工等)使用异常,而不是在整个应用程序中抛出它们。因此,如果您遵循相同的策略,您将不会遇到任何性能问题。

Lastly, if you have an application where you have to throw custom exceptions more often and maybe impact your performance, we are going to provide an alternative to exceptions in the first bonus chapter of this book (Chapter 32).
最后,如果您的应用程序必须更频繁地引发自定义异常,并且可能会影响您的性能,我们将在本书的附1章(第 32 章)中提供异常的替代方案。

6.2 Parent/Child Relationships in Web API

6.2 Web API 中的父/子关系

Up until now, we have been working only with the company, which is a parent (principal) entity in our API. But for each company, we have a related employee (dependent entity). Every employee must be related to a certain company and we are going to create our URIs in that manner.‌
到目前为止,我们只与公司合作,该公司是我们 API 中的父(主体)实体。但对于每家公司,我们都有一个相关的员工(依赖实体)。每个员工都必须与某家公司相关,我们将以这种方式创建我们的 URI。

That said, let’s create a new controller in the Presentation project and name it EmployeesController:
也就是说,让我们在 Presentation 项目中创建一个新控制器,并将其命名为 EmployeesController:

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

namespace CompanyEmployees.Presentation.Controllers
{
    [Route("api/companies/{companyId}/employees")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
        private readonly IServiceManager _service;
        public EmployeesController(IServiceManager service) => _service = service;
    }
}

We are familiar with this code, but our main route is a bit different. As we said, a single employee can’t exist without a company entity and this is exactly what we are exposing through this URI. To get an employee or employees from the database, we have to specify the companyId parameter, and that is something all actions will have in common. For that reason, we have specified this route as our root route.
我们熟悉这段代码,但是我们的主要路线有点不同。正如我们所说,没有公司实体就不能存在单个员工,这正是我们通过此 URI 公开的内容。要从数据库中获取一个或多个员工,我们必须指定 companyId 参数,这是所有作的共同点。因此,我们已将此路由指定为我们的根路由。

Before we create an action to fetch all the employees per company, we have to modify the IEmployeeRepository interface:
在我们创建一个动作来获取每个公司的所有员工之前,我们必须修改 IEmployeeRepository 接口:

using Entities.Models;

namespace Contracts
{
    public interface IEmployeeRepository
    {
        IEnumerable<Employee> GetEmployees(Guid companyId, bool trackChanges);
    }
}

After interface modification, we are going to modify the EmployeeRepository class:
修改接口后,我们将修改 EmployeeRepository 类:

using Contracts;
using Entities.Models;

namespace Repository
{
    public class EmployeeRepository : RepositoryBase<Employee>, IEmployeeRepository
    {
        public EmployeeRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
        public IEnumerable<Employee> GetEmployees(Guid companyId, bool trackChanges) => FindByCondition(e => e.CompanyId.Equals(companyId), trackChanges).OrderBy(e => e.Name).ToList();
    }
}

Then, before we start adding code to the service layer, we are going to create a new DTO. Let’s name it EmployeeDto and add it to the Shared/DataTransferObjects folder:
然后,在我们开始向服务层添加代码之前,我们将创建一个新的 DTO。让我们将其命名为 EmployeeDto 并将其添加到 Shared/DataTransferObjects 文件夹中:

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

Since we want to return this DTO to the client, we have to create a mapping rule inside the MappingProfile class:
由于我们想将此 DTO 返回给客户端,因此我们必须在 MappingProfile 类中创建一个映射规则:

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

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

Now, we can modify the IEmployeeService interface:
现在,我们可以修改 IEmployeeService 接口:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface IEmployeeService { 
        IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges); 
    }
}

And of course, we have to implement this interface in the EmployeeService class:
当然,我们必须在 EmployeeService 类中实现这个接口:

using AutoMapper;
using Contracts;
using Entities.Exceptions;
using Service.Contracts;
using Shared.DataTransferObjects;

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;
        }

        public IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges)
        {
            var company = _repository.Company.GetCompany(companyId, trackChanges);
            if (company is null) throw new CompanyNotFoundException(companyId);
            var employeesFromDb = _repository.Employee.GetEmployees(companyId, trackChanges);
            var employeesDto = _mapper.Map<IEnumerable<EmployeeDto>>(employeesFromDb);
            return employeesDto;
        }
    }
}

Here, we first fetch the company entity from the database. If it doesn’t exist, we return the NotFound response to the client. If it does, we fetch all the employees for that company, map them to the collection of EmployeeDto and return it to the caller.
在这里,我们首先从数据库中获取 company 实体。如果不存在,我们将 NotFound 响应返回给客户端。如果是这样,我们将获取该公司的所有员工,将它们映射到 EmployeeDto 的集合,并将其返回给调用方。

Finally, let’s modify the Employees controller:
最后,让我们修改 Employees 控制器:

[HttpGet]
public IActionResult GetEmployeesForCompany(Guid companyId)
{
    var employees = _service.EmployeeService.GetEmployees(companyId, trackChanges: false);
    return Ok(employees);
}

This code is pretty straightforward — nothing we haven’t seen so far — but we need to explain just one thing. As you can see, we have the companyId parameter in our action and this parameter will be mapped from the main route. For that reason, we didn’t place it in the [HttpGet] attribute as we did with the GetCompany action.
这段代码非常简单 — 到目前为止我们还没有见过 — 但我们只需要解释一件事。如您所见,我们的作中有 companyId 参数,此参数将从主路由映射。因此,我们没有像对 GetCompany作那样将其放在 [HttpGet] 属性中。

That done, we can send a request with a valid companyId:
完成后,我们可以发送具有有效 companyId:
https://localhost:5001/api/companies/c9d4c053-49b6-410c-bc78-2d54a9991870/employees

alt text

And with an invalid companyId:
并且使用无效的 companyId:
https://localhost:5001/api/companies/c9d4c053-49b6-410c-bc78-2d54a9991873/employees

alt text

Excellent. Let’s continue by fetching a single employee.
非常好。让我们继续获取单个员工。

6.3 Getting a Single Employee for Company

6.3 为公司招聘一名员工

So, as we did in previous sections, let’s start with the‌ IEmployeeRepository interface modification:
因此,正如我们在前面的部分中所做的那样,让我们从 IEmployeeRepository 接口修改开始:

using Entities.Models;

namespace Contracts
{
    public interface IEmployeeRepository
    {
        IEnumerable<Employee> GetEmployees(Guid companyId, bool trackChanges);
        Employee GetEmployee(Guid companyId, Guid id, bool trackChanges);
    }
}

Now, let’s implement this method in the EmployeeRepository class:
现在,让我们在 EmployeeRepository 类中实现此方法:

using Contracts;
using Entities.Models;

namespace Repository
{
    public class EmployeeRepository : RepositoryBase<Employee>, IEmployeeRepository
    {
        public EmployeeRepository(RepositoryContext repositoryContext) : base(repositoryContext) { }
        public IEnumerable<Employee> GetEmployees(Guid companyId, bool trackChanges) => FindByCondition(e => e.CompanyId.Equals(companyId), trackChanges).OrderBy(e => e.Name).ToList();
        public Employee GetEmployee(Guid companyId, Guid id, bool trackChanges) => FindByCondition(e => e.CompanyId.Equals(companyId) && e.Id.Equals(id), trackChanges).SingleOrDefault();

    }
}

Next, let’s add another exception class in the Entities/Exceptions folder:
接下来,让我们在 Entities/Exceptions 文件夹中添加另一个异常类:

namespace Entities.Exceptions
{
    public class EmployeeNotFoundException : NotFoundException
    {
        public EmployeeNotFoundException(Guid employeeId) : base($"Employee with id: {employeeId} doesn't exist in the database.") { }
    }
}

We will soon see why do we need this class.
我们很快就会明白为什么我们需要这个类。

To continue, we have to modify the IEmployeeService interface:
要继续,我们必须修改 IEmployeeService 接口:

using Shared.DataTransferObjects;

namespace Service.Contracts
{
    public interface IEmployeeService { 
        IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges);
        EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges);
    }
}

And implement this new method in the EmployeeService class:
并在 EmployeeService 类中实现这个新方法:

using AutoMapper;
using Contracts;
using Entities.Exceptions;
using Service.Contracts;
using Shared.DataTransferObjects;

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;
        }

        public IEnumerable<EmployeeDto> GetEmployees(Guid companyId, bool trackChanges)
        {
            var company = _repository.Company.GetCompany(companyId, trackChanges);
            if (company is null) 
                throw new CompanyNotFoundException(companyId);
            var employeesFromDb = _repository.Employee.GetEmployees(companyId, trackChanges);
            var employeesDto = _mapper.Map<IEnumerable<EmployeeDto>>(employeesFromDb);
            return employeesDto;
        }

        public EmployeeDto GetEmployee(Guid companyId, Guid id, bool trackChanges)
        {
            var company = _repository.Company.GetCompany(companyId, trackChanges);
            if (company is null) 
                throw new CompanyNotFoundException(companyId);
            var employeeDb = _repository.Employee.GetEmployee(companyId, id, trackChanges);
            if (employeeDb is null) 
                throw new EmployeeNotFoundException(id);
            var employee = _mapper.Map<EmployeeDto>(employeeDb);
            return employee;
        }
    }
}

This is also a pretty clear code and we can see the reason for creating a new exception class.
这也是一个非常清晰的代码,我们可以看到创建新的异常类的原因。

Finally, let’s modify the EmployeeController class:
最后,让我们修改 EmployeeController 类:

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

namespace CompanyEmployees.Presentation.Controllers
{
    [Route("api/companies/{companyId}/employees")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
        private readonly IServiceManager _service;
        public EmployeesController(IServiceManager service) => _service = service;

        [HttpGet]
        public IActionResult GetEmployeesForCompany(Guid companyId)
        {
            var employees = _service.EmployeeService.GetEmployees(companyId, trackChanges: false);
            return Ok(employees);
        }

        [HttpGet("{id:guid}")]
        public IActionResult GetEmployeeForCompany(Guid companyId, Guid id)
        {
            var employee = _service.EmployeeService.GetEmployee(companyId, id, trackChanges: false);
            return Ok(employee);
        }
    }
}

Excellent. You can see how clear our action is.
非常好。你可以看到我们的行动是多么明确。

We can test this action by using already created requests from the Bonus 2-CompanyEmployeesRequests.postman_collection.json file placed in the folder with the exercise files:
我们可以使用位于包含练习文件的文件夹中的 Bonus 2-CompanyEmployeesRequests.postman_collection.json 文件中已创建的请求来测试此作:

https://localhost:5001/api/companies/c9d4c053-49b6-410c-bc78-2d54a9991870/employees/86dba8c0-d178-41e7-938c-ed49778fb52a

alt text

When we send the request with an invalid company or employee id:
当我们使用无效的公司或员工 ID 发送请求时:
https://localhost:5001/api/companies/c9d4c053-49b6-410c-bc78-2d54a9991870/employees/86dba8c0-d178-41e7-938c-ed49778fb52c

alt text

alt text

Our responses are pretty self-explanatory, which makes for a good user experience.
我们的回答不言自明,这带来了良好的用户体验。

Until now, we have received only JSON formatted responses from our API. But what if we want to support some other format, like XML for example?
到目前为止,我们只收到了来自 API 的 JSON 格式的响应。但是,如果我们想要支持一些其他格式,例如 XML,该怎么办?

Well, in the next chapter we are going to learn more about Content Negotiation and enabling different formats for our responses.
那么,在下一章中,我们将了解有关 Content Negotiation 和为我们的响应启用不同格式的更多信息。

Ultimate ASP.NET Core Web API 5 GLOBAL ERROR HANDLING

5 GLOBAL ERROR HANDLING

5 全局错误处理

Exception handling helps us deal with the unexpected behavior of our system. To handle exceptions, we use the try-catch block in our code as well as the finally keyword to clean up our resources afterward.‌
异常处理有助于我们处理系统的意外行为。为了处理异常,我们在代码中使用 try-catch 块,然后使用 finally 关键字来清理我们的资源。

Even though there is nothing wrong with the try-catch blocks in our Actions and methods in the Web API project, we can extract all the exception handling logic into a single centralized place. By doing that, we make our actions cleaner, more readable, and the error handling process more maintainable.
即使 Web API 项目中的 Actions 和 methods 中的 try-catch 块没有问题,我们也可以将所有异常处理逻辑提取到一个集中的位置。通过这样做,我们使我们的作更清晰、更具可读性,并且错误处理过程更易于维护。

In this chapter, we are going to refactor our code to use the built-in middleware for global error handling to demonstrate the benefits of this approach. Since we already talked about the middleware in ASP.NET Core (in section 1.8), this section should be easier to understand.
在本章中,我们将重构我们的代码,以使用内置中间件进行全局错误处理,以演示这种方法的好处。由于我们已经在 ASP.NET Core 中讨论了中间件(在 1.8 节中),因此本节应该更容易理解。

5.1 Handling Errors Globally with the Built-In Middleware

5.1 使用内置中间件全局处理错误

The UseExceptionHandler middleware is a built-in middleware that we can use to handle exceptions. So, let’s dive into the code to see this middleware in action.‌
UseExceptionHandler 中间件是一个内置中间件,我们可以使用它来处理异常。因此,让我们深入研究代码,看看这个中间件的实际效果。

We are going to create a new ErrorModel folder in the Entities project, and add the new class ErrorDetails in that folder:
我们将在 Entities 项目中创建一个新的 ErrorModel 文件夹,并在该文件夹中添加新的类 ErrorDetails:

using System.Text.Json;

namespace Entities.ErrorModel
{
    public class ErrorDetails
    {
        public int StatusCode { get; set; }
        public string? Message { get; set; }
        public override string ToString() => JsonSerializer.Serialize(this);
    }
}

We are going to use this class for the details of our error message.
我们将使用这个类来获取错误消息的详细信息。

To continue, in the Extensions folder in the main project, we are going to add a new static class: ExceptionMiddlewareExtensions.cs.
要继续,在主项目的 Extensions 文件夹中,我们将添加新的静态类:ExceptionMiddlewareExtensions.cs。

Now, we need to modify it:
现在,我们需要修改它:

using Contracts;
using Entities.ErrorModel;
using Microsoft.AspNetCore.Diagnostics;
using System.Net;

namespace CompanyEmployees.Extensions
{
    public static class ExceptionMiddlewareExtensions
    {
        public static void ConfigureExceptionHandler(this WebApplication app, ILoggerManager logger)
        {
            app.UseExceptionHandler(appError =>
            {
                appError.Run(async context =>
                {
                    context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    context.Response.ContentType = "application/json";
                    var contextFeature = context.Features.Get<IExceptionHandlerFeature>();
                    if (contextFeature != null)
                    {
                        logger.LogError($"Something went wrong: {contextFeature.Error}");
                        await context.Response.WriteAsync(new ErrorDetails()
                        {
                            StatusCode = context.Response.StatusCode,
                            Message = "Internal Server Error.",
                        }.ToString());
                    }
                });
            });
        }
    }
}

In the code above, we create an extension method, on top of the WebApplication type, and we call the UseExceptionHandler method. That method adds a middleware to the pipeline that will catch exceptions, log them, and re-execute the request in an alternate pipeline.
在上面的代码中,我们在 WebApplication 类型之上创建了一个扩展方法,并调用 UseExceptionHandler 方法。该方法将中间件添加到管道中,该中间件将捕获异常、记录异常,并在备用管道中重新执行请求。

Inside the UseExceptionHandler method, we use the appError variable of the IApplicationBuilder type. With that variable, we call the Run method, which adds a terminal middleware delegate to the application’s pipeline. This is something we already know from section 1.8.
在 UseExceptionHandler 方法中,我们使用 IApplicationBuilder 类型的 appError 变量。使用该变量,我们调用 Run 方法,该方法将终端中间件委托添加到应用程序的管道中。这是我们从 1.8 节中已经知道的。

Then, we populate the status code and the content type of our response, log the error message and finally return the response with the custom-created object. Later on, we are going to modify this middleware even more to support our business logic in a service layer.
然后,我们填充响应的状态代码和内容类型,记录错误消息,最后返回包含自定义创建对象的响应。稍后,我们将进一步修改这个中间件,以支持我们在服务层中的业务逻辑。

Of course, there are several namespaces we should add to make this work:
当然,我们应该添加几个命名空间才能实现此目的:

using Contracts;
using Entities.ErrorModel;
using Microsoft.AspNetCore.Diagnostics;
using System.Net;

5.2 Program Class Modification

5.2 Program类修改

To be able to use this extension method, let’s modify the Program class:‌
为了能够使用这个扩展方法,让我们修改 Program 类:

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

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
LogManager.LoadConfiguration(
    string.Concat(Directory.GetCurrentDirectory(),
    "/nlog.config"));
builder.Services.ConfigureLoggerService();

builder.Services.ConfigureCors();
builder.Services.ConfigureIISIntegration();
builder.Services.ConfigureRepositoryManager();
builder.Services.ConfigureServiceManager();
builder.Services.ConfigureSqlContext(builder.Configuration);
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllers().AddApplicationPart(typeof(CompanyEmployees.Presentation.AssemblyReference).Assembly);

var app = builder.Build();

// Configure the HTTP request pipeline.
var logger = app.Services.GetRequiredService<ILoggerManager>();
app.ConfigureExceptionHandler(logger);

app.UseHttpsRedirection();

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

app.UseCors("CorsPolicy");
app.UseAuthorization();

app.MapControllers();

app.Run();

Here, we first extract the ILoggerManager service inside the logger variable. Then, we just call the ConfigureExceptionHandler method and pass that logger service. It is important to know that we have to extract the ILoggerManager service after the var app = builder.Build() code line because the Build method builds the WebApplication and registers all the services added with IOC.
在这里,我们首先提取 logger 变量中的 ILoggerManager 服务。然后,我们只需调用 ConfigureExceptionHandler 方法并传递该 Logger 服务。请务必知道,我们必须在 var app = builder.Build() 之后提取 ILoggerManager 服务,因为 Build 方法构建 WebApplication 并注册使用 IOC 添加的所有服务。

Additionally, we remove the call to the UseDeveloperExceptionPage method in the development environment since we don’t need it now and it also interferes with our error handler middleware.
此外,我们在开发环境中删除了对 UseDeveloperExceptionPage 方法的调用,因为我们现在不需要它,它还会干扰我们的错误处理程序中间件。

Finally, let’s remove the try-catch block from the GetAllCompanies service method:
最后,让我们从 GetAllCompanies 服务方法中删除 try-catch 块:

using AutoMapper;
using Contracts;
using Entities.Models;
using Service.Contracts;
using Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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;
        //    }
        //}

        public IEnumerable<CompanyDto> GetAllCompanies(bool trackChanges)
        {
            var companies = _repository.Company.GetAllCompanies(trackChanges);
            var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
            return companiesDto;
        }

    }
}

And from our GetCompanies action:
从我们的 GetCompanies action中:

using Microsoft.AspNetCore.Mvc;
using Service.Contracts;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CompanyEmployees.Presentation.Controllers
{
    [Route("api/[controller]")]
    [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");
        //    }
        //}

        [HttpGet]
        public IActionResult GetCompanies()
        {
            var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
            return Ok(companies);
        }

    }
}

And there we go. Our methods are much cleaner now. More importantly, we can reuse this functionality to write more readable methods and actions in the future.
好了。我们现在的方法要干净得多。更重要的是,我们可以在未来重用此功能来编写更多可读的方法和作。

5.3 Testing the Result

5.3 测试结果

To inspect this functionality, let’s add the following line to the‌ GetCompanies action, just to simulate an error:
为了检查此功能,让我们将以下行添加到 GetCompanies作中,以模拟错误:

        [HttpGet]
        public IActionResult GetCompanies()
        {
            throw new Exception("Exception");
            var companies = _service.CompanyService.GetAllCompanies(trackChanges: false);
            return Ok(companies);
        }

NOTE: Once you send the request, Visual Studio will stop the execution inside the GetCompanies action on the line where we throw an exception. This is normal behavior and all you have to do is to click the continue button to finish the request flow. Additionally, you can start your app with CTRL+F5, which will prevent Visual Studio from stopping the execution. Also, if you want to start your app with F5 but still to avoid VS execution stoppages, you can open the Tools->Options->Debugging->General option and uncheck the Enable Just My Code checkbox.
注意:发送请求后,Visual Studio 将在引发异常的行上的 GetCompanies作中停止执行。这是正常行为,您所要做的就是单击 continue (继续) 按钮以完成请求流。此外,您可以使用 Ctrl+F5 启动应用程序,这将阻止 Visual Studio 停止执行。此外,如果您想使用 F5 启动应用程序,但仍要避免 VS 执行停止,则可以打开 Tools->Options->Debugging->General 选项,并取消选中 Enable Just My Code 复选框。

And send a request from Postman:
从 Postman发送请求:
https://localhost:5001/api/companies

We can check our log messages to make sure that logging is working as well.
我们可以检查日志消息以确保日志记录也正常工作。