Safety of AsyncLocal in ASP.NET Core
For .NET Core, AsyncLocal
is the replacement for CallContext
. However, it is unclear how "safe" it is to use in ASP.NET Core.
In ASP.NET 4 (MVC 5) and earlier, the thread-agility model of ASP.NET made CallContext
unstable. Thus in ASP.NET the only safe way to achieve the behavior of a per-request logical context, was to use HttpContext.Current.Items. Under the covers, HttpContext.Current.Items is implemented with CallContext
, but it is done in a way that is safe for ASP.NET.
In contrast, in the context of OWIN/Katana Web API, the thread-agility model was not an issue. I was able to use CallContext safely, after careful considerations of how correctly to dispose it.
But now I'm dealing with ASP.NET Core. I would like to use the following middleware:
public class MultiTenancyMiddleware
{
private readonly RequestDelegate next;
static int random;
private static AsyncLocal<string> tenant = new AsyncLocal<string>();
//This is the new form of "CallContext".
public static AsyncLocal<string> Tenant
{
get { return tenant; }
private set { tenant = value; }
}
//This is the new verion of [ThreadStatic].
public static ThreadLocal<string> LocalTenant;
public MultiTenancyMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
//Just some garbage test value...
Tenant.Value = context.Request.Path + random++;
await next.Invoke(context);
//using (LocalTenant = new AsyncLocal<string>()) {
// Tenant.Value = context.Request.Path + random++;
// await next.Invoke(context);
//}
}
}
So far, the above code seems to be working just fine. But there is at least one red flag. In the past, it was critical to ensure that CallContext
was treated like a resource that must be freed after each invocation.
Now I see there is no self-evident way to "clean up" AsyncLocal
.
I included code, commented out, showing how ThreadLocal<T>
works. It is IDisposable
, and so it has an obvious clean-up mechanism. In contrast, the AsyncLocal
is not IDisposable
. This is unnerving.
Is this because AsyncLocal
is not yet in release-candidate condition? Or is this because it is truly no longer necessary to perform cleanup?
And even if AsyncLocal
is being used properly in my above example, are there any kinds of old-school "thread agility" issues in ASP.NET Core that are going to make this middleware unworkable?
Special Note
For those unfamiliar with the issues CallContext
has within ASP.NET apps, in this SO post, Jon Skeet references an in-depth discussion about the problem (which in turn references commentary from Scott Hanselman). This "problem" is not a bug - it is just a circumstance that must be carefully accounted for.
Furthermore, I can personally attest to this unfortunate behavior. When I build ASP.NET applications, I normally include load-tests as part of my automation test infrastructure. It is during load tests that I can witness CallContext
become unstable (where perhaps 2% to 4% of requests show CallContext
being corrupted. I have also seen cases where a Web API GET
has stable CallContext
behavior, but the POST
operations are all unstable. The only way to achieve total stability is to rely on HttpContext.Current.Items.
However, in the case of ASP.NET Core, I cannot rely on HttpContext.Items...there is no such static access point. I'm also not yet able to create load tests for the .NET Core apps I'm tinkering with, which is partly why I've not answered this question for myself. :)
Again: Please understand that the "instability" and "problem" I'm discussing is not a bug at all. CallContext is not somehow flawed. The issue is simply a consequence of the thread dispatch model employed by ASP.NET. The solution is simply to know the issue exists, and to code accordingly (e.g. use
HttpContext.Current.Items
instead ofCallContext
, when inside an ASP.NET app).
My goal with this question is to understand how this dynamic applies (or does not) in ASP.NET Core, so that I don't accidentally build unstable code when using the new AsyncLocal
construct.
Solution 1:
I'm just looking into the source code of the ExecutionContext class for CoreClr: https://github.com/dotnet/coreclr/blob/775003a4c72f0acc37eab84628fcef541533ba4e/src/mscorlib/src/System/Threading/ExecutionContext.cs
Base on my understanding of the code, the async local values are fields/variables of each ExecutionContext instance. They are not based on ThreadLocal or any thread specific persisted data store.
To verify this, in my testing with thread pool threads, an instance left in async local value is not accessible when the same thread pool thread is reused, and the "left" instance's destructor for cleaning up itself got called on next GC cycle, meaning the instance is GCed as expected.
Solution 2:
Adding my two cents if someone lands on this page (like I did) after googling if AsyncLocal
is "safe" in ASP.NET classic (non Core) application (some commenters have been asking this, and also I see a deleted answer asking about the same).
I wrote a small test that simulates asp.net's ThreadPool behavior
AsyncLocal
is always cleared between requests even if thread pool re-uses an existing thread. So it is "safe" in that regard, no data will be leaked to another thread.However,
AsyncLocal
can be cleared even within the same context (for example between code that runs in global.asax and the code that runs in controller). Because MVC-methods sometimes runs on a separate thread from non-MVC code, see this question for example: asp.net mvc 4, thread changed by model binding?Using
ThreadLocal
is not safe b/c it preserves the value after the thread from Thread Pool is re-used. Never use ThreadLocal in web-applications. I know the question is not about ThreadLocal I'm just adding this warning to whoever considering using it, sorry.
Tested under ASP.NET MVC 5 .NET 4.7.2.
Overall, AsyncLocal
seems like a perfect alternative to short-time caching stuff in HttpContext.Current
in cases where you can't access the latter directly. You might end up re-calculating the cached value a bit more often though, but that's not a big problem.