ASP.Net Core 2.0 SignInAsync返回异常值不能为null,提供程序 [英] ASP.Net Core 2.0 SignInAsync returns exception Value cannot be null, provider

查看:165
本文介绍了ASP.Net Core 2.0 SignInAsync返回异常值不能为null,提供程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个ASP.Net Core 2.0 Web应用程序,正在使用单元测试(使用NUnit)进行改进.该应用程序可以正常运行,到目前为止,大多数测试都可以正常运行.

I have an ASP.Net Core 2.0 web application I am retrofitting with unit tests (using NUnit). The application works fine, and most of the tests thus far work fine.

但是,测试身份验证/授权(用户是否登录并可以访问[Authorize]过滤的操作)失败...

However, testing the authentication/authorization (does a user get logged in and can access [Authorize] filtered actions) is failing with...

System.ArgumentNullException: Value cannot be null.
Parameter name: provider

...之后...

await HttpContext.SignInAsync(principal);

...但是目前尚不清楚根本原因是什么.代码执行在此处的被调用方法中停止,并且IDE中未显示任何异常,但是代码执行返回到调用方,然后终止(但我仍然在VS的输出窗口中看到The program '[13704] dotnet.exe' has exited with code 0 (0x0)..)

...but it is not clear what in fact is the underlying cause. Code execution stops in the called method here and no exception is shown in the IDE but code execution returns to the caller, then terminates (yet I still see The program '[13704] dotnet.exe' has exited with code 0 (0x0). in the output window of VS.)

测试资源管理器"显示为红色,并给出了引用的异常(否则,我对该问题一无所知.)

The Test Explorer shows red and gives the exception referenced (otherwise I would have no idea as to the problem.)

我正在努力创建一个副本,以向人们指出(到目前为止,涉及到的部分内容.)

I am working on creating a repro to point folks to (turning out to bit a bit involved thus far.)

有人知道如何找出根本原因吗?这是一个与DI相关的问题(测试中没有提供但可以正常执行的东西)吗?

