Я разрабатываю SDK и использую DI (я думаю неправильно) для своих классов и их зависимостей. Кажется, я перепутал DI и сервисный локатор. Мои проблемы:
Кажется, это DI через Builder эквивалентен анти-шаблону локатора сервисов? то, что мне нужно, но на него нет ответа.
Я определяю свой контейнер с помощью .NET DI:
public class ServiceProvider
{
private Microsoft.Extensions.DependencyInjection.ServiceProvider serviceProvider;
protected readonly IServiceCollection serviceCollection = new ServiceCollection();
public ServiceProvider()
{
serviceCollection.AddScoped<IScopedService, ScopedService>();
serviceCollection.AddSingleton<ISingletonService, SingletonService>();
}
}
Теперь, когда ScopedService зависит от SingletonService, это делается с помощью правильного внедрения конструктора:
public ScopedService(ISingletonService singletonService)
{
_singletonService = singletonService
}
Но классы потребления (конечного пользователя) выполняются через локатор службы, например.
private IScopedService _scopedService;
private IScopedService ScopedService
{
get
{
return _scopedService ?? _scopedService = serviceProvider.GetService<IScopedService>();
}
}
Все мои потребляющие классы наследуются от базового класса, который, я считаю, создает новый экземпляр ServiceProvider каждый раз, когда создается экземпляр класса.
public abstract class ComponentBase
{
protected internal ServiceProvider serviceProvider = new ServiceProvider();
}
Проблема: создается несколько объектов Singleton
var singleton1 = consumingClass.serviceProvider.GetService<ISingletonService>();
var singleton2 = consumingClass.serviceProvider.GetService<ISingletonService>();
Assert.AreEqual(singleton1, singleton2) // Test failure
Я ожидаю, что службы Singleton будут одним и тем же экземпляром. Должен ли я сделать ServiceProvider в базовом классе статическим? Это решило бы проблему, но также, похоже, еще больше склоняется к анти-паттерну локатора сервисов.
Стоит ли переходить на фабричный шаблон? Где я регистрирую все потребляющие классы в поставщике услуг, и только фабрика знает о контейнере DI? Вы бы тогда получили класс, вызвав геттера на фабрике?
Я бы написал, что каждый класс зависит от того, от чего он должен зависеть. Если вы это сделаете, почти всегда есть способ настроить ваш контейнер для предоставления этих зависимостей. Обычно это легко. Если это сложно, то это проблема, которую нужно решить. Ваши классы будут написаны правильно. Решайте проблему там, где она есть, а не позволяя ей просачиваться в ваши классы.
Спасибо @scotthannen, это очень полезная статья.
Пожалуйста, не делайте больше работы для других людей, искажая свои посты. Размещая на Stack Overflow, вы предоставляете SO безотзывное право в соответствии с Лицензия CC BY-SA 3.0 распространять этот контент. В соответствии с политикой SO любой вандализм будет пресекаться. Если вы хотите узнать больше об удалении сообщения, прочитайте дополнительную информацию на странице Как работает удаление?.
Я бы посоветовал не иметь свой собственный IServiceProvider
в SDK, подобный тому, который был указан в связанной статье.
Вместо этого предоставьте точку расширения для вашего SDK, чтобы подключиться к DI потребителя.
public static MySDKServiceCollectionExtensions {
public static IServiceCollection AddMySDK(this IServiceCollection services) {
services.AddScoped<IScopedService, ScopedService>();
services.AddSingleton<ISingletonService, SingletonService>();
//...additional services.
return services
}
}
Если вашим потребляющим компонентам нужен доступ к этим службам, используйте явный принцип зависимости через внедрение конструктора.
public abstract class ComponentBase {
protected ISingletonService singleton;
public ComponentBase(ISingletonService singleton) {
this.singleton = singleton;
}
}
Наличие IServiceProvider
в вашем SDK рассматривается как проблема реализации, которая не должна иметь значения для вашего SDK.
A DI Container should only be referenced from the Composition Root. All other modules should have no reference to the container.
Потребляющий проект/приложение (конечный пользователь) добавит ваш SDK в корень своей композиции.
//...
IServiceCollection serviceCollection = new ServiceCollection();
//...Add SDK
serviceCollection.AddMySDK();
//Add my services.
serviceCollection.AddScoped<IConsumer, Consumer>()
//...
и использовать его соответственно по мере необходимости
public class Consumer: IConsumer {
protected internal IScopedService service;
protected Consumer(IScopedService service) {
this.service = service
}
private IScopedService ScopedService {
get {
return service;
}
}
//...
}
Мне очень нравится этот ответ. Метод расширения для добавления моих сервисов в сервисы конечных пользователей очень удобен. Однако у меня есть вопрос: таким образом, мне нужно будет регистрировать мои конечные пользовательские/потребляющие классы в качестве служб в ServiceCollection? И получить с var component = services.GetService<IConsumer>();
?
@JohnCurran да, если вы хотите, чтобы контейнер выполнял всю тяжелую работу по разрешению всего.
@JohnCurran, но это не должно тебя беспокоить. Они зарегистрируют свои классы по желанию
В то время как IMO бывают случаи, когда что-то, напоминающее локатор службы, является меньшим из двух зол, прямое внедрение контейнера в класс определенно является анти-шаблоном, потому что зависимость становится неясной. Класс может запросить что-нибудь из этого контейнера. Он создает неопределенную, открытую зависимость. Вот статья (мой), который показывает пример, где что-то вроде локатора сервисов может быть полезным и как свести к минимуму его зло.