Рабочий процесс ASP.NET.

Веб-приложения, поддерживающие продолжительные операции.

Майкл Кеннеди

Загружаемый файл с кодом доступен в коллекции кода MSDN
Обзор кода в интерактивном режиме

В данной статье рассматриваются следующие вопросы.
  • Рабочие процессы, независимые от процессов
  • Синхронные и асинхронные действия
  • Рабочие процессы, действия и сохранение состояния
  • Интеграция с ASP.NET
В данной статье используются следующие технологии:
Windows Workflow Foundation, ASP.NET

Cодержание

Впрягая рабочие процессы
Синхронные и асинхронные действия
Что именно имеется в виду под простоем?
Превращение синхронных задач в асинхронные
Рабочие процессы и действия
Сохранение состояния
Воплощение в реальность
Интеграция ASP.NET
Несколько вещей, которые стоит осмыслить
Подводя итоги

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

Но для начала давайте рассмотрим более мягкую проблему с точки зрения ASP.NET: необходимо сконструировать решение для операции подсчета стоимости покупок в сетевом магазине. В силу своей продолжительности это решение требует особого подхода. В частности, разработчик может выбрать хранение данных корзины в сеансе ASP.NET. Он может даже решить перенести этот сеанс в находящийся вне процесса сервер или базу данных, чтобы сделать возможными обновления на веб-узле и балансировку нагрузки. Даже если так, он найдет, что все средства, необходимые для легкого решения данной проблемы, предоставляются самой ASP.NET.

Однако, когда продолжительность операции начинает превосходить типичную продолжительность сеанса ASP.NET (20 минут) или требует нескольких действующих лиц (как в моем примере найма), ASP.NET не предлагает достаточной поддержки. Можно вспомнить, что рабочие процессы ASP.NET автоматически отключаются при бездействии и периодически перерабатывают себя. Это вызовет большие проблемы для продолжительных операций, поскольку состояние, содержащееся внутри этих процессов, будет потеряно.

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

Но действительно ли это решит проблему? Вероятно нет. Что если серверу требуется балансировка нагрузки? Это становится очень сложным в случае привязки к единственному процессу. Что еще хуже, что если необходимо перезагрузить сервер или процесс терпит сбой? Тогда все работавшие операции будут потеряны.

На практике, когда на завершение операций уходят дни или недели, необходимо решение, независимое от жизненного цикла исполняющего его процесса. Это верно в целом, но особенно верно для веб-приложений ASP.NET.

Впрягая рабочие процессы

Windows Workflow Foundation (WF) – это не та технология, которая приходит на ум, если речь заходит о создании веб-приложений. Однако WF предоставляет несколько ключевых функций, делающих решение рабочих процессов заслуживающим внимания. WF дает возможность достигнуть независимости процессов для долгосрочных операций, целиком выгружая простаивающие рабочие процессы из пространства процессов и автоматически перезагружая их в активный процесс, когда они больше не простаивают (см. рис. 1). Используя WF, можно подняться над недетерминированным жизненным циклом рабочего процесса ASP.NET и обеспечить все необходимое для продолжительных операций внутри веб-приложения.

fig01.gif

Рис. 1. Операции сохранения рабочих процессов между экземплярами процесса

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

Эта независимость процессов имеет и другие преимущества. Она предоставляет простой способ балансировки нагрузок а также надежности – устойчивости к сбоям при сбоях процесса или сервера.

Синхронные и асинхронные действия

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

Примером синхронного действия может быть действие, вычисляющее налог на заказ в сетевом магазине. Давайте подумаем о том, как может быть реализовано такое действие. Подобно большинству действий WF, основная работа происходит в переопределенном методе Execute. Этапы этого метода могут выглядеть примерно следующим образом:

  1. Получение данных о заказе от предыдущего действия. Обычно это проделывается через привязку данных и пример этого можно будет увидеть позже.
  2. Поиск клиента, связанного с заказом, в базе данных.
  3. Поиск ставки налога в базе данных, в зависимости от места проживания клиента.
  4. Выполнение некоторых простых вычислений, использующих ставку налога и элементы заказа, связанные с заказом.
  5. Сохранение суммы налога в свойстве, к которому последующие действия смогут выполнять привязку для завершения процесса подсчета стоимости покупок.
  6. Отправка рабочей среде процесса сигнала о том, что этой действие завершено, путем возвращения флажка состояния Completed («Завершено») от методов Execute («Исполнить»).

