如何避免使用 ASP.NET Core 依赖项注入的大量 [FromService] 参数?

Kei*_*ith 1 c# dependency-injection asp.net-core

我有一个使用大量依赖注入的 ASP.NET Core 项目。

问题是这些开始堆积在我的控制器动作上:

public async Task LoginAsync(
    [FromBody] LoginModel login,
    [FromServices] IConnectionMultiplexer redis,
    [FromServices] ISerialiserFactory serialiser,
    [FromServices] IDataService dataService,
    [FromServices] ILookupNormalizer normaliser,
    [FromServices] IPasswordHasher hasher,
    ...
Run Code Online (Sandbox Code Playgroud)

我可以将它们放在构造函数中,但大多数方法不使用它们,而那些使用它们的方法并不总是使用所有这些。

我可以直接实例化它们,但是我失去了在启动时注入它们的能力。

有没有更简单的方法来获得这些注入的服务?理想情况下,我想调用类似的东西:

// It turns out I need the injected serialiser
var serialiser = services.Get<ISerialiserFactory>();
Run Code Online (Sandbox Code Playgroud)

有没有办法在 ASP.NET Core 中做到这一点?

Tse*_*eng 6

正如评论中指出的那样,如果您在单个控制器操作中拥有如此多的依赖项,那么这是对抽象错误的代码的一个很好的感叹:您的控制器做得比它应该做的要多。

理想情况下,控制器动作每个动作应该只有几行代码(经验法则,10-15 行代码)。如果你有更多,你可能在里面做了很多。

控制器操作应该只接受来自用户(表单或 WebApi 风格)的输入,验证它并将其委托给服务以及处理 http 状态代码。

IE

public class LoginService : ILoginService
{
    public IConnectionMultiplexer redis,
    public ISerialiserFactory serialiser,
    public IDataService dataService,
    public ILookupNormalizer normaliser,
    public IPasswordHasher hasher

    public LoginService(/* inject your services here */) 
    {

    }

    public async Task<bool> Login(LoginModel login) 
    {
        // Do your logic here and perform the login

        return /*true or false*/;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后将其注入您的控制器或您的操作:

[HttpPost]
public async Task<IActionResult> LoginAsync([FromBody]LoginModel login, [FromServices]ILoginService loginService) 
{
    // Validate input, only rough validation. No business validation here
    if(!Model.IsValid) 
    {
        return BadRequest(Model);
    }

    bool success = await loginService.Login(model);

    if(success) 
    {
        return RedirectTo("Login");
    }

    return Unauthorized();
}
Run Code Online (Sandbox Code Playgroud)

如果你得到更多的代码,那就是代码异味。特别是如果你做一些逻辑等。你的控制器应该尽可能薄。控制器很难测试(与ILoginService我的示例相比)。

您永远不必new LoginService(...)在任何时候调用(除非您创建了一个抽象工厂)。

此外,您应该始终更喜欢使用构造函数注入。[FromServices]仅在一个操作中需要服务时使用。如果在多个操作中需要它,请始终使用构造函数注入

public LoginController : Controller
{
    public ILoginService loginService;

    public LoginController(ILoginService loginService)
    {
        if(loginService==null)
            throw new ArgumentNullException(nameof(loginService));

        this.loginService = loginService
    }

    public async Task<IActionResult> LoginAsync([FromBody]LoginModel login)
    {
        // Do your stuff from above
        ...
        bool success = await loginService.Login(login);
        ...
    }
}
Run Code Online (Sandbox Code Playgroud)

也没有问题,如果依赖项有不同的生命周期,只要主对象的生命周期比它的依赖项的生命周期短。

即,如果您的上述依赖项之一是有范围的,那么您也ILoginService必须是有范围的。它将在请求结束时处理。

services.AddSingleton<ISerialiserFactory, ...>();
services.AddSingleton<IConnectionMultiplexer, ...>();
services.AddScoped<IDataService, ...>();
services.AddScoped<ILookupNormalizer, ...>();
services.AddScoped<IPasswordHasher, ...>();
services.AddScoped<ILoginService, LoginService>();
Run Code Online (Sandbox Code Playgroud)

那会工作得很好。

services.AddSingleton<ISerialiserFactory, ...>();
services.AddSingleton<IConnectionMultiplexer, ...>();
services.AddScoped<IDataService, ...>();
services.AddScoped<ILookupNormalizer, ...>();
services.AddScoped<IPasswordHasher, ...>();

// This will create trouble
services.AddSingleton<ILoginService, LoginService>();
Run Code Online (Sandbox Code Playgroud)

但这不会。现在, ILoginService 将是单例的,但它的依赖项将在第一个请求后被释放。后续请求会在调用IDataService或IPasswordHasher...“xyz 已被处理”时触发异常。