Advertisements

Patching HttpContext.Current (The nuclear option)

Patching HttpContext.Current (The nuclear option)

December 17, 2018 Packages 0

Background

So, as previously published, my day job is at an enterprise. At this enterprise, one of my responsibilities is a security package that is used by our main web application. This web application is a legacy ASP.Net application and is maintained by another team, which I used to be a part of.

Plans are for the security package to be used in multiple new web applications and since a project came to my desk to update this package, I decided to jump on the change to make it async native. However, since the main web application is using an interface from the old package which only has synchronous methods, I had to create a legacy package, that would call the new package to accommodate them.

// This is in the new package
public interface INewAsyncInterface
{
    Task DoSomethingAsync();
    Task<string> GetTheStringAsync();
}

// This is in the legacy package
public interface IOldInterface
{
    void DoSomething();
    string GetTheString();
}

// This is in the legacy package
public class LegacyImplementation : IOldInterface
{
    private INewAsyncInterface _inner;

    public LegacyImplementation(INewAsyncInterface inner)
    {
        _inner = inner;
    }

    public void DoSomething()
    {
        RunSynchronously(() => _inner.DoSomethingAsync());
    }

    public string GetTheString()
    {
        return RunSynchronously(() => _inner.GetTheStringAsync());
    }

    private void RunSynchronously(Func func)
    {
        RunSynchronously<object>(async () =>
        {
            await func();
            return null;
        };
    }    

    private T RunSynchronously<T>(Func<Task<T>> func)
    {
        // This will be filled out in the blog post
    }
}

If possible, always have your stack be async all the way through. I just didn’t have that luxury. So the big problem was, how do we fill out the RunSynchronously method and ensure HttpContext.Current won’t get lost?

First try

My first approach was pretty naive and worked for the simplest async flow.

private T RunSynchronously<T>(Func<Task<T>> func)
{
    var context = HttpContext.Current;
    var task = Task.Run<T>(async () =>
    {
        HttpContext.Current = context;
        var result = await func();
        return result;
    });
    
    task.Wait();
    return task.Result;
}

This obviously doesn’t do a lot to ensure we keep our state.

Second try

My second approach was a bit more robust and used AsyncLocal. I basically wrote an AsyncHelper class that held HttpContext for me for the duration of the async flow and everywhere I needed to use HttpContext.Current for any reason, I used AsyncHelper.HttpContext instead.

public class LegacyImplementation : IOldInterface
{
    private INewAsyncInterface _inner;

    public LegacyImplementation(INewAsyncInterface inner) { ... }

    public void DoSomething() { ... }

    public string GetTheString() { ... }

    private void RunSynchronously(Func func) { ... } 
    
    private T RunSynchronously<T>(Func<Task<T>> func)
    {
        AsyncHelper.HttpContext = HttpContext.Current;
        var task = Task.Run<T>(async () =>
        {
            var result = await func();
            AsyncHelper.HttpContext = null;
            return result;
        });
        
        task.Wait();
        return task.Result;
    }
}

public static class AsyncHelper
{
    private static AsyncLocal<HttpContext> _asyncHttpContext = new AsyncLocal<HttpContext>();
    
    public HttpContext HttpContext { get => _asyncHttpContext.Value; set => _asyncHttpContext.Value = value; }
}

There is a problem with this approach and that has to do with the security package that I was updating. There are extension points in the security package that allow the consumer to feed user context into the security mechanism. I wasn’t going to be able to force the consumer to use AsyncHelper.HttpContext everywhere. What to do?

Third try, the nuclear option

I want to prepend this section and say, no, I do not recommend this. If at all possible, use async down the whole stack and you won’t have this headache. However, if you have an application that is difficult to update and need to use async, you could try this method.

So what is the method?

Use IL to patch HttpContext.Current.

I can just hear you say, what the actual f@$k!?

At first I was going to do it manually, but after about an hour of looking into IL, I decided to google a bit more and found a library called Harmony. It gave the option to add a postfix method on to an existing method, so I could have a method that only acts differently if HttpContext.Current is actually null.

public class LegacyImplementation : IOldInterface
{
    private INewAsyncInterface _inner;
    
    static LegacyImplementation()
    {
        PatchHttpContext();
    }

    public LegacyImplementation(INewAsyncInterface inner) { ... }

    public void DoSomething() { ... }

    public string GetTheString() { ... }

    private void RunSynchronously(Func func) { ... } 
    
    private T RunSynchronously<T>(Func<Task<T>> func)
    {
        AsyncHelper.HttpContext = HttpContext.Current;
        var task = Task.Run<T>(async () =>
        {
            var result = await func();
            AsyncHelper.HttpContext = null;
            return result;
        });
        
        task.Wait();
        return task.Result;
    }

    private static PatchHttpContext()
    {
        var harmony = HarmonyInstance.Create("System.Web.HttpContext");
        var method = typeof(HttpContext).GetMethod("get_Current", BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.Public);
        var postfix = typeof(LegacyImplementation).GetMethod("GetAsyncCurrent", BindingFlags.Static | BindingFlags.NonPublic);
        harmony.Patch(method, postfix: new HarmonyMethod(postfix));
    }
    
    private static HttpContext GetAsyncCurrent(HttpContext __result)
    {
        if (__result != null) return __result;
        return AsyncHelper.HttpContext;
    }
}

This is obviously called the nuclear option because it affects everything in the application, but as you can see it only does anything in the context of the async flow and only if the original HttpContext.Current is null to begin with.

The nuclear option package

I decided to create a nuget package that does just this because I thought it would be pretty evil to hide something like this in my large security package. This package is open source and you can find it on github.

private T RunSynchronously<T>(Func<Task<T>> func)
{
    return HttpContext.Current.Run(func);
}

As you can see, this is much easier to look at and if you check out the github repo, you can see the code is quite simple

Advertisements

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.