Parallelizing synchronous tasks while retaining the HttpContext.Current in ASP.NET
Unfortunately, this library for service communication is made to be synchronous, and we want to parallelize its use.
throws null exception when HttpContext isn't set:
The obvious answer (HttpContext.Current = parentContext) can't work because there's some async code underneath (for whatever reasons), and that would cause it to sometimes not return to the same thread, and basically abandon the Context, again resulting in null
There's an important part of your question in the example code comment. :)
Normally, HttpContext
shouldn't be shared across threads. It's just not threadsafe at all. But you can set HttpContext.Current
(for some reason), so you can choose to live dangerously.
The more insidious problem here is that the library has a synchronous API and is doing sync-over-async - but somehow without deadlocking (?). At this point, I must be honest and say the best approach is to fix the library: make the vendor fix it, or submit a PR, or just rewrite it if you have to.
However, there is a tiny chance that you can get this kinda sorta working by adding Even More Dangerous code.
So, here's the information you need to know:
- ASP.NET (pre-Core) uses an
AspNetSynchronizationContext
. This context:- Ensures that only one thread runs in this context at a time.
- Sets
HttpContext.Current
for any thread that is running in the context.
Now, you could capture the SynchronizationContext.Current
and install it on the thread pool threads, but in addition to being Very Dangerous, it would not achieve your actual goal (parallelization), since the AspNetSynchronizationContext
only allows one thread in at a time. The first portion of the 3rd-party code would be able to run in parallel, but anything queued to the AspNetSynchronizationContext
would run one thread at a time.
So, the only way I can think of making this work is to use your own custom SynchronizationContext
that resumes on the same thread, and set HttpContext.Current
on that thread. I have an AsyncContext
class that can be used for this:
[HttpPost]
public async Task<IHttpActionResult> DoIt(IEnumerable<int> inputs)
{
var context = HttpContext.Current;
var tasks = inputs.Select(i =>
Task.Run(() =>
AsyncContext.Run(() =>
{
HttpContext.Current = context;
var results = Some3rdPartyTool.CallEndpointSynchronously(MyRestEndpointConfig[i]);
return results;
})));
var outcome = await Task.WhenAll(tasks);
}
So for each input, a thread is grabbed from the thread pool (Task.Run
), a custom single-threaded synchronization context is installed (AsyncContext.Run
), HttpContext.Current
is set, and then the code in question is run. This may or may not work; it depends on how exactly Some3rdPartyTool
uses its SynchronizationContext
and HttpContext
.
Note that there are several bad practices in this solution:
- Using
Task.Run
on ASP.NET. - Accessing the same
HttpContext
instance simultaneously from multiple threads. - Using
AsyncContext.Run
on ASP.NET. - Blocking on asynchronous code (done by
AsyncContext.Run
and also presumablySome3rdPartyTool
.
In conclusion, I again recommend updating/rewriting/replacing Some3rdPartyTool
. But this pile of hacks might work.
The cross-thread usage of HttpContext.Current property and related things
There are four things working together to cause the behavior you are asking about:
- HttpContext is an instance object whose reference can be found in
HttpContext.Current
- Thread is also an instance object whose reference can be found in
Thread.CurrentThread
Thread.CurrentThread
is static but references a differentThread
object in every threadHttpContext.Current
actually points toThread.CurrentThread.ExecutionContext.IllogicalCallContext.HostContext
Conclusions we can draw from the above givens:
- Because
HttpContext
is an instance object and not static we need its reference to access it - Because
HttpContext.Current
actually points to a property onThread.CurrentThread
, changingThread.CurrentThread
to a different object will likely changeHttpContext.Current
- Because
Thread.CurrentThread
' changes when switching threads,HttpContext.Current
also changes when switching threads (in this caseHttpContext.Current
becomes null).
Bringing this all together, what causes HttpContext.Current
to not work in a new Thread? The Thread.CurrentThread
reference change, which happens when switching threads, changes the HttpContext.Current
reference, which prevents us from getting to the HttpContext instance we want.
To reiterate, the only magic thing going on here is Thread.CurrentThread
referencing a different object in every Thread. HttpContext works just like any other instance object. Since threads in the same AppDomain can reference the same objects, all we have to do is pass a reference for HttpContext to our new thread. There is no context info to load or anything like that. (there are some fairly serious potential gotchas with passing around HttpContext to other threads but nothing to prevent you from doing it).
A few final side notes I came across while researching:
In some cases a Thread's ExecutionContext is 'flowed' (copied) from one Thread to another. Why then is HttpContext not 'flowed' to our new Thread? Because HttpContext doesn't implement the ILogicalThreadAffinative interface. A class stored in the ExecutionContext is only flowed if it implements ILogicalThreadAffinative.
How does ASP.NET move HttpContext from Thread to Thread (Thread-Agility) if it isn't flowed? I'm not entirely sure, but it looks like it might pass it in
HttpApplication.OnThreadEnter()
.
Related Topics
Use Xml Includes or Config References in App.Config to Include Other Config Files' Settings
Aspect Oriented Programming in C#
Can't Find System.Windows.Media Namespace
Regex to Get Number Only from String
Linq to SQL Multiple Tables Left Outer Join
Anonymous Type Result from SQL Query Execution Entity Framework
The Type Must Be a Reference Type in Order to Use It as Parameter 'T' in the Generic Type or Method
Unblock File from Within .Net 4 C#
How to Distinguish Between Multiple Input Devices in C#
Why Do We Need the New Keyword and Why Is the Default Behavior to Hide and Not Override
Raise Event Thread Safely - Best Practice
Single App.Config Multi-Project C#
Creating an Instance Using Ninject with Additional Parameters in the Constructor