简体   繁体   English

ASP.Net Core 2.0 SignInAsync返回异常值不能为null,提供者

[英]ASP.Net Core 2.0 SignInAsync returns exception Value cannot be null, provider

I have an ASP.Net Core 2.0 web application I am retrofitting with unit tests (using NUnit). 我有一个ASP.Net Core 2.0 Web应用程序我正在使用单元测试进行改造(使用NUnit)。 The application works fine, and most of the tests thus far work fine. 该应用程序工作正常,迄今为止大多数测试工作正常。

However, testing the authentication/authorization (does a user get logged in and can access [Authorize] filtered actions) is failing with... 但是,测试身份验证/授权(用户是否登录并可以访问[Authorize]过滤的操作)失败了......

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

...after... ...后...

await HttpContext.SignInAsync(principal);

...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.) 代码执行在此处停止在被调用的方法中,并且IDE中没有显示异常,但代码执行返回给调用者,然后终止(但我仍然看到The program '[13704] dotnet.exe' has exited with code 0 (0x0).在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.) 我正在努力创建一个指向人们的复制品(到目前为止有点参与其中)。

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)? 这是DI相关的问题(测试中没有提供但是正常执行中需要的东西)?

UPDATE1: Providing requested authentication code... UPDATE1:提供请求的验证码......

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: Based on chat @nkosi suggested that this is a problem stemming from my not fulfilling the needs of the dependency injection requirements for HttpContext . 更新3:基于聊天 @nkosi 建议这是一个问题源于我没有满足HttpContext的依赖注入要求的需求。

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? SUT(控制器)只接受一个I​​Repository参数(这就是在任何情况下提供的所有参数。)为什么创建一个重载的ctor(或mock)只是为了测试,当现有的ctor是在运行程序时调用的所有ctor和它运行没有问题?

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. 更新4 :虽然@Nkosi用解决方案回答了错误/问题,但我仍然想知道为什么IDE没有准确/一致地呈现基础异常。 Is this a bug, or due to the async/await operators and the NUnit Test Adapter/runner? 这是一个错误,还是由于async / await运算符和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)? 为什么在调试测试时出现异常“弹出”,退出代码仍为零(通常表示成功返回状态)?

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? SUT(控制器)只接受一个IRepository参数(这就是在任何情况下提供的所有参数。)为什么创建一个重载的ctor(或mock)只是为了测试,当现有的ctor是在运行程序时调用的所有ctor和它运行没有问题?

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. 您应该将控制器具有的所有直接依赖项定义为构造函数的参数,以便当它作为应用程序的一部分运行时,依赖项注入容器将向控制器提供这些依赖项。

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). 依赖于单元测试中的依赖注入是一个非常好的迹象,表明您不是单元测试而是进行集成测试(至少除非您实际测试DI容器)。

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. 理想情况下,我们使用模拟,因此我们不依赖于单元测试中的外部行为。

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. 不幸的是,控制器有一些特殊之处:控制器具有在MVC生命周期中自动提供的ControllerContext属性。 Some other components within MVC have similar things (eg the ViewContext is also automatically provided). MVC中的一些其他组件具有类似的东西(例如,也自动提供ViewContext )。 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. 根据控制器的作用,在单元测试控制器时,您可能还需要设置这些属性。


Coming to your unit test, you are using HttpContext.SignInAsync(principal) inside your controller action, so unfortunately, you are operating with the HttpContext directly. 进入你的单元测试,你在你的控制器动作中使用HttpContext.SignInAsync(principal) ,所以不幸的是,你正在使用HttpContext直接操作。

SignInAsync is an extension method which will basically do the following : SignInAsync是一种扩展方法, 基本上将执行以下操作

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

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. 因此,对HttpContext这一个方法调用将HttpContext进一步的隐式依赖关系,您只能在测试失败时发现这些依赖关系。 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. - 但是在这里,这是一种方便的方法,所以我们将不得不忍受这一点,只需调整测试即可。

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

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 . 当然,这将是此控制器的特殊情况,并且不适用于HttpContext上的每个可能的扩展方法调用。 So let's tackle how we can test this properly: 那么让我们来解决我们如何正确测试这个问题:

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 . 我们可以从代码中看到SignInAsync实际上做了什么,我们需要为HttpContext.RequestServices提供一个IServiceProvider ,并使其能够返回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);

Then, we can pass that service provider in the ControllerContext after creating the controller: 然后,我们可以在创建ControllerContext后在ControllerContext传递该服务提供者:

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

That's all we need to do to make HttpContext.SignInAsync work. 这就是我们需要做的就是让HttpContext.SignInAsync工作。

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. 正如我在另一个答案 (您已经发现)中所解释的那样,当您在单元测试中设置RequestServices时,从控制器返回RedirectToActionResult将导致问题。 Since RequestServices are not null, the implementation of RedirectToAction will attempt to resolve an IUrlHelperFactory , and that result has to be non-null. 由于RequestServices不为null, RedirectToAction的实现将尝试解析IUrlHelperFactory ,并且该结果必须为非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);

