.Net Core 2.0中Controller和BaseController中的依赖注入重复问题

21

如果我在我的Asp.Net Core 2.0 Web应用程序中创建一个BaseController,封装了一些常见的依赖项,它们是否仍然需要在实际的控制器中使用?

例如,默认的MVC 6 Web应用程序中的标准账户(Account)和管理(Manage)控制器。

public class AccountController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IEmailSender _emailSender;
    private readonly ILogger _logger;

    public AccountController(
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ILogger<AccountController> logger)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _emailSender = emailSender;
        _logger = logger;
    }
   //rest of code removed
}

public class ManageController : Controller
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IEmailSender _emailSender;
    private readonly ILogger _logger;
    private readonly UrlEncoder _urlEncoder;

    private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";

    public ManageController(
      UserManager<ApplicationUser> userManager,
      SignInManager<ApplicationUser> signInManager,
      IEmailSender emailSender,
      ILogger<ManageController> logger,
      UrlEncoder urlEncoder)
    {
        _userManager = userManager;
        _signInManager = signInManager;
        _emailSender = emailSender;
        _logger = logger;
        _urlEncoder = urlEncoder;
    }
    // rest of code removed
}
在我正在构建的自定义Web应用程序模板中,我将Account Controller重构为三个不同的Controller:RegisterController(处理用户注册的所有内容),LoginController(处理登录和注销),以及第三个Controller负责余额。我将Manage Controller拆分为两个Controller:ManagePasswordController(与密码相关的所有内容)和UserManageController(其他所有内容)。
每个Controller的依赖注入要求有很多共同点,我想把它们放在一个BaseController中。看起来像这样?
public abstract class BaseController : Controller
{
    private readonly IConfiguration _config;
    private readonly IEmailSender _emailSender;
    private readonly ILogger _logger;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly UserManager<ApplicationUser> _userManager;

     protected BaseController(IConfiguration iconfiguration,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ILogger<ManageController> logger)
    {
        _config = iconfiguration;
        _userManager = userManager;
        _signInManager = signInManager;
        _emailSender = emailSender;
        _logger = logger;
    }
    //rest of code removed
}

但是似乎这样做没有任何作用?因为在我看来,我仍然必须注入所有东西。我可能错了(我对DI还很陌生),但BaseController应该允许我在BaseController和RegisterController之间不进行常见的DI。我错了吗?我该如何实现我想做的事情?

public class RegisterController : BaseController
{
    private const string ConfirmedRegistration = "User created a new account with password.";

    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;
    private readonly IEmailSender _emailSender;
    private readonly ILogger _logger;
    private readonly IConfiguration _config;

     public RegisterController(
        IConfiguration config,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ILogger<AccountController> logger) : base(config, userManager, signInManager, emailSender, logger)

    {
        _userManager = userManager;
        _signInManager = signInManager;
        _emailSender = emailSender;
        _logger = logger;
        _config = config;
    }
    //rest of code removed
}

更新

按照Rufo大人的建议

public abstract class BaseController : Controller
{
    protected UserManager<ApplicationUser> UserManager { get; }
    protected SignInManager<ApplicationUser> SignInManager { get; }
    protected IConfiguration Config { get; }
    protected IEmailSender EmailSender { get; }
    protected ILogger AppLogger { get; }

    protected BaseController(IConfiguration iconfiguration,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ILogger<ManageController> logger)
    {
        AppLogger = logger;
        EmailSender = emailSender;
        Config = iconfiguration;
        SignInManager = signInManager;
        UserManager = userManager; 
    }
}

继承的控制器

public class TestBaseController : BaseController
{

    public TestBaseController() : base()
    {

    }
}

这不起作用。 Resharper告诉我必须在TestBaseController构造函数中添加参数到基本构造函数调用中。

此外,BaseController在.NET Core 2.0中应该从Controller或ControllerBase继承?


1
如果BaseController要求您在构造函数调用中提供所有这些内容,那么是的。但是,您可以重构BaseController以不需要所有这些内容。另外,为什么在BaseController和其子类中都保存引用? - fredrik
1
通过在BaseController中使用受保护的属性发布注入的引用,以便从任何派生类中访问它们。 - Sir Rufo
是的,您建议创建一个 ControllerBase 类,并将 5 个常见的 DI 作为该类的受保护属性(基本上只需向当前的 BaseController 类添加 5 个受保护属性),以便我可以在其他创建的控制器中调用它们,例如 this.Logger、this.Configuration? - dinotom
在BaseController中声明: protected ILogger Logger { get; } 并在构造函数内部加入 *Logger = logger;*。只需将 private fields 替换为 protected properties,即可从每个派生类中访问它们。现在,派生类将具有一个空构造函数(不是无参数)。 - Sir Rufo
让我们在聊天中继续这个讨论。点击此处进入聊天室 - dinotom
显示剩余12条评论
3个回答

47

Microsoft.AspNetCore.MVC.Controller类带有扩展方法:

HttpContext.RequestServices.GetService<T>

