0

I am attempting to implement some async code in my Application. I start a new Task and dont wait for the results. This task that has been started news up another Task which does await the result. The second Task uses Http.Context (as I need to get the User from the http context) as the secondary Tasks which I wait on fires off an API call which uses the http.context.current.user.

I was using this answer in order to pass the current context into the Task.

So my code is as below:

            var context = HttpContext.Current;
            Task.Factory.StartNew(() =>
            {
                HttpContext.Current = context;
                ExecuteMethodAndContinue();
            });

    private static void ExecuteMethodAndContinue()
    {
        var myService = ServiceManager.GetMyService();
        var query = GetQuery();
        var files = myService.GetFiles(query).ToList();

        //Remaining code removed for brevity
    }

The implementation of GetFiles which is called from other places in the code as well is as below:

    public IDictionary<FileName, FileDetails> GetFiles(MyQuery query)
    {
        var countries = GetAllCountries();
        var context = HttpContext.Current;

        var taskList = countries.Select(c => Task.Factory.StartNew(() =>
        {
           HttpContext.Current = context;
           return new Dictionary<FileName, FileDetails> { { c, GetFilesInCountry(query, c) } };

        })).ToList();

        try
        {
            // Wait on all queries completing
            Task.WaitAll(taskList.ToArray<Task>());
        }
        catch (AggregateException ae)
        {
            throw new ApplicationException("Failed.", ae);
        }

        // Return collated results
        return taskList.SelectMany(t => t.Result).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
    }

The GetFilesInCountry method that actually contains the API call which relies on the Http.Context.Current.User. However when I hit a breakpoint on the return new line in GetFiles I can see the http.current.context.user is correctly set as expected. When I breakpoint into the GetFilesInCountry method if I hover over the Http.Context.Current.User in GetFiles I find that it is null.

I think this is due to the fact that the http request from where I started the first call (ExecuteMethodAndContinue) is finished so this is why the User on the current context is null.

Is there something straight forward I can do to correctly work around this?

Community
  • 1
  • 1
Ctrl_Alt_Defeat
  • 3,933
  • 12
  • 66
  • 116
  • Sure. Don't pass the whole context - change your method to accept the user object instead. Dirty hacks don't tend to work very reliably :) The question you referred to is completely ignoring the part where you *shouldn't access `HttpContext.Current` from anywhere but the request thread*. If you really can't change the method to accept a user directly, make another thread-static context to take it from, and to have somewhere to inject the user from the outside. – Luaan Mar 30 '15 at 12:06
  • @Luaan - could you expand with an answer showing your two options mentioned - Thanks :) – Ctrl_Alt_Defeat Mar 30 '15 at 12:12
  • 1
    @Ctrl_Alt_Defeat: You really, really should **not** be doing this on ASP.NET. I recommend setting up a reliable queue with an independent worker application that processes the queue. – Stephen Cleary Mar 30 '15 at 19:22
  • @StephenCleary - yeah noted - have decided to go with Hangfire - lightweight solution but should give me what I need – Ctrl_Alt_Defeat Mar 30 '15 at 21:31

1 Answers1

1

The easiest way of course would be to never use HttpContext.Current. It's not a good practice anyway - you should only access the HttpContext in the request thread it's associated with. Instead, you can just make sure all the methods that require e.g. a user name, get the user name as an argument:

var username = HttpContext.Current.User.Identity.Name;

var taskList = countries.Select(c => Task.Factory.StartNew(() =>
{
  return new Dictionary<FileName, FileDetails> { { c, GetFilesInCountry(query, c, username) } };

})).ToList();

If this is impractical for some reason (it probably isn't a very good reason, but fixing legacy applications to work like this can be a chore), you can replace the HttpContext.Current accesses with something a bit more specific, and not tied to a particular request. And, uh, thread-safe:

public static class UserContext
{
  [ThreadStatic]
  public static string Username;
}

So your calling code would look something like this:

var username = HttpContext.Current.User.Identity.Name;

var taskList = countries.Select(c => Task.Factory.StartNew(() =>
{
  UserContext.Username = username;

  return new Dictionary<FileName, FileDetails> { { c, GetFilesInCountry(query, c) } };

})).ToList();

And whenever you'd usually use HttpContext.Current.User.Identity.Name, you'll use UserContext.Username instead (don't forget to also fill the UserContext in the main request thread).

The huge caveat with this is that it gets completely crazy when you have asynchronous code in there; you're on the thread-pool, so you're not the exclusive user of those threads, and any awaits or continuations are free to be performed on any available thread-pool thread (there's no marshalling to a synchronization context). So anywhere you're creating more tasks, be it through manual Task.Run, await, ContinueWith or whatever, you'll losing this context. Just as importantly, there's no place where you can clear this information - this can obviously be a huge security hole, as concurrent requests may have different parts of the code execute with different user contexts. If you choose to go this path, you better read up a lot about making this kind of thing safe. You'll probably have to code your own synchronization context to hold this information, and make sure all the asynchronous stuff in your application marshalls back to this synchronization context. In short - don't do this. Really. It isn't worth it. You'll have so many obscure bugs that are very hard to reproduce, there's no way it will be worth it.

Luaan
  • 62,244
  • 7
  • 97
  • 116