Hidden Workings of Execution Context in .NET

Nakib
.NET Under the hood
9 min readAug 18, 2023
Photo by Muha Ajjan on Unsplash

This is the second blog post of the Unveiling Asynchronous & Parallel Programming in .NET Blog Series.

The previous blog post of this series shows how tasks are scheduled and executed on threads in .NET. The previous blog post is the Prerequisite of this blog post.

If we recall from the last blog post, the Thread-Per-Request approach executes each request in a dedicated thread, whereas in the Task-Based Asynchronous approach, Tasks of the same request are not guaranteed to get executed on the same thread.

And that is a major difference between these two approaches that forces us to think, design, and do things differently on them.

Today, we will discuss a problem where the solution is solved differently in these two approaches. The problem described here is from a Server’s Perspective.

Let’s start…

How to access Request Specific Data from Anywhere? (The Problem):

In a server, some data/information is particular to a Request. For example, when the user arrives, we get the user’s Information from the claim, or sometimes we retrieve additional information from the database.

We generally retrieve such information inside a Middleware or the Controller class.

However, we may also need this information later than the Controller or Middleware, for example, inside a Business logic class.

Now the question is, how we can pass the data to the business logic class from the Controller or Authorization Middleware?

The naive solution would be passing the Data as a method parameter everywhere. But in this approach, you need to take the burden of passing it in methods that don’t use it. Moreover, there may be many other request-specific data, and handling all of them in such a way is not feasible.

Request Specific Global Context(The idea):

A more feasible solution is to maintain a Request Specific Global Context for each Request that stores data in it. The global Context should be accessible from any task of that request.

Then all we need to do is to store data in that Context, and when we need the data, we can just get it from the Context.

With the help of such a Context, we can pass data from anywhere to anywhere. For example, from Controller/Middleware to Business logic class, from a business logic-related task to another business logic-related task, etc.

Where to store the Request Specific Global Context? (The problem):

Now the question is, where do we store this Request Specific Global Context that can be accessed from any task of that request?

In the Thread-Per-Request approach, we could store this in the Thread-Local-Storage since the whole request gets processed by the same thread.

But the problem in Task-Based Asynchronous Programming is that tasks of the same request are not guaranteed to be executed on the same thread.

So here we can’t simply store this context in Thread-Local-Storage.

The Solution:

To solve this problem, .NET provides a built-in Request Specific Global Context where we can store Request Specific data. And that is the Execution Context.

If you like my Content, You may support me at the link below

Execution Context:

Execution Context is just a state bag where we store Request-Specific Data.

The Execution Context isn’t stored in Thread-Local-Storage; rather, it is attached to tasks and loaded to the thread before the Execution of the Task. In this way, it flows between threads. That’s why the data stored in the Execution Context can be accessed by any thread of the same request.

Flow(Capturing and Restoring) of Execution Context:

Whenever a Task is created, the Parent Task’s Execution Context is Captured and stored on an Internal Variable of the newly created Task.

And before the Execution of that task on a thread, the Execution Context is Restored on the thread so that the Task can access the Request Specific data.

So it doesn’t matter on which thread a task is Executing; it can always access Request specific data because of the flow of the Execution Context described above.

Generally, Execution Context is captured whenever a new task is created. For example, on Task.Run(), Task.ContinueWith(), await points, etc.

But there are also some Callback-based APIs where ExecutionContext is Captured. For example, ThreadPool.QueueUserWorkItem(Callback), CancellationToken.Register(Callback), etc.

Most of the time, you need not worry about Execution Context and its flow, but knowing what happens under the hood will help you to code efficiently and safely and also to solve critical bugs.

Suppressing the Flow:

We can Suppress the flow using ExecutionContext.SupressFlow and then Restore it later using ExecutionContext.RestoreFlow.

The tasks created between the ExecutionContext.SuppressFlow and ExecutionContext.RestoreFlow won’t capture the Execution Context.

So these tasks will be executed without Execution Context.

Caution: You should only use ExecutionContext.SupressFlow if you know what you are doing.

Sometimes it is done to optimize performance when we don’t need the Execution Context to flow. However, in most cases, it is better to let the Execution Context flow instead of doing premature optimization that may create bugs.

How to set and get data from the Execution Context:

We can’t manipulate Execution Context directly. We get and set data on Execution Context through AsyncLocal type class fields. It is recommended to keep the field Static.

AsyncLocal:

We store data in Execution Context using a Static AsyncLocal<T> field.

Internally, AsyncLocal fields Stores and Retrieves Data from the Execution Context.

But why the AsyncLocal field needs to be static? To understand this, you must know how AsyncLocal stores and retrieves data in the Execution Context. It is discussed at the end of the post.

AsyncLocal Gotchas:

Working with AsyncLocal may create bugs if we aren’t careful enough. Some common gotchas are described below.

Thread Safety of Execution Context:

If a parent task creates several child tasks, then the same parent task’s Execution Context will flow to all of them. So what happens if someone makes some update in the Execution Context? What is the Impact?

