Моя логика контроллера вызывает статический метод ServiceProviderServiceExtensions GetServices (этот поставщик IServiceProvider)
Здесь все начинает идти не так, как надо.Вызов GetServices<T>
из вашего контроллера - это применение антишаблона Service Locator .Все проблемы, которые у вас возникли, происходят из-за этого неправильного использования.
Вместо этого вы должны:
- Использовать только конструктор-инъекцию
- Никогда не внедрять
IServiceProvider
другой абстракции, представляющейконтейнер в конструкторе любого класса вне вашего корня композиции
Так что это означает, что вы должны иметь конструктор следующим образом:
public Controller1(IProvider provider)
Ваш IProvider
неоднозначно, но давайте на минутку предположим, что это нормально.Однако не все в порядке, если код приложения справляется с этой неоднозначностью.Вместо этого вам следует обрабатывать эту двусмысленность исключительно в корне композиции, что можно сделать следующим образом:
services.AddSingleton<CustomProvider1>();
services.AddTransient<Controller1>(c => new Controller1(
c.GetRequiredService<CustomProvider1>()));
services.AddSingleton<CustomProvider2>();
services.AddTransient<Controller2>(c => new Controller2(
c.GetRequiredService<CustomProvider2>()));
Обратите внимание, что по умолчанию ASP.NET Core MVC не разрешает контроллеры из контейнера DI (что действительно очень странно по умолчанию).Таким образом, чтобы заставить MVC использовать DI-контейнер для разрешения контроллеров и, таким образом, использовать вышеуказанные регистрации, вам необходимо добавить следующий код:
services.AddMvc()
.AddControllersAsServices();
Вышеуказанная регистрация работает только в том случае, если эти контроллеры имеют толькоодна зависимость, потому что этот метод эффективно отключает автоматическое подключение.В случае, если у класса больше зависимостей, более поддерживаемой конструкцией будет следующее:
services.AddTransient<Controller1>(c =>
ActivatorUtilities.CreateInstance<Controller1>(
c,
c.GetRequiredService<CustomProvider1>()));
services.AddTransient<Controller2>(c =>
ActivatorUtilities.CreateInstance<Controller2>(
c,
c.GetRequiredService<CustomProvider1>()));
. Используется класс ActivatorUtilities
.NET Core, который позволяет классу автоматически подключаться при передачев некоторых зависимостях.
Обратите внимание, что ActivatorUtilities
имеет определенные недостатки, такие как невозможность обнаружения циклических зависимостей.Вместо этого будет выдано исключение переполнения стека (гадость).
Но ... как я уже отмечал ранее, ваша абстракция IProvider
неоднозначна, поскольку существует две реализации, а потребителям требуется другая реализация.Хотя это само по себе неплохо, вы всегда должны проверять, не нарушаете ли вы принцип замены Лискова .
Вы можете проверить, нарушаете ли выLSP, меняя местами две реализации.Поэтому спросите себя: что происходит с Controller1
, когда ему вводят CustomProvider2
, и что будет с Controller2
, когда ему вводят CustomProvider1
.Если ответ заключается в том, что они перестанут работать, это означает, что вы нарушаете LSP, и это является проблемой проектирования.
Если контроллер выходит из строя, это означает, что обе реализации ведут себя очень по-разному, в то время как потребители должны быть в состоянии предположить, чтовсе реализации ведут себя в соответствии с их абстракцией.Нарушение LSP означает добавление сложности.
Когда вы определили, что нарушает LSP в этом случае, решение состоит в том, чтобы дать каждой реализации собственную абстракцию:
interface IProvider1 { }
interface IProvider2 { }
Независимо от того, имеют ли эти два интерфейса одинаковую сигнатуру, не имеет значения, потому что нарушение LSP сигнализирует о том, что оба интерфейса на самом деле очень различаются по поведению, потому что замена реализаций нарушает их клиентов.
Обратите внимание, что даже еслипрямой потребитель продолжает работать, это может означать, что приложение начинает работать неправильно, когда реализации меняются местами.Это не значит, что вы нарушаете SRP.Например, когда provider1 регистрируется на диске, а provider2 регистрируется в базе данных, ожидаемое поведение заключается в том, что вызов controller1 приведет к добавлению журнала на диск.Поэтому, наоборот, это не то, чего вы хотите достичь, а то, что вы хотите настроить в своем корне композиции.Это не является признаком нарушения LSP;в этом случае договор все еще ведет себя так, как ожидают потребители.
Если обмен реализациями не оказывает заметного влияния на их потребителей, вы не нарушаете LSP, и это означает, что данные регистрации являются подходящим способом.Или вы можете упростить ситуацию, начав нам «настоящий» DI-контейнер; -)