Важно осознать, что ни на одном этапе не происходит ожидания. Все время работа. Метод Execute просто проходит через этапы и завершает работу в короткий срок. Это суть синхронного действия: вся работа выполняется в методе Execute.

Асинхронные действия различаются. В отличие от их синхронных аналогов, асинхронные действия исполняются в течение какого-то периода времени и затем ждут внешнего стимула. При ожидании действия становятся простаивающими. Когда происходит действие, действие возобновляет работу и завершает исполнение.

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

Что именно имеется в виду под простоем?

На этом этапе семантики русского языка и архитектуры расходятся. Давайте отвлечемся от WF и подумаем о том, что означает простой.

Представьте себе следующий класс, использующий веб-службы для изменения пароля:

public class PasswordOperation : Operation {
  Status ChangePassword(Guid userId, string pw) {
    // Create a web service proxy:
    UserService svc = new UserService();

    // This can take up to 20 sec for 
    // the web server to respond:
    bool result = svc.ChangePassword( userId, pw );

    Logger.AccountAction( "User {0} changed pw ({1}).",
      userId, result);
    return Status.Completed;
  }
}

Простаивает ли метод ChangePassword когда-либо? Если да, где?

Поток этого метода блокируется, ожидая запроса HTTP от UserService. Так что концептуально ответ – «да», поток простаивает, ожидая ответа службы. Но может ли поток выполнять другую работу, пока он ожидает ответа службы? Нет, не в том виде, в котором он сейчас используется. Таким образом, с точки зрения WF, этот «рабочий процесс» никогда не простаивает.

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

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

Превращение синхронных задач в асинхронные

Необходимую прозрачность планировки можно добавить к операции, разбив ее на две части: одну, исполняющуюся до момента, когда операция потенциально простаивает и другую, исполняющую код после состояния простоя.

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

На рис. 2 я создал улучшенную версию метода изменения паролей, именуемую ChangePasswordImproved. Я создаю прокси веб-службы, как и ранее. Затем метод регистрирует метод обратного вызова, который должен быть уведомленным когда сервер ответит. Затем я асинхронно исполняю вызов службы и докладываю планировщику, что операция простаивает, но не завершена, возвращая Status.Executing. Этот этап важен – он позволяет планировщику исполнять другую работу, пока мой код простаивает. Наконец, когда происходит завершенное событие, я вызываю планировщик, чтобы сигнализировать о завершении операции и том, что он может действовать дальше.

Рис. 2. Простой вызов службы изменения пароля

public class PasswordOperation : Operation {
  Status ChangePasswordImproved(Guid userId, string pw) {
    // Create a web service proxy:
    UserService svc = new UserService();

    svc.ChangePasswordComplete += svc_ChangeComplete;
    svc.ChangePasswordAsync( userId, pw );
    return Status.Executing;
  }

  void svc_ChangeComplete(object sender, PasswordArgs e) {
    Logger.AccountAction( "User {0} changed pw ({1}).",
      e.UserID, e.Result );

    Scheduler.SignalCompleted( this );
  }
}

Рабочие процессы и действия.

Теперь я применю концепцию простоя операции к созданию действий в WF. Это очень похоже на то, что было показано раньше, но теперь я должен работать внутри модели WF.

WF включает в себя много встроенных действий. Однако тем, кто начал создавать реальные системы с помощью WF, быстро понадобится начать создавать свои собственные повторно используемые действия. Это несложно. Достаточно определить класс, происходящий от вездесущего класса Activity («Действие»). Вот базовый пример:

class MyActivity : Activity {
  override ActivityExecutionStatus 
    Execute(ActivityExecutionContext ctx) {

    // Do work here.
    return ActivityExecutionStatus.Closed;
  }
}

Чтобы действие делало что-то полезное, необходимо переопределить метод Execute («Исполнить»). При создании короткоживущего синхронного действия, достаточно просто применить операцию действия внутри этого метода и возвратить состояние Closed («Закрыто»).

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

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

