Как смешать декораторы в автофаке?

Я хотел бы иметь возможность смешивать и сочетать декораторы с Autofac.

Например, допустим, у меня есть интерфейс IRepository, реализованный классом Repository.
Я мог бы иметь следующие декораторы: RepositoryLocalCache, RepositoryDistributedCache, RepositorySecurity, RepositoryLogging..., вы получите идею.

Основываясь на настройках конфигурации, я бы хотел украсить базовую реализацию необходимыми декораторами. Это может быть один, один или несколько декораторов.

Я знаком с синтаксисом регистрации одного декоратора или их цепочки в фиксированном порядке, но как я могу сделать это динамическим?

3 ответа

Решение

Как указывает Стивен выше, RegisterDecorator методы в Autofac на самом деле не предназначены для этого сценария и довольно неуклюжи в использовании. Они были созданы для некоторых ситуаций, которые было трудно реализовать с помощью обычной регистрации Autofac - "родной" способ сделать это намного чище.

Например, IFoo это сервис и Impl это конкретная (например, репозиторий) реализация.

interface IFoo { }

class Impl : IFoo { }

class DecoratorA : IFoo
{
    public DecoratorA(IFoo decorated) { }
}

class DecoratorB : IFoo
{
    public DecoratorB(IFoo decorated) { }
}

Сначала зарегистрируйте все компоненты, используя их конкретные типы:

var builder = new ContainerBuilder();

builder.RegisterType<Impl>();
builder.RegisterType<DecoratorA>();
builder.RegisterType<DecoratorB>();

Лямбда-регистрации тоже хорошо, просто убедитесь, что они не используют As<IFoo>(),

Теперь оболочка, которая объединяет их в цепочку для предоставления полностью настроенного сервиса:

bool useA = true, useB = false;

builder.Register(c =>
{
    IFoo result = c.Resolve<Impl>();

    if (useA)
        result = c.Resolve<DecoratorA>(TypedParameter.From(result));

    if (useB)
        result = c.Resolve<DecoratorB>(TypedParameter.From(result));

    return result;
}).As<IFoo>();

useA а также useB ваши динамически предоставленные значения из конфигурации.

Теперь решаем (или принимаем зависимость от) IFoo Вы получите динамически построенную цепочку декораторов.

using (var container = builder.Build())
{
    var foo = container.Resolve<IFoo>();

Если вы используете дженерики, то все сложнее, так как вы не упоминаете их, я не буду вдаваться в подробности, но если вы это сделаете, пожалуйста, напишите другой вопрос.

Условно применять декораторы в Autofac довольно сложно. Давайте сделаем это в два этапа. Сначала давайте напишем код, который бы безоговорочно применял эти декораторы:

var builder = new ContainerBuilder();

builder.RegisterType<Repository>().Named<IRepository>("implementor");

builder.RegisterDecorator<IRepository>(
    (c, inner) => new RepositoryLocalCache(inner),
    fromKey: "implementor",
    toKey: "decorator1");

builder.RegisterDecorator<IRepository>(
    (c, inner) => new RepositoryDistributedCache(inner),
    fromKey: "decorator1",
    toKey: "decorator2");

builder.RegisterDecorator<IRepository>(
    (c, inner) => new RepositorySecurity(inner),
    fromKey: "decorator2",
    toKey: "decorator3");

builder.RegisterDecorator<IRepository>(
    (c, inner) => new RepositoryLogging(inner),
    fromKey: "decorator3",
    toKey: null);

Применение декораторов в Autofac осуществляется путем регистрации нескольких компонентов с одинаковым типом службы (IRepository в вашем случае) с помощью регистраций с ключом (toKey) и указать эти регистрации друг на друга, используя fromKey). Внешний декоратор должен быть без ключа, так как по умолчанию Autofac всегда разрешает регистрацию без ключа для вас.

Эти регистрации с ключами являются самой большой слабостью Autofac в этом отношении, поскольку из-за этих ключей декораторы жестко привязаны к следующему. Если вы просто оберните RepositoryDistributedCache в if-блок, конфигурация сломается, так как RepositorySecurity теперь будет указывать на регистрацию, которая не существует.

Решение этой проблемы состоит в том, чтобы динамически генерировать ключи и добавить дополнительный "фиктивный" декоратор без ключа, который не применяется условно:

int counter = 0;
Func<object> getCurrentKey => () => counter;
Func<object> getNextKey => () => ++counter;       
var builder = new ContainerBuilder();
builder.RegisterType<Repository>().Named<IRepository>(getCurrentKey());

if (config.UseRepositoryLocalCache) {
    builder.RegisterDecorator<IRepository>(
        (c, inner) => new RepositoryLocalCache(inner),
        fromKey: getCurrentKey(), toKey: getNextKey());
}

if (config.UseRepositoryDistributedCache) {
    builder.RegisterDecorator<IRepository>(
        (c, inner) => new RepositoryDistributedCache(inner),
        fromKey: getCurrentKey(), toKey: getNextKey());
}

