【问题标题】:HttpContext.Current is null after await (only in unit tests )HttpContext.Current 在等待后为空(仅在单元测试中)
【发布时间】:2014-11-24 23:01:03
【问题描述】:

我正在为 MVC 5 Web 应用程序编写单元测试。我从我的测试中嘲笑了HttpContext.Current。 当运行以下代码时,测试httpSessionStateAfter throw

System.AggregateException:发生一个或多个错误。
----> System.NullReferenceException : 对象引用未设置为对象的实例。

这仅在我运行单元测试时发生。当应用程序运行此工作正常。 我正在使用 Nunit 2.6.3 和 reshaper 测试运行器。

 var httpSessionStateBefour = System.Web.HttpContext.Current.Session;
 var Person= await Db.Persons.FirstOrDefaultAsync();
 var httpSessionStateAfter = System.Web.HttpContext.Current.Session;

如何解决这个问题?

这就是我模拟 HttpContext 的方式

 HttpContext.Current = Fakes.FakeHttpContext();            
 HttpContext.Current.Session.Add("IsUserSiteAdmin", true);
 HttpContext.Current.Session.Add("CurrentSite", null);

public static class Fakes
{
    public static HttpContext FakeHttpContext()
    {
        var httpRequest = new HttpRequest("", "http://stackoverflow/", "");
        var stringWriter = new StringWriter();
        var httpResponce = new HttpResponse(stringWriter);
        var httpContext = new HttpContext(httpRequest, httpResponce);

        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});

        return httpContext;
    }
}

【问题讨论】:

  • 你的问题到底是什么?
  • @Tragedian 我猜“为什么httpSessionStateAfter 为空,而httpSessionStateBefour 不是?”
  • 您使用的是哪个单元测试框架?你确定它支持执行上下文吗?例如,旧版本的 NUnit 对 await 一无所知,因此它们无法配置延续。在 ASP.NET 应用程序中,延续是在 不同的 ThreadPool 线程上执行的,该线程配置了原始执行上下文
  • 你在嘲笑HttpContext.Current吗?请出示此代码。
  • Nunit 2.6.3 和 Reshaper 8

标签: c# asp.net-mvc unit-testing async-await


【解决方案1】:

HttpContext.Current 被认为是一个非常糟糕的属性;它不会在其 ASP.NET 主页之外表现自己。 修复代码的最佳方法是停止查看此属性,并找到一种方法将其与您正在测试的代码隔离开来。例如,您可以创建一个表示当前会话数据的接口并将该接口公开给您正在测试的组件,并使用需要 HTTP 上下文的实现。


根本问题与HttpContext.Current 的工作方式有关。这个属性在 ASP.NET 框架中是“神奇的”,因为它对于请求-响应操作是唯一的,但会根据执行需要在线程之间跳转——它是有选择地在线程之间共享的。

当您在 ASP.NET 处理管道之外使用 HttpContext.Current 时,魔法就消失了。当你用异步编程风格切换线程时,属性为null

如果你绝对不能改变你的代码来移除对HttpContext.Current的硬依赖,你可以利用你的本地上下文来欺骗这个测试:当你声明一个延续时,本地范围内的所有变量都可用于继续。

// Bring the current value into local scope.
var context = System.Web.HttpContext.Current;

var httpSessionStateBefore = context.Session;
var person = await Db.Persons.FirstOrDefaultAsync();
var httpSessionStateAfter = context.Session;

需要明确的是,这适用于您当前的情况。如果在另一个作用域中在 this 前面引入await,代码会突然再次中断;这是一个快速而肮脏的答案,我鼓励您忽略它并寻求更强大的解决方案。

【讨论】:

  • 我做的第一件事就是集成测试。我为控制器编写测试。它将一直到数据库。我使用 HttpContext.Current 从 DbContext 中的 Session 获取详细信息。早些时候,我在 SaveChanges() 方法中执行此操作,该方法在一些异步调用后执行。而不是现在我将它添加为在异步调用之前执行的构造函数初始化属性。感谢您的帮助
【解决方案2】:

首先,我建议您尽可能将您的代码与HttpContext.Current 隔离开来;这不仅会使您的代码更易于测试,而且有助于您为 ASP.NET vNext 做好准备,它更像 OWIN(没有 HttpContext.Current)。

但是,这可能需要进行大量更改,而您可能还没有准备好。要正确模拟 HttpContext.Current,您需要了解它的工作原理。