Для создания большинства асинхронных действий необходимы следующие этапы.

  1. Создание класса, производного от Activity.
  2. Переопределение метода Execute.
  3. Создание очереди рабочего процесса, которую можно использовать для получения уведомления о том, что ожидаемое асинхронное событие завершилось.
  4. Подписка на событие QueueItemAvailable очереди.
  5. Инициализируйте начало продолжительной операции (например, отправьте электронное письмо, запрашивающее руководителя просмотреть резюме претендента на рабочее место).
  6. Ждите, пока внешнее событие не произойдет. Это, по сути, сигнализирует, что действие стало простаивающим. Это указывается среде рабочего процесса, путем возвращения ExecutionActivityStatus.Executing.
  7. Когда событие происходит, метод, обрабатывающий событие QueueItemAvailable удаляет элемент из очереди, преобразует его в ожидаемый тип данных и обрабатывает результаты.
  8. Обычно это завершает операцию действия. Среде рабочего процесса затем отправляется сигнал путем возвращения ActivityExecutionContext.CloseActivity.

Сохранение состояния

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

Службы рабочих процессов – это ключевая точка расширяемости для WF. Среда выполнения WF – это класс, экземпляр которого создается в приложении для размещения всех работающих рабочих процессов. У этого класса имеются две противоположные цели разработки, которые достигаются одновременно, через концепцию служб рабочих процессов. Первая цель состоит в том, чтобы среда выполнения этого рабочего процесса была бы простым объектом, который можно использовать во многих местах. Вторая цель состоит в том, чтобы среда выполнения предоставляла широкие возможности рабочим процессам, пока они работают. Например, она может предоставить возможность автоматически сохранять состояние простаивающих рабочих процессов, отслеживать ход рабочих процессов и поддерживать другие нестандартные возможности.

Среда рабочих процессов остается по умолчанию простой, поскольку лишь несколько из этих возможностей являются встроенными. Большинство тяжеловесных служб, таких как службы сохранения состояния и отслеживания, устанавливаются через модель служб и не обязательны. На самом деле, определение службы – это любая глобальная возможность, которую нужно предоставить рабочим процессам. Эти службы устанавливаются в среде выполнения путем простого вызова метода AddService на классе WorkflowRuntime:

void AddService(object service)

Поскольку AddService принимает ссылку на System.Object, можно добавить все, что может понадобиться рабочему процессу.

Я буду работать с двумя службами. Сперва я использую WorkflowQueuingService для доступа к очередям рабочих процессов, лежащим в основе создания асинхронных действий. Эта служба устанавливается по умолчанию и не может быть настроена. Другая служба – это SqlWorkflowPersistenceService. Эта служба, само собой, предоставляет возможности сохранения состояния и не установлена по умолчанию. К счастью, она не входит WF. Достаточно добавить ее к среде выполнения.

Можно поспорить, что база данных с именем SqlWorkflowPersistenceService будет требоваться где-нибудь. Для этой цели можно создать пустую базу данных или добавить таблицы в существующую базу данных. Лично я предпочитаю использовать выделенную базу данных, вместо смешивания данных сохранения состояния рабочих процессов с другими данными. Так что я создаю пустую базу данных в SQL Server, именуемую WF_Persist. Я создаю необходимую схему базы данных и сохраненные процедуры, выполняя несколько сценариев. Они устанавливаются как часть Microsoft .NET Framework и по умолчанию расположены в этой папке:

C:\Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN\

Необходимо сперва выполнить сценарий SqlPersistenceService_Schema.sql, а затем сценарий SqlPersistenceService_Logic.sql. Теперь эту базу данных можно использовать для сохранения состояния путем передачи строки подключения службе сохранения состояния:

SqlWorkflowPersistenceService sqlSvc = 
    new SqlWorkflowPersistenceService(
  @"server=.;database=WF_Persist;trusted_connection=true",
  true, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));

wfRuntime.AddService(sqlSvc);

Этот простой вызов метода AddService требуется для того, чтобы начать разгружать простаивающие рабочие процессы и сохранять их в базе данных, с последующим восстановлением их, когда они понадобятся снова. Обо всем остальном позаботится среда выполнения WF.