To be more precise, Updates can be of two types,

  1. Reassigning an AsyncLocal value.
  2. Mutating an object stored on AsyncLocal instead of Reassinging.

Reassigning an AsyncLocal value:

Execution Context is Immutable and uses the copy-on-write technique.

By default, the default Execution Context is set as the Execution Context on threads.

When any AsyncLocal value is Reassigned in some thread, a new Execution Context is created and set to that thread.

The new Execution Context contains the new/updated value and the other values from the previous Execution Context.

The Execution Context of the other threads remains the same. Only the thread that made the update gets the new Execution Context.

So it is thread safe to Reassign an AsyncLocal value.

Mutating an object stored on AsyncLocal:

But if you Mutate a mutable object stored in AsyncLocal, it won’t create a new ExecutionContext, so all the other threads will get that object’s change.

It is not thread-safe since multiple threads may simultaneously access and mutate the object.

Some of you may be already lost. But don’t worry, I have created a Console Application with these two types of Examples.

Get it from here. The code explains itself, and you may also download and run it if you want.

To ensure thread safety when using AsyncLocals, follow these rules of thumb in ascending order.

  • If the data need not be changed, make it read-only.
  • If it is needed to be changed, make it Immutable so that change in one thread doesn’t affect another.
  • If it can’t be made immutable, make the change thread-safe. For example, reassign to a new value instead of mutating, use thread-safe Collections, etc.

Other Issues:

Execution Context lifetime and Memory leak:

Like other .NET objects, Execution Context won’t be garbage collected while it is being referenced by some other objects.

Tasks keep the reference of the Execution Context where it was created. And before execution, the Execution Context is loaded to the thread, i.e. a reference of the Execution Context is stored in an Internal variable thread.

So here, the Execution Context can only be garbage collected when the task is ready to get garbage collected and the thread doesn’t refer to the Execution Context anymore(By loading another task’s Execution Context).

In this way, an Execution Context’s lifetime is more or less equal to the Request on which it is created if there is no independent task running even after the response is sent. By the way, it is ok if some independent task holding Request’s Execution Context runs a bit more than the Request’s lifetime.

But sometimes Execution Contexts may get unnecessarily long lifetimes causing huge memory leaks. For example, if Execution Contexts are referenced from singleton instances or something that has a lifetime longer than the Request.

Some other issues may happen if we aren’t careful.

For example, if we store a disposable object on the Asynclocal and the object is disposed of before the request ends, then trying to access it will throw an error.

.NET Guru David Fowler has discussed such issues with more explanation and Code examples. You can read it here.

A real-life example:

Say your new app project consists of a few Microservices. You have also created a Class library that is added as a DLL to those Microservices. And now you want to access the User’s Principle from that Class library.

How would you do it?

You may create a custom middleware where you will set User’s Context to the Thread.CurrentPrincipal. CurrentPrincipal is a Static property of the Thread class that stores data internally in a Private Static AsyncLocal field.

So the User’s Context can be accessed from any 3rd party library using the Thread.CurrentPrincipal property.

Why the AsyncLocal variable should be Static:

Maybe you have been surprised after knowing that Static fields are used to store Request-Specific data/Execution Context Data.

Since only one copy of the static field is created, how can it contain data of parallel requests/multiple Execution Context’s Data simultaneously?

Let’s discuss how AsyncLocal works and stores data on Execution Context to understand why the Static Field works and why using Static Field is recommended.

If we keep it simple, Execution Context is a State Bag that stores data as Key-Value pairs. Though they are different, you can imagine the State Bag as a Dictionary.

When we assign some value to the Value property of the AsyncLocal field, internally, an entry(Key-Value pair) is added to the Execution Context state bag where the Key is the AsyncLocal field itself, and the Value is the value we assigned at the Value property.

Read it again; the AsyncLocal field is not storing the data; it is just used as a Key to a State bag where the Key-Value pair is stored.

So using static AsyncLocal means that, the key is always the same/Shared for different Requests, not the value. For the same key, the value will be the same if the Execution Context is the same.

If the Execution Context is different, the same key will get different references.

But that doesn’t mean that you can’t use a non-static field. For non-static fields, you need to ensure that the setter and getter keys(AsyncLocal field) are from the Same class Instance for the same request at least.

Dependency on a Class instance and maintaining the same instance for getter and setter creates an extra burden. That’s why the Static AsyncLocal field is used mostly.

By the way, it is recommended not to expose the field directly. We generally keep a property that gets and sets the field, and the field is kept private. In our Scenario, we may use a Static Public property to get and set data for a Static Private field.

If you look at the examples in this repo, you will see that we have kept a private AsyncLocal field and a public property that gets and sets the private field.

If you like my Content, You may support me at the link below

Final words

Congratulations, we have finally reached the end.

Understanding how Execution Context works internally comes in handy, especially for ASP.NET Core developers.

But even for Non-Server applications like UI apps, knowing this would help you make better apps.

--

--

Nakib
.NET Under the hood

Software Engineer, Currently working in Angular/.NET stack. Language/Tool Agnostic, strongly focuses on the fundamentals, constant learner.