ObviousAwait 1.2.0
dotnet add package ObviousAwait --version 1.2.0
NuGet\Install-Package ObviousAwait -Version 1.2.0
<PackageReference Include="ObviousAwait" Version="1.2.0" />
paket add ObviousAwait --version 1.2.0
#r "nuget: ObviousAwait, 1.2.0"
// Install ObviousAwait as a Cake Addin #addin nuget:?package=ObviousAwait&version=1.2.0 // Install ObviousAwait as a Cake Tool #tool nuget:?package=ObviousAwait&version=1.2.0
If you ever had the joy of async programming in .NET, you might have seen this guy:
await anyTask.ConfigureAwait(false);
ConfigureAwait()
tells the .NET runtime how you want your code to continue after it waited for the completion of an instruction.
This sounds easy so you might be surprised how many blog posts you'll find about this topic on the internet.
The most important one you should know is this one from Stephen Toub:
If you don't want to fight through this massive blog post, I highly recommend watching the following video. It's not mine but I wish every .NET developer would get a chance to watch it:
So why should I care?
If you are using async and await in your code without looking after ConfigureAwait()
, you are very likely to either run into cross-thread exceptions and deadlocks or miss an opportunity to speed up async code.
To clarify, let's take two passages from Stephen's blog post:
Why would I want to use
ConfigureAwait(false)
?
ConfigureAwait(continueOnCapturedContext: false)
is used to avoid forcing the callback to be invoked on the original context or scheduler. This has a few benefits: [...] Improving performance & Avoiding deadlocks.
Why would I want to use
ConfigureAwait(true)
?You wouldn’t, unless you were using it purely as an indication that you were purposefully not using
ConfigureAwait(false)
. [...] TheConfigureAwait()
method accepts a Boolean because there are some niche situations in which you want to pass in a variable to control the configuration. But the 99% use case is with a hardcoded false argument value,ConfigureAwait(false)
.
The takeaway
- Use
ConfigureAwait(false)
to tell the runtime that you don't need to run on the same context as you were called from. - If you don't use
ConfigureAwait(false)
you basically say the oppositeConfigureAwait(true)
which tells the runtime that you want to proceed on the same context as you were called from. This makes it the default behavior.
What's the problem with ConfigureAwait()
?
1. The term "configure await" does not tell anything about the intent of these methods.
"Configure await" does configure the way we want the awaited code to be continued. So this is not wrong at all. But, what are the options we have? true
and false
are not what you would use in natural language, like if you'd tell a co-worker, for example.
It's just like you hopefully will never see a method that configures file access like this:
File.ConfigureAccess("import.csv", isWriteOperation: true);
But instead one of these:
File.OpenRead("import.csv");
File.OpenWrite("import.csv");
OpenRead()
and OpenWrite()
both configure the way the file is accessed, but it's less that we "configure" anything. It's more what we want to do with it: the intent.
2. Passing boolean arguments to methods is considered to be a bad practice.
Passing boolean arguments is very known to developers. In many cases this is introduced to existing code when requirements arise, but I dare to say this is nothing to be proud of.
Having code that prints an invoice, for example, might get an additional boolean argument once the customer wants to include draft watermarks:
public void PrintInvoice(int id, bool isDraft)
The problem with this is that the caller has no chance to know what the argument means unless he uses a named argument (this is basically the same for other arguments like integers or strings but in most cases they deliver more context).
PrintInvoice(4711, true); // what does 'true' mean here?
PrintInvoice(4711, isDraft: true); // ah, okay ...
Martin Fowler calls this the "FlagArgument anti-pattern".
So quick: What does that true
argument mean again? How should this continue?
public async Task AddUserAsync(int id)
{
var user = await _userService.GetAsync(id).ConfigureAwait(true);
Users.Add(user);
}
This makes you ponder immediately, doesn't it? It shouldn't.
3. Skipping ConfigureAwait(true)
hides the purpose.
Well, imagine you find this code:
public async Task AddUserAsync(int id)
{
var user = await _userService.GetAsync(id);
Users.Add(user);
}
Did the author of these lines just forget to add ConfigureAwait(false)
or was it on purpose? If he ran that in a Windows Forms app, he might have had cross-thread exceptions with ConfigureAwait(false)
because unless the first line, the second line does not run on the UI thread anymore. So maybe he wanted the continuation to happen on the captured context. We simply don't know.
If he added ConfigureAwait(true)
explicitly, we could be assured that the author did that on purpose.
public async Task AddUserAsync(int id)
{
var user = await _userService.GetAsync(id).ConfigureAwait(true);
Users.Add(user);
}
However, chances are that he was not really sure which one to take:
❇️ Introducing ObviousAwait
All of this above really bothered me for years. That's why I wanted to use obvious methods to define the behavior of code continuation. Methods which are ...
- Simple but expressive
- Easy to read but even easier to distinguish
- As short as possible
- Optional: Same length
I spent more time that I want to admit chosing the following two methods names. But I'm no native speaker, so you might find better ones. Please let me know.
Here they are ...
KeepContext()
a.k.a. ConfigureAwait(true)
This method is called at the end of an awaitable instruction and tells us that we want to keep the context that instruction was called with so we still have it afterwards and we can use it to execute the code that follows.
public async Task AddUserAsync(int id)
{
var user = await _userService.GetAsync(id).KeepContext();
Users.Add(user);
}
It's clear and precise. The author clearly had an intent as he added this.
FreeContext()
a.k.a. ConfigureAwait(false)
This method was harder to name: It's not just that we don't care that the current context is kept to execute the code that follows. We even tell the runtime that it basically can take whatever context it has to do so. By calling FreeContext()
we can fasten things up by freeing the runtime from the responsibility to wait for a certain context.
public async Task AddUserAsync(int id)
{
var user = await _userService.GetAsync(id).FreeContext();
Users.Add(user);
}
The developer showed the intent that he does not need the current context to be kept. "Free context" sounds like a positive thing, something that you want to do if you can. Which you can ... in about 99% of the cases if we believe Stephen Toub.
I tried alternatives, like EmitContext()
, IgnoreContext()
or ElideContext()
and much more but in my non-native-speaker opinion, they might be clearer about the fact that the current context can be ignored but they don't deliver the positive aspect that "freeing up" the choice of context might be a good thing.
Using ObviousAwait
In fact, these two methods just call ConfigureAwait(true)
or ConfigureAwait(false)
internally making this the smallest package I have ever built and very easy to use:
- Add the NuGet package
ObviousAwait
to your project(s) - Replace
ConfigureAwait(true)
withKeepContext()
- Replace
ConfigureAwait(false)
withFreeContext()
- Enjoy
Another dependency just for this?
Yes, you're right. You don't need another dependency for aliasing ConfigureAwait()
. We're on .NET and this is not npm (providing some really ridiculous packages).
But please, if you like the idea of aliasing but you don't want to have another package dependency, just head over to that single code file and copy it over to your projects.
Alternatively, you can use dotnet-file to download and update the file directly from here:
dotnet file add https://github.com/awaescher/ObviousAwait/blob/master/ObviousAwait/ObviousExtensions.cs
Every now and then you can run dotnet file update
to keep it up-to-date.
Of couse you can get awareness of the pitfalls with ConfigureAwait()
to all of your co-workers. Accepting that little extra task of deciphering whether true
or false
was the right way to go in that particular case. However, I'd say:
Making things easy to get and hard to fail is always the way to go on the long run.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp1.0 was computed. netcoreapp1.1 was computed. netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard1.0 is compatible. netstandard1.1 was computed. netstandard1.2 was computed. netstandard1.3 was computed. netstandard1.4 was computed. netstandard1.5 was computed. netstandard1.6 was computed. netstandard2.0 was computed. netstandard2.1 is compatible. |
.NET Framework | net45 was computed. net451 was computed. net452 was computed. net46 was computed. net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen30 was computed. tizen40 was computed. tizen60 was computed. |
Universal Windows Platform | uap was computed. uap10.0 was computed. |
Windows Phone | wp8 was computed. wp81 was computed. wpa81 was computed. |
Windows Store | netcore was computed. netcore45 was computed. netcore451 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 1.0
- NETStandard.Library (>= 1.6.1)
-
.NETStandard 2.1
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.