Воплощение в реальность

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

Я буду здесь работать с вымышленной консультационной компанией по .NET, именуемой Trey Research. Они хотели бы автоматизировать процесс поиска и найма своих консультантов. Так что я буду создавать веб-сайт ASP.NET для поддержки этого процесса найма. Я постараюсь максимально все упростить, но в процессе есть несколько этапов:

  1. Претендент на место посетит веб-сайт Trey Research и выразит интерес к работе.
  2. Руководителю будет отправлено электронное письмо, уведомляющее о появлении нового кандидата.
  3. Руководитель просмотрит резюме и одобрит назначение претендента на определенное место.
  4. Кандидату будет отправлено электронное письмо, с информацией о предлагаемом месте.
  5. Он посетит веб-сайт и примет работу или откажется от нее.

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

Это веб-приложение включено в исходный код для статьи. Однако, чтобы увидеть полные результаты, будет необходимо собрать базу данных сохранения состояния и настроить образец на ее использование. Я создал параметр для управления тем, включена или выключена служба сохранения состояния и выключил ее по умолчанию. Чтобы включить ее, установите usePersistDB на true («истина») в разделе AppSettings файла web.config. Если хочется поглядеть на ее работу, читая статью, можно заглянуть в асинхронное средство просмотра работы на моем веб-сайте.

fig03.gif

Рис. 3. Процесс найма как рабочий процесс

Я начну с разработки рабочего процесса, полностью независимого от ASP.NET. Чтобы построить рабочий процесс, я создам четыре собственных действия. Первое – это действие отправки электронной почты, и оно будет простым синхронным действием. Другие три представляют этапы 1, 3 и 5, показанные ранее, и будут асинхронными действиями. Эти действия являются ключом к успеху продолжительной операции. Я назову их GatherEmployeeInfoActivity, AssignJobActivity и ConfirmJobActivity, соответственно. Затем я скомбинирую эти действия с рабочим процессом, показанным на рис. 3.

Действие отправки электронной почты прямолинейно, так что в своей статье я не буду углубляться в его подробности. Это синхронное действие, подобно классу MyActivity, показанному ранее. Загляните в загружаемый файл кода за подробностями.

Это оставляет мне задачу создания трех асинхронных действий. Я сэкономлю себе массу работы, если я смогу заключить восьмиэтапный процесс создания асинхронного действия в общий базовый класс. Ради этой цели я определю класс, именуемый AsyncActivity (см. рис. 4). Обратите внимание на то, что в этот список не включены несколько внутренних вспомогательных методов или обработки ошибок, присутствующих в реальном коде. Эти детали были пропущены ради краткости.

Рис. 4. AsyncActivity

public abstract class AsyncActivity : Activity {
  private string queueName;

  protected AsyncActivity(string queueName) {
    this.queueName = queueName;
  }

  protected WorkflowQueue GetQueue(
      ActivityExecutionContext ctx) {
    var svc = ctx.GetService<WorkflowQueuingService>();
    if (!svc.Exists(queueName))
      return svc.CreateWorkflowQueue(queueName, false);

    return svc.GetWorkflowQueue(queueName);
  }

  protected void SubscribeToItemAvailable(
      ActivityExecutionContext ctx) {
    GetQueue(ctx).QueueItemAvailable += queueItemAvailable;
  }

  private void queueItemAvailable(
      object sender, QueueEventArgs e) {
    ActivityExecutionContext ctx = 
      (ActivityExecutionContext)sender;
    try { OnQueueItemAvailable(ctx); } 
    finally { ctx.CloseActivity(); }
  }

  protected abstract void OnQueueItemAvailable(
    ActivityExecutionContext ctx);
}

В этом базовом классе можно увидеть, что я заключил в одну оболочку несколько занудных и повторяющихся частей создания асинхронного действия. Давайте пройдемся по классу сверху вниз. Начиная с конструктора, я передаю строку для имени очереди. Очереди рабочих процессов являются точками ввода для размещающего приложения (веб-страниц) для передачи данных в действия при сохранении свободной привязки. Ссылки на эти очереди даются по имени и экземпляру процесса, так что каждому асинхронному действию нужно собственное имя очереди.