HttpContext.Current 是由 ASP.NET SynchronizationContext 控制的每线程变量。这个SynchronizationContext是一个“请求上下文”,代表当前请求;它是在收到新请求时由 ASP.NET 创建的。如果您有兴趣了解更多详细信息,我有一个 MSDN article on SynchronizationContext

正如我在async intro blog post 中解释的那样,当您将await 变为Task 时,默认情况下它将捕获当前“上下文”并使用它来恢复async 方法。当 async 方法在 ASP.NET 请求上下文中运行时,await 捕获的“上下文”是 ASP.NET SynchronizationContext。当async 方法恢复时(可能在不同的线程上),ASP.NET SynchronizationContext 将在恢复async 方法之前设置HttpContext.Current。这就是 async/await 在 ASP.NET 主机中的工作方式。

现在,当您在单元测试中运行相同的代码时,行为会有所不同。具体来说,没有 ASP.NET SynchronizationContext 来设置 HttpContext.Current。我假设您的单元测试方法返回Task,在这种情况下,NUnit 根本不提供SynchronizationContext。因此,当async 方法恢复时(可能在不同的线程上),它的HttpContext.Current 可能不是同一个。

有几种不同的方法可以解决此问题。一种选择是编写自己的SynchronizationContext,保留HttpContext.Current,就像ASP.NET 一样。一个更简单(但效率较低)的选项是使用a SynchronizationContext that I wrote called AsyncContext,它确保async 方法将在同一个线程上恢复。您应该能够安装my AsyncEx library from NuGet,然后将您的单元测试方法包装在对AsyncContext.Run 的调用中。请注意,单元测试方法现在是同步的:

[Test]
public void MyTest()
{
  AsyncContext.Run(async () =>
  {
    // Test logic goes here: set HttpContext.Current, etc.
  });
}

【讨论】:

  • 您的AsyncContext 是否也适用于嵌套的await?我刚刚在 .NET 4.5 项目中尝试过,我的测试方法将 HttpContext.Current 设置为假的,然后调用在内部使用 await 的方法。对于顶级await,它可以正常工作(即使没有AsyncContext,它也可以正常工作,但是在嵌套await 之后,HttpContext.Current 再次为空。嵌套的await 正在从一个调用派生方法FindByNameAsync()派生自 Microsoft.AspNet.Identity.UserManager 的类。
  • @JustAMartin: AsyncContext 只保证该方法将在同一个线程上恢复。它对HttpContext.Current 没有任何特别之处。听起来有些 ASP.NET 代码可能正在清除 HttpContext.Current。除非你的代码使用ConfigureAwait(false),否则会导致线程跳转。
【解决方案3】:

我在代码中遇到问题时遇到了这个问题... HTTPContext.Current 在Async MVC Action 中的等待 之后为空。我在这里发布这个是因为像我这样的其他人可能会登陆这里。

我的一般建议是从会话中获取任何你想要的东西到一个局部变量中,就像上面讨论的其他人一样,但不要担心保留上下文,而只是担心获取你想要的实际项目。

public async Task<ActionResult> SomeAction(SomeModel model)
{
int id = (int)HttpContext.Current.Session["Id"];

/* Session Exists Here */

var somethingElseAsyncModel = await GetSomethingElseAsync(model);

/* Session is Null Here */

// Do something with id, thanks to the fact we got it when we could
}

【讨论】:

  • 我很感激它并赞成,但是,伙计,希望你有一些关于如何在等待之后访问 Session 变量的信息。
  • @Barry,您可以尝试将变量指向它(会话),但我没有尝试过。对我来说,抓住我需要的东西似乎更安全、更干净,但这是假设你在大多数情况下不会修改会话。另外,我通常会尽量少使用会话。当用户开始使用两个单独的浏览器窗口/选项卡时,过度使用会话(考虑全局)可能会导致不确定的行为。
  • 不得不同意,我最终按照您的模型预先获取所需的数据。我正在努力转换严重依赖会话的遗留代码,这在几个层面上肯定是不安全的。我很感激这方面的信息,对我有很大帮助!
猜你喜欢
  • 1970-01-01
  • 2016-03-20
  • 1970-01-01
  • 2019-11-13
  • 1970-01-01
  • 2014-09-09
  • 1970-01-01
  • 1970-01-01
  • 2010-12-26
相关资源
最近更新 更多