In this post, we are going to convert the 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 project from the .NET Core series and rewrite it in an async manner.

In Part 4 of the series, we have created Generic Repository Pattern and in Part 5 and Part 6 the Controller with Actions that consumes that repository. We recommend reading those articles if you are not familiar with Generic Repository Pattern or if you find any of the concepts in this article hard to understand. Those articles will help you follow along with this article much easier because we won’t dive into its business logic.

We are going to modify the code, step by step, to show you how easy is to convert the synchronous code to an asynchronous one. Hopefully, this will help you understand how asynchronous code works and how to write it from scratch in your applications.

To download the source code for our starting project, you can visit our GitHub repo for the starting project.

For the finished project refer to our GitHub repo for the finished project.

We are going to cover the following sections in this article:

What is Asynchronous Programming

Async programming is a parallel programming technique, which 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 the visual representation of the asynchronous workflow:

Asynchronous Generic Pattern

Now that we cleared that out we can learn how to implement the asynchronous code in .NET Core.

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.

For example, if we want to create a method in an asynchronous manner, we need to add the async keyword next to the method’s return type:

async Task<IEnumerable<Owner>> GetAllOwnersAsync()

By using the async keyword, we are enabling the await keyword and modifying the way of how method results are handled (from synchronous to asynchronous):
await FindAllAsync();

In asynchronous programming we have three return types:

  • Task<TResult>, for an async method that returns a value
  • Task, to use it for an async method that does not return a value
  • void, which we can use for an event handler

What does this mean?

Well, we can look at this through the synchronous programming glasses. If our sync method returns  int then in the async mode, it should return Task<int>, or if sync method returns IEnumerable<string> then the async method should return Task<IEnumerable<string>>.

But if our sync method returns no value (has a void for the return type), then our async method should usually returnTask. This means that we can use the await keyword inside that method but without the return keyword.

You may wonder, why not returning 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 GetAwaiter method.

Now, when we have all the information, let’s do some refactoring in our completely synchronous code.

Overview of the IRepositoryBase Interface and the RepositoryBase Class

Our complete repository is interface based, so let’s take a look at our base interface. In the Contracts project open the IRepositoryBase.cs file. We can see different method signatures:

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

It is important to notice the FindAll and FindByCondition method signatures. Both of them return IQueryable that allows us to attach async calls to them.

The Create, Update, and Delete methods  don’t modify any data, they just track changes to an entity and wait for the EF Core’s SaveChanges method to execute. So, they stay unchanged as well.

Let’s just take a look at the RepositoryBase class:

public abstract class RepositoryBase<T> : IRepositoryBase<T> where T : class
{
    protected RepositoryContext RepositoryContext { get; set; }

    public RepositoryBase(RepositoryContext repositoryContext)
    {
        this.RepositoryContext = repositoryContext;
    }

    public IQueryable<T> FindAll()
    {
        return this.RepositoryContext.Set<T>().AsNoTracking();
    }

    public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression)
    {
        return this.RepositoryContext.Set<T>()
            .Where(expression).AsNoTracking();
    }

    public void Create(T entity)
    {
        this.RepositoryContext.Set<T>().Add(entity);
    }

    public void Update(T entity)
    {
        this.RepositoryContext.Set<T>().Update(entity);
    }

    public void Delete(T entity)
    {
        this.RepositoryContext.Set<T>().Remove(entity);
    }
}

This class is the implementation of the previous interface and it is a base class for accessing the data from the database.

Modifying the IOwnerRepository Interface and the OwnerRepository Class

Now, let’s continue on the other parts of our repository. In the Contracts project, there is also the IOwnerRepository interface with all the synchronous method signatures which we should change too.

So let’s do that:

public interface IOwnerRepository : IRepositoryBase<Owner>
{
    Task<IEnumerable<Owner>> GetAllOwnersAsync();
    Task<Owner> GetOwnerByIdAsync(Guid ownerId);
    Task<Owner> GetOwnerWithDetailsAsync(Guid ownerId);
    void CreateOwner(Owner owner);
    void UpdateOwner(Owner owner);
    void DeleteOwner(Owner owner);
}

