Chapter14:ASYNCHRONOUS CODE

14 ASYNCHRONOUS CODE

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.
在本章中,我们将在 Core 内部将同步代码转换为异步代码 ASP.NET。首先,我们将学习一些关于异步编程的知识,以及为什么要编写异步代码。然后,我们将使用前几章中的代码并以异步方式重写它。

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 Whatis Asynchronous Programmming?

Async programming is a parallel programming technique that allows the working process to run separately from the main application thread. As soon as the work completes, it informs the main thread about the result whether it was successful or not.
异步编程是一种并行编程技术,它允许工作进程与主应用程序线程分开运行。一旦工作完成,它就会通知主线程结果是否成功。

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.
了解这一点非常重要,如果我们向端点发送请求,并且应用程序需要三秒或更长时间来处理该请求,我们可能无法在异步模式下更快地执行此请求。这将花费与同步请求相同的时间。

The only advantage is that in the async mode the thread won’t be blocked three or more seconds, and thus it will be able to process other requests. This is what makes our solution scalable.
唯一的优点是,在异步模式下,线程不会被阻塞三秒或更长时间,因此它将能够处理其他请求。这就是使我们的解决方案具有可扩展性的原因。

Here is a visual representation of the asynchronous workflow:
下面是异步工作流的可视化表示形式:

Alt text

Now that we’ve cleared that out, we can learn how to implement asynchronous code in .NET Core.
现在我们已经清除了它,我们可以学习如何在 .NET Core 中实现异步代码。

14.2 Async,Await Keywords, and Return Types

The async and await keywords play a crucial part in asynchronous programming. By using those keywords, we can easily write asynchronous methods without too much effort.
async 和 await 关键字在异步编程中起着至关重要的作用。通过使用这些关键字,我们可以轻松地编写异步方法,而无需太多努力。

For example, if we want to create a method asynchronously, we need to add the async keyword next to the method’s return type:
例如,如果我们想异步创建一个方法,我们需要在方法的返回类型旁边添加 async 关键字:

async Task<IEnumerable<Company>> GetAllCompaniesAsync()

By using the async keyword, we are enabling the await keyword and modifying how method results are handled (from synchronous to asynchronous):
通过使用 async 关键字,我们将启用 await 关键字并修改方法结果的处理方式(从同步到异步):

await FindAllAsync();

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

  • Task, for an async method that returns a value.
    Task,用于返回值的异步方法。
  • Task, for an async method that does not return a value.
    任务,用于不返回值的异步方法。
  • 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 — or if the sync method returns IEnumerable, then the async method should return Task<IEnumerable>.
好吧,我们可以通过同步编程眼镜来看待这一点。如果我们的同步方法返回一个 int,那么在异步模式下它应该返回 Task — 或者如果同步方法返回 IEnumerable,则异步方法应返回 Task<IEnumerable>。

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.
但是,如果我们的同步方法不返回任何值(返回类型为空),那么我们的异步方法应该返回 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.
你现在可能想知道,为什么不一直返回任务?好吧,我们应该只对需要空返回类型。除此之外,我们应该始终返回一个任务。

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

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

14.2.1 The IRepositoryBase Interface and the RepositoryBase Class Explanation

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.
我们不会更改提到的接口和类。这是因为我们希望为存储库用户类保留同步或异步方法执行的可能性。有时,异步代码可能会变得比同步代码慢,因为 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

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

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

    public interface ICompanyRepository
    {
        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.
“创建”和“删除”方法签名保持同步。这是因为,在这些方法中,我们没有对数据库。我们所做的只是将实体的状态更改为“已添加”和“已删除”。

So, in accordance with the interface changes, let’s modify our CompanyRepository.cs class, which we can find in the Repository project:
所以,根据接口的变化,让我们修改我们的

CompanyRepository.cs类,我们可以在存储库项目中找到它:

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 IEnumerable<Company> GetByIds(IEnumerable<Guid> ids, bool trackChanges) =>
    //    FindByCondition(x => ids.Contains(x.Id), trackChanges).ToList();

    public void CreateCompany(Company company) => Create(company);
    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 async Task<IEnumerable<Company>> GetByIdsAsync(IEnumerable<Guid> ids, bool trackChanges) => 
        await FindByCondition(x => ids.Contains(x.Id), trackChanges).ToListAsync();
}

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

14.4 IRepositoryManager and RepositoryManager Changes

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

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

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

}

And class modification:
和类修改:

//RepositoryManager.cs

//public void Save() => _repositoryContext.SaveChanges();
public Task SaveAsync() => _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()… 方法是等待的,我们可以使用 await 关键字;因此,我们的方法需要有异步关键字和任务作为返回类型。

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 Controller Modification

