
When we use a method that needs a long time to run (i.e., for reading a large size file or downloading heavy resources from the network), in a synchronized application, the application itself stops running until the activity is completed. In these cases, the asynchronous programming is very helpful: it allows us to perform parallel execution of different tasks and wait for their completion if needed.
There are many different model types for this approach to programming: APM (Asynchronous Programming Model), the asynchronous model based on events (EAP), and the TAP, the asynchronous model based on tasks (Task). Let’s see how we can implement the third method in C#, using the keywords async, and await.
One of the main problems of writing asynchronous code is maintainabilty: in fact, this programming method tends to complicate the code. Luckily, C#5 introduced a simplified approach, in which the compilator runs the difficult job, previously made by the developer, and the application preserves a logical structure, similar to the synchronous code.
Let’s take an example. Suppose we have a .NET Core project, in which we should manage three entities: Area, Company, and Resource.
public class Area
{
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
}
public class Company
{
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
}
public class Resource
{
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
}
Now suppose we should save the values of these entities on a database using Entity Framework Core. The DbContext would be as:
public class AppDbContext : DbContext
{
public DbSet<Area> Areas { get; set; }
public DbSet<Company> Companies { get; set; }
public DbSet<Resource> Resources { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}
override protected void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Area> ().HasData(
new Area { Id = 1, Name = "Area1"},
new Area { Id = 2, Name = "Area2"},
new Area { Id = 3, Name = "Area3"},
new Area { Id = 4, Name = "Area4"},
new Area { Id = 5, Name = "Area5"});
modelBuilder.Entity<Company> ().HasData(
new Area { Id = 1, Name = "Company1"},
new Area { Id = 2, Name = "Company2"},
new Area { Id = 3, Name = "Company3"},
new Area { Id = 4, Name = "Company4"},
new Area { Id = 5, Name = "Company5"});
modelBuilder.Entity<Resource>().HasData(
new Area { Id = 1, Name = "Resource1"},
new Area { Id = 2, Name = "Resource2"},
new Area { Id = 3, Name = "Resource3"},
new Area { Id = 4, Name = "Resource4"},
new Area { Id = 5, Name = "Resource5"});
}
}
As you can see from the code, we insert some sample data to work on. Suppose now we want to expose these data with a Controller API, both individually (for each entity), and with a method that joins them all, returning them with a single call.
With the synchronous approach, the Controller API would be:
[ApiController]
[Route("[controller]")]
public class DataController : ControllerBase
{
private readonly AppDbContext db = null;
public DataController(AppDbContext db)
{
this.db = db;
}
public IActionResult Get()
{
var areas = this.GetAreas();
var companies = this.GetCompanies();
var resources = this.GetResources();
return Ok(new { areas = areas, companies = companies, resources = resources });
}
[Route("areas")]
public Area[] GetAreas()
{
return this.db.Areas.ToArray();
}
[Route("companies")]
public Company[] GetCompanies()
{
return this.db.Companies.ToArray();
}
[Route("resources")]
public Resource[] GetResources()
{
return this.db.Resources.ToArray();
}
}
The Get() method callS inside it the three methods that return single results, and it waits for the execution of each of them to be completed before it passes to the next. The three methods are not interrelated, then you don’t need to wait for the execution of one of them in order to invoke another. You can then create three independent Tasks to parallelize the execution.
A first approach can be based on the method Run of the Task class that queues the specified job to be ran in the thread-pool and returns a Task object and it represents this job. In this way, methods are run at the same time on different threads of the pool:
public IActionResult Get()
{
var areas = Task.Run(() = > this.GetAreas());
var companies = Task.Run(() = > this.GetCompanies());
var resources = Task.Run(() = > this.GetResources());
Task.WhenAll(areas, companies, resources);
return Ok(new { areas = areas.Result, companies = companies.Result, resources = resources.Result });
}
The Result property of Task contains the result of the elaboration. The method WhenAll permits to suspend the present thread execution until the completion of all Tasks. Running the code, we can note an interesting thing: the call interrupts, and it launches the following exception:
AggregateException: One or more errors occurred. (A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913 .)
This error message tells us that methods are executed simultaneously on different threads, but an exception raises since they use the same instance as the DbContext for the connection to DB,
The DbContext class can’t assure a threadsafe functioning: we can easily bypass this problem, avoiding that the dependency injection engine of .NET Core creates a single instance, and we create a separate instance for each method. As an example, let’s see how the method GetAreas() would change:
public class DataController : ControllerBase
{
private readonly DbContextOptionsBuilder <AppDbContext> optionsBuilder = null;
public DataController(IConfiguration configuration)
{
this.optionsBuilder = new DbContextOptionsBuilder <AppDbContext> ()
.UseSqlite(configuration.GetConnectionString("DefaultConnection"));
}
[Route("areas")]
public Area[] GetAreas()
{
using(var db = new AppDbContext(this.optionsBuilder.Options))
{
return db.Areas.ToArray();
}
}
}
Well, now it works. We should note that Entity Framework Core provides some methods to make asynchronous calls with the same DbContext, as the method ToArrayAsync, for example, which creates an array from an IQueryable<T> enumerating it asynchronously. This method returns a Task<TSource[]>, an activity that represents the asynchronous operation. In this way, we don’t need to use Task.Run() anymore:
public IActionResult Get()
{
var areas = this.GetAreas();
var companies = this.GetCompanies();
var resources = this.GetResources();
Task.WhenAll(areas, companies, resources);
return Ok(new { areas = areas.Result, companies = companies.Result, resources = resources.Result });
}
[Route("areas")]
public Task<Area[]> GetAreas()
{
return db.Areas.ToArrayAsync();
}
Microsoft doesn’t assure that these asynchronous methods work in every situation, anyway because the DbContext has not been designed to be threadsafe. You can consult this link for more information: https://docs.microsoft.com/en-us/ef/core/querying/async
The best practice when you are working with Entity Framework Core is to have a DbContext for every asynchronous operation or waiting for each of them to be finished, before launching another one.
This best practice is ok when we have to make an asynchronous call and return the result. But what happens if we want to make some operations with the result before returning it? What if we want to add an element to the list? We should wait for results, add the element, and then return the modified list:
[Route("companies")]
public Task<Company[]> GetCompanies()
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
var data = this.db.Companies.ToListAsync().Result;
data.Insert(0, new Company() { Id = 0, Name = "-"});
return data.ToArray();
}
}
Unfortunately, this code doesn’t compile because the data.ToArray() returns an array and not a Task. In fact, we need three threads here: the main caller (Get()), the query on the database (this.db.Companies.ToListAsync()), and a thread, which adds one value to the list. We have three ways to do that: let’s see them all with our three single methods. The first one, which we have already seen, can use the method Task.Run():
[Route("companies")]
public Task<Company[]> GetCompanies()
{
return Task.Run(() =>
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
var data = db.Companies.ToList();
data.Insert(0, new Company() { Id = 0, Name = "-" });
return data.ToArray();
}
});
}
As an alternative, we can use the method ContinueWith(), that can be applied to the Tasks and in which we can specify a new Task to run, as soon as the previous one is finished:
[Route("resources")]
public Task <Resource[]> GetResources()
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
return db.Resources.ToListAsync()
.ContinueWith(dataTask = >
{
var data = dataTask.Result;
dataTask.Result.Insert(0, new Resource() { Id = 0, Name = "-" });
return data.ToArray();
});
}
}
We can get the compiler to do the “dirty work” and use the keywords async and await, that can create the Task for us:
[Route("areas")]
public async Task <Area[]> GetAreas()
{
using (var db = new AppDbContext(this.optionsBuilder.Options))
{
var data = await db.Areas.ToListAsync();
data.Insert(0, new Area() { Id = 0, Name = "-" });
return data.ToArray();
}
}
As you can see in this last method, the code is much simpler and hides to us the creation of the Task, allowing us to give an asynchronous return. Let’s imagine a scenario where calls are more than one, and how this approach makes everything more linear.
The side effect of our refactoring is that the method GetAreas() has become an asynchronous action. This fact means that when different requests arrive at this API, the thread of the pool allocated for the request will be released to be available for further requests until the DbContext terminate the data fetch.
I wish I intrigued you enough to analyze the argument in deep. The use of async and await is very convenient in many scenarios and, besides making the code much clean and linear, it can bring to an increment of application’s general performances.
The code is available here: https://github.com/fvastarella/Programmazione-asincrona-con-async-await
See you next!