So, we just change signatures of the first three GET methods by assigning the Task<TResult> to the return type. We don’t have to change the other three methods because, as we said, they just change the state of the entity and wait for the SaveChanges to execute.

Now, we have to implement this interface by using the async and await keywords. Using the await keyword is not mandatory though. Of course, if we don’t use it, our async methods will execute synchronously, and that is not our goal here.

So, in accordance with the interface changes, let’s modify our OwnerRepository.cs class, that we may find in the Repository project:

public class OwnerRepository : RepositoryBase<Owner>, IOwnerRepository 
{ 
    public OwnerRepository(RepositoryContext repositoryContext) 
        : base(repositoryContext) 
    { 
    }

    public async Task<IEnumerable<Owner>> GetAllOwnersAsync() 
    { 
         return await FindAll()
            .OrderBy(ow => ow.Name)
            .ToListAsync(); 
    }

    public async Task<Owner> GetOwnerByIdAsync(Guid ownerId)
    {
        return await FindByCondition(owner => owner.Id.Equals(ownerId))
            .FirstOrDefaultAsync();
    }

    public async Task<Owner> GetOwnerWithDetailsAsync(Guid ownerId)
    {
        return await FindByCondition(owner => owner.Id.Equals(ownerId))
            .Include(ac => ac.Accounts)
            .FirstOrDefaultAsync();
    }

    public void CreateOwner(Owner owner)
    {
        Create(owner);
    }

    public void UpdateOwner(Owner owner)
    {
        Update(owner);
    }

    public void DeleteOwner(Owner owner)
    {
        Delete(owner);
    }
}

IRepositoryWrapper and RepositoryWrapper Changes

We have to modify the Save method in the mentioned interface and the class as well:

public interface IRepositoryWrapper 
{ 
    IOwnerRepository Owner { get; } 
    IAccountRepository Account { get; } 
    Task SaveAsync(); 
}

And let’s just modify the Save method in the RepositoryWrapper class:
public async Task SaveAsync() 
{
    await _repoContext.SaveChangesAsync();
}

Now, we can continue to the controller modification.

Controller Modification

Finally, we need to modify all of our actions in the OwnerController to work asynchronously.

So, let’s first start with the GetAllOwners method:

[HttpGet] 
public async Task<IActionResult> GetAllOwners() 
{ 
    try 
    { 
        var owners = await _repository.Owner.GetAllOwnersAsync(); 
        _logger.LogInfo($"Returned all owners from database.");

        var ownersResult = _mapper.Map<IEnumerable<OwnerDto>>(owners);
        return Ok(ownersResult); 
    } 
    catch (Exception ex) 
    { 
        _logger.LogError($"Something went wrong inside GetAllOwners action: {ex.Message}"); 
        return StatusCode(500, "Internal server error"); 
    } 
 }

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 GetAllOwnersAsync() method. And that is pretty much what we should do in all the actions in our controller.

So let’s modify all the other actions.

GetOwnerById:

[HttpGet("{id}", Name = "OwnerById")] 
public async Task<IActionResult> GetOwnerById(Guid id) 
{ 
    try 
    { 
        var owner = await _repository.Owner.GetOwnerByIdAsync(id); 
        if (owner == null) 
        { 
            _logger.LogError($"Owner with id: {id}, hasn't been found in db."); 
            return NotFound(); 
        } 
        else 
        { 
            _logger.LogInfo($"Returned owner with id: {id}");

            var ownerResult = _mapper.Map<OwnerDto>(owner);
            return Ok(ownerResult); 
        } 
    } 
    catch (Exception ex) 
    { 
        _logger.LogError($"Something went wrong inside GetOwnerById action: {ex.Message}"); 
        return StatusCode(500, "Internal server error"); 
    } 
}