I am still wondering why the IDE isn't accurately/consistently presenting the underlying exception. 我仍然想知道为什么IDE没有准确/一致地呈现基础异常。 Is this a bug, or due to the async/await operators and the NUnit Test Adapter/runner? 这是一个错误,还是由于async / await运算符和NUnit Test Adapter / runner?

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). 我不记得在Visual Studio和xUnit的最新版本中看到这一点(我个人使用的是xUnit,而不是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. 如果有帮助,使用dotnet test从命令行运行测试通常会正常工作,您将获得适当的(异步)堆栈跟踪故障。

Is this a DI related issue (something needed that isn't being provided in the test but is in normal execution)? 这是DI相关的问题(测试中没有提供但是正常执行中需要的东西)?

YES

You are calling features that the framework would setup for you at run time. 您正在调用框架在运行时为您设置的功能。 During isolated unit tests you will need to set these up yourself. 在隔离的单元测试期间,您需要自己设置它们。

The Controller's HttpContext is missing an IServiceProvider which it uses to resolve IAuthenticationService . Controller的HttpContext缺少一个用于解析IAuthenticationServiceIServiceProvider That service is what actually calls SignInAsync 该服务实际上称为SignInAsync

In order to let.... 为了......

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

...in the Registration action to execute to completion during the unit test you will need to mock a service provider so that the SignInAsync extension method does not fail. ...在单元测试期间执行完成的Registration操作中,您需要模拟服务提供者,以便SignInAsync扩展方法不会失败。

Update the unit test arrangement 更新单元测试安排

//...code removed for brevity

auth.ControllerContext.HttpContext = new DefaultHttpContext() {
    RequestServices = createServiceProviderMock()
};

//...code removed for brevity

Where createServiceProviderMock() is a small method used to mock a service provider that will be used to populate the HttpContext.RequestServices 其中createServiceProviderMock()是一个小方法,用于模拟将用于填充HttpContext.RequestServices的服务提供者

public IServiceProvider createServiceProviderMock() {
    var authServiceMock = new Mock<IAuthenticationService>();
    authServiceMock
        .Setup(_ => _.SignInAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<ClaimsPrincipal>(), It.IsAny<AuthenticationProperties>()))
        .Returns(Task.FromResult((object)null)); //<-- to allow async call to continue

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

    return serviceProviderMock.Object;
}

I would also suggest mocking the Repository for the purposes of an isolated unit test of that controller action to make sure it flows to completion without any negative effects 我还建议模拟Repository以便对该控制器操作进行隔离单元测试,以确保它流向完成而没有任何负面影响

as @poke mentioned you better not use Dependency Injection in unit tests and provide dependencies explicitly (using mocking) but however, I had this issue in my integration tests and I figured that the problem arises from RequestServices property of HttpContext which is not properly initialized in tests (since we don't use actual HttpContext in tests) so I registered my HttpContextAccessor like below and passed all of it's required service myself (manually) and problem solved. 正如@poke所提到的,你最好不要在单元测试中使用依赖注入并明确地提供依赖(使用模拟)但是,我在集成测试中遇到了这个问题,并且我认为问题来自HttpContext RequestServices属性,该属性未正确初始化测试(因为我们在测试中没有使用实际的HttpContext)所以我在下面注册了我的HttpContextAccessor并且自己(手动)传递了所有它所需的服务并解决了问题。 see code below 见下面的代码

Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });

I agree it's not a very clean solution but note that I wrote and used this code only in my tests in order to provide required HttContext dependencies (which were not supplied automatically in test method), in your application IHttpContextAccessor , HttpContext and their required services are automatically provided by framework. 我同意它不是一个非常干净的解决方案,但请注意我在我的测试中编写和使用此代码,以便在您的应用程序IHttpContextAccessorHttpContext及其所需服务中提供所需的HttContext依赖项(在测试方法中不会自动提供)。由框架自动提供。

here is all of my dependency registration method in my tests base class constructor 这是我的测试基类构造函数中的所有依赖注册方法

 public class MyTestBaseClass
 {
  protected ServiceCollection Services { get; set; } = new ServiceCollection();
  MyTestBaseClass
 {

   Services.AddDigiTebFrameworkServices();
        Services.AddDigiTebDBContextService<DigiTebDBContext> 
        (Consts.MainDBConnectionName);
        Services.AddDigiTebIdentityService<User, Role, DigiTebDBContext>();
        Services.AddDigiTebAuthServices();
        Services.AddDigiTebCoreServices();
        Services.AddSingleton<IHttpContextAccessor>(new HttpContextAccessor() { HttpContext = new DefaultHttpContext() { RequestServices = Services.BuildServiceProvider() } });
}
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 GetExternalLoginInfoAsync在ASP.NET Core 2.0中返回null - GetExternalLoginInfoAsync returns null in asp.net core 2.0 单元测试 ASP.NET 核心 3.1 MVC controller - 值不能是 null。 (参数“提供者”) - Unit testing ASP.NET Core 3.1 MVC controller - value cannot be null. (Parameter 'provider') ASP.NET Core 2.0 值不能为空。 参数名称:名称 - ASP.NET Core 2.0 Value cannot be null. Parameter name: Name Asp.Net Core 2.0 ArgumentNullException:值不能为null。 参数名称:connectionString - Asp.Net Core 2.0 ArgumentNullException: Value cannot be null. Parameter name: connectionString Asp.Net核心挑战返回Null URI异常错误 - Asp.Net Core Challenge returns Null URI Exception error ASP.NET 内核 razor 页值不能为 null - ASP.NET core razor page value cannot be null 带有ASP.NET Core 2.0的Github OAuth提供程序不起作用 - Github OAuth provider with ASP.NET Core 2.0 does not work ASP.NET 内核 Ajax 返回 null - ASP.NET Core Ajax returns null ASP.NET Core 2.0模型针对未在表单中使用的项目返回null - ASP.NET Core 2.0 Model returns null for items not used in a form 在IAsyncPageFilter-Asp.Net Core 2.0 Razor Pages中的OnPageHandlerExecutionAsync中,PageResult始终返回null - PageResult always returns null in IAsyncPageFilter - OnPageHandlerExecutionAsync in Asp.Net Core 2.0 Razor Pages
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM