Паралельный вызов удаленных методов за один http-запрос используя WCF [Parallel executing remote methods in a one http-request using WCF]
Окружение
В данной статья будет рассмотрено клиент-серверное приложение. Клиента - Silverlight приложение, сервер - ASP.NET, коммуникация осуществляется WCF'ом. Но зачем нам все это...
Задача
Требование привычное, это повседневная форма редактирования полей сущности.
Допустим сущность - Книга, с полями: Название, Автор, Категория, Язык.
UI должен содержать контролы:
- Название - текстовое поле;
- Автор - выпадающий список, с перечнем всех авторов;
- Категория - выпадающий список, с перечнем доступных категорий;
- Язык - выпадающий список, с перечнем всех языков
- Кнопка сохранения
Проблема
Что бы загрузить все данные, необходимые для нашей формы редактирования, требуется вызвать 4 удаленных серверных метода:
- Загрузить конкретную Книгу по ее идентификатору;
- Загрузить список всех Авторов, для заполнения ими выпадающего списка;
- Загрузить список доступных Категорий, для заполнения выпадающего списка;
- Загрузить список всех Языков, для заполнения выпадающего списка.
Решение
Каждый из запросов независим от других, значит их можно распараллеливать. Выход из такой ситуации довольно прост:
- Добавляем DTOшку Instruction на сервер. Собственно это инструкция, описывающая вызов метода MethodName c аргументами Arguments у типа InstanceType
[DataContract] public class Instruction { [DataMember] public string InstanceType { get; set; } [DataMember] public string MethodName { get; set; } [DataMember] public object[] Arguments { get; set; } }
- Добавляем метод ParallelInvoke к нашей WCF службе, который будет обрабатывать список инструкций, выполнять их, и возвращать список результатов
[ServiceContract] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class MyService { ... ... ... [OperationContract] public object[] ParallelInvoke(Instruction[] instruction) { .... } }
- Осталось реализовать ParallelInvoke, это просто: рефлекшином ищем тип указанный в инструкции, пропускаем/разрешаем его через IoC контейнер, ищем рефлекшином метод и запускаем его.
Типизированная обертка. Здесь поле для фантазии, что-либо просте придумать так и не удалось, по этому опишу как я это сделал у себя.
- Все обращения к серверу осуществляются через объект-посредник, также я прикрутил вот эту штуку так что все мои запросы к серверу синхронны. В результате, например, для того чтобы загрузить сущность на клиент получим:
[Dependency] public IBookRepository BookRepository { get; set; } private void OnLoad(){ ... Book book = BookRepository.Load(bookId); ... }
- Внутри объект-посредник, делает всю грязную работу: формирует инструкцию и дергает серверный метод ParallelInvoke. В данном случае отправляется массив с одной инструкцией. Также, реализацию интерфейсов-посредников я осуществил используя DynamicProxy. Да, но это пока вызов одного метода, как быть с параллельным вызовом.
- Я поставил цель, что бы код приведенный ниже работал:
[Dependency] public IRemoteCall RemoteCall { get; set; } [Dependency] public IBookRepository BookRepository { get; set; } [Dependency] public IAuthorRepository AuthorRepository { get; set; } [Dependency] public ICategoryRepository CategoryRepository { get; set; } [Dependency] public ILanguageRepository LanguageRepository { get; set; } private void OnLoad(){ ... Book book = null; IList<Author> authors = null; IList<Category> categories = null; IList<Language> languages = null; RemoteCall.Parallel( () => book = BookRepository.Load(bookId), () => authors = AuthorRepository.LoadAll(), () => categories = CategoryRepository.LoadAll(), () => languages = LanguageRepository.LoadAll() ); ... }
Потратив пару часов девелопмента и код заработал. Словами описать решение я не рискнул, так что код ниже. Важное правило: все объекты-посредники (BookRepository, AuthorRepository, CategoryRepository, LanguageRepository) должны внутри вызывать метод public object Invoke(Instruction instruction) представленный в типе RemoteCallManager.
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; public class RemoteCallManager { private class ProcessInfo { public Instruction Instruction; public object Result; public ManualResetEvent InstructionReady = new ManualResetEvent(false); public ManualResetEvent ResultReady = new ManualResetEvent(false); public ManualResetEvent ProcessIsDone = new ManualResetEvent(false); } public void Parallel(params Action[] actions) { IList<ProcessInfo> processes = new List<ProcessInfo>(); ExtractInstructions(processes, actions); object[] results = ExtractResults(processes); ReturnResults(processes, results); } public object Invoke(Instruction instruction) { object deferredResult; if (TryReturnDeferredResult(instruction, out deferredResult)) { return deferredResult; } else { return ParallelInvoke(new[] { instruction }).First(); } } private static object[] ParallelInvoke(Instruction[] instructions) { // var client = GetServiceClient(); IAsyncResult asyncResult = client.BeginParallelInvoke(new ObservableCollection<Instruction>(instructions), null, null); return client.EndParallelInvoke(asyncResult); return results.ToArray(); } #region Deferred call engine [ThreadStatic] private static ProcessInfo deferredCall; private const int InstructionReadyTimeout = 10000; private static void ExtractInstructions(IList<ProcessInfo> processes, Action[] actions) { foreach (Action action in actions) { ProcessInfo process = new ProcessInfo(); processes.Add(process); Thread thread = new Thread(() => { deferredCall = process; action(); deferredCall = null; process.ProcessIsDone.Set(); }); thread.Start(); if (process.InstructionReady.WaitOne(InstructionReadyTimeout) == false) { throw new NotSupportedException("Action must use only Mediator inside"); } } } private static bool TryReturnDeferredResult(Instruction instruction, out object deferredResult) { deferredResult = null; if (deferredCall != null) { ProcessInfo processInfo = deferredCall; processInfo.Instruction = instruction; processInfo.InstructionReady.Set(); processInfo.ResultReady.WaitOne(); deferredResult = processInfo.Result; return true; } else { return false; } } private static object[] ExtractResults(IList<ProcessInfo> processes) { Instruction[] instructions = processes.Select(e => e.Instruction).ToArray(); return ParallelInvoke(instructions); } private static void ReturnResults(IList<ProcessInfo> processes, object[] results) { if (results.Length != processes.Count) { throw new NotSupportedException("Number of client calls is not equal to number of server results"); } for (int i = 0; i < results.Length; i++) { ProcessInfo processInfo = processes[i]; processInfo.Result = results[i]; processInfo.ResultReady.Set(); processInfo.ProcessIsDone.WaitOne(); } } #endregion }
P.S.: Буду признателен за комментарии, вопросы и идеи.
Комментарии
Отправить комментарий