if (config.UseRepositorySecurity) {    
    builder.RegisterDecorator<IRepository>(
        (c, inner) => new RepositorySecurity(inner),
        fromKey: getCurrentKey(), toKey: getNextKey());
}

if (config.UseRepositoryLogging) {    
    builder.RegisterDecorator<IRepository>(
        (c, inner) => new RepositoryLogging(inner),
        fromKey: getCurrentKey(), toKey: getNextKey());
}

// The keyless decorator that just passes the call through.
builder.RegisterDecorator<IRepository>(
    (c, inner) => new RepositoryPassThrough(inner),
    fromKey: getCurrentKey(), toKey: null);    

Здесь мы используем counter переменная и создать getNextKey а также getCurrentKey лямбды, которые облегчают настройку. Снова обратите внимание на последнее RepositoryPassThrough декоратор. Этот декоратор должен просто позвонить своему декоратору и больше ничего не делать. Наличие этого дополнительного декоратора значительно упрощает настройку; иначе было бы намного труднее решить, каким будет последний декоратор.

Одной из вещей, которая делает это намного сложнее с Autofac, является отсутствие поддержки автоматической разводки для неуниверсальных декораторов. Насколько я знаю, это единственная часть в API Autofac, где автоматическая разводка (т. Е. Разрешение контейнеру выяснить, какие аргументы конструктора нужно вставить) не поддерживается. Было бы намного проще, если бы регистрации могли быть выполнены с использованием типа вместо делегата, поскольку в этом случае мы могли бы создать начальный список декораторов для применения, а не просто повторять этот список. Мы все еще должны иметь дело с этими ключевыми регистрациями все же.

Я только что наткнулся на эту тему и хотел бы поделиться, как я это делаю:

Некоторое время назад я написал несколько методов расширения, чтобы упростить эту проблему. Методы аналогичны ответу @Steven в том смысле, что они создают имена для реализаций на лету. Тем не менее, они не используют RegisterDecoratorЭто означает, что нет необходимости в "сквозной" реализации.

Методы могут быть использованы так:

builder.RegisterDecorated<EnquiryService, IEnquiryService>();

builder.RegisterDecorator<ServiceBusEnquiryServiceDecorator, IEnquiryService>();
builder.RegisterDecorator<EmailNotificationDecorator, IEnquiryService>();

У этой реализации есть несколько преимуществ:

  • Декораторы могут быть условно включены или выключены
  • Может быть зарегистрировано ноль, один или много декораторов, если есть хотя бы одна регистрация, которая не является декоратором
  • однажды RegisterDecorated был вызван, вы можете позвонить RegisterDecorator где бы вы ни создавали свои регистрации. Это означает, что вы можете зарегистрировать декораторы из совершенно отдельного модуля Autofac, который находится в сборке или проекте, отличном от оригинальной реализации, для декорирования.
  • Вы можете контролировать порядок размещения декораторов, изменяя порядок регистраций. Внешний декоратор будет последним декоратором, который будет зарегистрирован.

Методы расширения выглядят так:

public static class ContainerBuilderExtensions
{
    private static readonly IDictionary<Type, string> _implementationNames = new ConcurrentDictionary<Type, string>();

    public static void RegisterDecorated<T, TImplements>(this ContainerBuilder builder) where T : TImplements
    {
        builder.RegisterType<T>()
            .As<TImplements>()
            .Named<TImplements>(GetNameOf<TImplements>());
    }

    public static void RegisterDecorator<T, TImplements>(this ContainerBuilder builder) where T : TImplements
    {
        var nameOfServiceToDecorate = GetOutermostNameOf<TImplements>();

        builder.RegisterType<T>();

        builder.Register(c =>
        {
            var impl = c.ResolveNamed<TImplements>(nameOfServiceToDecorate);

            impl = c.Resolve<T>(TypedParameter.From(impl));

            return impl;
        })
            .As<TImplements>()
            .Named<TImplements>(GetNameOf<TImplements>());
    }

    private static string GetNameOf<T>()
    {
        var type = typeof(T);
        var name = type.FullName + Guid.NewGuid();

        _implementationNames[type] = name;

        return name;
    }

    private static string GetOutermostNameOf<T>()
    {
        var type = typeof(T);

        if (!_implementationNames.ContainsKey(type))
        {
            throw new Exception("Cannot call RegisterDecorator for an implementation that is not decorated. Ensure that you have called RegisterDecorated for this type before calling RegisterDecorator.");
        }

        return _implementationNames[typeof(T)];
    }
}

Приведенный выше фрагмент кода настолько прост, насколько это возможно, но он хорошо мне послужил. Конечно, было бы возможно внести изменения, если у вас есть более сложные требования.

Эта концепция легла в основу моего вклада в Orchard CMS, который добавил возможности декоратора: https://github.com/OrchardCMS/Orchard/pull/6233.

Другие вопросы по тегам