Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add HttpContext into HangFire

I am a beginner with HangFire and looking forward to using HangFire to call some actions in my web application monthly. But these actions require HttpContext.

Then my question is: Is there any way to add (or create) a httpcontext in HangFire project?

I tried to google but no suitable answer. Thanks for your help!

I found a short discussion. Sad to see the answer is "no way". Update: Ref https://discuss.hangfire.io/t/passing-site-url-to-hangfire-recurrent-jobs/2641

like image 585
Duc Tran Avatar asked Nov 21 '17 02:11

Duc Tran


2 Answers

I have a similar scenario where the system relies heavily on Session, and I took this same approach of faking an HttpContext and passing along session variables.

In my method I receive a serializable context, which contain all session variables:

public static void MyMethod(Hangfire.Server.PerformContext context, Hangfire.IJobCancellationToken cancellationToken, 
        SerializeableHttpContext serializeableHttpContext, ... etc)
{
    using (var fakeContext = serializeableHttpContext.CreateFakeHttpContext()) {
    // ...
    }
}

During enqueue, I pass current context to my serializable context, which will capture all current variables:

// null and null are injected by Hangfire
Hangfire.BackgroundJob.Enqueue(() => MyMethod(null, null, new SerializeableHttpContext(System.Web.HttpContext.Current), etc..);

And this is where the magic happens. This will save all Session variables, and will restore them. Please note that using IDispose is important because your next Hangfire job does NOT want to inherit a fake HttpContext from the previous job, so you need to clean up HttpContext.

/// <summary>
/// This serializes HttpContext with primitive Session variables
/// </summary>
[Serializable]
public class SerializeableHttpContext
{
    public Uri RequestUrl { get; set; }
    public Dictionary<string, object> SessionVariables { get; set; }

    /// <summary>
    /// Given a real HttpContext (you can pass System.Web.HttpContext.Current), this saves all useful information 
    /// into this serializable class, so that you can later reuse (restore) a cloned fake HttpContext
    /// </summary>
    /// <param name="httpContext">You'll probably want to pass System.Web.HttpContext.Current</param>
    public SerializeableHttpContext(HttpContext httpContext)
    {
        this.RequestUrl = httpContext.Request.Url;

        // Save all Session variables
        this.SessionVariables = new Dictionary<string, object>();
        foreach (object objkey in httpContext.Session.Keys)
        {
            string key = objkey as string;
            if (key == null || httpContext.Session[key] == null)
                continue;
            Type type = httpContext.Session[key].GetType();

            if (type.IsPrimitive || type == typeof(string))
            {
                try
                {
                    // ignore if not serializable
                    object val = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(httpContext.Session[key]));
                    this.SessionVariables.Add(key, httpContext.Session[key]);
                }
                catch (Exception) { }
            }
        }

    }

    /// This is for internal usage, when deserializing.
    public SerializeableHttpContext()
    {
    }

    /// <summary>
    /// Deserializes into a Fake HttpContext
    /// </summary>
    /// <returns></returns>
    protected HttpContext Deserialize()
    {
        var httpRequest = new HttpRequest("", this.RequestUrl.AbsoluteUri, "");
        var stringWriter = new StringWriter();
        var httpResponse = new HttpResponse(stringWriter);
        var httpContext = new HttpContext(httpRequest, httpResponse);

        var sessionContainer = new HttpSessionStateContainer("id", new SessionStateItemCollection(),
                                                new HttpStaticObjectsCollection(), 10, true,
                                                HttpCookieMode.AutoDetect,
                                                SessionStateMode.InProc, false);

        httpContext.Items["AspSession"] = typeof(HttpSessionState).GetConstructor(
                                    BindingFlags.NonPublic | BindingFlags.Instance,
                                    null, CallingConventions.Standard,
                                    new[] { typeof(HttpSessionStateContainer) },
                                    null)
                            .Invoke(new object[] { sessionContainer });

        // Restore session variables
        if (this.SessionVariables != null)
            foreach (string key in this.SessionVariables.Keys)
                httpContext.Session[key] = this.SessionVariables[key];

        // Restore context variables
        if (this.ContextVariables != null)
            foreach (string key in this.ContextVariables.Keys)
                httpContext.Items[key] = this.ContextVariables[key];

        return httpContext;
    }

    /// <summary>
    /// Deserializes this class back into a fake HttpContext, and automatically sets that into System.Web.HttpContext.Current
    /// Don't forget to DISPOSE this instance at the end, so that the Context is cleared (else Hangfire will reuse this thread with previous HttpContext)
    /// </summary>
    public FakeHttpContext CreateFakeHttpContext()
    {
        return new FakeHttpContext(this.Deserialize());
    }

    public class FakeHttpContext : IDisposable
    {
        HttpContext previousContext;
        public FakeHttpContext(HttpContext context)
        {
            previousContext = HttpContext.Current;
            HttpContext.Current = context;
        }
        public void Dispose()
        {
            HttpContext.Current = previousContext; // previousContext is probably null, but one might be using FakeHttpContexts even inside an existing web context
        }
    }
}


like image 67
drizin Avatar answered Sep 21 '22 18:09

drizin


After 3 days with this problem, I found that it is POSSIBLE to create a fake HttpContext inside HangFire. There are many things need to be constructed in this fake HttpContext. However, you can just initialize properties that you need, no need to define all.

Big thank @jbl

like image 29
Duc Tran Avatar answered Sep 20 '22 18:09

Duc Tran