Finally, we need to modify all of our actions in the CompaniesController to work asynchronously.
最后,我们需要修改我们所有的操作

So, let’s first start with the GetCompanies method:
公司控制器异步工作。因此,让我们首先从 GetCompanies 方法开始:

[HttpGet]
public async Task<IActionResult> GetCompanies()
{
    var companies = await _repository.Company.GetAllCompaniesAsync(trackChanges: false);
    var companiesDto = _mapper.Map<IEnumerable<CompanyDto>>(companies);
    return Ok(companiesDto);
}

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() 方法。这几乎是我们在控制器中的所有操作中应该做的事情。

So, let’s modify all the other actions.
因此,让我们修改所有其他控制器。

GetCompany:

[HttpGet("{id}", Name = "CompanyById")]
public async Task<IActionResult> GetCompany(Guid id)
{
    var company = await _repository.Company.GetCompanyAsync(id, trackChanges: false);
    if (company == null)
    {
        _logger.LogInfo($"Company with id: {id} doesn't exist in the database.");
        return NotFound();
    }
    else
    {
        var companyDto = _mapper.Map<CompanyDto>(company);
        return Ok(companyDto);
    }
}

GetCompanyCollection:

[HttpGet("collection/({ids})", Name = "CompanyCollection")]
public async Task<IActionResult> GetCompanyCollection([ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<Guid> ids)
{
    if (ids == null)
    {
        _logger.LogError("Parameter ids is null");
        return BadRequest("Parameter ids is null");
    }
    var companyEntities = await _repository.Company.GetByIdsAsync(ids, trackChanges: false);
    if (ids.Count() != companyEntities.Count())
    {
        _logger.LogError("Some ids are not valid in a collection");
        return NotFound();
    }
    var companiesToReturn = _mapper.Map<IEnumerable<CompanyDto>>(companyEntities);
    return Ok(companiesToReturn);
}

CreateCompany:

[HttpPost]
public async Task<IActionResult> CreateCompany([FromBody] CompanyForCreationDto company)
{
    if (company == null)
    {
        _logger.LogError("CompanyForCreationDto object sent from client is null.");
        return BadRequest("CompanyForCreationDto object is null");
    }
    if (!ModelState.IsValid)
    {
        _logger.LogError("Invalid model state for the CompanyForCreationDto object");
        return UnprocessableEntity(ModelState);
    }
    var companyEntity = _mapper.Map<Company>(company);
    _repository.Company.CreateCompany(companyEntity);
    await _repository.SaveAsync();
    var companyToReturn = _mapper.Map<CompanyDto>(companyEntity);
    return CreatedAtRoute("CompanyById", new { id = companyToReturn.Id }, companyToReturn);
}

CreateCompanyCollection:

[HttpPost("collection")]
public async Task<IActionResult> CreateCompanyCollection([FromBody] IEnumerable<CompanyForCreationDto> companyCollection)
{
    if (companyCollection == null)
    {
        _logger.LogError("Company collection sent from client is null.");
        return BadRequest("Company collection is null");
    }
    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 CreatedAtRoute("CompanyCollection", new { ids }, companyCollectionToReturn);
}

DeleteCompany:

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCompany(Guid id)
{
    var company = await _repository.Company.GetCompanyAsync(id, trackChanges: false);
    if (company == null)
    {
        _logger.LogInfo($"Company with id: {id} doesn't exist in the database.");
        return NotFound();
    }
    _repository.Company.DeleteCompany(company);
    await _repository.SaveAsync();
    return NoContent();
}

UpdateCompany:

[HttpPut("{id}")]
public async Task<IActionResult> UpdateCompany(Guid id, [FromBody] CompanyForUpdateDto company)
{
    if (company == null)
    {
        _logger.LogError("CompanyForUpdateDto object sent from client is null.");
        return BadRequest("CompanyForUpdateDto object is null");
    }
    if (!ModelState.IsValid)
    {
        _logger.LogError("Invalid model state for the CompanyForUpdateDto object");
        return UnprocessableEntity(ModelState);
    }
    var companyEntity = await _repository.Company.GetCompanyAsync(id, trackChanges: true);
    if (companyEntity == null)
    {
        _logger.LogInfo($"Company with id: {id} doesn't exist in the database.");
        return NotFound();
    }
    _mapper.Map(company, companyEntity);
    await _repository.SaveAsync();
    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 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 类中实现异步后,您可以尝试发送不同的请求(来自任何章节)来测试异步操作。所有这些都应该像以前一样工作,没有错误,但这次是以异步方式。

发表评论