Dynamic Proxy + Expression Tree

В своей работе я всегда пытаюсь абстрагироваться как можно сильнее от всех раздражающих факторов, будь то шум в офисе или сложность управления WCF-сервисом. И вот как раз с последним мне недавно пришла интересная идея. Суть в чем: есть некий класс, который внутри себя полностью управляет каким-то WCF-сервисом, то есть поднимает его (если это self-hosted), подключается к нему, обрабатывает все ошибки, а также завершает работу сервиса. При этом API написано самым приятным образом, и конечному пользователю вообще не нужно думать о том, что у него сервисы какие-то используются, в коде всё выглядит максимально прозрачно. Например:

Понимаете? То есть конечному пользователю кода, который не хочет вручную каждый раз поднимать сервис, нужно только реализовать класс-заглушку, где идет обращение к конкретному методу сервиса, а всё остальное сделает API. Неплохо? Неплохо, но есть одно но: для каждого сервиса нужно написать реализацию-пустышку, где идет практически копипаста. Конечно, таких сервисов не очень много, но возникла идея для автоматизации и этого рутинного дела, поскольку давненько у меня руки чесались попробовать проксирование.

Типичный пример прокси — это как раз WCF-прокси, когда пользователь работает с неким интерфейсом сервиса, а все задачи по сериализации и передаче данных по каналам берет на себя некая прокси. Такое проксирование можно сделать и руками,  но всё это очень муторно, тем более когда есть удобные механизмы для людей, например,  Castle DynamicProxy. С ним и поэксперементируем, но для начала определимся, что нам нужно. Исходя из примера выше (см. MyServiceInvoker), можно понять, что реального объекта, реализующего конкретный интерфейс, у нас не будет. Ну, точнее, он будет, но уже внутрях API — это объект, который возвратит WCF. То есть, нам по сути нужно создать прокси над WCF-прокси. Этим и займемся. Исходя из терминологии Castle, нам нужно сделать interface proxy without target. По этому поводу есть отличная статья, которая дает возможность быстренько влиться в тему. Теперь о тестовом проекте:

Итак, есть у нас ServiceInvoker, есть некий контракт сервиса и есть класс, его реализующий. Для упрощения сервис будет простым классом, без всяких WCF. Итого, я хочу, чтобы все обращения к методам ITestInterface выполнялись через service invoker и при этом без реальной реализации интерфейса этим invoker-ом. Типа вот так:

Делаем новый проект типа Class Library, добавляет нугет-пакет на Castle.Core и пишем класс:

Ничего сложного, по примеру из статьи выше. Единственное, как я уже писал, в строке 6 инициализируется класс, который реализует запрошенный интерфейс, так делать не надо, объект должен создаваться где-то внутри ServiceInvoker, а здесь для упрощения лишь.

Теперь самое интересное, нам нужно будет переадресовать запрошенный метод контракта сервиса в метод ServiceInvoker.Communicate, причем таких методов два, один войд, другой возвращает результат. Не буду томить, скину весь код сразу:

Как можно заметить, здесь идет конкретная наркомания с Expression Tree, но если присмотреться, вчитаться и смириться, то ничего необычного не происходит. В самом начале мы определяем, какой из методов ServiceInvoker.Communicate нам нужен, войдовый или который возвращает результат. Тот, который void, самый простой для реализации.

void Communicate(Action<T> svcAction)

Необычный заголовок, мне нравится. Итак, нам нужно сделать объект Action<T>, который затем можно скормить ServiceInvoker.Communicate(action). Это всё делается просто, стоит только подебажить этот код. Вкратце, я беру оригинальный метод контракта, например, IMyservice.BeNice(srtring):

foo.BeNice(“John”);

Объект invocation содержит Method (метод контракта, который дернули у прокси, в нашем примере это BeNice(string)) и список аргументов Arguments, у нас это одна строка “John”. Затем я делаю Action вида

svc => svc.BeNice(“John”)

И передаю этот экшн внутреннему ServiceInvoker! И дальше уже он вызывает метод на объекте, реализующем контракт IMyService, в нашем тестовом примерчике это класс TestInterface.

TRes Communicate<T, TRes> Communicate(Func<T, TRes> svcFunc)

Здесь всё сложнее, поскольку мы не знаем конкретного типа TRes, и, соответственно, не можем нормально создать Func<T, TRes> с конкретным типом TRes. А поэтому делаем финт ушами и создаем два Expression Tree — один для Func<T, TRes>, а другой — для вызова внутреннего ServiceInvoker.Communicate(svcFunc).

foo.GetCount(“One”, “Two”);

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

Для начала, нам нужно получить MethodInfo для метода TRes Communicate<T, TRes>(Func<T,TRes>), а не void Communicate(Action<T>). Ну, это довольно легко, получаем все методы invoker, затем берем тот, у кого имя Communicate и дальше проверяем, что return type не void.

Далее, метод-то мы получили, только вот он generic по TRes, поэтому его нельзя использовать в выражении. Но есть замечательный метод MethodInfo.MakeGenericMethod, который возвращает конкретный MethodInfo для заданного параметра типа, а это в нашем случае int Communicate(Func<T, int>).

Ну а дальше проще, для начала мы делаем Func<T, int> типа

svc => svc.GetCount(“One”, “Two”)

Дальше делаем лямбду для вызова invoker типа

invoker => invoker.Communicate(svc => svc.GetCount(“One”, “Two”))

Затем компилируем и присваиваем результат в invocation.ReturnValue, которое и будет возвращено как результат работы метода IMyService.GetCount(string, string).

Ну что, похоже наша затея удалась.

Поле для улучшений

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *