EF Core’s AddAsync v. Add Method
Since learning about async programming with C#, if I’m presented with an option of async v. non-async method I’ll always choose the async version. Why be blocking? Am I right?
A co-worker and I recently had a conversation about EF Core’s
AddAsync() method, and some googling led me to the conclusion that I’ve not been needing the async nature of
AddAsync(). The regular
Add() method fits most of the add use cases.
Let’s give a little background how EF Core tracks objects and then explain in detail why the
AddAsync() exists and how to choose between the two.
Unit of Work Pattern
EF Core’s DbContext adheres to the Unit of Work pattern.
A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you’re done, it figures out everything that needs to be done to alter the database as a result of your work.
When adding, creating, updating, deleting objects through a DbContext, those operations aren’t actually performed against the database. They’re performed against the context’s in memory representation of your database. Only when saving, are all the database operations pushed to the database.
Take the following example, we’re adding three users to our users table. These objects are initially only added to DbContext’s in memory tracking.
Only on line 9 when saving, do those objects get sent to the database and persisted.
But for example, if the
SaveChanges() method was never called, the three users would not get inserted into our database.
Since EF Core does a lot of things in memory, such as adding objects, why do I need an async version of a method that operates in memory?
An async method usually implies an network call. There is an overhead to async methods, so if I’m not needing any async-ness, am I just reducing the performance of my app?
So why does the
Add() method need an async version? A quick google search led me to the following fact:
This method is async only to allow special value generators, such as the one used by
Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy.SequenceHiLo, to access the database asynchronously. For all other cases the non async method should be used.
What are special value generators? What is SequenceHiLo? In brief, it’s a way to prevent unnecessary round trips to the database when creating primary keys. If a table’s primary key is an
int and it’s auto-incrementing, you’ll always need to ask the database what the next primary key is. SequenceHiLo allows reservation of a block of primary keys so trips to the database can be reduced. Here’s a great article describing SequenceHiLo in more detail.
Since I’ve never even encountered those terms before, the
Add() method fits the use case of every object I’ve ever created through EF Core.
EF Core 3.0 Changes
Unfortunately there’s a cost associated with asynchronous methods. For every
Task<T>, the C# compiler actually builds a state machine that tracks and coordinates the resuming of that task.
If asynchronous things are occurring, the cost of the state machine is negligible compared to the cost same method implemented in a blocking fashion. But if no asynchronous is needed, such as my uses with
AddAscyn(), that overhead can add up and reduce performance of your app.
With the EF Core’s 3.0 release, this method slightly changed. Instead of returning a
Task<T>, this method now returns a
This change reduces the number of heap allocations incurred when invoking these methods, improving general performance.
ValueTask is a struct, which can offer better performance that regular objects. It is the preferred return type of a method that could complete synchronously or asynchronously. If it needs to be synchronous, there is no overhead, but if it needs to make an asynchronous call then the normal
Task<T> overhead is invoked.
Which Add Method Should I Use?
I would default to using async version of methods if available. I believe it’s better to use async methods and potentially incur some overhead versus not using async methods and having blocking calls.
Pre 3.0, if performance is critical to your app, than I would recommend using the
Add() if you don’t need special value generators.
With 3.0 and later, go ahead and use the
AddAsync() method because the
ValueTask<T> prevents that overhead if it’s unnecessary.
The tools that we all use have a lot of complications and subtleties to them. Blanket statements like always use async methods don’t hold up under scrutiny. There are always exceptions.
That’s why its important to spend time understanding the tools that you’re working with. You never know what you don’t know, and what you don’t might causes issues for you.