Класс проксирования перехвата Ninject с непустым конструктором через динамический прокси замка

Я основываю большую часть своей текущей реализации на информации, представленной здесь:

Ninject Intercept любого метода с определенным атрибутом?

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

Пример использования:

Kernel.Components.Add<IPlanningStrategy, CustomPlanningStrategy<LoggingAttribute, LoggerInterceptor>>();

Затем он будет искать любые методы, которые имеют атрибут [Logging], а затем будет использовать перехватчик ведения журнала.

Однако в настоящее время я получаю InvalidProxyConstructorArgumentsException от динамического прокси, когда он пытается проксировать методы со связанными атрибутами. Теперь я помню, как читал, что вам нужны виртуальные методы, однако я не помню, чтобы вы ДОЛЖНЫ иметь конструктор без параметров.

Все привязки выполняются к интерфейсам, а перехватчики AOP происходят через атрибуты и пользовательский класс планирования прокси, упомянутый в ссылке выше.

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


person Grofit    schedule 12.03.2014    source источник
comment
Пожалуйста, будьте конкретны. Какие у вас привязки? Какая у вас реализация перехватчика? Вы используете пользовательский IPlanningStrategy или просто .Intercept().With<SomeInterceptor>()?   -  person BatteryBackupUnit    schedule 12.03.2014
comment
Ссылка выше детализирует это, я использую атрибуты (которые не наследуются от InterceptorAttribute), а затем использую собственный класс стратегии планирования, чтобы пройти через классы и проверить методы, которые имеют атрибуты, а затем проксировать их. Обновлю основной вопрос примером.   -  person Grofit    schedule 12.03.2014


Ответы (2)


Альтернативным подходом может быть использование основанной на соглашении привязки для всех классов с помощью метода с атрибутом [Logging]. Однако это означает, что добавление атрибута [Logging] к методу повлияет на привязку объекта, что может быть нежелательно.

Вот как это будет работать (проверено на работу):

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class LoggingAttribute : Attribute
{
}

public interface IClassNotToBeIntercepted
{
    void DoSomething();
}

public class ClassNotToBeIntercepted : IClassNotToBeIntercepted
{
    public void DoSomething() { }
}

public interface IClassToBeIntercepted
{
    void DoNotLogThis();
    void LogThis();
    void LogThisAsWell();
}

public class ClassToBeIntercepted : IClassToBeIntercepted
{
    public void DoNotLogThis() { }

    [Logging]
    public void LogThis() { }

    [Logging]
    public void LogThisAsWell() { }
}

public class LoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine("interceptor before {0}", BuildLogName(invocation));

        invocation.Proceed();

        Console.WriteLine("interceptor after {0}", BuildLogName(invocation));
    }

    private static string BuildLogName(IInvocation invocation)
    {
        return string.Format(
            "{0}.{1}", 
            invocation.Request.Target.GetType().Name,
            invocation.Request.Method.Name);
    }
}

public class DemoModule : NinjectModule
{
    public override void Load()
    {
        this.Bind(convention => convention
            .FromThisAssembly()
            .SelectAllClasses()
            .Where(ContainsMethodWithLoggingAttribute)
            .BindDefaultInterface()
            .Configure(x => x
                .Intercept()
                .With<LoggingInterceptor>()));

        this.Bind<IClassNotToBeIntercepted>()
            .To<ClassNotToBeIntercepted>();
    }

    private static bool ContainsMethodWithLoggingAttribute(Type type)
    {
        return type
            .GetMethods()
            .Any(method => method.HasAttribute<LoggingAttribute>());
    }
}

И тест:

    [Fact]
    public void InterceptorTest()
    {
        var kernel = new StandardKernel();
        kernel.Load<DemoModule>();

        kernel.Get<IClassNotToBeIntercepted>()
            .DoSomething();

        kernel.Get<IClassToBeIntercepted>()
            .LogThis();
    }

Результатом будет следующий вывод консоли:

interceptor before ClassToBeIntercepted.LogThis
interceptor after ClassToBeIntercepted.LogThis
person BatteryBackupUnit    schedule 13.03.2014
comment
Это кажется интересным способом решения проблемы, мы пометим это как ответ, поскольку это дает нам необходимое разделение, а также позволяет нам настроить перехватчики для каждого атрибута. Спасибо, что потратили время на обучение меня и других :) - person Grofit; 13.03.2014

Глядя на код генерации прокси: https://github.com/ninject/ninject.extensions.interception/blob/master/src/Ninject.Extensions.Interception.DynamicProxy/DynamicProxyProxyFactory.cs

    if (targetType.IsInterface)
        {
            reference.Instance = this.generator.CreateInterfaceProxyWithoutTarget(targetType, additionalInterfaces, InterfaceProxyOptions, wrapper);
        }
        else
        {
            object[] parameters = context.Parameters.OfType<ConstructorArgument>()
                .Select(parameter => parameter.GetValue(context, null))
                .ToArray();
            reference.Instance = this.generator.CreateClassProxy(targetType, additionalInterfaces, ProxyOptions, parameters, wrapper);
        }

можно видеть, что расширение динамического прокси ninject передает генератору динамических прокси Castle только ConstructorArguments.

Итак, без изменений в расширении ninject или создания собственного — вам нужно передать все зависимости в качестве аргументов конструктора. Вы также можете проверить, работает ли внедрение свойств/методов (см. https://github.com/ninject/ninject/wiki/Injection-Patterns).

Если вы контролируете код, вы можете добавить интерфейсы к прокси-классам, а затем использовать "прокси интерфейса с целью". Это позволяет отделить создание прокси-сервера от создания экземпляра целевого (прокси-класса) --> цель может иметь зависимости ctor, внедренные без каких-либо изменений в ninject (-extensions).

Пояснение: наличие следующего класса, который следует проксировать:

public interface IBar { }

public class Foo 
{
     public Foo(IBar bar)
     {
     }
}

И следующая привязка:

Bind<Foo>().ToSelf().Intercept().With<SomeInterceptor>();
Bind<IBar>().To<Bar>();

А затем извлечение Foo из контейнера ninject:

IResolutionRoot.Get<Foo>();

не будет работать.

Помещение всех аргументов конструктора в контекст нинекта, чтобы заставить его работать

Однако мы можем изменить получение Foo, чтобы оно заработало:

var bar = IResolutionRoot.Get<IBar>();
IResolutionRoot.Get<Foo>(new ConstructorArgument("bar", bar);

Теперь это неоптимально, потому что ninject не выполняет автоматическое разрешение зависимостей.

Добавление интерфейса в прокси-класс, чтобы он работал лучше

Мы можем обойти эту проблему, используя «интерфейсный прокси с целью». Во-первых, мы добавляем интерфейс к проксируемому классу:

public interface IFoo{ }

public class Foo : IFoo
{
     public Foo(IBar bar)
     {
     }
}

И затем мы меняем привязку на:

Bind<IFoo>().To<Foo>().Intercept().With<SomeInterceptor>();

А затем извлечение Foo из контейнера ninject:

IResolutionRoot.Get<Foo>();

работает.

Еще одно, возможно, более простое (и более уродливое?) решение Согласно @Daniel, это работает: добавьте два конструктора к прокси-типу:

  • один конструктор protected без параметров. Это для DynamicProxy для создания прокси.
  • один конструктор public/internal с аргументами, который будет использоваться ninject для создания экземпляра прокси-типа.

Ninject автоматически выберет конструктор с наибольшим количеством аргументов, которые он может разрешить.

person BatteryBackupUnit    schedule 12.03.2014
comment
Ах, хорошо, если есть какие-либо конструкторы с неоднозначными зависимостями, он не будет знать, что делать, и взорвется. - person Grofit; 12.03.2014
comment
может быть, я придираюсь / может быть, я вас неправильно понимаю: каждый аргумент конструктора, который принимает прокси-класс, должен быть доступен в контексте ninject как параметр типа ConstructorArgument. Для прокси-класса нет разрешения зависимостей. За исключением, когда вы используете интерфейс с целевым прокси: тогда класс создается как обычно с разрешением зависимостей, и только интерфейс-прокси создается без разрешения зависимостей - он в любом случае не требует этого. - person BatteryBackupUnit; 12.03.2014
comment
Просто чтобы уточнить, все проходит через Ninject, поэтому все классы и аргументы конструктора находятся в дереве зависимостей. Ручных .WithConstructorArgument("someArg", someObj) нет, так как все находится в дереве, поэтому заполняется, а сами классы прекрасно работают без участия АОП. Это как раз тот момент, когда я пытаюсь подключить перехват атрибута, показанный выше, он не может проксировать классы, я предполагаю, подумав над вашим ответом, что это может быть потому, что какой-то конструктор может иметь неоднозначный аргумент, например аргументы, которые имеют один и тот же интерфейс, но имеют 2 разные реализации. - person Grofit; 12.03.2014
comment
AFAIK: если .WithConstructorArgument(..) нет, но прокси-серверу нужен параметр конструктора, он завершится ошибкой. Потому что ninject не выполняет инъекцию конструктора для проксируемого класса прокси-класса без цели. Даже если зависимость недвусмысленная, это не удастся. - person BatteryBackupUnit; 12.03.2014
comment
Это то, чего я боялся, у меня сложилось впечатление от других статей, что он передает параметры в замок, но в каком-то порядке, поэтому, если замок не может определить нужный тип, он взорвется. Кажется очень важным, если ВСЕ классы АОП должны иметь конструкторы без параметров, поскольку основная причина, по которой вы используете инфраструктуру IoC, заключается в элегантной обработке зависимостей вашего конструктора, поэтому, если вы не можете использовать АОП, пока использование DI, по-видимому, означает, что это полезно только для классов без зависимостей, которые, вероятно, не будут DI, поэтому все равно не будут перехвачены... грустное лицо панды... - person Grofit; 12.03.2014
comment
Однако, если вы добавите интерфейс к прокси-классу, он будет работать. Разрешение зависимостей отсутствует только для прокси класса, но не для интерфейса с целевыми прокси. Если вы выполняете модульное тестирование / TDD, вы, скорее всего, все равно используете интерфейсы, поэтому эта функция, вероятно, была очень низкой. Возможно, инъекция зависимости прокси класса работает с расширением прокси Linfu, я не знаю. - person BatteryBackupUnit; 12.03.2014
comment
Это то, что я нахожу странным, все привязано как интерфейс к конкретному классу, то есть Bind<ISomething>().To<Something>(). Однако я не знаю, как указать ему проксировать на уровне интерфейса, а не на уровне класса, но, поскольку я использую этот настраиваемый подход к проксированию, основанный на атрибутах, очень мало информации о том, как это сделать, чтобы я мог проксировать интерфейсы . Первоначально я думал, что это будет более широко распространено, чем есть на самом деле, я, кажется, единственный человек, пытающийся делать такие вещи, мне нравится думать об этом как о MVC ActionFilters для приложений. - person Grofit; 12.03.2014
comment
Что можно сделать относительно легко, так это использовать расширение соглашения, чтобы найти все классы с 1+ методами с атрибутом [Logging], а затем привязать их к их интерфейсу и перехватить с помощью «LoggingInterceptor». - person BatteryBackupUnit; 13.03.2014
comment
Не могли бы вы привести пример того, как это сделать? поскольку я пытался найти это, но не смог найти способ сделать это. Я ожидал, что это будет через расширения лямбда-перехвата, но примеров нет, и я не уверен, будет ли он делать вывод о самом интерфейсе или прокси-сервере класса, или мне нужно подсказать/сказать ему, чтобы он сделал одно или другое. - person Grofit; 13.03.2014
comment
Последнее решение работает, и конструктору без параметров даже не нужно быть общедоступным. Его можно просто защитить. - person Daniel; 15.04.2015