HttpContext.Current在异步回调中为空

23
尝试在方法回调中访问HttpContext.Current以便修改Session变量,但是我收到了异常,提示HttpContext.Currentnull。当_anAgent对象触发它时,回调方法会异步启动。

在查看SO上的similar questions后,我仍然不确定解决方案。

我的代码简化版本如下:
public partial class Index : System.Web.UI.Page

  protected void Page_Load()
  {
    // aCallback is an Action<string>, triggered when a callback is received
    _anAgent = new WorkAgent(...,
                             aCallback: Callback);
    ...
    HttpContext.Current.Session["str_var"] = _someStrVariable;
  }

  protected void SendData() // Called on button click
  {
    ...
    var some_str_variable = HttpContext.Current.Session["str_var"];

    // The agent sends a message to another server and waits for a call back
    // which triggers a method, asynchronously.
    _anAgent.DispatchMessage(some_str_variable, some_string_event)
  }

  // This method is triggered by the _webAgent
  protected void Callback(string aStr)
  {
    // ** This culprit throws the null exception **
    HttpContext.Current.Session["str_var"] = aStr;
  }

  [WebMethod(EnableSession = true)]
  public static string GetSessionVar()
  {
    return HttpContext.Current.Session["str_var"]
  }
}

我不确定是否必要,但我的WorkAgent类看起来像这样:

public class WorkAgent
{
  public Action<string> OnCallbackReceived { get; private set; }

  public WorkAgent(...,
                   Action<string> aCallback = null)
  {
    ...
    OnCallbackReceived = aCallback;
  }

  ...

  // This method is triggered when a response is received from another server
  public BackendReceived(...)
  {
    ...
    OnCallbackReceived(some_string);
  }
}

代码的作用:
点击一个按钮会调用SendData()方法,在此方法内,_webAgent将消息分派到另一个服务器并等待回复(此时用户仍然可以与该页面交互并引用相同的SessionID)。一旦接收到回复,它就会调用BackendReceived()方法,然后在.aspx.cs页面中调用Callback()方法。

问题:
WorkAgent触发Callback()方法时,它尝试访问HttpContext.Current,但是它为null。如果我忽略异常继续进行,为什么我仍然可以使用ajax返回的GetSessionVar()方法获取相同的SessionIDSession变量?

我是否应该启用aspNetCompatibilityEnabled设置?
我是否应该创建某种异步模块处理程序
这是否与集成/经典模式有关?


为什么要使用回调函数来使事情变得复杂呢?更好的解决方案可能是从客户端使用ajax,这样用户仍然可以与网站进行交互。而对其他系统的调用只需是普通的方法调用即可。 - 3dd
Ajax在大多数情况下是从客户端使用的,只是上面的代码没有包括它(它更新HttpContext Session变量和SQL数据库)。唯一不是ajax调用的方法是SendData()。这将数据发送到其他服务器。我只是困惑为什么回调时HttpContect.Current变成了null - Serge P
请看我的回答,了解为什么会发生这种情况。 - 3dd
3个回答

11

这里有一个基于类的解决方案,在 MVC5 中简单情况下可以工作(MVC6 支持基于 DI 的上下文)。

using System.Threading;
using System.Web;

namespace SomeNamespace.Server.ServerCommon.Utility
{
    /// <summary>
    /// Preserve HttpContext.Current across async/await calls.  
    /// Usage: Set it at beginning of request and clear at end of request.
    /// </summary>
    static public class HttpContextProvider
    {
        /// <summary>
        /// Property to help ensure a non-null HttpContext.Current.
        /// Accessing the property will also set the original HttpContext.Current if it was null.
        /// </summary>
        static public HttpContext Current => HttpContext.Current ?? (HttpContext.Current = __httpContextAsyncLocal?.Value);

        /// <summary>
        /// MVC5 does not preserve HttpContext across async/await calls.  This can be used as a fallback when it is null.
        /// It is initialzed/cleared within BeginRequest()/EndRequest()
        /// MVC6 may have resolved this issue since constructor DI can pass in an HttpContextAccessor.
        /// </summary>
        static private AsyncLocal<HttpContext> __httpContextAsyncLocal = new AsyncLocal<HttpContext>();

        /// <summary>
        /// Make the current HttpContext.Current available across async/await boundaries.
        /// </summary>
        static public void OnBeginRequest()
        {
            __httpContextAsyncLocal.Value = HttpContext.Current;
        }

        /// <summary>
        /// Stops referencing the current httpcontext
        /// </summary>
        static public void OnEndRequest()
        {
            __httpContextAsyncLocal.Value = null;
        }
    }
}

可以从 Global.asax.cs 中挂钩使用它:

    public MvcApplication() // constructor
    {            
        PreRequestHandlerExecute += new EventHandler(OnPreRequestHandlerExecute);
        EndRequest += new EventHandler(OnEndRequest);
    } 

    protected void OnPreRequestHandlerExecute(object sender, EventArgs e)
    {
        HttpContextProvider.OnBeginRequest();   // preserves HttpContext.Current for use across async/await boundaries.            
    }

    protected void OnEndRequest(object sender, EventArgs e)
    {
        HttpContextProvider.OnEndRequest();
    }

那么就可以使用这个代替 HttpContext.Current:

    HttpContextProvider.Current

由于我目前不理解这个相关答案,所以可能会出现问题。请留下评论。

参考文献:AsyncLocal (需要 .NET 4.6)


如果由于静态变量而同时发生两个请求,这个解决方案会混淆上下文吗? - Simon The Cat
1
@SimonTheCat,AsyncLocal参考页面包括一个类似的静态示例,该示例返回多个值。 AsyncLocal源代码表明它实际上是一种围绕线程特定上下文ExecutionContext的包装器。 - crokusek

5

当使用线程或async函数时,HttpContext.Current不可用。

请尝试使用:

HttpContext current;
if(HttpContext != null && HttpContext.Current != null)
{
  current = HttpContext.Current;
}
else
{
    current = this.CurrentContext; 
    //**OR** current = threadInstance.CurrentContext; 
}

一旦您使用适当的实例设置了“ current”,则余下的代码就是独立的,无论是从线程还是直接从“ WebRequest”调用。

4
请参阅以下文章,了解Session变量为空的原因及可能的解决方法。

http://adventuresdotnet.blogspot.com/2010/10/httpcontextcurrent-and-threads-with.html

引用自文章;

当前的HttpContext实际上在线程本地存储中,这就解释了为什么子线程无法访问它

作为一种提议的解决方法,作者说

在您的子线程中传递一个对它的引用。在您的回调方法的“状态”对象中包含对HttpContext的引用,然后您可以将其存储到该线程的HttpContext.Current


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接