Does anyone know how to pinpoint the underlying cause? Is this a DI related issue (something needed that isn't being provided in the test but is in normal execution)?

UPDATE1::提供请求的验证码...

UPDATE1: Providing requested authentication code...

public async Task<IActionResult> Registration(RegistrationViewModel vm) {
    if (ModelState.IsValid) {
        // Create registration for user
        var regData = createRegistrationData(vm);
        _repository.AddUserRegistrationWithGroup(regData);

        var claims = new List<Claim> {
            new Claim(ClaimTypes.NameIdentifier, regData.UserId.ToString())
        };
        var ident = new ClaimsIdentity(claims);
        var principal = new ClaimsPrincipal(ident);

        await HttpContext.SignInAsync(principal); // FAILS HERE

        return RedirectToAction("Welcome", "App");
    } else {
        ModelState.AddModelError("", "Invalid registration information.");
    }

    return View();
}

失败的测试代码...

The test code that fails...

public async Task TestRegistration()
{
    var ctx = Utils.GetInMemContext();
    Utils.LoadJsonData(ctx);
    var repo = new Repository(ctx);
    var auth = new AuthController(repo);
    auth.ControllerContext = new ControllerContext();
    auth.ControllerContext.HttpContext = new DefaultHttpContext();

    var vm = new RegistrationViewModel()
    {
        OrgName = "Dev Org",
        BirthdayDay = 1,
        BirthdayMonth = "January",
        BirthdayYear = 1979 
    };

    var orig = ctx.Registrations.Count();
    var result = await auth.Registration(vm); // STEPS IN, THEN FAILS
    var cnt = ctx.Registrations.Count();
    var view = result as ViewResult;

    Assert.AreEqual(0, orig);
    Assert.AreEqual(1, cnt);
    Assert.IsNotNull(result);
    Assert.IsNotNull(view);
    Assert.IsNotNull(view.Model);
    Assert.IsTrue(string.IsNullOrEmpty(view.ViewName) || view.ViewName == "Welcome");
}

UPDATE3 :基于聊天 @ nkosi 建议,这是我没有满足.

UPDATE3: Based on chat @nkosi suggested that this is a problem stemming from my not fulfilling the needs of the dependency injection requirements for HttpContext.

但是,尚不清楚的是:实际上是否存在问题没有提供适当的服务依赖关系的原因,为什么代码可以正常工作(未测试时). SUT(控制器)仅接受一个I​​Repository参数(因此无论如何都提供该参数.)为什么要创建一个重载的ctor(或模拟)仅用于测试,而在运行程序时仅调用现有ctor并创建一个重载的ctor(或模拟).它运行没有问题吗?

However, what isn't yet clear is: if it is, in fact, an issue of not providing the proper service dependency, why does the code work normally (when not being tested). The SUT (controller) only accepts an IRepository parameter (so that is all that is provided in any case.) Why create an overloaded ctor (or mock) just for test, when the existing ctor is all that is called when running the program and it runs without issue?

UPDATE4 :@Nkosi用解决方案回答了该错误/问题时,我仍然想知道为什么IDE无法准确/一致地呈现底层异常.这是错误还是由于async/await运算符和NUnit测试适配器/运行器?为什么异常不会像我在调试测试时所期望的那样突然弹出",并且退出代码仍为零(通常表示成功的返回状态)?

UPDATE4: While @Nkosi answered the bug/problem with a solution, I am still wondering why the IDE isn't accurately/consistently presenting the underlying exception. Is this a bug, or due to the async/await operators and the NUnit Test Adapter/runner? Why aren't exceptions "popping" like I would expect while debugging the test, and the exit code is still zero (typically indicating a successful return state)?

推荐答案

尚不清楚的是:实际上,如果这是一个不提供适当的服务依赖关系的问题,那么为什么代码可以正常工作(当不进行测试时). SUT(控制器)仅接受IRepository参数(因此无论如何都提供).为什么要创建一个过载的ctor(或模拟)以用于测试,而在运行现有ctor时只调用现有的ctor就可以了.程序,它可以正常运行吗?

What isn't yet clear is: if it is, in fact, an issue of not providing the proper service dependency, why does the code work normally (when not being tested). The SUT (controller) only accepts an IRepository parameter (so that is all that is provided in any case.) Why create an overloaded ctor (or mock) just for test, when the existing ctor is all that is called when running the program and it runs without issue?

您在这里混淆了一些事情:首先,您不需要创建单独的构造函数.不是为了测试,也不是为了在您的应用程序中实际运行它.

You are mixing up a few things here: First of all, you don’t need to create separate constructors. Not for testing, and not for actually running this as part of your application.

您应该将控制器具有的所有直接依赖项定义为构造函数的参数,以便当其作为应用程序的一部分运行时,依赖项注入容器会将这些依赖项提供给控制器.

You should define all the direct dependencies your controller has as parameters to the constructor, so that when this runs as part of the application, the dependency injection container will provide those dependencies to the controller.

但这也是重要的一点:运行应用程序时,有一个依赖项注入容器,负责创建对象并提供所需的依赖项.因此,您实际上不必担心它们的来源.但是,在进行单元测试时,这是不同的.在单元测试中,我们不想使用依赖项注入,因为这只会隐藏依赖项,因此可能会与我们的测试产生冲突.依靠单元测试中的依赖项注入是一个很好的信号,表明您不是 unit 测试,而是进行了 integration 测试(至少除非您实际在测试DI容器, ).

But that’s also the important bit here: When running your application, there is a dependency injection container that is responsible of creating objects and providing the required dependencies. So you actually don’t need to worry too much about where they come from. This is different when unit testing though. In unit tests, we don’t want to use dependency injection since that will just hide dependencies, and as such possible side effects that may conflict with our test. Relying on dependency injection within a unit test is a very good sign that you are not unit testing but doing an integration test instead (at least unless you are actually testing a DI container).

相反,在单元测试中,我们要显式创建所有对象,明确地提供所有依赖关系.这意味着我们要新建控制器并传递控制器具有的所有依赖关系.理想情况下,我们使用模拟,因此我们在单元测试中不依赖外部行为.

Instead, in unit tests, we want to create all objects explicitly providing all dependencies explicitly. This means that we new up the controller and pass all dependencies the controller has. Ideally, we use mocks so we don’t depend on external behavior in our unit test.

在大多数情况下,这一切都非常简单.不幸的是,控制器有一些特殊之处:控制器具有ControllerContext属性,该属性在MVC生命周期中自动提供. MVC中的其他一些组件具有类似的功能(例如,ViewContext也自动提供).这些属性不是构造函数注入的,因此依赖项不是显式可见的.根据控制器的功能,在对控制器进行单元测试时,可能还需要设置这些属性.

This is all pretty straight forward most of the time. Unfortunately, there is something special about controllers: Controllers have a ControllerContext property that is automatically provided during the MVC lifecycle. Some other components within MVC have similar things (e.g. the ViewContext is also automatically provided). These properties are not constructor injected, so the dependency is not explicitly visible. Depending on what the controller does, you might need to set these properties too when unit testing the controller.

谈到单元测试,您正在控制器动作中使用HttpContext.SignInAsync(principal),因此很遗憾,您直接使用HttpContext进行操作.

Coming to your unit test, you are using HttpContext.SignInAsync(principal) inside your controller action, so unfortunately, you are operating with the HttpContext directly.

SignInAsync是一种扩展方法,它

SignInAsync is an extension method which will basically do the following:

context.RequestServices.GetRequiredService<IAuthenticationService>().SignInAsync(context, scheme, principal, properties);

因此,为纯粹方便起见,此方法将使用服务定位器模式来检索服务从依赖项注入容器中执行登录.因此,只需在HttpContext上进行此方法调用,即可引入进一步的隐式依赖关系,这些依赖关系只有在测试失败时才能发现.在为什么应该避免使用服务定位器模式 a>:构造函数中的显式依赖项更易于管理. –但是在这里,这是一种便捷的方法,因此我们将不得不接受这种方法,而只是调整测试以使其适用.

So this method, for pure convenience, will use the service locator pattern to retrieve a service from the dependency injection container to perform the sign-in. So just this one method call on the HttpContext will pull in further implicit dependencies that you only discover about when your test fails. That should serve as a good example on why you should avoid the service locator pattern: Explicit dependencies in the constructor are much more manageable. – But here, this is a convenience method, so we will have to live with that and just adjust the test to work with this.

实际上,在继续之前,我想在此提一个很好的替代解决方案:由于控制器是AuthController,我只能想象其核心目的之一是进行身份验证,向用户签入和签出东西. .因此,实际上,不使用HttpContext.SignInAsync而是将IAuthenticationService作为控制器上的显式依赖项并直接在其上调用方法可能是一个好主意.这样,您就可以在测试中实现明确的依赖关系,而无需参与服务定位器.

Actually, before moving on, I want to mention a good alternative solution here: Since the controller is a AuthController I can only imagine that one of its core purposes is to do authentication stuff, signing users in and out and things. So it might actually be a good idea not to use HttpContext.SignInAsync but instead have the IAuthenticationService as an explicit dependency on the controller, and calling the methods on it directly. That way, you have a clear dependency that you can fulfill in your tests and you don’t need to get involved with the service locator.

当然,这将是此控制器的一种特殊情况,不适用于所有可能在HttpContext上调用扩展方法.因此,让我们解决如何正确测试它:

Of course, this would be a special case for this controller and won’t work for every possible call of the extension methods on the HttpContext. So let’s tackle how we can test this properly:

从代码中可以看到SignInAsync的实际作用,我们需要为HttpContext.RequestServices提供一个IServiceProvider并使之能够返回IAuthenticationService.因此,我们将嘲笑这些:

As we can see from the code what SignInAsync actually does, we need to provide a IServiceProvider for HttpContext.RequestServices and make that be able to return an IAuthenticationService. So we’ll mock these:

var authenticationServiceMock = new Mock<IAuthenticationService>();
authenticationServiceMock
    .Setup(a => a.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
    .Returns(Task.CompletedTask);

var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock
    .Setup(s => s.GetService(typeof(IAuthenticationService)))
    .Returns(authenticationServiceMock.Object);

然后,我们可以在创建控制器后在ControllerContext中传递该服务提供商:

Then, we can pass that service provider in the ControllerContext after creating the controller:

var controller = new AuthController();
controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext()
    {
        RequestServices = serviceProviderMock.Object
    }
};

我们要做的就是让HttpContext.SignInAsync正常工作.

That’s all we need to do to make HttpContext.SignInAsync work.

不幸的是,还有更多的东西.正如我在其他答案(您已经找到)中所解释的那样,从控制器返回RedirectToActionResult会导致在单元测试中设置RequestServices时出现问题.由于RequestServices不为null,因此RedirectToAction的实现将尝试解析IUrlHelperFactory,并且该结果必须为非null.因此,我们需要对模拟进行一些扩展以提供该模拟:

Unfortunately, there is a bit more to it. As I’ve explained in this other answer (which you already found), returning a RedirectToActionResult from a controller will cause problems when you have the RequestServices set up in a unit test. Since RequestServices are not null, the implementation of RedirectToAction will attempt to resolve an IUrlHelperFactory, and that result has to be non-null. As such, we need to expand our mocks a bit to also provide that one:

var urlHelperFactory = new Mock<IUrlHelperFactory>();
serviceProviderMock
    .Setup(s => s.GetService(typeof(IUrlHelperFactory)))
    .Returns(urlHelperFactory.Object);

幸运的是,我们不需要执行任何其他操作,也不需要在工厂模拟中添加任何逻辑.只要就在那里就足够了.

Luckily, we don’t need to do anything else, and we also don’t need to add any logic to the factory mock. It’s enough if it’s just there.

因此,我们可以正确测试控制器的动作:

So with this, we can test the controller action properly:

// mock setup, as above
// …

// arrange
var controller = new AuthController(repositoryMock.Object);
controller.ControllerContext = new ControllerContext
{
    HttpContext = new DefaultHttpContext()
    {
        RequestServices = serviceProviderMock.Object
    }
};

var registrationVm = new RegistrationViewModel();

// act
var result = await controller.Registration(registrationVm);

// assert
var redirectResult = result as RedirectToActionResult;
Assert.NotNull(redirectResult);
Assert.Equal("Welcome", redirectResult.ActionName);


我仍然想知道为什么IDE不能准确/一致地呈现底层异常.这是一个错误,还是由于async/await运算符和NUnit Test Adapter/runner?

I am still wondering why the IDE isn't accurately/consistently presenting the underlying exception. Is this a bug, or due to the async/await operators and the NUnit Test Adapter/runner?

我过去在异步测试中也看到过类似的情况,即我无法正确调试它们或无法正确显示异常.我不记得在Visual Studio和xUnit的最新版本中看到过这种情况(我个人使用的是xUnit,而不是NUnit).如果有帮助,使用dotnet test从命令行运行测试通常可以正常运行,并且您将获得正确的(异步)堆栈跟踪以了解失败.

I have seen something similar in the past too with my asynchronous tests, that I could not debug them properly or that exceptions wouldn’t be displayed correctly. I don’t remember seeing this in recent versions of Visual Studio and xUnit (I’m personally using xUnit, not NUnit). If it helps, running the tests from the command line with dotnet test will usually work properly and you will get proper (async) stack traces for failures.

这篇关于ASP.Net Core 2.0 SignInAsync返回异常值不能为null,提供程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