Unity (не путать с игровым движком Unity3D) — это универсальный расширяемый контейнер для встраивания зависимостей (dependency injection, DI) с поддержкой перехвата для использования в любом типе приложения на основе Microsoft .NET Framework. Unity разработан и поддерживается группой Microsoft Patterns & Practices (microsoft.com/practices). Его легко добавить в свое приложение через NuGet. Центр с обучающими ресурсами по Unity вы найдете на msdn.com/unity.
В этой статье основное внимание уделяется перехвату в Unity. Перехват — полезный метод, когда нужно модифицировать поведение индивидуальных объектов, не влияя на поведение других объектов того же класса; это во многом аналогично шаблону Decorator (определение этого шаблона в Википедии см. по ссылке bit.ly/1gZZUQu). Перехват обеспечивает гибкий подход к добавлению новых поведений к объекту в период выполнения. Эти поведения, как правило, реализуют некоторые так называемые поперечные обязанности (crosscutting concerns), например протоколирование или проверку данных. Перехват часто используется как нижележащий механизм для аспектно-ориентированного программирования (АОП). Unity-механизм перехвата в период выполнения позволяет эффективно перехватывать вызовы методов к объектам и выполнять пред- и постобработку этих вызовов.
Поддержка перехвата в Unity-контейнере состоит из двух основных элементов: перехватчиков (interceptors) и поведений перехвата (interception behaviors). Перехватчики определяют механизм, используемый для перехвата вызовов методов в объекте, а поведения перехвата — операции, выполняемые с перехваченными вызовами методов. Перехватываемый объект снабжается конвейером поведений перехвата. Когда перехватывается вызов метода, каждое поведение в конвейере получает возможность проверять и даже модифицировать параметры вызова метода; в конечном счете вызывается исходная реализация метода. При возврате управления этим методом каждое поведение может проверить и заменить возвращаемые значения или исключения, сгенерированные исходной реализацией либо предыдущим поведением в конвейере. Наконец, исходный вызвавший получает конечное возвращаемое значение, если таковое есть, или конечное исключение. Механизм перехвата показан на рис. 1.
Увеличить
Рис. 1. Механизм перехвата в Unity
Client | Клиент |
Return Value | Возвращаемое значение |
Proxy Object or Derived Class | Прокси-объект или производный класс |
Invoke GetNextHandlerDelegate | Вызов GetNextHandlerDelegate |
Message | Сообщение |
Behavior | Поведение |
Behaviors Pipeline | Конвейер поведений |
Invoke Object | Вызов объекта |
Target Object or Original Class Method | Целевой объект или исходный метод класса |
Return Value | Возвращаемое значение |
Существует два типа методов перехвата: экземпляра и типа. При перехвате экземпляра Unity динамически создает прокси-объект, который вставляется между клиентом и целевым объектом. После этого прокси-объект отвечает за передачу инициируемых клиентом вызовов целевому объекту через поведения. При таком варианте можно перехватывать объекты, создаваемые как Unity-контейнером, так и вне контейнера, и использовать его для перехвата виртуальных и не виртуальных методов. Однако динамически создаваемый тип прокси нельзя приводить к типу целевого объекта. При перехвате типа Unity динамически создает новый тип, производный от типа целевого объекта, и он включает поведения, которые выполняют поперечные обязанности. Unity-контейнер создает экземпляры объектов производного типа в период выполнения. Перехват экземпляра позволяет перехватывать только открытые методы экземпляра, а перехват типа — и открытые, и защищенные виртуальные методы. Учитывайте, что из-за ограничений платформы Unity-механизм перехвата не поддерживается в разработке приложений Windows Phone и Windows Store, хотя сам Unity-контейнер поддерживается.
Учебное пособие по Unity см. в «Dependency Injection with Unity» (Microsoft Patterns & Practices, 2013) по ссылке amzn.to/16rfy0B. Подробнее о перехвате в Unity-контейнере см. статью «Interception using Unity» в MSDN Library по ссылке bit.ly/1cWCnwM.
Перехват асинхронных методов TAP
Механизм перехвата достаточно прост, но что будет, если перехватываемый метод представляет асинхронную операцию, которая возвращает объект Task? В каком-то смысле это ничего не меняет: метод вызывается и возвращает значение (объект Task) или генерирует исключение, поэтому его можно перехватывать так же, как и любой другой метод. Но, возможно, вы заинтересованы в том, как работать с реальным результатом асинхронной операции, а не с объектом Task, представляющим его. Например, вам нужно протоколировать возвращаемое значение Task или обрабатывать любое исключение, которое мог вызвать Task.
К счастью, наличие объекта, представляющего результат операции, делает перехват при использовании этого асинхронного шаблона сравнительно простым. Другие асинхронные шаблоны заметно сложнее в перехвате: в модели асинхронного программирования (Asynchronous Programming Model) (bit.ly/ICl8aH) одну асинхронную операцию представляют два метода, а в асинхронном шаблоне на основе событий (Event-based Asynchronous Pattern) (bit.ly/19VdUWu) асинхронные операции представляются методом, который инициирует операцию и связанное событие, сигнализирующее о ее окончании.
Для перехвата асинхронной операции, использующей асинхронный шаблон на основе задач (Task-based Asynchronous Pattern, TAP), вы можете заменить Task, возвращаемый методом, новым Task, который выполняет необходимую постобработку после завершения исходной задачи. Код, вызвавший перехватываемый метод, получит новый Task, соответствующий сигнатуре метода, и увидит результат от реализации перехватываемого метода, модифицированный любой дополнительной обработкой, выполняемой поведением перехвата.
К счастью, наличие объекта, представляющего результат операции, делает перехват при использовании этого асинхронного шаблона сравнительно простым.
Мы создадим пример реализации базового подхода к перехвату асинхронных операций TAP, в котором мы будем протоколировать завершение асинхронных операций. Вы можете адаптировать этот пример для создания собственных поведений, способных перехватывать асинхронные операции.
Простой случай
Начнем с простого случая: перехвата асинхронных методов, которые возвращают необобщенный Task. Нам нужно иметь возможность обнаруживать, что перехватываемый метод возвращает Task, и заменять этот Task новым, который выполняет соответствующее протоколирование.
В качестве отправной точки можно задействовать поведение перехвата «no op» (нет операции), показанное на рис. 2.
Рис. 2. Простой перехват
public class LoggingAsynchronousOperationInterceptionBehavior
: IInterceptionBehavior
{
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
// Выполняем остальной конвейер
// и получаем возвращаемое значение
IMethodReturn value = getNext()(input, getNext);
return value;
}
#region additional interception behavior methods
public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
}
public bool WillExecute
{
get { return true; }
}
#endregion
}
Затем мы добавляем код для распознавания методов, возвращающих задачу, и заменяем возвращаемый Task новой оболочкой Task, которая протоколирует результат. Для этого вызывается CreateMethodReturn во входном объекте, чтобы создать новый объект IMethodReturn; он представляет обертывающий Task, созданный новым методом CreateWrapperTask в поведении (рис. 3).
Рис. 3. Возврат Task
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
// Выполняем остальной конвейер
// и получаем возвращаемое значение
IMethodReturn value = getNext()(input, getNext);
// При необходимости работает с задачами
var method = input.MethodBase as MethodInfo;
if (value.ReturnValue != null
&& method != null
&& typeof(Task) == method.ReturnType)
{
// Если этот метод возвращает Task,
// переопределяем исходное возвращаемое значение
var task = (Task)value.ReturnValue;
return input.CreateMethodReturn(this.CreateWrapperTask(
task, input), value.Outputs);
}
return value;
}
Новый метод CreateWrapperTask возвращает Task, который ожидает завершения исходного Task и протоколирует его результат, как показано на рис. 4. Если задача приводит к исключению, этот метод после протоколирования заново сгенерирует данное исключение. Заметьте, что в этой реализации результат исходного Task не изменяется, но другое поведение могло бы заменять или игнорировать исключения, порождаемые исходным Task.
Рис. 4. Протоколирование результата
private async Task CreateWrapperTask(Task task,
IMethodInvocation input)
{
try
{
await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0}",
input.MethodBase.Name);
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}",
input.MethodBase.Name, e);
throw;
}
}
Работа с обобщениями
Работа с методами, возвращающими Task<T>, немного сложнее, особенно если вы хотите избежать падения производительности. Давайте пока не будем выяснять, что представляет собой «T», и предположим, что он уже известен. Как показано на рис. 5, можно написать обобщенный метод, способный обрабатывать Task<T> для известного «T», используя преимущества асинхронных языковых средств в C# 5.0.
Рис. 5. Обобщенный метод для обработки Task<T>
private async Task<T> CreateGenericWrapperTask<T>(Task<T> task,
IMethodInvocation input)
{
try
{
T value = await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0} with value: {1}",
input.MethodBase.Name, value);
return value;
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
throw;
}
}
Как и в простом случае, этот метод просто ведет протоколирование, не меняя исходное поведение. Но, поскольку обернутый Task теперь возвращает значение, при необходимости это поведение могло бы заменять данное значение.
Как нам вызвать этот метод, чтобы получить замену Task? Нам нужно прибегнуть к отражению, чтобы извлечь T из обобщенного типа, возвращаемого перехватываемым методом, создать закрытую (closed) версию этого обобщенного метода для данного T, создать на его основе делегат и, наконец, вызвать делегат. Этот процесс может оказаться весьма дорогостоящим, поэтому имеет смысл кешировать эти делегаты. Если бы T был частью сигнатуры метода, мы не смогли бы создать делегат из метода и вызвать его, не зная, что представляет собой T. Ввиду этого мы разделим наш предыдущий метод на два: один будет иметь нужную сигнатуру, а другой будет использовать языковые средства C#, как показано на рис. 6.
Рис. 6. Разбиение метода создания делегата
private Task CreateGenericWrapperTask<T>(Task task, IMethodInvocation input)
{
return this.DoCreateGenericWrapperTask<T>((Task<T>)task, input);
}
private async Task<T> DoCreateGenericWrapperTask<T>(Task<T> task,
IMethodInvocation input)
{
try
{
T value = await task.ConfigureAwait(false);
Trace.TraceInformation("Successfully finished async operation {0} with value: {1}",
input.MethodBase.Name, value);
return value;
}
catch (Exception e)
{
Trace.TraceWarning("Async operation {0} threw: {1}", input.MethodBase.Name, e);
throw;
}
}
Далее мы изменяем метод перехвата, чтобы использовать правильный делегат для обертывания исходной задачи, которую мы получаем вызовом нового метода GetWrapperCreator с передачей ожидаемого типа задачи. Нам не требуется особый случай для необобщенного Task, поскольку он укладывается в подход с делегатом так же, как и обобщенный Task<T>. Обновленный методы Invoke показан на рис. 7.
Рис. 7. Обновленный метод Invoke
public IMethodReturn Invoke(IMethodInvocation input,
GetNextInterceptionBehaviorDelegate getNext)
{
IMethodReturn value = getNext()(input, getNext);
var method = input.MethodBase as MethodInfo;
if (value.ReturnValue != null
&& method != null
&& typeof(Task).IsAssignableFrom(method.ReturnType))
{
// Если этот метод возвращает Task,
// переопределяем исходное возвращаемое значение
var task = (Task)value.ReturnValue;
return input.CreateMethodReturn(
this.GetWrapperCreator(method.ReturnType)(task, input), value.Outputs);
}
return value;
}
Осталось реализовать лишь метод GetWrapperCreator. Он будет выполнять дорогостоящие вызовы через механизм отражения, чтобы создавать делегаты и использовать ConcurrentDictionary для их кеширования. Эти делегаты создателя оболочки имеют тип Func<Task, IMethodInvocation, Task>; мы хотим получать исходную задачу, объект IMethodInvocation, представляющий вызов для запуска асинхронного метода, и возвращать обернутый Task. Все это представлено на рис. 8.
Рис. 8. Реализация метода GetWrapperCreator
private readonly ConcurrentDictionary<Type, Func<Task, IMethodInvocation, Task>>
wrapperCreators = new ConcurrentDictionary<Type, Func<Task,
IMethodInvocation, Task>>();
private Func<Task, IMethodInvocation, Task> GetWrapperCreator(Type taskType)
{
return this.wrapperCreators.GetOrAdd(
taskType,
(Type t) =>
{
if (t == typeof(Task))
{
return this.CreateWrapperTask;
}
else if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Task<>))
{
return (Func<Task, IMethodInvocation, Task>)this.GetType()
.GetMethod("CreateGenericWrapperTask",
BindingFlags.Instance | BindingFlags.NonPublic)
.MakeGenericMethod(new Type[] { t.GenericTypeArguments[0] })
.CreateDelegate(typeof(Func<Task, IMethodInvocation, Task>), this);
}
else
{
// Другие случаи не поддерживаются
return (task, _) => task;
}
});
}
Для случая с необобщенным Task отражение не требуется, в качестве делегата можно использовать существующий необобщенный метод. При операциях с Task<T> выполняются необходимые вызовы механизма отражения, чтобы создать соответствующий делегат. Наконец, мы не можем поддерживать любой другой тип Task, поскольку не знаем, как его создать, и в таком случае возвращается пустой делегат (no-op delegate), который просто возвращает исходную задачу.
Это поведение теперь можно использовать применительно к перехватываемому объекту, и оно будет протоколировать результаты задач, возвращаемых методами этого объекта, в том числе генерируемые исключения. Пример на рис. 9 показывает, как можно сконфигурировать контейнер на перехват объекта и использовать это новое поведение и конечный вывод при вызове разных методов.
Рис. 9. Конфигурирование контейнера на перехват объекта и использование нового поведения
using (var container = new UnityContainer())
{
container.AddNewExtension<Interception>();
container.RegisterType<ITestObject, TestObject>(
new Interceptor<InterfaceInterceptor>(),
new InterceptionBehavior<LoggingAsynchronousOperationInterceptionBehavior>());
var instance = container.Resolve<ITestObject>();
await instance.DoStuffAsync("test");
// Выполняем какую-то другую работу
}
Output:
vstest.executionengine.x86.exe Information: 0 :
Successfully finished async operation DoStuffAsync with value: test
vstest.executionengine.x86.exe Warning: 0 :
Async operation DoStuffAsync threw:
System.InvalidOperationException: invalid
at AsyncInterception.Tests.AsyncBehaviorTests2.TestObject.<
DoStuffAsync>d__38.MoveNext() in d:\dev\interceptiontask\
AsyncInterception\AsyncInterception.Tests\
AsyncBehaviorTests2.cs:line 501
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.
HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at AsyncInterception.LoggingAsynchronousOperationInterceptionBehavior.<
CreateWrapperTask>d__3.MoveNext() in d:\dev\interceptiontask\
AsyncInterception\AsyncInterception\
LoggingAsynchronousOperationInterceptionBehavior.cs:line 63
Заметаем следы
Как видно в конечном выводе на рис. 9, подход, использованный в этой реализации, приводит к небольшому изменению в трассировке стека исключений, отражая способ, которым исключение генерируется заново при ожидании задачи через await. Альтернативный подход — использование метода ContinueWith с TaskCompletionSource<T> вместо ключевого слова await, чтобы избежать этой проблемы за счет более сложной (и потенциально более дорогостоящей) реализации; пример подобной реализации представлен на рис. 10.
Рис. 10. Использование ContinueWith вместо ключевого слова await
private Task CreateWrapperTask(Task task, IMethodInvocation input)
{
var tcs = new TaskCompletionSource<bool>();
task.ContinueWith(
t =>
{
if (t.IsFaulted)
{
var e = t.Exception.InnerException;
Trace.TraceWarning("Async operation {0} threw: {1}",
input.MethodBase.Name, e);
tcs.SetException(e);
}
else if (t.IsCanceled)
{
tcs.SetCanceled();
}
else
{
Trace.TraceInformation("Successfully finished async operation {0}",
input.MethodBase.Name);
tcs.SetResult(true);
}
},
TaskContinuationOptions.ExecuteSynchronously);
return tcs.Task;
}
Заключение
Мы обсудили несколько стратегий для перехвата асинхронных методов и продемонстрировали их на примере, в котором протоколируются результаты завершения асинхронных операций. Вы можете адаптировать этот пример для создания собственных поведений перехвата, способных поддерживать асинхронные операции.