如何处理 ASP.NET Core 中包含两个构造函数的控制器?

How to deal with a Controller containing two constructors in ASP.NET Core?

提问人:Aboliii 提问时间:11/15/2023 更新时间:11/15/2023 访问量:53

问:

我有一个 API,我的意思是通过 xUnit 和 Moq 进行单元测试。它实际上有一个构造函数,但我必须根据 Visual Studio 的建议添加另一个构造函数以用于测试的特定目的。它非常适合测试,但是当我尝试通过 Swagger 测试 API 时,我遇到了另一个错误。“System.InvalidOperationException:在类型'Product.API.Controllers.ProductController'中找到了多个接受所有给定参数类型的构造函数。应该只有一个适用的构造函数”。 以下是控制器的内容

public class ProductController : ControllerBase
{
    private readonly IMediator _mediator;
    private readonly IConfiguration _configuration;
    private readonly UserManager<User> _userManager;
    private readonly SignInManager<User> _signInManager;
    private readonly ProductDbContext _productDbContext;
    private readonly IUserService _userService;

    public ProductController(IConfiguration configuration, UserManager<User> userManager, ProductDbContext productDbContext
        , SignInManager<User> signInManager, IUserService userService)
    {
        _configuration = configuration;
        _userManager = userManager;
        _productDbContext = productDbContext;
        _signInManager = signInManager;
        _userService = userService;
    }
    public ProductController(IMediator mediator)
    {
        _mediator = mediator;
    }
    [HttpGet]
    [AllowAnonymous]
    public async Task<IActionResult> GetAllProducts()
    {
        var products = await _mediator.Send(new GetAllProductsQuery());
        return Ok(products);
    }

下面是单元测试的代码

private readonly Mock<IMediator> _mediatorMock;
private readonly ProductController _controller;
public ProductControllerTest()
{
    _mediatorMock = new Mock<IMediator>();
    _controller = new ProductController(_mediatorMock.Object);
}
[Fact]
public async Task ProductController_GetAllProducts_ShouldReturnStatusCode200()
{
    //arrange
    var expected = new List<ProductModelDTO>();
    _mediatorMock.Setup(m=> m.Send(It.IsAny<GetAllProductsQuery>(), default(CancellationToken))).ReturnsAsync(expected);
    //act
    var result = await _controller.GetAllProducts();
    //assert
    var okResult = result as OkObjectResult;
    okResult?.StatusCode.Should().Be(200);
    okResult?.Value.Should().BeOfType<List<ProductModelDTO>>();
}

我遇到了一个可能的解决方案;将 [FromServices] 添加到第二个构造函数。但是我遇到了同样的错误!

C# 单元测试 依赖项注入 构造函数

评论

1赞 Guru Stron 11/15/2023
TBH 目前尚不清楚为什么你有 2 个 ctors。不使用第一个依赖项(至少在显示的代码中)。
0赞 Aboliii 11/15/2023
我没有把它们包含在帖子中。我在控制器的其他 CRUD 操作中使用它们
0赞 David 11/15/2023
@Aboliii:“Visual Studio 的建议”——我很好奇实际的建议是什么以及是什么导致了它。我不明白为什么在具有不同依赖项的控制器中需要两个构造函数,而不是一个具有所有依赖项的构造函数。为什么不能只有一个构造函数?
0赞 Guru Stron 11/15/2023
“我在控制器的其他 CRUD 操作中使用它们”——然后创建一个 ctor,它将接受所有必需的依赖项(以及第一个依赖项中的所有依赖项)。IMediator
1赞 Steven 11/15/2023
依赖注入反模式:多个构造函数

答:

2赞 David 11/15/2023 #1

在 C# 语言级别,使用多个构造函数没有错。但是,在框架级别(其中 ASP.NET 中使用的依赖注入器正在尝试自动创建控制器的实例),多个构造函数会混淆框架。

使用一个构造函数。

如果您的控制器具有以下依赖项:

private readonly IMediator _mediator;
private readonly IConfiguration _configuration;
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly ProductDbContext _productDbContext;
private readonly IUserService _userService;

然后把它们都放在构造函数中:

public ProductController(
  IConfiguration configuration,
  UserManager<User> userManager,
  ProductDbContext productDbContext,
  SignInManager<User> signInManager,
  IUserService userService,
  IMediator mediator)
{
    _configuration = configuration;
    _userManager = userManager;
    _productDbContext = productDbContext;
    _signInManager = signInManager;
    _userService = userService;
    _mediator = mediator;
}

这样,框架就会知道要使用哪个构造函数,因为只有一个构造函数。每当需要添加新的依赖项时,请将该依赖项添加到构造函数中,而不是仅为该依赖项添加全新的构造函数。

2赞 Guru Stron 11/15/2023 #2

将所有依赖项移动到单个 ctor:

public ProductController(IConfiguration configuration, 
    UserManager<User> userManager, 
    ProductDbContext productDbContext, 
    SignInManager<User> signInManager, 
    IUserService userService, 
    IMediator mediator)
{
    // ...
}

然后在测试中将所有值传递给它:

public ProductControllerTest()
{
    _mediatorMock = new Mock<IMediator>();
    // mock and pass everything:
    _controller = new ProductController(..., _mediatorMock.Object); 
}

或者,您可以使用 ASP.NET Core 功能来使用方法注入(使用 FromServices 进行操作注入),尽管您仍然需要将所有通用参数传递给 ctor,但您可以减少它们的总数,并仅在需要它们的地方管理非共享参数:ProductController

public ProductController(IConfiguration configuration, UserManager<User> userManager, ProductDbContext productDbContext
    , SignInManager<User> signInManager, IUserService userService)
{
    _configuration = configuration;
    _userManager = userManager;
    _productDbContext = productDbContext;
    _signInManager = signInManager;
    _userService = userService;
}

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> GetAllProducts([FromServices] IMediator mediator)
{
    var products = await _mediator.Send(new GetAllProductsQuery());
    return Ok(products);
}

请注意,一般来说,注入数据库上下文和单独的服务之类的东西,并且看起来像一种气味,常见的方法是(几乎)使用它来使用所有东西。IMediatorIMediator

评论

0赞 Aboliii 11/15/2023
感谢您抽出时间接受采访。我首先尝试过这个,但是在单元测试文件中初始化_controller属性时出现错误;_controller = 新的 ProductController(_mediatorMock.Object) 这就是 VS 建议我添加另一个仅使用 IMediator 作为参数的 ctor 的地方
2赞 Guru Stron 11/15/2023
@Aboliii 请阅读答案,您需要传递所有依赖项(用 和 comment 表示),即...new ProductController(_configMock.Object, userManagerMock.Object, ..., _mediatorMock.Object)
0赞 Aboliii 11/15/2023
明白了!非常感谢