So far, all of our database operations with Entity Framework have been synchronous. In other words, our .NET program waited for any given database operation, such as a query or an update, to complete before moving forward. In many cases, there is nothing wrong with this approach. There are use cases, however, where an ability to perform such operations asynchronously is important. In these cases, we let .NET use its execution thread while the software waits for the database operation to complete. For example, if you are creating a web application utilizing the asynchronous approach, we can be more efficient with server resources, releasing web worker threads back to the thread pool while we are waiting for the database to finish processing a request, whether it is a save or retrieve operation. Even in a desktop application, the asynchronous API is useful because the user can potentially perform other tasks in the application, instead of waiting on a possibly time-consuming query or save operation. In other words, the .NET thread does not need to wait for a database thread to complete its work. In a number of applications, the asynchronous API does not provide benefits and could even be harmful from the performance perspective due to the thread context switching. Before using the asynchronous API, developers need to make sure it will benefit them.
Entity Framework exposes a number of asynchronous operations. By convention, all such methods end with the Async
suffix. For save operations, we can use the SaveChangesAsync
method on DbContext
. There are many methods for query operations. For example, many aggregate functions have asynchronous counterparts, such as SumAsync
or AverageAsync
. We can also asynchronously read a result set into a list or an array using ToListAsync
or ToArrayAsync
, respectively. Also, we can enumerate through the results of a query using ForEachAsync
. Let's look at a few examples.
This is how we can get the list of objects from a database asynchronously:
private static async Task<IEnumerable<Company>> GetCompaniesAsync() { using (var context = new Context()) { return await context.Companies .OrderBy(c => c.CompanyName) .ToListAsync(); } }
It is important to notice that we follow typical async/await usage patterns. Our function is flagged as async and returns a task object, specifically a task of a list of companies. We create a DbContext
object inside our function. Then, we create creating a query that returns all companies, ordered by their names. Then, we return the results of this query wrapped inside an asynchronous list generation. We have to await this return value, as we need to follow async/await patterns. Here is how this code looks in VB.NET:
Private Async Function GetCompaniesAsync() As Task(Of IEnumerable(Of Company)) Using context = New Context() Return Await context.Companies.OrderBy( _ Function(c) c.CompanyName).ToListAsync() End Using End Function
Next, let's create a new record asynchronously:
private static async Task<Company> AddCompanyAsync(Company company) { using (var context = new Context()) { context.Companies.Add(company); await context.SaveChangesAsync(); return company; } }
Again, we are wrapping the operation inside the async
function. We are accepting a parameter, company
in our case. We are adding this company
to the context. Finally, we save asynchronously and return the saved company
. Here is how the code looks in VB.NET:
Private Async Function AddCompanyAsync(company As Company) As Task(Of Company) Using context = New Context() context.Companies.Add(company) Await context.SaveChangesAsync() Return company End Using End Function
Next, we can locate a record asynchronously. We can use any number of methods here, such as Single
or First
. Both of them have asynchronous versions. We will use the Find
method in our example, as shown in the following code snippet:
private static async Task<Company> FindCompanyAsync(int companyId) { using (var context = new Context()) { return await context.Companies .FindAsync(companyId); } }
We see the familiar asynchronous pattern in this code snippet as well. We just use FindAsync
, which takes the exact same parameters as the synchronous version. In general, all asynchronous methods in Entity Framework have the same signature, as far as parameters are concerned, as their synchronous counterparts.
Here is how the same method looks in VB.NET:
Private Async Function FindCompanyAsync(companyId As Integer) As Task(Of Company) Using context = New Context() Return Await context.Companies.FindAsync(companyId) End Using End Function
As we mentioned before, aggregate functions have the async
versions as well. For example, here is how we compute count asynchronously:
private static async Task<int> ComputeCountAsync() { using (var context = new Context()) { return await context.Companies .CountAsync(c => c.IsActive); } }
We use the CountAsync
method and pass in a condition, which is exactly what we would have done if we were to call the synchronous version, the Count
function. Here is how the code looks in VB.NET:
Private Async Function ComputeCountAsync() As Task(Of Integer) Using context = New Context() Return Await context.Companies.CountAsync( _ Function(c) c.IsActive) End Using End Function
If you would like to loop asynchronously through query results, you can also use ForEachAsync
, which you can attach to any query as well. For example, here is how we can loop through Companies
:
private static async Task LoopAsync() { using (var context = new Context()) { await context.Companies.ForEachAsync(c => { c.IsActive = true; }); await context.SaveChangesAsync(); } }
In the preceding code, we run through all type Companies
, but we could have just as easily run through a result of any query, after applying an order or a filter. Here is what this method looks like in VB.NET:
Private Async Function LoopAsync() As Task Using context = New Context() Await context.Companies.ForEachAsync( _ Sub(c) c.IsActive = True End Sub) Await context.SaveChangesAsync() End Using End Function
We followed the accepted naming conventions, adding the Async
suffix to our asynchronous functions. Usually, these functions would be called from a method that is also flagged as async
and we would have awaited a result of our asynchronous functions. If this is not possible, we can always use the Task API and wait for a task to complete. For example, we can access the result of a task, causing the current thread to pause and let the task finish executing, as shown in the following code snippet:
Console.WriteLine( FindCompanyAsync(companyId).Result.CompanyName);
In this example, we call the previously defined function and then access the Result
property of the task to cause the asynchronous function to finish executing. This is how the code would look in VB.NET:
Console.WriteLine(FindCompanyAsync(companyId).Result.CompanyName)
When deciding whether or not to use the asynchronous API, we need to research and make sure that there is a reason to do so. We should also ensure that the entire calling chain of methods is asynchronous to gain maximum coding benefits. Finally, resort to the Task API when you need to.
18.190.217.253