Storing Context Data in C# using AsyncLocal
source link: https://vainolo.com/2022/02/23/storing-context-data-in-c-using-asynclocal/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
In procedural and functional programming, the way to pass data between methods is by either a parameter (incoming) or a return value (outgoing). This works, but it has a downside – the state you are passing left and right is now part of every API. One way to get rid of this is by using a Context
object that can be accessed from anywhere in the code. And in a multi-threaded, async codebase (which is very popular nowadays), matching between the current flow and the correct context can be tricky. For this purpose, we have the AsyncLocal
class.
As written in the docs: “Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.” So what exactly does this mean? Let’s take a look.
I’ll start with a simple .net
console project (which I create using dotnet new console
in a clean directory). In this sample, I’ve written a typed context provider class that does two things: 1) Initializes a context and returns it to the caller, and 2) Returns the current context to the caller. This is how it looks:
public
class
ContextProvider<T>
where
T :
new
()
{
public
T InitContext()
{
_asyncLocal.Value =
new
T();
return
_asyncLocal.Value;
}
public
T GetContext()
{
return
_asyncLocal.Value;
}
private
static
readonly
AsyncLocal<T> _asyncLocal =
new
AsyncLocal<T>();
}
Now let’s see how this behaves. I’m going to create a context class that contains a Guid
, run a couple of async tasks and see what happens:
Random r =
new
Random();
var
ctxProvider =
new
ContextProvider<Context>();
async
Task InitializeAndPrintContextAsync(
int
id)
{
var
ctx = ctxProvider.InitContext();
var
guid = Guid.NewGuid();
Console.WriteLine($
"Init context for id: {id} - {guid}."
);
ctx.id = guid;
await
Task.Delay(r.Next(1000));
ctx = ctxProvider.GetContext();
Console.WriteLine($
"Current context for id: {id} - {ctx.id}."
);
}
InitializeAndPrintContextAsync(1);
InitializeAndPrintContextAsync(2);
InitializeAndPrintContextAsync(3);
Console.Read();
public
class
Context
{
public
Guid id {
get
;
set
;}
}
This program executes 3 async tasks in parallel (all three InitializeAndPrintContextAsync
tasks run in parallel since they are not awaited). Inside, each one initializes a new context, stores a value in the context, sleeps, and prints the stored value. The magic here is performed by the AsyncLocal
class – note that all calls to GetContext()
access the same _asyncLocal
instance and fetch the same Value
property. But for each flow, a different value is fetched:
> dotnet run
Init context for id: 1 - b05b6551-d25f-4fc2-8496-307c0c03725f.
Init context for id: 2 - 8bde25df-0356-453c-be29-afd44f7bdc0b.
Init context for id: 3 - c18954bc-372a-4751-952a-ea4f6e0ae777.
Current context for id: 3 - c18954bc-372a-4751-952a-ea4f6e0ae777.
Current context for id: 1 - b05b6551-d25f-4fc2-8496-307c0c03725f.
Current context for id: 2 - 8bde25df-0356-453c-be29-afd44f7bdc0b.
We can take this a step further and see that this also works if the call is done deeper in the async flow, and adding more data to the context:
Random r =
new
Random();
var
ctxProvider =
new
ContextProvider<Context>();
async
Task InitializeAndPrintContextAsync(
int
id)
{
var
ctx = ctxProvider.InitContext();
var
guid = Guid.NewGuid();
Console.WriteLine($
"Init context for id: {id} - {guid}."
);
ctx.id = guid;
await
Task.Delay(r.Next(1000));
ctx = ctxProvider.GetContext();
Console.WriteLine($
"Current context for id: {id} - {ctx.id}."
);
await
Level1(id);
}
async
Task Level1(
int
id)
{
await
Task.Delay(r.Next(1000));
var
ctx = ctxProvider.GetContext();
ctx.name = $
"MyName{id}"
;
Console.WriteLine($
"Level1 - Current context for id: {id} - {ctx.id}."
);
await
Level2(id);
}
async
Task Level2(
int
id)
{
await
Task.Delay(r.Next(1000));
var
ctx = ctxProvider.GetContext();
Console.WriteLine($
"Level2 - Current context for id: {id} - {ctx.id}, name {ctx.name}."
);
}
InitializeAndPrintContextAsync(1);
InitializeAndPrintContextAsync(2);
InitializeAndPrintContextAsync(3);
Console.Read();
public
class
Context
{
public
Guid id {
get
;
set
;}
public
string
name {
get
;
set
;}
}
And the output again matches our expectations:
> dotnet run
Init context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b.
Init context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc.
Init context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6.
Current context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc.
Current context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b.
Current context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6.
Level1 - Current context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc.
Level1 - Current context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6.
Level1 - Current context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b.
Level2 - Current context for id: 2 - 9033ef6d-e05a-49c6-802b-4c384c8829bc, name MyName2.
Level2 - Current context for id: 3 - 9a27e304-015c-4755-abbf-d18b21bca6e6, name MyName3.
Level2 - Current context for id: 1 - 038c4376-abbd-4532-a75e-57433788ae1b, name MyName1.
Nothing like magic to get the job done :-).
The code for this post (and for many other things) can be found in my GitHub repo. As always, happy coding!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK