HttpContext.Current имеет значение null в асинхронном обратном вызове

Попытка доступа к HttpContext.Current в вызове метода, поэтому я могу изменить переменную Session, однако получаю исключение, что HttpContext.Current - null. Метод обратного вызова запускается асинхронно, когда объект _anAgent запускает его.

Я все еще не уверен в решении этого вопроса после просмотра аналогичного questions в формате SO.

Упрощенная версия моего кода выглядит так:

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. Почему это так, когда я продолжаю, игнорируя исключение, я все же могу получить ту же переменную SessionID и Session, используя возвращаемый метод ajax GetSessionVar().

Должен ли я включать параметр aspNetCompatibilityEnabled? Должен ли я создавать какой-то обработчик асинхронного модуля? Это связано с Интегрированный/классический режим?

Ответ 1

Здесь решение на основе класса, которое работает в простых случаях до сих пор в 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)

Ответ 2

Пожалуйста, обратитесь к следующей статье для объяснения причин, по которым переменная Session имеет значение null, и возможная работа вокруг

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

цитируется из статьи;

текущий HttpContext фактически находится в потоковом локальном хранилище, что объясняет, почему дочерние потоки не имеют к нему доступа

И как предлагаемая работа вокруг автора говорит

передать ссылку на нее в дочернем потоке. Включите ссылку на HttpContext в "состоянии" объекта вашего метода обратного вызова, а затем вы можете сохранить его в HttpContext.Current в этом потоке

Ответ 3

При использовании потоков или async функция HttpContext.Current недоступна.

Попробуйте использовать:

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

Как только вы установите current с соответствующим экземпляром, остальная часть вашего кода будет независимой, вызвана ли она из потока или непосредственно из WebRequest.