GetOwnerWithDetails:
[HttpGet("{id}/account")] 
public async Task<IActionResult> GetOwnerWithDetails(Guid id) 
{ 
    try 
    { 
	var owner = await _repository.Owner.GetOwnerWithDetailsAsync(id); 
	if (owner == null) 
	{ 
	    _logger.LogError($"Owner with id: {id}, hasn't been found in db."); 
	    return NotFound(); 
	} 
	else 
	{ 
	    _logger.LogInfo($"Returned owner with details for id: {id}");

	    var ownerResult = _mapper.Map<OwnerDto>(owner);
	    return Ok(ownerResult); 
	} 
} 
    catch (Exception ex) 
    { 
        _logger.LogError($"Something went wrong inside GetOwnerWithDetails action: {ex.Message}"); 
	return StatusCode(500, "Internal server error"); 
    }
}

CreateOwner:
[HttpPost]
public async Task<IActionResult> CreateOwner([FromBody]OwnerForCreationDto owner)
{
    try
    {
	if (owner == null)
	{
	    _logger.LogError("Owner object sent from client is null.");
	    return BadRequest("Owner object is null");
	}

	if (!ModelState.IsValid)
	{
	    _logger.LogError("Invalid owner object sent from client.");
	    return BadRequest("Invalid model object");
	}

	var ownerEntity = _mapper.Map<Owner>(owner);

	_repository.Owner.CreateOwner(ownerEntity);
	await _repository.SaveAsync();

	var createdOwner = _mapper.Map<OwnerDto>(ownerEntity);

	return CreatedAtRoute("OwnerById", new { id = createdOwner.Id }, createdOwner);
    }
    catch (Exception ex)
    {
    	_logger.LogError($"Something went wrong inside CreateOwner action: {ex.Message}");
	return StatusCode(500, "Internal server error");
    }
}

UpdateOwner:
[HttpPut("{id}")]
public async Task<IActionResult> UpdateOwner(Guid id, [FromBody]OwnerForUpdateDto owner)
{
    try
    {
        if (owner == null)
	{
            _logger.LogError("Owner object sent from client is null.");
	    return BadRequest("Owner object is null");
	}

	if (!ModelState.IsValid)
	{
	    _logger.LogError("Invalid owner object sent from client.");
	    return BadRequest("Invalid model object");
	}

	var ownerEntity = await _repository.Owner.GetOwnerByIdAsync(id);
	if (ownerEntity == null)
	{
	    _logger.LogError($"Owner with id: {id}, hasn't been found in db.");
	    return NotFound();
	}

	_mapper.Map(owner, ownerEntity);

	_repository.Owner.UpdateOwner(ownerEntity);
	await _repository.SaveAsync();

	return NoContent();
    }
    catch (Exception ex)
    {
  	_logger.LogError($"Something went wrong inside UpdateOwner action: {ex.Message}");
	return StatusCode(500, "Internal server error");
    }
}

DeleteOwner:
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteOwner(Guid id)
{
    try
    {
        var owner = await _repository.Owner.GetOwnerByIdAsync(id);
        if (owner == null)
        {
            _logger.LogError($"Owner with id: {id}, hasn't been found in db.");
            return NotFound();
        }

        if (_repository.Account.AccountsByOwner(id).Any()) 
        {
            _logger.LogError($"Cannot delete owner with id: {id}. It has related accounts. Delete those accounts first"); 
            return BadRequest("Cannot delete owner. It has related accounts. Delete those accounts first"); 
        }

        _repository.Owner.DeleteOwner(owner);
        await _repository.SaveAsync();

        return NoContent();
    }
    catch (Exception ex)
    {
        _logger.LogError($"Something went wrong inside DeleteOwner action: {ex.Message}");
        return StatusCode(500, "Internal server error");
    }
}

Excellent. Now we are talking async 😀

We can make our actions even more maintainable and readable by implementing Global Error Handling to remove try-catch blocks and Action Filters as well to remove validation code repetitions.

Conclusion

There you go. We have seen how easy is to convert the synchronous repository to asynchronous and how easy is to write async code overall. With a couple of changes, we gave more breathing space to our API and created a more responsive application.

It is a good practice to write async methods (when we have an opportunity) that handles the I/O actions because it is easy to write those methods and the benefits are indisputable.

Thank you for reading the article and we hope you found something useful in it.