Два дня потратил на то, чтобы более-менее разобраться в Workflow Foundation 4-й версии.
Из новых замечательных возможностей – FlowChart диаграмма и возможность выставить содержащую её Workflow в сеть в виде “обычного” WCF-сервиса.
Сама по себе FlowChart диаграмма очень удобна, прежде всего своей наглядностью (сразу понятно, как протекает процесс), а так же тем, что практически в точности отражает те блок-схемы, которые вырисовываются на досках и бумажках при продумывании схемы работы. Наиболее счастливые разработчики сразу получают задание от специалиста по требованиям в виде примерно такой вот схемы с фразой “я хочу, чтобы было так!” в заголовке :)
Программисту остаётся только открыть Visual Studio, выбрать там проект, который называется “WCF Workflow Service Application”, и начать определять свой сервис в файле Service1.xamlx (лучше переименовать, или создать новый ;))
Я попробовал сделать небольшой сервис по обработке tickets (извините, я буду писать “тикетов”, так как не сумел придумать нормальный перевод).
Система примерно такая: тикет регистрируется в системе, после чего может изменяться до тех пор, пока не перейдёт в состояние Cancelled или Closed.
При этом он может попасть в состояние WaitingForCustomer. В этом состоянии он может ожидать неделю. Если клиент ничего не ответил, то тикет закрывается.
В результате получается что-то вроде вот такой диаграммы:
Блок “Receive and Update Ticket” – это просто Activity, определённая в отдельном .xaml-файле.
Он отвечает за то, чтобы получить тикет посредством WCF-метода, сохранить его где-то, возможно, изменив, и отправить изменённый тикет обратно (для отображения).
Выглядит это так:
Здесь я просто “перетянул” мышкой в дизайнере ReceiveAndSendReply Activity, в результате чего образовались два блока, отмеченные стрелками. Первый – это получение WCF-сообщения, второй – возвращение значения.
Между получением значения и возвращением результата можно делать какие-то действия. Любые, в общем-то. Можно туда ещё один workflow вставить, который будет что-то важное делать, подготавливая данные к отправке, например, ещё одну FlowChart диаграмму :)
В моём случае всё предельно просто: я только добавляю запись в коллекцию History у тикета и возвращаю его обратно. Хотя, неплохо было бы сначала изменить историю, а потом уже сохранить ;)
Понятно, что если значение возвращать не надо, то можно воспользоваться просто Receive Activity.
Важно здесь то, что прямо в этих avtivities указывается имя метода (ProcessTicket), тип контракта, который будет принимать/получать метод (или можно указать кучку параметров) и что этот метод будет возвращать (это в SendReplyToReceive). Всё остальное будет сделано автоматически, метод станет виден WCF-клиентам как самый обычный WCF-метод.
Здесь MessageType = Ticket – это тип принимаемого сообщения (я принимаю Ticket), в Message data = _ticket – это имя переменной, куда я хочу положить принятый результат.
А ещё, поскольку мы получаем данные, производим над ними работу, а потом отправляем результат назад, нам нужно обеспечить для каждого такого процесса свой собственный, выделенный экземпляр workflow. Сделать это просто: нужно в свойствах Receive Activity включить галку CanCreateInstance. Впрочем, если Вы забудете это сделать, то при запуске Вам напомнят, да.
Итак, после этого у нас каждый тикет будет обрабатываться собственным экземпляром workflow. Что нам, собственно, и надо, ибо жизненные пути тикетов различны…
LogTicketInfo Activity я описывать не буду, скажу только, что это просто класс, activity, унаследованная от CodeActivity и она не делает ничего полезного, кроме Debug.WriteLine(..).
В WF4 предполагается наследоваться от CodeActivity и CodeActivity<T> для выполнения каких-то программных действий, либо от NativeActivity и NativeActivity<T>, если нужно как-то при этом взаимодействовать с Workflow Runtime. Generic-версии используются в том случае, если нужно вернуть какой-то результат заданного типа.
Блок, отвечающий за ожидание ответа клиента в течение недели выглядит так:
Тут я использовал стандартную Pick Activity для выбора одного из “направлений движения”. Их у меня всего два: либо клиент ответил, и тогда ничего делать не надо (анализируем новый статус как обычно), либо клиент НЕ ответил в установленный срок, и тогда закрываем тикет.
Кстати, для получения ответа клиента я использую ту же самую activity, что в самом начале, то есть, в моём случае, используется тот же самый WCF-метод. Что уже, в принципе, должно навести на мысли… ;)
Ну вот, а после получения тикета и записи лога (см. flowchart, первую картинку) всё стекается в блок Switch по полю Status тикета, который и решает, что мы должны делать дальше.
Казалось бы – должно работать! И если мы запустим проект и откроем любимый WcfTestClient.exe, то увидим, что оно и работает! Но если поиграться чуть дольше (или догадаться чуть раньше), то станет понятно, что работает оно неправильно.
Неправильно по двум причинам:
- Все Workflow, находящиеся в статусе отличном от Cancelled или Closed, висят в памяти неопределённое время (пока ресурсы не кончатся).
- Сколько бы мы ни пытались сказать, что тикет такой-то переходит из состояния WaitingForCustomer в любое другое – мы всё время попадаем в начальный блок “Receive And Update Ticket”, а не в тот, что находится в “Wait For Customer”. Иначе говоря, каждое обращение к методу у нас фактически активирует новый процесс, так как наш сервис не понимает, что это тот же самый тикет продолжает свой жизненный путь, а не просто новый “с неба свалился”. Таким образом, экземпляры всё копятся и копятся в памяти.
Самое время решить эти проблемы.
Первую решить довольно-таки просто и очевидно: воспользоваться стандартным persistence service, который уже есть в WF4. Сделаем так, что если workflow не активна, то она сохраняет себя в базе данных SQL Server и не занимает места в памяти и прочих ресурсов. Там, в базе, она может лежать годами, и никому от этого плохо не будет.
Для этого нам всего лишь нужно подготовить таблицы в БД, выполнив последовательно вот эти два (отмечены галочками, в том же порядке) скрипта:
Они создадут структуру таблиц и необходимые процедуры. Делать это можно в любой базе данных, например прямо в БД Вашего приложения, ничего там не сломается, просто добавятся новые таблички :)
Далее нужно просто пойти в web.config файл и прописать там следующие строки (понятное дело, строка подключения к БД у каждого своя):
Всё, как только наша workflow попадает в состояние ожидания чего-либо, она сразу же выгружается из памяти в базу данных, освобождая ресурсы для вещей более полезных, нежели праздное висение в памяти.
Хорошо, что в .NET 4.0 всё, что только можно, уже прописано в “базовые” конфигурационные файлы :) Все конфигурационные файлы стали на порядок легче. Поэтому он уже “знает”, что такое эти два тега и может их использовать.
Со второй проблемой несколько сложнее.
Она ведь в чём состоит, проблема-то? Состоит она в том, что для обработки каждого тикета Workflow Runtime создаёт отдельный экземпляр workflow, но когда в метод ProcessTicket приходит “тот же самый” тикет, Workflow Runtime снова создаёт экземпляр, вместо того, чтобы использовать уже существующий.
Почему это происходит? Да потому, что Workflow Runtime просто не знает, что значит “тот же самый”. Как только она научится это знать – так сразу всё встанет “на свои места”.
Такой процесс “узнавания” называется “кореляцией” (correlation). Иными словами, мы должны провести кореляцию между нашим тикетом и экземпляром workflow.
Делается это тоже несложно: в Workflow для этого предусмотрен специальный тип переменной, называемый CorrelationHandle.
Так, как я использовал ReceiveAndSendReply Activity, то он создал мне такую переменную автоматически.
Я не буду тут отвлекаться на различные аспекты кореляции, (коих несколько, и есть дажа пара activities на данную тему), это отдельный разговор, здесь мы просто решим “насущную” задачу: укажем, что кореляция проводится по идентификатору тикета.
Для этого достаточно пойти в свойства Receive Activity, кликнуть там поле CorrelatesOn и указать нужный нам correlation handler (был создан автоматически, смотрите список переменных) и параметр для кореляции (Id):
В результате получится что-то такое:
Это означает, что когда в наш сервис в метод ProcessTicket приходит сообщение, то происходит его анализ по полю, указанному вот в этом вот XPath выражении, что было построено автоматически. Если значение этого выражения совпадает с каким-либо экземпляром workflow, то это экземпляр будет “поднят” из базы данных и это сообщение и будет обрабатываться именно этим экземпляром, что нам и нужно было сделать :)
Поэтому, когда в экземпляр, ждущий ответа от клиента, приходит такое сообщение, он продолжает свою работу именно с блока “Жду клиента”, а не попадает в начало процесса.
И, вроде бы, всё должно работать. И всё будет работать! :) И всё будет хорошо!
Но недолго. Кто знает, до каких пор? ;)