Затем я определяю метод GetQueue. Как можно увидеть, доступ к очередям рабочих процессов и их создание просты, но несколько занудны. Я создал этот метод как вспомогательный метод для использования внутри этого класса и производных классов.

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

  1. Начало продолжительной операции и вызов SubscribeToItemAvailable.
  2. Сообщение среде рабочего процесса, что действие простаивает.
  3. Экземпляр рабочего процесса сериализуется в базе данных службой сохранения состояния.
  4. Когда операция завершается, элемент отправляется очереди рабочего процесса.
  5. Это вызывает восстановление экземпляра рабочего процесса из базы данных.
  6. Метод абстрактного шаблона OnQueueItemAvailable исполняется базовым AsyncActivity.
  7. Действие завершает свою операцию.

Чтобы увидеть этот класс AsyncActivity в действии, давайте реализуем класс AssignJobActivity. Два других асинхронных действия похожи и входят в загрузку кода.

На рис. 5 можно увидеть, как AssignJobActivity использует базовый шаблон, предоставленный классом AsyncActivity. Я переопределяю Execute для выполнения любой предварительной работы, необходимой для начала долговременного действия, хотя в данном случае таковой и нет. Затем я подпишусь на событие, для того момента, когда станут доступными новые данные.

Рис. 5. AssignJobActivity

public partial class AssignJobActivity : AsyncActivity {
  public const string QUEUE NAME = "AssignJobQueue";

  public AssignJobActivity()
    : base(QUEUE_NAME) 
  {
    InitializeComponent();
  }

  protected override ActivityExecutionStatus Execute(
      ActivityExecutionContext ctx) {
    // Runs before idle period:
    SubscribeToItemAvailable(ctx);
    return ActivityExecutionStatus.Executing;
  }

  protected override void OnQueueItemAvailable(
      ActivityExecutionContext ctx) {
    // Runs after idle period:
    Job job = (Job)GetQueue(ctx).Dequeue();

    // Assign job to employee, save in DB.
    Employee employee = Database.FindEmployee(this.WorkflowInstanceId);
    employee.Job = job.JobTitle;
    employee.Salary = job.Salary;
  }
}

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

Интеграция с ASP.NET

Так это работает внутри рабочего процесса. Но как запустить рабочий процесс? Как веб-страница на деле собирает предложения работы от диспетчера? Как она передает работу действию?

Все по порядку. Давайте взглянем на то, как запустить рабочий процесс. На рекламной странице веб-сайта есть ссылка Apply Now («Подать заявление сейчас»). Когда кандидат щелкает эту ссылку, она параллельно запускает как рабочий процесс, так и переходы по интерфейсу пользователя:

protected void LinkButtonJoin_Click(
    object sender, EventArgs e) {
  WorkflowInstance wfInst = 
    Global.WorkflowRuntime.CreateWorkflow(typeof(MainWorkflow));

  wfInst.Start();
  Response.Redirect(
    "GatherEmployeeData.aspx?id=" + wfInst.InstanceId);
}

Я просто вызываю CreateWorkflow на среде рабочего процесса и запускаю экземпляр рабочего процесса. После этого я отслеживаю экземпляр рабочего процесса, передавая идентификатор экземпляра всем последующим веб-страницам как параметр запроса.

Как я отправлю данные с веб-страницы обратно в рабочий процесс? Давайте взглянем на страницу выделенной работы на рис. 6, где руководитель выбирает работу для претендента.

Рис. 6. Выделение работы

public class AssignJobPage : System.Web.UI.Page {
  /* Some details omitted */
  void ButtonSubmit_Click(object sender, EventArgs e) {
    Guid id = QueryStringData.GetWorkflowId();
    WorkflowInstance wfInst = Global.WorkflowRuntime.GetWorkflow(id);

    Job job = new Job();
    job.JobTitle = DropDownListJob.SelectedValue;
    job.Salary = Convert.ToDouble(TextBoxSalary.Text);

    wfInst.EnqueueItem(AssignJobActivity.QUEUE_NAME, job, null, null);

    buttonSubmit.Enabled = false;
    LabelMessage.Text = "Email sent to new recruit.";
  }
}

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

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

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

