Встраивание конструктора базового контроллера в ASP.NET MVC с Unity

У меня есть базовый контроллер в моем проекте MVC 5, который реализует некоторые общие функции. Эта функциональность требует определенных зависимостей. Я использую Unity 3, чтобы внедрить эти реализации в мои контроллеры, шаблон, который работал нормально, пока я не переключил свои контроллеры на наследование с этого базового контроллера. Теперь я столкнулся с следующей проблемой:

public class BaseController : Controller
{
    private readonly IService _service;

    public BaseController(IService service)
    {
        _service = service;
    }
}

public class ChildController : BaseController
{
    private readonly IDifferentService _differentService;

    public ChildController(IDifferentService differentService)
    {
        _differentService = differentService;
    }
}

'BaseController' does not contain a constructor that takes 0 arguments что ошибка 'BaseController' does not contain a constructor that takes 0 arguments. Unity не разрешает построение BaseController, поэтому он не может вставлять в него зависимостей. Я вижу два очевидных способа решения этой проблемы:

1.) Явным образом вызовите BaseController ctor и попросите каждого ChildController ctor ввести зависимости BaseController

public class ChildController : BaseController
{
    private readonly IDifferentService _differentService;

    public ChildController(IDifferentService differentService,
                           IService baseService)
        : base(baseService)
    {
        _differentService = differentService;
    }
}

Мне не нравится этот подход по нескольким причинам: один из них, потому что ChildControllers не используют дополнительные зависимости (поэтому он вызывает раздутие конструктора в дочерних контроллерах без причины) и, что более важно, если я когда-либо меняю подпись конструктора базового контроллера, я должен изменить сигнатуры конструктора каждого дочернего контроллера.

2.) Внедрение зависимостей BaseController посредством вставки свойств

public class BaseController : Controller
{
    [Dependency]
    public IService Service { get; set; }

    public BaseController() { }
}

Мне нравится этот подход лучше - я не использую никаких зависимостей в коде конструктора BaseController, но это делает стратегию инъекций зависимостей кода непоследовательной, что тоже не идеально.

Вероятно, есть еще лучший подход, который включает в себя какую-то функцию разрешения зависимостей BaseController, которая требует контейнера Unity для сортировки сигнатуры метода ctor, но прежде чем я начну писать что-либо слишком, мне было интересно, разрешил ли кто-нибудь эту проблему раньше? Я нашел несколько решений, плавающих в Интернете, но они были обходными решениями, такими как Service Locator, которые я не хочу использовать.

Благодарю!

Ответ 1

Первое, что вам нужно понять, это то, что вы не создаете базовый контроллер. Вы создаете дочерний контроллер, который наследует интерфейс и функциональность базового контроллера. Это важное различие. Когда вы говорите: "ChildControllers не используют дополнительные зависимости", то вы абсолютно ошибаетесь. Поскольку ChildController также является базовым контроллером. Создано не два разных класса. Только один класс, который реализует обе функции.

Итак, поскольку ChildController IS BaseController, нет ничего неправильного или странного в передаче параметров в конструкторе дочерних контроллеров, который вызывает конструктор базовых классов. Так оно и должно быть сделано.

Если вы измените базовый класс, вам, вероятно, придется менять свои дочерние классы. Невозможно использовать инсталляцию конструктора для ввода зависимостей базового класса, которые не включены в дочерний класс.

Я не рекомендую вложение свойств, так как это означает, что ваши объекты могут быть созданы без правильной инициализации, и вы должны помнить, что их правильно настроить.

BTW, правильными терминами являются подкласс и суперкласс. "Ребенок" является подклассом, родителем является "суперкласс".

Ответ 2

С ASP.Net 5 и он построен в DI

public class BaseController : Controller
{
    protected ISomeType SomeMember { get; set; }

    public BaseController(IServiceProvider serviceProvider)
    {
        //Init all properties you need
        SomeMember = (SomeMember)serviceProvider.GetService(typeof(ISomeMember));
    }
}

public class MyController : BaseController  
{
public MyController(/*Any other injections goes here*/, 
                      IServiceProvider serviceProvider) : 
 base(serviceProvider)
{}
}

ОБНОВИТЬ

Существует также метод расширения в Microsoft.Extensions.DependencyInjection, чтобы сделать его короче

SomeMember = serviceProvider.GetRequiredService<ISomeMember>();

Ответ 3

Я принял несколько иной (но для меня, довольно очевидный и, вероятно, общий) подход. Это работает для меня, но могут быть ошибки, о которых я не знаю.

Я создал общие свойства сервиса в моем классе BaseController, который используется большинством моих контроллеров. Они получают экземпляр, когда они необходимы/указаны.

Если есть услуга, которая требуется конкретному контроллеру, которая меньше используется, я ввожу ее в конструктор контроллера, как обычно.

using JIS.Context;
using JIS.Managers;
using JIS.Models;
using JIS.Services;
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;

namespace JIS.Controllers
{
    public class BaseController : Controller
    {
        public BaseController()
        {
            ViewBag.User = UserManager?.User;
        }

        private IUserManager userManager;
        public IUserManager UserManager
        {
            get
            {
                if (userManager == null)
                {
                    userManager = DependencyResolver.Current.GetService<IUserManager>();
                }
                return userManager;
            }
            set
            {
                userManager = value;
            }
        }

        private ILoggingService loggingService;
        public ILoggingService LoggingService
        {
            get
            {
                if (loggingService == null)
                {
                    loggingService = DependencyResolver.Current.GetService<ILoggingService>();
                }
                return loggingService;
            }
            set { loggingService = value; }
        }

        private IUserDirectory userDirectory;
        public IUserDirectory UserDirectory
        {
            get
            {
                if (userDirectory == null)
                {
                    userDirectory = DependencyResolver.Current.GetService<IUserDirectory>();
                }
                return userDirectory;
            }
            set { userDirectory = value; }
        }

        private ApplicationDbContext appDb;
        public ApplicationDbContext AppDb
        {
            get
            {
                if (appDb == null)
                {
                    appDb = new ApplicationDbContext();
                }
                return appDb;
            }
            set
            {
                appDb = value;
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && appDb != null)
            {
                appDb.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

В моем контроллере я просто подкласс из этого BaseController:

public class UsersController : BaseController
{
    // and any other services I need are here:
    private readonly IAnotherService svc;
    public UsersController(IAnotherService svc)
    {
        this.svc = svc;
    }
...
}

Таким образом, общие службы генерируются "на лету", когда они необходимы, и доступны для моих контроллеров без большого количества шаблонов.