Pull to refresh
0

Условное внедрение зависимостей в ASP.NET Core. Часть 2

Reading time 6 min
Views 7.5K


В первой части статьи были показаны настройки инъектора зависимостей для реализации условного внедрения зависимости с использованием механизмов 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 инъектор зависимостей поддерживает множественную реализацию, но, к сожалению, не имеет возможности присваивать идентификаторы для отдельной реализации. К счастью :) можно самим реализовать разрешение сервиса по идентификатору, написав немного кода.

Одним из способов реализации этой функциональности было расширение класса ServiceDescriptor свойством ServiceName и использованием его для получения сервиса. Но после изучения исходных кодов стало понятно, что доступ к реализации ServiceProvider закрыт (у класса модификатор доступа internal), и поменять логику метода GetService нам не удастся.

Отказавшись от идеи использовать рефлексию, а также от написания собственного ServiceProvider'а, мы решили хранить структуру соответствия имени и типа сервиса непосредственно в контейнере, чтобы использовать её при получении сервиса. О том, как повлиять на логику получения сервиса, рассказано в первой части статьи.

Для начала создадим структуру, которая будет содержать в себе маппинг идентификаторов и соответствующих реализаций для каждого интерфейса.

Это будет словарь такого вида:

Dictionary<Type, Dictionary<string, Type>>

Здесь ключом словаря будет тип интерфейса, а значением — словарь, в котором (прошу прощения за тавтологию) ключом будет идентификатор, а значением — тип реализации интерфейса.

Добавлять сервисы в эту структуру будем следующим образом:

private readonly Dictionary<Type, Dictionary<string, Type>> serviceNameMap =
    new Dictionary<Type, Dictionary<string, Type>>();

public void RegisterType(Type service, Type implementation, string name)
{
    if (this.serviceNameMap.ContainsKey(service))
    {
        var serviceNames = ServiceNameMap[service];
        if (serviceNames.ContainsKey(name))
        {
            /* overwrite existing name implementation */
            serviceNames[name] = implementation;
        }
        else
        {
            serviceNames.Add(name, implementation);
        }
    }
    else
    {
        this.serviceNameMap.Add(service, new Dictionary<string, Type>
        {
            [name] = implementation
        });
    }
}

А вот так мы будем получать сервис из контейнера (как вы помните из предыдущей статьи, IoC-контейнер в ASP.NET Core представлен интерфейсом IServiceProvider):

public object Resolve(IServiceProvider serviceProvider, Type serviceType, string name)
{
    var service = serviceType;
    if (service.GetTypeInfo().IsGenericType)
    {
        return this.ResolveGeneric(serviceProvider, serviceType, name);
    }
    var serviceExists = this.serviceNameMap.ContainsKey(service);
    var nameExists = serviceExists && this.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(this.serviceNameMap[service][name]);
}

Теперь остается написать набор методов расширения для удобной настройки контейнера, например:

public static IServiceCollection AddScoped<TService, TImplementation>(this IServiceCollection services, string name)
    where TService : class
    where TImplementation : class, TService
    {
        return services.Add(typeof(TService), typeof(TImplementation), ServiceLifetime.Scoped, name);
    }

private static IServiceCollection Add(this IServiceCollection services, Type serviceType, Type implementationType, ServiceLifetime lifetime, string name)
{
    var namedServiceProvider = services.GetOrCreateNamedServiceProvider();

    namedServiceProvider.RegisterType(serviceType, implementationType, name);

    services.TryAddSingleton(namedServiceProvider);
    services.Add(new ServiceDescriptor(implementationType, implementationType, lifetime));

    return services;
}

private static NamedServiceProvider GetOrCreateNamedServiceProvider(this IServiceCollection services)
{
    return services.FirstOrDefault(descriptor => 
        descriptor.ServiceType == typeof(NamedServiceProvider))?.ImplementationInstance as NamedServiceProvider
        ?? new NamedServiceProvider();
}

В приведенном выше коде мы добавляем идентификатор в структуру соответствия типов и имен, а тип реализации просто помещаем в контейнер. Метод получения сервиса по идентификатору:

public static TService GetService<TService>(this IServiceProvider serviceProvider, string name)
    where TService : class
    {
        return serviceProvider
            .GetService<NamedServiceProvider>()
            .Resolve<TService>(serviceProvider, name);
    }

Всё готово для использования:

services.AddScoped<IService, LocalhostService>("local");
services.AddScoped<IService, CloudService>("cloud");

var service1 = this.serviceProvider.GetService<IService>("local"); // resolves LocalhostService
var service2 = this.serviceProvider.GetService<IService>("cloud"); // resolves CloudService

Можно пойти еще немного дальше и создать аттрибут, позволяющий производить инъекцию в параметр экшена, наподобие аттрибута MVC Core [FromServices] вот с таким синтаксисом:

public IActionResult Local([FromNamedServices("local")] IService service) { ... }

Для того, чтобы реализовать такой подход, нужно немного глубже разобраться в процессе Привязки модели (Model binding) в ASP.NET Core.

Коротко говоря, аттрибут параметра определяет, какой ModelBinder (класс, реализующий интерфейс IModelBinder) будет создавать объект параметра. Например, аттрибут [FromServices], входящий в состав ASP.NET Core MVC, указывает на то, что для привязки модели будет использован IoC-контейнер, следовательно, для этого параметра будет использован класс ServicesModelBinder, который попытается получить тип параметра из IoC-контейнера.

В нашем случае, мы создадим два дополнительный класса. Первый — это ModelBinder, который будет получать сервис из IoC-контейнера по идентификатору, а второй — свой собственный аттрибут FromNamedServices, который будет принимать в конструкторе идентификатор сервиса, и который будет указывать на то, что для привязки следует использовать определенный ModelBinder, который мы создали.

[AttributeUsage(AttributeTargets.Parameter)]
public class FromNamedServicesAttribute : ModelBinderAttribute
{
    public FromNamedServicesAttribute(string serviceName)
    {
        this.ServiceName = serviceName;
        this.BinderType = typeof(NamedServicesModelBinder);
    }
    public string ServiceName { get; set; }
    public override BindingSource BindingSource => BindingSource.Services;
}

public class NamedServicesModelBinder : IModelBinder
{
    private readonly IServiceProvider serviceProvider;

    public NamedServicesModelBinder(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    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 model = this.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/nix-user/AspNetCoreDI
Tags:
Hubs:
+14
Comments 23
Comments Comments 23

Articles

Information

Website
www.nixsolutions.com
Registered
Founded
1994
Employees
1,001–5,000 employees