每当HttpContext在管道中可用时都可以使用此方法(例如,如果从控制器的构造函数调用,则HttpContext属性将为空)。

尝试这种模式:

注意: 确保包含此指令 using Microsoft.Extensions.DependencyInjection;

基础控制器

public abstract class BaseController<T> : Controller where T: BaseController<T>
{

    private ILogger<T> _logger;

    protected ILogger<T> Logger => _logger ?? (_logger = HttpContext.RequestServices.GetService<ILogger<T>>());

子控制器

[Route("api/authors")]
public class AuthorsController : BaseController<AuthorsController>
{

    public AuthorsController(IAuthorRepository authorRepository)
    {
        _authorRepository = authorRepository;
    }

    [HttpGet("LogMessage")]
    public IActionResult LogMessage(string message)
    {
        Logger.LogInformation(message);

        return Ok($"The following message has been logged: '{message}'");
    }

毋庸置疑,记得在Startup.cs文件中的ConfigureServices方法中注册你的服务。

5
我必须写上 using Microsoft.Extensions.DependencyInjection; 才能使用通用的 GetService 方法。 - Leniel Maccaferri
2
这是我在基类和派生类控制器中需要的真正代码!该功能的特点是,代码不会污染派生控制器的构造函数以及像dbContext、logger和其他放置在BaseController中的东西。 - Lapenkov Vladimir

7

在MVC中使用BaseController的好理由很少。在这种情况下,基础控制器只会增加更多需要维护的代码,而没有真正的好处。

对于真正的横切关注点,在MVC中处理它们的最常见方法是使用全局过滤器,尽管在MVC核心中有一些值得考虑的新选项。

然而,你的问题看起来不太像是横切关注点,而更像是违反了单一职责原则。也就是说,如果一个控制器有超过3个注入依赖项,那么就是代码异味,说明你的控制器做了太多事情。最实际的解决方案是重构为聚合服务

在这种情况下,我认为您至少有一个隐含的服务需要明确 - 即,UserManagerSignInManager应该包装成自己的服务。从那里开始,您可以将其他3个依赖项潜在地注入该服务中(当然取决于它们的使用方式)。因此,您可能将其缩减为AccountControllerManageController的单个依赖项。
一些控制器做得太多的迹象:
  1. 有很多“helper”方法包含业务逻辑,在操作之间共享。
  2. 操作方法不仅仅是简单的HTTP请求/响应内容。操作方法通常只调用处理输入和/或生成输出并返回视图和响应代码的服务。
在这种情况下,值得一看的是是否可以将该逻辑移动到自己的服务中,并将任何共享逻辑移动到该服务的依赖项中等等。

11
然而,MVC 6的默认Web应用程序带有一个具有4个注入依赖项的AccountController和一个具有5个注入依赖项的ManageController。这就是这次练习的全部目的,即将通用的注入依赖项减少到一个基类中,以便新创建的控制器可以从基类中直接获取它们。我不是专业的程序员,只是试图学习Core 2.0以构建自己的Web应用程序模板作为学习机制。"你至少有一个隐式服务需要显式声明"的示例会很有帮助。 - dinotom

1
根据Calc和Sir Rufo的建议,这个方案可行。
 public abstract class BaseController : Controller
{
    protected UserManager<ApplicationUser> UserManager { get; }
    protected SignInManager<ApplicationUser> SignInManager { get; }
    protected IConfiguration Config { get; }
    protected IEmailSender EmailSender { get; }
    protected ILogger AppLogger { get; }

    protected BaseController(IConfiguration iconfiguration,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ILogger<ManageController> logger)
    {
        AppLogger = logger;
        EmailSender = emailSender;
        Config = iconfiguration;
        SignInManager = signInManager;
        UserManager = userManager; 
    }

    protected BaseController()
    {
    }
}

参数仍然需要注入到继承的控制器中,并传递给基础构造函数。
public class TestBaseController : BaseController
{
    public static IConfigurationRoot Configuration { get; set; }

    public TestBaseController(IConfiguration config,
        UserManager<ApplicationUser> userManager,
        SignInManager<ApplicationUser> signInManager,
        IEmailSender emailSender,
        ILogger<ManageController> logger) : base(config,userManager,signInManager,emailSender,logger)
    {
    }

    public string TestConfigGetter()
    {

        var t = Config["ConnectionStrings:DefaultConnection"];
        return t;
    }

    public class TestViewModel
    {
        public string ConnString { get; set; }
    }
    public IActionResult Index()
    {
        var tm = new TestViewModel { ConnString = TestConfigGetter() };
        return View(tm);
    }
}

现在所有注入的对象都将拥有实例。

希望最终解决方案不需要将常用实例注入到每个继承的控制器中,只需要为该特定控制器需要的任何其他实例对象进行注入。从代码重复方面,我真正解决的是每个控制器中私有字段的删除。

仍然在思考BaseController是否应该继承自Controller或ControllerBase?


1
这就是我们试图避免的。 - Sam

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