I've scoured the SO for answers but found none that pertain to the problem at hand, although this one nails it on "why", but isn't solving it.
I have a REST endpoint that needs to gather data from other endpoints - in doing so, it accesses the HttpContext
(setting authentication, headers, etc... all done with 3rd party lib I don't have access to).
Unfortunately, this library for service communication is made to be synchronous, and we want to parallelize its use.
In the following example (abstracted) code, the issue is that CallEndpointSynchronously
unfortunately uses some built in authentication, which throws null exception when HttpContext
isn't set:
public class MyController: ApiController
//...
[HttpPost]
public async Task<IHttpActionResult> DoIt(IEnumerable<int> inputs)
{
var tasks = inputs.Select(i =>
Task.Run(()=>
{
/* call some REST endpoints, pass some arguments, get the response from each.
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 */
var results = Some3rdPartyTool.CallEndpointSynchronously(MyRestEndpointConfig[i]);
return results;
});
var outcome = await Task.WhenAll(tasks);
// collect outcome, do something with it, render outputs...
}
Is there a cure for this?
We want to optimize for single requests, not interested in maximizing parallel users at this moment.
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:
AspNetSynchronizationContext
. This context:
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:
Task.Run
on ASP.NET.HttpContext
instance simultaneously from multiple threads.AsyncContext.Run
on ASP.NET.AsyncContext.Run
and also presumably Some3rdPartyTool
.In conclusion, I again recommend updating/rewriting/replacing Some3rdPartyTool
. But this pile of hacks might work.
Great answer, thanks for making it perfectly clear! As for the
Some3rdPartyTool
, as far as I found it's using webClient underneath to call the endpoints, so it's synchronous by nature (no blocking code) - my original assumption that it had some async inside was wrong@veljkoz: If it's not doing sync-over-async, then you might be able to capture
SynchronizationContext.Current
andHttpContext.Current
and use those to temporarily set them on other threads threads (SynchronizationContext.SetSynchronizationContext
/HttpContext.Current
), and that may work without using a differentSynchronizationContext
.