Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Executing a task through ASP.NET synchronization context safely and without AsyncManager.Sync

I'm trying to mix AsyncController with dependency injection. The MVC app in question gets nearly all of its data through async web service calls. We are wrapping the async work in Tasks from the TPL, and notifying the controller's AsyncManager when these tasks complete.

Sometimes we have to touch the HttpContext in continuations of these tasks - adding a cookie, whatever. The correct way to do this according to Using an Asynchronous Controller in ASP.NET MVC is to call the AsyncManager.Sync method. This will propagate the ASP.NET thread context, including the HttpContext, to the current thread, execute the callback, then restore the previous context.

However, that article also says:

Calling Sync() from a thread that is already under the control of ASP.NET has undefined behavior.

This isn't a problem if you do all of your work in the controller, since you generally know what thread you should be on in the continuations. But what I'm trying to do is create a middle layer in between our async data access and our async controllers. So these are also async. Everything is wired up by a DI container.

So like before, some of the components in a call chain will need to work with the "current" HttpContext. For example, after logon we want to store the "session" token we get back from a single-sign-on service. The abstraction for the thing that does that is ISessionStore. Think of a CookieSessionStore that puts a cookie on the response, or grabs the cookie from the request.

Two problems I can see with this:

  1. The components do not have access to AsyncManager or even know they're being used within a controller.
  2. The components do not know which thread they are being invoked from, and so AsyncManager.Sync or any equivalent is theoretically problematic anyway.

To solve #1, I am basically injecting an object that grabs TaskScheduler.FromCurrentSynchronizationContext() at the beginning of the request, and can invoke an action via a Task started with that scheduler, taking the HttpContextBase as an argument.

That is, from my components, I can call something similar to:

MySyncObject.Sync(httpContext => /* Add a cookie or something else */);

I have not yet observed any problems with this, but I am concerned about problem #2. I have looked at both AsyncManager and SynchronizationContextTaskScheduler in Reflector, and they operate similarly, executing the callback on the ASP.NET SynchronizationContext. And that scares me :)

I had a bit of hope when I saw that the task scheduler implementation will invoke the directly rather than going through the synchronization context if it's inlining. But unfortunately, this doesn't seem to happen through the normal Task.Start(scheduler) code-path. Rather, tasks can be inlined in other circumstances, like if they are being waited upon before they start.

So my questions are:

  1. Am I going to run into trouble here with this approach?
  2. Is there a better way?
  3. Is the usefulness of synchronizing in this scenario merely to serialize access to the non-thread-safe HttpContext? i.e. could I get away with a thread-safe HttpContextBase wrapper instead (ick)?
like image 376
Jeremy Rosenberg Avatar asked Nov 14 '22 03:11

Jeremy Rosenberg


1 Answers

Relying on thread-local statics is rarely a good idea. While HttpContext.Current relies on that mechanism and it has worked for years, now that we're going async, that approach is quickly deteriorating. It's much better to capture the value of this static as a local variable and pass that around with your async work so you always have it. So, for example:

public async Task<ActionResult> MyAction() {
    var context = HttpContext.Current;
    await Task.Yield();
    var item = context.Items["something"];
    await Task.Yield();
    return new EmptyResult();
}

Or even better, avoid HttpContext.Current altogether if you're in MVC:

public async Task<ActionResult> MyAction() {
    await Task.Yield();
    var item = this.HttpContext.Items["something"];
    await Task.Yield();
    return new EmptyResult();
}

Arguably, your middleware business logic especially shouldn't be relying on HttpContext or anything else in the ASP.NET libraries. So assuming your middleware calls back out to your controller (via callbacks, interfaces, etc.) to set cookies, you'll then have this.HttpContext available to use for accessing that context.

like image 84
Andrew Arnott Avatar answered Feb 08 '23 23:02

Andrew Arnott