ASP.NET Core 依赖注入与多个构造函数

82
我在我的ASP.NET Core应用程序中有一个带有多个构造函数的标签助手。当ASP.NET 5尝试解析类型时,这会导致运行时出现以下错误:
InvalidOperationException: 在类型'MyNameSpace.MyTagHelper'中找到了多个接受所有给定参数类型的构造函数。应该只有一个适用的构造函数。
其中一个构造函数是无参数的,另一个构造函数有一些参数,其参数不是注册类型。我希望它使用无参数的构造函数。
有没有办法让ASP.NET 5依赖注入框架选择特定的构造函数?通常可以通过使用属性来实现,但我找不到任何东西。
我的用例是,我正在尝试创建一个既是TagHelper又是HTML助手的单个类,如果解决了这个问题,这是完全可能的。

6
你的可注入对象应该只有一个构造函数。拥有多个构造函数是一种反模式。 - Steven
是的,这不是最理想的解决方案。我会想出一个变通方法。 - Muhammad Rehan Saeed
1
如果您完整阅读这篇文章,您会发现答案很简单。如果这个MyTagHelper是您控制的类型:删除一个构造函数即可。如果这个MyTagHelper是第三方或框架类型,则使用工厂委托进行注册,并在该委托内调用特定的构造函数。 - Steven
想知道是否有一种方法可以针对库中所有控制器进行单元测试以检测此错误。 - Bernard Vander Beken
@Steven,当您需要为单元测试保留额外的构造函数时,您该怎么做? - jeancallisti
@jeancallisti:我从来不需要为我的单元测试编写额外的构造函数。对我来说,这表明该类存在问题。但这当然是一个非常通用的答案。如果您想要更精确的答案,请在SO上发布一个新问题,其中详细描述您的设计、手头的问题,并展示被测试类及其测试的具体示例,以及为什么您认为需要第二个构造函数。将该问题标记为“依赖注入”,并在评论中发布该问题的链接,我会尽力查看。 - Steven
6个回答

102

ActivatorUtilitiesConstructorAttribute应用于您希望 DI 使用的构造函数:

[ActivatorUtilitiesConstructor]
public MyClass(ICustomDependency d)
{
}

这需要使用ActivatorUtilities类来创建你的MyClass。从.NET Core 3.1开始,Microsoft依赖注入框架内部使用ActivatorUtilities;在早期版本中,你需要手动使用它:

services.AddScoped(sp => ActivatorUtilities.CreateInstance<MyClass>(sp));

4
太棒了,因为我可以有两个构造函数。一个用于 DI 注入 IConfiguration,另一个用于我的集成测试,我可以在其中指定连接字符串。 - Quan
4
运作得很好,应该被标记为已接受的答案。 - Mohammad Roshani
1
这是正确的答案。所有抱怨它不起作用的人都可以发帖提问,我99%确定问题在你的代码中的其他地方。 - Ian Kemp

70

Illya是正确的:内置的解析器不支持暴露多个构造函数的类型......但是没有什么阻止你注册一个委托来支持这种情况:

Illya说得对:内置解析器不支持暴露多个构造函数的类型......但是,您可以注册委托来支持此场景:
services.AddScoped<IService>(provider => {
    var dependency = provider.GetRequiredService<IDependency>();

    // You can select the constructor you want here.
    return new Service(dependency, "my string parameter");
});

注意: 支持多个构造函数是在后续版本中添加的,正如其他答案所示。现在,DI 堆栈会愉快地选择它能解析的参数最多的构造函数。例如,如果你有两个构造函数 - 一个有 3 个指向服务的参数,另一个有 4 个 - 它将更喜欢带有 4 个参数的那一个。


谢谢,这对于连接我无法控制的第三方库非常有用。 - SimonGates
1
我该如何对控制器进行单元测试?我想通过构造函数注入依赖项。 - Teoman shipahi
尽管上面的评论支持稍后添加多个构造函数,但截至今天,在 .Net 5.0 中它并不起作用 <难过>...此外,似乎没有一种方法可以指定...请注意,实际的 ASP.NET 代码工作正常,只有在集成测试中存在问题。 - David V. Corbin

2

ASP.NET Core 1.0答案

对于无参数构造函数,其他答案仍然适用,即如果您有一个具有无参数构造函数和一个带参数的构造函数的类,则会抛出问题中的异常。

如果您有两个带参数的构造函数,则行为是使用已知参数的第一个匹配的构造函数。您可以查看ConstructorMatcher类的源代码了解详细信息这里


1

Azure Functions .NET 7 Isolated

在Kévin Chalet的答案基础上,如果你正在使用Azure Functions隔离版,你可以调用GetService函数。

var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(s =>    {
    
    s.AddHttpClient(); 
    s.AddSingleton<DataLookup>(l => { 

        var dependency = l.GetService<IHttpClientFactory>();

        return new DataLookup(dependency);
    });
})
.Build();

0
我会说,最好使用IOptions或IConfiguration来注入配置类型的依赖项。这在大多数情况下都很方便,比如自动化测试、设计时使用和运行时。这可以极大地减少构造函数的数量。我倾向于在大多数情况下使用IConfiguration。而在服务接口的配置在测试、设计时和运行时之间的底层实现差异很大,并且相同的配置元素不共享的情况下,我会使用IOptions,因为每个情况都需要独特的选项和值。

0

ASP.NET Core答案

在他们修复/改进之前,我最终采用了以下解决方法。

首先,在控制器中仅声明一个构造函数(仅传递所需的配置设置),考虑到在构造函数中传递的设置对象可以为null(如果您在Startup方法中配置它们,则.NET Core会自动注入它们):

public class MyController : Controller
{
    public IDependencyService Service { get; set; }

    public MyController(IOptions<MySettings> settings)
    {
        if (settings!= null && settings.Value != null)
        {
            Service = new DependencyServiceImpl(settings.Value);
        }
    }
}

然后,在您的测试方法中,您可以以两种方式实例化控制器:

  1. 在构造测试对象时模拟IOptions对象
  2. 在所有参数中传递null,然后存根您将在测试中使用的依赖项。以下是一个示例:
[TestClass]
    public class MyControllerTests
    {
        Service.Controllers.MyController controller;
        Mock<IDependencyService> _serviceStub;

        [TestInitialize]
        public void Initialize()
        {
            _serviceStub = new Mock<IDependencyService>();
            controller = new Service.Controllers.MyController(null);
            controller.Service = _serviceStub.Object;
        }
    }

从这个点开始,您可以在.NET Core中使用依赖注入和模拟进行完整的测试。

希望能对您有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接