В первой части статьи были показаны настройки инъектора зависимостей для реализации условного внедрения зависимости с использованием механизмов Environment, Configuration, а также получение сервиса в рамках HTTP запроса, основываясь на данных запроса.
Во второй части вы увидите, как можно расширить возможности инъектора зависимостей на примере выбора необходимой реализации по идентификатору во время выполнения приложения.
Получение сервиса по идентификатору (Resolving service by ID)
Многие популярные IoC-фреймворки предоставляют примерно следующий функционал, позволяющий присваивать имена конкретным типам, реализующим интерфейсы
var container = new UnityContainer(); // да простят меня ненавистники Unity...
container.RegisterType<IService, LocalService>("local");
container.RegisterType<IService, CloudService>("cloud");
IService service;
if (context.IsLocal)
{
service = container.Resolve<IService>("local");
}
else
{
service = container.Resolve<IService>("cloud");
}
или так
public class LocalController
{
public LocalController([Dependency("local")] IService service)
{
this.service = service;
}
}
public class CloudController
{
public CloudController([Dependency("cloud")] IService service)
{
this.service = service;
}
}
Это позволяет выбирать нужную нам реализацию в зависимости от контекста.
Встроенный в ASP.NET Core инъектор зависимостей поддерживает множественную реализацию, но к сожалению, не имеет возможности присваивать идентификаторы для отдельной реализации. К счастью :) можно самим реализовать разрешение сервиса по идентификатору, написав немного кода.
Для начала создадим структуру, которая будет содержать в себе маппинг идентификаторов и соответствующих реализаций для каждого интерфеса.
Это будет словарь такого вида
Dictionary<Type, Dictionary<string, Type>>
Здесь ключом словаря будет тип интерфейса, а значением — словарь, в котором (прошу прощения за тавтологию) ключом будет идентификатор, а значением тип реализации интерфейса.
Добавлять сервисы в эту структуру будем следующим образом
private static readonly Dictionary<Type, Dictionary<string, Type>> ServiceNameMap =
new Dictionary<Type, Dictionary<string, Type>>();
public static void RegisterType(Type service, Type implementation, string name)
{
if (ServiceNameMap.ContainsKey(service))
{
var serviceNames = ServiceNameMap[service];
if (serviceNames.ContainsKey(name))
{
/* overwrite existing name implementation */
serviceNames[name] = implementation;
}
else
{
serviceNames.Add(name, implementation);
}
}
else
{
ServiceNameMap.Add(service, new Dictionary
{
[name] = implementation
});
}
}
И вот так получать сервис из контейнера (как вы помните из предыдущей статьи, IoC-контейнер в ASP.NET Core представлен интерфейсом IServiceProvider)
public static TService Resolve<TService>(IServiceProvider serviceProvider, string name)
where TService : class
{
var service = typeof(TService);
if (service.IsGenericType)
{
return ResolveGeneric<TService>(serviceProvider, name);
}
var serviceExists = ServiceNameMap.ContainsKey(service);
var nameExists = serviceExists && ServiceNameMap[service].ContainsKey(name);
/* Return `null` if there is no mapping for either service type or requested name */
if (!(serviceExists && nameExists))
{
return null;
}
return serviceProvider.GetService(ServiceNameMap[service][name]) as TService;
}
Теперь все, что остается сделать, это написать набор методов расширения для удобной настройки контейнера, например
public static IServiceCollection AddScoped<TService, TImplementation>(this IServiceCollection services, string name)
where TService : class
where TImplementation : class, TService
{
ServiceNamesMap.RegisterType<TService, TImplementation>(name);
return services.AddScoped<TImplementation>();
}
В приведенном выше коде, мы добавляем идентификатор в структуру соответсвия типов и имен, а также тип реализации просто в контейнер. Метод получения сервиса по идентификатору
public static TService GetService(this IServiceProvider serviceProvider, string name)
where TService : class
{
return ServiceNamesMap.Resolve(serviceProvider, name);
}
Все готово для исползьования
services.AddScoped<IService, LocalhostService>("local");
services.AddScoped<IService, CloudService>("cloud");
var service1 = this.serviceProvider.GetService<IService>("local"); // will resolve LocalhostService
var service2 = this.serviceProvider.GetService<IService>("cloud"); // will resolve CloudService
Можно пойти еще немного дальше и создать аттрибут, позволяющий производить инъекцию в параметр экшена, наподобие аттрибута MVC Core [FromServices] вот с таким синтаксисом
public IActionResult Local([FromServices("local")] IService service) { ... }
Для того, чтобы реализовать такой подход, нужно немного глубже разобраться в процессе Привязки модели (Model binding) в ASP.NET Core
Коротко говоря, аттрибут параметра определяет, какой ModelBinder (класс, реализующий интерфейс IModelBinder) будет создавать объект параметра. Например, аттрибут [FromServices], входящий в состав ASP.NET Core MVC, указывает на то, что для привязки модели будет использован IoC-контейнер, и следовательно для этого параметра будет использован класс ServicesModelBinder, который попытается получить тип параметра из IoC-контейнера.
В нашем случае, мы создадим два дополнительный класса. Первый — это ModelBinder, который будет получать сервис из IoC-контейнера по идентификатору, а второй — свой собственный аттрибут FromServices, который будет принимать в конструкторе идентификатор сервиса, и который будет указывать на то, что для привязки следует использовать определенный ModelBinder, который мы создали.
[AttributeUsage(AttributeTargets.Parameter)]
public class FromServicesAttribute : ModelBinderAttribute
{
public FromServicesAttribute(string serviceName)
{
this.ServiceName = serviceName;
this.BinderType = typeof(NamedServicesModelBinder);
}
public string ServiceName { get; set; }
public override BindingSource BindingSource => BindingSource.Services;
}
public class NamedServicesModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var serviceName = GetServiceName(bindingContext);
if (serviceName == null) return Task.FromResult(ModelBindingResult.Failed());
var serviceProvider = bindingContext.HttpContext.RequestServices;
var model = serviceProvider.GetService(bindingContext.ModelType, serviceName);
bindingContext.Model = model;
bindingContext.ValidationState[model] = new ValidationStateEntry { SuppressValidation = true };
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
private static string GetServiceName(ModelBindingContext bindingContext)
{
var parameter = (ControllerParameterDescriptor)bindingContext
.ActionContext
.ActionDescriptor
.Parameters
.FirstOrDefault(p => p.Name == bindingContext.FieldName);
var fromServicesAttribute = parameter
?.ParameterInfo
.GetCustomAttributes(typeof(FromServicesAttribute), false)
.FirstOrDefault() as FromServicesAttribute;
return fromServicesAttribute?.ServiceName;
}
}
На этом все :) Исходный код примеров можно скачать по ссылке:
github.com/izaruba/AspNetCoreDI
Автор: ivanzaruba