将 IServiceProvider 注入 API 控制器或单个服务

Inject IServiceProvider into an API controller or Individual Services

我刚从一个地方来,那里的 API 控制器只需要注入它需要的服务...

[ApiController]
public class SomeController : ControllerBase
{
    private IFirstService firstService
    private ISecondService secondService
    public SomeController(IFirstService firstService, ISecondService secondService)
    {
        this.firstService = firstService;
        this.secondService = secondService;
    }

    [HttpGet]
    public IActionResult SomeMethod()
    {
        var data = firstService.GetSomething();
        return OkObjectResult(data);
    }
}

现在我发现自己在一家做这个的商店里......

[ApiController]
public class SomeController : ControllerBase
{
    private IServiceProvider services;
    public SomeController(IServiceProvider services)
    {
        this.services = services;
    }

    [HttpGet]
    public IActionResult SomeMethod()
    {
        var service = servies.Get<IFirstService>();
        if(service is null)
        {
            //...
        }
        var data = firstService.GetSomething();
        return OkObjectResult(data);
    }
}

现在,我无法真正解释原因,但这似乎 是错误的。

我只是经历了 StuckInMyWaysitis 还是这真的是我的骨头告诉我的不良做法?或者,实际上是否有更广泛接受的方式来做“正确”的事情?

这是错误的,因为这意味着每次需要服务时都必须明确请求它,然后检查实例是否为空。这是没有任何好处的不必要的代码重复。

这也违反了explicit dependencies principle, which Microsoft recommends you use to architect your code

几乎可以肯定这是因为有人无法弄清楚 DI 是如何工作的,或者他们忘记注册服务并且无法正确修复它,所以他们只是放弃了 IServiceProvider 而不是,这最终奏效了,然后他们到处都对它进行了货物崇拜。也就是说,懒and/or无知。

当您尝试使用显式依赖项修复此问题时,您可能会遇到阻力。诀窍是让提倡这种混乱的人解释为什么这种混乱比遵循良好的体系结构实践更好,尤其是来自 Microsoft 的那些。

当您编程时间足够长时,您就会学会相信自己的直觉。如果感觉不好,几乎总是这样。

注入 IServiceProvider 实现服务定位器模式,该模式通常被认为是反模式。

在您的第一个示例中,注入了两个服务。你可以很容易地分辨出控制器依赖什么。当我们看到注入了 5、10 或 20 个依赖项时,更容易判断 class 是否开始依赖太多东西。当发生这种情况时,我们通常会进行重构,因为依赖项的数量表明 class 正在做太多事情。

在第二个示例中,我们无法从注入的依赖项 (IServiceProvider) 中分辨出 class 依赖什么。唯一的判断方法是查看 class 中对 services 的每一次使用,看看从中得到了什么解决。 class 最终可能依赖于许多其他 classes,即使我们在构造函数中只看到一个依赖项。

这也让单元测试变得更加困难。在第一个示例中,我们可能必须为一个或两个服务创建伪造或模拟。在第二个示例中,我们必须模拟 IServiceProvider 到 return 模拟或创建一个 IServiceCollection,将模拟注册为服务实现,然后从中构建一个 ServiceProvider .两者都使测试更加复杂。

有些人认为 API 控制器是一个例外,让它们依赖于服务定位器之类的东西是可以的。 (MediatR 是一个常见的例子。)这是一个意见:只要控制器很少或没有逻辑并且仅用于将 HTTP 请求路由到某些更高级别的代码,它就不错。

如果我们使用 MediatR 或类似 ICommandHandler<TCommand> 的一些类似抽象,那么至少我们已经限制了 class 向处理程序提交查询或命令。它不像注入 IServiceProvider 那样糟糕,它允许 class 解析任何已注册的服务。

首先,让我们重构第二个代码以消除一些代码味道,

[ApiController]
public class SomeController : ControllerBase
{
    private IFirstService firstService
    private ISecondService secondService
    private IServiceProvider services;

    public SomeController(IServiceProvider services)
    {
        this.services = services;
        this.firstService= servies.Get<IFirstService>();
        this.secondService= servies.Get<ISecondService>();

    }

    [HttpGet]
    public IActionResult SomeMethod()
    {
        var data = firstService.GetSomething();
        return OkObjectResult(data);
    }
}

为什么?

  1. 您会自动摆脱所有检查,现在您可以根据需要在构造函数中执行此操作。
  2. 如果许多方法都需要实例,那么所有方法都可能有这样的重复代码。
  3. 它违反了 SRP,因为这些方法做的比他们应该做的多。

现在,如果我们看它更接近您的第一个代码。有一个区别,实例化服务与注入服务。这个 IMO 有一些问题,

  1. DI 容器是工具,它们不属于我们的领域。通过 IServiceProvider,我们正在尝试为他们提供服务。这意味着我们总是需要一些 DI 提供程序。

  2. 其次,这也隐藏了我们的依赖关系,这使得集成 难的。构造函数就像信使,清楚地告诉我们 在我们实例化一个之前,我们需要事先准备好什么 Class。如果我们隐藏此信息,您可能不知道是否某些 在没有 运行 应用程序的情况下是否配置了依赖项。 在构造函数中明确定义依赖关系,我们不能跳过 这部分。

  3. 此外,就像我们的方法中有重复代码一样,现在我们在不同服务的构造函数中也有重复代码。每个服务都将调用这些 Get 方法。那么为什么不在一个地方做呢?如果您考虑到这一点并进行重构,您会自动找到第一个示例。

[ApiController]
public class SomeController : ControllerBase
{
    private IFirstService firstService
    private ISecondService secondService
    public SomeController(IFirstService firstService, ISecondService secondService)
    {
        this.firstService = firstService;
        this.secondService = secondService;
    }
}

public class Startup()
{
   public void Start()
   {
     //.... 
     //....
     var service1 = servies.Get<IFirstService>();
     var service2 = servies.Get<IFirstService>();
     SomeController= new Controller(service1,service2); 
     //or just servies.Get<SomeController>();
   } 
} 

如果您使用像 AutoFac 这样的容器,这就是实例化发生的方式。