На рис. 6 можно было увидеть, что я получил доступ к среде рабочего процесса через класс глобального приложения, следующим образом:

WorkflowInstance wfInst = 
  Global.WorkflowRuntime.GetWorkflow(id);

Это приводит меня к завершающему моменту интеграции Windows Workflow в наше веб-приложение: все рабочие процессы исполняются внутри среды выполнения рабочего процесса. Хотя в AppDomain можно иметь столько сред выполнения рабочих процессов, сколько хочется, обычно имеет смысл иметь одну. В силу этого и поскольку объект среды выполнения WF безопасен с точки зрения потоков, я сделал его открытым статическим свойством глобального класса приложения. Кроме того, я запускаю среду выполнения рабочего процесса в событии запуска приложения и останавливаю ее в событии остановки приложения. Рис. 7 - это сокращенная версия глобального класса приложения.

Рис. 7. Запуск среды рабочего процесса

public class Global : HttpApplication {
  public static WorkflowRuntime WorkflowRuntime { get; set; }

  protected void Application_Start(object sender, EventArgs e) {
    WorkflowRuntime = new WorkflowRuntime();
    InstallPersistenceService();
    WorkflowRuntime.StartRuntime();
    // ...
  }

  protected void Application_End(object sender, EventArgs e) {
    WorkflowRuntime.StopRuntime();
    WorkflowRuntime.Dispose();
  }

  void InstallPersistenceService() {
    // Code from listing 4.
  }
}

В событии запуска приложения я создаю среду выполнения, устанавливаю службу сохранения состояния и запускаю среду выполнения. И в событии конца приложения я останавливаю среду выполнения. Это важный шаг. Он будет блокирован при наличии работающих рабочих процессов, если они не были выгружены. После остановки среды выполнения я вызываю Dispose. Хотя вызов StopRuntime и затем Dispose может показаться избыточным, это не так. Необходимо вызвать оба метода в этом порядке.

Несколько вещей, которые стоит осмыслить

Позвольте мне представить вам несколько предметов для обдумывания, которых я пока не касался напрямую, представленных в формате вопрос/ответ. Почему я не использовал ManualWorkflowSchedulerService? Часто, когда говорится об интеграции WF с ASP.NET, подчеркивается, что планировщик для рабочих процессов по умолчанию (использующий пул потоков) следует заменить на службу, именуемую ManualWorkflowSchedulerService. Причина состоит в том, что она не требуется и не особенно подходит для наших долговременных целей. Ручной планировщик хорош, когда ожидается доведение единого рабочего процесса до выполнения внутри определенного запроса. Куда меньше смысла его использовать, когда рабочий процесс будет исполняться в нескольких жизненных циклах процесса, тем более запросах.

Есть ли способ отследить текущий ход заданного экземпляра рабочего процесса? Да, существует целая служба отслеживания, встроенная в WF и используемая подобно службе сохранения состояния SQL. См. статью из рубрики «Системы Foundation» за март 2007 года «Службы отслеживания в Windows Workflow Foundation» от Мэтта Милнера (Matt Milner).

Подводя итоги

Я могу просуммировать методики, обсуждавшиеся в этой статье, в несколько приемов. Я начал с того, что обрисовал, почему рабочие процессы и модель процессов ASP.NET в целом не подходят для очень продолжительных операций. Чтобы избежать этого ограничения, я воспользовался двумя функциями WF, сочетаемыми для достижения независимости процессов: асинхронными действиями и сохранением состояния рабочих процессов.

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

Наконец, я продемонстрировал, что интеграция рабочего процесса в ASP.NET состоит из двух базовых частей: связи с действиями через очередь рабочего процесса и размещение среды выполнения в глобальном классе приложения.

Теперь, когда читатели увидели интеграцию WF с ASP.NET для поддержки продолжительных операций, у них есть еще одно мощное средство создания решений на основе .NET Framework.

Майкл Кеннеди (Michael Kennedy) – инструктор DevelopMentor, специализирующийся на основных технологиях.NET, а также гибких методологиях разработки и методологиях TDD. Поддерживайте связь с Майклом через его веб-сайт и блог на michaelckennedy.net.