AOP времени исполнения в Unity

Для тех кто не знает – AOP это Aspect-Oriented Programming, Аспектно-ориентированное программирование.

При написании любой программы программист производит функциональную декомпозицию, то есть разбивает большие блоки функциональности на более маленькие. Но всегда существуют так называемые cross-cutting concerns  или сквозная функциональность, которая используется всеми остальными частями программы, которую невозможно выделить  в отдельный модуль\класс\метод,
Чаще всего такой функциональностью является логгирование, разграничение доступа, управление транзакциями.

Концепция AOP заключается в том что сквозная функциональность выделяется в отдельные сущности , называемые аспектами, и декларативно задается использование аспектов  в коде.

AOP для .NET может быть реализован двумя способами: изменение кода при компиляции инструментами типа PostSharp или макросами языка Nemerle, или перехват вызовов на стадии выполнения.

В составе Unity есть сборка Microsoft.Practices.Unity.Interception, которая содержит расширение контейнера Unity для перехвата вызовов объектов собираемых контейнером.

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

Перехватчики – это классы, реализующие интерфейс IInterceptor. В библиотеке есть классы InterfaceInterceptor для перехвата методов интерфейса, VirtualMethodInterceptor – для перехвата виртуальных методов класса, TransparentProxyInterceptor – для перехвата с помощью прокси-классов, используемых для .NET Remoting.

Обработчики вызовов – это классы, которые реализуют интерфейс ICallHandler, в котором только один нужный метод – Invoke.

Политики бывают двух видов – управляемая атрибутами (AttributeDrivenPolicy) и управляемая правилами (RuleDrivenPolicy).
По-умолчанию используется AttributeDrivenPolicy, которая заключается в том что обработчики вызовов задаются атрибутами, унаследованными от HandlerAttribute, и перехватываются только те методы, для которых заданы эти атрибуты (или атрибуты заданы для классов).
RuleDrivenPolicy позволяет задавать какие методы будут перехватываться с помощью набора правил (IMatchingRule)  и какие обработчики будут при этом вызываться.

Подробнее по этой ссылке http://msdn.microsoft.com/en-us/library/dd140045.aspx

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

Пример

Сначала создадим обработчик, который просто выводит Hello, world на консоль

public sealed class HelloWorldAttribute : HandlerAttribute
{
    public override ICallHandler CreateHandler(IUnityContainer container)
    {
        return new HelloWorldCallHandler();
    }
}

public class HelloWorldCallHandler: ICallHandler
{
    public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
    {
        Console.WriteLine("Hello, world");
        return getNext()(input, getNext);
    }

    public int Order { get; set; }
}

Теперь создадим класс, унаследованный от MarshalByRefObject, чтобы его можно было перехватывать с помощью TransparentProxyInterceptor. Атрибут HelloWorld объявлен выше.

[HelloWorld]
public class SomeService1: MarshalByRefObject
{
    public void Method1()
    {
        Console.WriteLine("SomeService1.Method1");
    }
}

В Main напишем код

container
    .AddNewExtension<Interception>()
    .Configure<Interception>()
        .SetDefaultInterceptorFor<SomeService1>(new TransparentProxyInterceptor());

var s = container.Resolve<SomeService1>();
s.Method1();
Console.ReadLine();

При выполнении будет выведено

Hello, world
SomeService1.Method1

Предположим что SomeService1 нам недоступен и уберем атрибут HelloWorld. Чтобы получить такую же функциональность программы надо дописать несколько строк кода

container
    .AddNewExtension<Interception>()
    .Configure<Interception>()
        .SetDefaultInterceptorFor<SomeService1>(new TransparentProxyInterceptor())
            .AddPolicy("Policy")
                .AddMatchingRule(new TypeMatchingRule(typeof(SomeService1)))
                .AddCallHandler<HelloWorldCallHandler>();

var s = container.Resolve<SomeService1>();
s.Method1();
Console.ReadLine();
Аналогичные настройки можно задать в конфигурационном файле.


Что делать если Unity-контейнер надо передавать как зависимость в другие компоненты?

Правильный ответ – ничего.

var container = new UnityContainer();
var resolvedContainer = container.Resolve<IUnityContainer>();

При выполнении такого кода resolvedContainer получит ссылку на сам контейнер. Это значит что если в любом, собираемом из контейнера, классе есть зависимость типа IUnityContainer, то она автоматически будет получать ссылку на контейнер.



Конфигурация Unity

Существует четыре различных способа конфигурирования контейнера.

1)Использование соглашений. Фактически отсутствие явного конфигурирования. В Unity используется очень простое соглашение, что в классе должен быть один конструктор, который принимает все зависимости параметрами.
Такой способ подходит для 90% случаев если вы пишите код сами.

2)Указание зависимостей с помощью атрибутов. Для свойств есть DependencyAttribute, для конструктора указывается InjectionConstructorAttribute, для метода InjectionMethodAttribute. Для параметров конструктора и injection-методов также можно указывать DependencyAttribute.
При навешивании DependencyAttribute на свойство или параметр можно указать имя зависимости.

3)Задание конфигурации в коде при добавлении элемента в контейнер.
Последним параметром метода RegisterType является массив InjectionMember. В этом массиве можно передать объекты типа InjectionProperty, InjectionConstructor и InjectionMethod чтобы указать с помощью каких членов класса проводить инъекцию.
При указании InjectionConstructor и InjectionMethod для каждого параметра можно указать конкретное значение или попросить контейнер резолвить нужный параметр.
Такой способ дает гораздо более многословен, чем все остальные способы, но дает гораздо большую гибкость. С помощью конфигурации в коде можно использовать IoC с классами, к исходникам которых нет доступа.

