72

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 of CallContext, 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.

Community
  • 1
  • 1
Brent Arias
  • 29,277
  • 40
  • 133
  • 234
  • You're seeing *modern* (4.5) logical call contexts fail? – Stephen Cleary Apr 09 '16 at 00:19
  • 1
    @StephenCleary Most of my test data showing instability is from MVC 3, MVC 4, and Web API 2.0-2.2. Using HttpContext.Current.Items instead of `CallContext` always fixes the problem. Perhaps MVC 5 "fixed" the issue and I am simply unaware; I would bet "not". The issue was never about `CallContext` itself being broken, but instead the thread dispatching model used by ASP.NET is incompatible (except for when ASP.NET takes direct control, via HttpContext.Current.Items). – Brent Arias Apr 09 '16 at 00:29
  • To clarify, you were using the *logical* call context? – Stephen Cleary Apr 09 '16 at 01:12
  • Yes, "logical" as demonstrated by [Richter](http://www.wintellect.com/devcenter/jeffreyr/logical-call-context-flowing-data-across-threads-appdomains-and-processes). Furthermore, the pre .net 4.5 lack of "copy on write" was not an issue since the logical callcontext was treated as immutable in these applications I tested (set once per request, not touched thereafter). Other unrelated trivia: it is also unstable in WCF if it is established anywhere except the [`DispatchOperation` extension points](https://sankarsan.wordpress.com/2008/12/28/wcf-extension-points-dispatcher/). – Brent Arias Apr 09 '16 at 19:08
  • I recommend that you send your results to the ASP.NET team. Breaking the logical call context is serious. – Stephen Cleary Apr 09 '16 at 22:11
  • They (ASP.NET team, Indigo team) already know the issue. They, like me, don't see it as being a bug. It is simply a matter of understanding the thread dispatching models of the respective frameworks, and applying the right tool for the job, at the right point in the pipeline. – Brent Arias Apr 09 '16 at 22:34
  • @StephenCleary I have updated my post with links from Scott Hanselman and Jon Skeet that point to a more detailed explanation of the matter. I have also clarified this is not a bug in `CallContext`, but just a circumstance of the dispatch model of the ASP.NET (and WCF) framework(s). – Brent Arias Apr 09 '16 at 22:58
  • I read through all those, and none of them say that the **logical** call context is broken on ASP.NET. They essentially all say that you shouldn't use the "illogical" call context or `ThreadStatic`, which I am in agreement with. – Stephen Cleary Apr 09 '16 at 23:22
  • Looking forward to the result. I'm also converting my library, which depends on CallContext, to CoreCLR, it is really nervous if the life-cycle of AsyncLocal instances is unclear. – Teddy Ma May 01 '16 at 13:49
  • We have major problems with the class ATM running inside docker container 1.1 runtime. We didn't have problems until now in development. But now in these production containers we seem to get a lot of nullreference exceptions on concurrent requests. Anyone with the same experience? – Joel Harkes Mar 02 '17 at 16:05
  • 1
    I found that while in ASP.NET AsyncLocal is not very reliable (can be lost when switching between global.asax and controllers) but it is reliable in ASP.NTE Core. IN fact, HttpContext.Current in ASP.NET Core is done via an AsyncLocal var – Alex from Jitbit Mar 08 '20 at 13:20
  • @BrentArias Updated [Richter](https://www.atmosera.com/blog/logical-call-context-flowing-data-across-threads-appdomains-and-processes/) link – Chris R. Donnelly Aug 13 '22 at 19:16

2 Answers2

19

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.

Teddy Ma
  • 1,126
  • 6
  • 12
  • 1
    @brient-aras Ok but how about AsyncLocal in classic .net 4.6+? Is it also considered safe or not? – Pawel Jan 24 '19 at 13:13
  • Yes, it should also be safe. The implementations of AsyncLocal in .net 4.6+ and .net core are slightly different at very beginning, but they should eventually become the same one. – Teddy Ma Jan 25 '19 at 03:07
  • 2
    @TeddyMa, it *should* be safe .NET 4.6+, but it's unfortunately not. For the reasons stated here - https://github.com/MiniProfiler/dotnet/issues/173 – Jeff Fischer Mar 19 '20 at 13:26
17

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

  1. 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.

  2. 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?

  3. 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.

Alex from Jitbit
  • 53,710
  • 19
  • 160
  • 149
  • 1
    But AsyncLocal is not about threads. "AsyncLocal - represents ambient data that is local to a given asynchronous control flow..." (https://learn.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1?view=net-7.0) I expect that AsyncLocal values will available from the begin till the end of web request. – Igor Dec 23 '22 at 08:31