Как внедрить зависимость следующего обработчика в цепочке ответственности?

В моем текущем проекте я использую довольно много шаблонов цепочки ответственности.

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

Учитывая эту модель:

public interface IChainOfResponsibility 
{
    IChainOfResponsibility Next { get; }
    void Handle(Foo foo);
}

public class HandlerOne : IChainOfResponsibility 
{
    private DbContext _dbContext;

    public HandlerOne(IChainOfResponsibility next, DbContext dbContext)
    {
        Next = next;
        _dbContext = dbContext;
    }

    public IChainOfResponsibility Next { get; }

    public void Handle(Foo foo) { /*...*/}
}

public class HandlerTwo : IChainOfResponsibility 
{
    private DbContext _dbContext;

    public HandlerTwo(IChainOfResponsibility next, DbContext dbContext)
    {
        Next = next;
        _dbContext = dbContext;
    }

    public IChainOfResponsibility Next { get; }

    public void Handle(Foo foo) { /*...*/}
}

Мой стартап становится:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IChainOfResponsibility>(x => 
        new HandlerOne(x.GetRequiredService<HandlerTwo>(), x.GetRequiredService<DbContext>())
    );

    services.AddTransient(x => 
        new HandlerTwo(null, x.GetRequiredService<DbContext>())
    );
}

Как настроить мою цепочку ответственности более четко?


person Ortiga    schedule 02.04.2019    source источник


Ответы (2)


Я взломал простое решение, так как не смог найти ничего, что делало бы то, что я хотел. Он работает нормально, так как использует IServiceProvider.GetRequiredService для разрешения всех зависимостей конструктора всех обработчиков цепочки.

Мой класс запуска становится:

public void ConfigureServices(IServiceCollection services)
{
    services.Chain<IChainOfResponsibility>()
        .Add<HandlerOne>()
        .Add<HandlerTwo>()
        .Configure();
}

Что я делаю, так это динамически генерирую лямбду в вопросе с помощью Expression. Затем он компилируется и регистрируется в файле IServiceCollection.AddTransient.

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

Вот код, который творит чудеса:

public static class ChainConfigurator
{
    public static IChainConfigurator<T> Chain<T>(this IServiceCollection services) where T : class
    {
        return new ChainConfiguratorImpl<T>(services);
    }

    public interface IChainConfigurator<T>
    {
        IChainConfigurator<T> Add<TImplementation>() where TImplementation : T;
        void Configure();
    }

    private class ChainConfiguratorImpl<T> : IChainConfigurator<T> where T : class
    {
        private readonly IServiceCollection _services;
        private List<Type> _types;
        private Type _interfaceType;

        public ChainConfiguratorImpl(IServiceCollection services)
        {
            _services = services;
            _types = new List<Type>();
            _interfaceType = typeof(T);
        }

        public IChainConfigurator<T> Add<TImplementation>() where TImplementation : T
        {
            var type = typeof(TImplementation);

            _types.Add(type);

            return this;
        }

        public void Configure()
        {
            if (_types.Count == 0)
                throw new InvalidOperationException($"No implementation defined for {_interfaceType.Name}");

            foreach (var type in _types)
            {
                ConfigureType(type);
            }
        }

        private void ConfigureType(Type currentType)
        {
            // gets the next type, as that will be injected in the current type
            var nextType = _types.SkipWhile(x => x != currentType).SkipWhile(x => x == currentType).FirstOrDefault();

            // Makes a parameter expression, that is the IServiceProvider x 
            var parameter = Expression.Parameter(typeof(IServiceProvider), "x");

            // get constructor with highest number of parameters. Ideally, there should be only 1 constructor, but better be safe.
            var ctor = currentType.GetConstructors().OrderByDescending(x => x.GetParameters().Count()).First();

            // for each parameter in the constructor
            var ctorParameters = ctor.GetParameters().Select(p =>
            {
                // check if it implements the interface. That's how we find which parameter to inject the next handler.
                if (_interfaceType.IsAssignableFrom(p.ParameterType))
                {
                    if (nextType is null)
                    {
                        // if there's no next type, current type is the last in the chain, so it just receives null
                        return Expression.Constant(null, _interfaceType);
                    }
                    else
                    {
                        // if there is, then we call IServiceProvider.GetRequiredService to resolve next type for us
                        return Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { nextType }, parameter);
                    }
                }
                
                // this is a parameter we don't care about, so we just ask GetRequiredService to resolve it for us 
                return (Expression)Expression.Call(typeof(ServiceProviderServiceExtensions), "GetRequiredService", new Type[] { p.ParameterType }, parameter);
            });

            // cool, we have all of our constructors parameters set, so we build a "new" expression to invoke it.
            var body = Expression.New(ctor, ctorParameters.ToArray());
            
            // if current type is the first in our list, then we register it by the interface, otherwise by the concrete type
            var first = _types[0] == currentType;
            var resolveType = first ? _interfaceType : currentType;
            var expressionType = Expression.GetFuncType(typeof(IServiceProvider), resolveType);

            // finally, we can build our expression
            var expression = Expression.Lambda(expressionType, body, parameter);

            // compile it
            var compiledExpression = (Func<IServiceProvider, object>)expression.Compile();

            // and register it in the services collection as transient
            _services.AddTransient(resolveType, compiledExpression );
        }
    }
}

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

person Ortiga    schedule 02.04.2019
comment
Func‹IServiceProvider, объект› x = (Func‹IServiceProvider, объект›)expression.Compile(); Вы используете x в выражении linq, поэтому измените имя переменной - person Gourav Garg; 29.04.2020
comment
@GouravGarg отредактировано, спасибо! Оглядываясь назад, поскольку я писал это больше года, я мог бы еще немного улучшить этот код. Надеюсь, на следующей неделе у меня будет время - person Ortiga; 19.06.2020

Быстрое решение, работающее для простейших случаев цепочек зависимостей.

    public static IServiceCollection AddChained<TService>(this IServiceCollection services, params Type[] implementationTypes)
    {
        if (implementationTypes.Length == 0)
        {
            throw new ArgumentException("Pass at least one implementation type", nameof(implementationTypes));
        }

        foreach(Type type in implementationTypes)
        {
            services.AddScoped(type);
        }

        int order = 0;
        services.AddTransient(typeof(TService), provider =>
        {
            //starts again
            if (order > implementationTypes.Length - 1)
            {
                order = 0;
            }

            Type type = implementationTypes[order];
            order++;

            return provider.GetService(type);
        });

        return services;
    }

а потом

services.AddChained<IService>(typeof(SomeTypeWithIService), typeof(SomeType));
person feihoa    schedule 10.12.2019