4)Задание конфигурации в XML.  Этот способ имеет смысл применять только если нужно изменять конфигурацию без  перекомпиляции  программы. Конфигурация в XML не позволяет описывать произвольные типы, поэтому обладает меньшей мощностью по сравнению с конфигурацией в коде.

Подробнее о возможностях конфигурирования Unity можно почитать по ссылкам
http://msdn.microsoft.com/en-us/library/dd203225.aspx
http://msdn.microsoft.com/en-us/library/dd203208.aspx
http://msdn.microsoft.com/en-us/library/dd203195.aspx
http://msdn.microsoft.com/en-us/library/dd203156.aspx
http://msdn.microsoft.com/en-us/library/dd203230.aspx

Overconfiguration или Темная сторона силы

Часто программисты, познакомившись с IoC-контейнерами и оценив их возможности декларативного конфигурирования, начинают использовать конфигурацию по любому поводу. Это приводит к тому что конфиги становятся монструозных размеров и абсолютно нечитаемые. При этом по коду программы абсолютно невозможно понять какой код когда выполняется. Любые изменения затрагивают не только код, но и конфигурацию, что может привести к проблемам при развертывании.

Поэтому при использовании любой технологии, которая может очень сильно конфигурироваться, предпочитайте пользоваться соглашениями (Convetions over Configuration) и размещайте конфигурацию как можно ближе к месту использования. Для этого используйте атрибуты и помещайте код конфигурирования в ту же сборку, где находятся конфигурируемые классы.



Управление временем жизни объектов в Unity

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

Класс LifetimeManager очень простой, в нем всего три метода GetValue, SetValue и RemoveValue, причем последний не используется.
При помещении объекта в контейнер вызывается метод SetValue, а при необходимости получить объект вызывается GetValue и если он вернул null, то создается новый объект.

В библиотеку Microsoft.Practices.Unity входит несколько менеджеров.

TransientLifetimeManager – ничего не сохраняет, GetValue всегда возвращает null, поэтому объект создается каждый раз. Этот менеджер используется по-умолчанию при вызове RegisterType.

ContainerControlledLifetimeManager – сохраняет объект в локальной переменной. Поэтому объект живет столько же, сколько и контейнер. Этот (вернее другой, но с таким же поведением) менеджер используется по-умолчанию при вызове RegisterInstance.

ExternallyControlledLifetimeManager – сохраняет слабую ссылку (WeakReference) на объект. При использовании этого менеджера и вызове RegisterInstance сам вызывающий код должен управлять временем жизни объекта, помещенного в контейнер. Когда используется RegisterType этот менеджер будет выдавать уже существующий экземпляр объекта если он есть.

PerThreadLifetimeManager – сохраняет объекты в ThreadStatic словаре. Таким образом каждый поток в программе будет использовать свой набор объектов.

Для применения Unity в ASP.NET приложениях очень легко реализовать LifetimeManager, который сохраняет объект в контексте или в сессии.

Другие области применения LifetimeManager

В Unity нет возможности регистрации метода создания объектов, но это очень легко исправить с помощью своего LifetimeManager и пары extension-методов.

public class FactoryLifetimeManager<T>: LifetimeManager
{
    Func<T> _factoryMethod;
    LifetimeManager _baseManager;

    public FactoryLifetimeManager(Func<T> factoryMethod, LifetimeManager baseManager)
    {
        _factoryMethod = factoryMethod;
        _baseManager = baseManager;
    }

    public override object GetValue()
    {
        var obj = _baseManager.GetValue();
        if (obj == null)
        {
            obj = _factoryMethod();
            SetValue(obj);
        }
        return obj;
    }

    public override void RemoveValue()
    {
        _baseManager.RemoveValue();
    }

    public override void SetValue(object newValue)
    {
        _baseManager.SetValue(newValue);
    }
}

public static class UnityFactoryMethodExtensions
{

    public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod)
    {
        return container.RegisterFactory<T>(factoryMethod, new TransientLifetimeManager());
    }
    
    public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod, LifetimeManager lifetimeManager)
    {
        return container.RegisterType<T>(new FactoryLifetimeManager<T>(factoryMethod, lifetimeManager));
    }

    public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod, string name)
    {
        return container.RegisterFactory<T>(factoryMethod, name, new TransientLifetimeManager());
    }
    
    public static IUnityContainer RegisterFactory<T>(this IUnityContainer container, Func<T> factoryMethod, string name, LifetimeManager lifetimeManager)
    {
        return container.RegisterType<T>(name, new FactoryLifetimeManager<T>(factoryMethod, lifetimeManager));
    }
}


Инъекция массивов в Unity

Unity умеет резолвить зависимости-массивы. Для такой зависимости контейнер возвращает объекты всех подходящих типов. Даже при разрешении зависимостей с указанием имени будут возвращены все подходящие типы.

Также этого Unity поддерживает разрешение обобщенных массивов.

Пример

Интерфейс и классы логгеров

public interface ILogger
{
    void Write(string message);
}

public class TraceLogger: ILogger
{
    public void Write(string message)
    {
        Trace.Write(message);
    }
}

public class ConsoleLogger: ILogger
{
    public void Write(string message)
    {
        Console.WriteLine(message);
    }
}

Сервис, принимающий обобщенный массив

public class SomeService2<T>
{
    public SomeService2(T[] array)
    {

    }
}

Код, получающий нужный экземпляр

var container = new UnityContainer();
container
    .RegisterType<ILogger, ConsoleLogger>("ConsoleLogger")
    .RegisterType<ILogger, TraceLogger>("TraceLogger");

var service = container.Resolve<SomeService2<ILogger>>();

При вызове такого кода в конструктор SomeService2 будет передан массив из двух логгеров.