Fluxo de trabalho do ASP.NET

Aplicativos Web que oferecem suporte a operações de longa duração

Michael Kennedy

Código disponível para download na MSDN Code Gallery
Navegue pelo código online

Este artigo aborda:

  • Fluxos de trabalho independentes de processos
  • Atividades síncronas e assíncronas
  • Fluxos de trabalho, atividades e persistência
  • Integração com o ASP.NET
Este artigo usa as seguintes tecnologias:
Windows Workflow Foundation, ASP.NET

Sumário

Utilizando fluxos de trabalho
Atividades síncronas e assíncronas
O que exatamente significa ocioso?
Transformando tarefas síncronas em assíncronas
Fluxos de trabalho e atividades
Persistência
Tornando real
Integração com o ASP.NET
Alguns pontos a considerar
Resumo

Frequentemente, é solicitado que os desenvolvedores de software criem aplicativos Web que ofereçam suporte a operações de longa duração. Um exemplo é o processo de check-out de uma loja online, que pode levar vários minutos para ser concluído. Apesar de, por alguns padrões, essa ser uma operação de longa duração, neste artigo explorarei operações de uma escala completamente diferente: operações que podem levar dias, semanas ou até meses para serem concluídas. Um exemplo de uma operação como essas é o processo de inscrição para contratações, que pode envolver interações entre diversas pessoas e a troca de vários documentos reais.

Primeiro, vamos considerar um problema mais benigno do ponto de vista do ASP.NET: você precisa arquitetar uma solução para uma operação de check-out em uma loja online. É necessário fazer considerações especiais para essa solução devido à sua duração. Por exemplo, você pode optar por armazenar os dados do carrinho de compras em uma sessão do ASP.NET. Você pode até mesmo escolher mover o estado dessa sessão em um banco de dados ou servidor de estado externo ao processo para permitir atualizações do site e o balanceamento de carga. Ainda assim, você descobrirá que todas as ferramentas necessárias para solucionar facilmente esse problema são fornecidas pelo próprio ASP.NET.

No entanto, quando a duração da operação ultrapassa a duração típica de sessões do ASP.NET (20 minutos) ou exige vários atores (como no meu exemplo de contratação), o ASP.NET não oferece suporte suficiente. Você deve se lembrar que os processos de trabalho do ASP.NET são desligados automaticamente quando ociosos e se reciclam periodicamente. Isso causará grandes problemas para operações de longa duração, pois os estados mantidos nesses processos serão perdidos.

Imagine por um momento que você deveria hospedar essas operações de duração muito longa em um único processo. Obviamente, o processo de trabalho do ASP.NET não é adequado a elas pelos motivos que acabamos de apresentar. Assim, talvez fosse possível criar um serviço do Windows cuja única responsabilidade seria executar essas operações. Se você nunca reiniciar esse serviço, estará mais perto de uma solução que ao usar o ASP.NET diretamente pois, teoricamente, ter simplesmente um processo de serviço que não é reiniciado automaticamente assegura que o estado das operações de longa duração não será perdido.

Mas, isso vai realmente resolver o problema? Provavelmente, não. E se o servidor exigir balanceamento de carga? Tudo fica muito difícil quando você está vinculado a um único processo. E pior, e se for necessário reiniciar o servidor ou se o processo falhar? Então, todas as operações que estavam em execução serão perdidas.

De fato, quando as operações levam dias ou semanas para serem concluídas, você precisa de uma solução que seja independente do ciclo de vida do processo que a está executando. Isso é verdadeiro de forma geral, mas é especialmente importante para aplicativos Web do ASP.NET.

Utilizando fluxos de trabalho

Talvez não se pense no Windows Workflow Foundation (WF) como a tecnologia para criar aplicativos Web. No entanto, há diversos recursos importantes fornecidos pelo WF que fazem valer a pena considerar uma solução de fluxo de trabalho. O WF permite obter independência do processo para operações de longa duração, descarregando completamente os fluxos de trabalho ociosos do espaço de processo e os recarregando automaticamente no processo ativo quando não estiverem mais ociosos (veja a Figura 1). Usando o WF, você pode ultrapassar o ciclo de vida não determinístico do processo de trabalho do ASP.NET e permitir operações de longa duração no aplicativo Web.

fig01.gif

Figura 1 Os fluxos de trabalho preservam operações entre instâncias de processos

Dois recursos principais do WF se combinam para fornecer essa capacidade. Primeiro, as atividades assíncronas sinalizam para o runtime do fluxo de trabalho que o fluxo de trabalho está ocioso enquanto aguarda em um evento externo. Segundo, um serviço de persistência descarregará fluxos de trabalho ociosos do processo, os salvará em um local de armazenamento durável, como um banco de dados, e os recarregará quando estiverem prontos para serem executados novamente.

Essa independência dos processos tem outras vantagens. Ela oferece uma forma simples de balanceamento de carga, além de durabilidade e tolerância a falhas, no caso de falhas de processo ou de servidor.

Atividades síncronas e assíncronas

Atividades são os elementos atômicos do WF. Todos os fluxos de trabalho são criados a partir de atividades em um padrão semelhante ao padrão de design composto. Na verdade, os fluxos de trabalho são simplesmente atividades especializadas. Essas atividades podem ser classificadas como síncronas ou assíncronas. Uma atividade síncrona executa todas as suas instruções do início ao fim.

Um exemplo de uma atividade síncrona seria a computação dos impostos em um pedido de uma loja online. Vamos pensar sobre como essa atividade seria implementada. Como a maioria das atividades do WF, a maior parte do trabalho ocorre no método Execute substituído. As etapas desse método seriam semelhantes ao seguinte:

  1. Obter os dados do pedido a partir de uma atividade anterior. Geralmente, isso é feito por meio da associação de dados; você verá um exemplo posteriormente.
  2. Pesquisar o cliente associado ao pedido em um banco de dados.
  3. Pesquisar a taxa de impostos em um banco de dados de acordo com o local do cliente.
  4. Fazer alguns cálculos matemáticos simples usando a taxa de impostos e os itens do pedido associados ao pedido.
  5. Armazenar o total de impostos em uma propriedade à qual as atividades subsequentes podem se associar para concluir o processo de check-out.
  6. Sinalizar para o runtime do fluxo de trabalho que essa atividade foi concluída por meio do retorno do sinalizador de status Completed dos métodos Execute.

É importante reconhecer que você não aguarda em nenhum momento. Você trabalha o tempo todo. O método Execute é executado simplesmente por meio das etapas e é concluído rapidamente. Essa é a essência de uma atividade síncrona: todo o trabalho é feito no método Execute.

As atividades assíncronas são diferentes. Diferentemente de suas correspondentes síncronas, as atividades assíncronas são executadas por um período e depois aguardam um estímulo externo. Enquanto aguardam, as atividades ficam ociosas. Quando o evento ocorre, a atividade reinicia a operação e conclui sua execução.

Um exemplo de atividade assíncrona é a etapa no processo de contratação quando uma inscrição para uma função precisa ser analisada por um gerente. Considere o que poderia acontecer se o gerente estivesse em férias e não tivesse a oportunidade de analisar a inscrição até a próxima semana. Esse bloqueio no meio do método Execute, enquanto se aguarda essa resposta, é totalmente inaceitável. Quando o software aguarda por uma pessoa, talvez ele precise aguardar por muito tempo. Você precisa considerar isso no seu design.

O que exatamente significa ocioso?

Neste ponto, a semântica do idioma inglês e a semântica arquitetônica divergem. Vamos voltar para uma etapa anterior ao WF e pensar de modo mais genérico sobre o que significa estar ocioso.

Considere a seguinte classe que usa Web services para alterar uma senha:

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;
  }
}

O método ChangePassword fica ocioso? Se fica, onde?

O thread desse método é bloqueado ao aguardar uma resposta HTTP do UserService. Assim, conceitualmente a resposta é sim, o thread fica ocioso enquanto aguarda a resposta do serviço. Mas, o thread pode realmente executar outras tarefas enquanto você aguarda o serviço? Não, não da maneira como ele é utilizado no momento. Assim, da perspectiva do WF, esse "fluxo de trabalho" nunca fica ocioso.

Por que ele nunca fica ocioso? Imagine que você tinha alguma classe de agendador maior que deveria executar operações como ChangePassword de forma eficiente. Eficiente quer dizer executar diversas operações em paralelo, usando o número mínimo de threads necessários para um paralelismo completo e assim por diante. Acontece que a chave para essa eficiência é saber quando a operação está em execução e quando está ociosa. Isso porque quando uma operação se torna ociosa, o agendador pode usar o thread que estava executando a operação para executar outras tarefas, até que a operação esteja pronta para ser executada novamente.

Infelizmente, o método ChangePassword é completamente opaco para o agendador. Porque, apesar de haver um período em que ele fica efetivamente ocioso, visto de fora pelo agendador, esse método é uma unidade única de trabalho de bloqueio. O agendador não consegue dividir essa unidade de trabalho e reutilizar o thread durante o período ocioso.

Transformando tarefas síncronas em assíncronas

Você pode adicionar esta transparência de agendamento necessária à operação dividindo a operação em duas partes: uma que é executada até o momento em que a operação fica possivelmente ociosa e outra que executa o código após o estado ocioso.

Para o exemplo hipotético mostrado anteriormente, você poderia usar os recursos assíncronos fornecidos pelo próprio proxy do Web service. Lembre-se de que isso é uma simplificação e que o WF realmente funciona de forma um pouco diferente, como você verá em breve.

Na Figura 2, eu criei uma versão aprimorada do método de alteração de senha chamado ChangePasswordImproved. Eu criei o proxy do Web service da mesma forma que antes. Então, o método registra um método de retorno de chamada a ser notificado quando o servidor tiver respondido. Em seguida, eu executo de forma assíncrona a chamada de serviço e informo ao agendador que a operação está ociosa, mas não encerrada, retornando Status.Executing. Esta etapa é importante; é ela que permite que outras tarefas sejam realizadas pelo agendador enquanto o meu código está ocioso. Finalmente, quando o evento concluído ocorrer, eu chamo o agendador para sinalizar que a operação foi concluída e que ele pode continuar.

Figura 2 Chamada simples do serviço de alteração de senha

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 );
  }
}

Fluxos de trabalho e atividades

Agora, vou aplicar o conceito de ociosidade de uma operação à criação de atividades no WF. Isso é bastante semelhante ao que vimos antes, mas agora eu preciso trabalhar no modelo do WF.

O WF é fornecido com diversas atividades internas. Entretanto, ao iniciar a criação de sistemas reais com o WF pela primeira vez, rapidamente você desejará começar a criar suas próprias atividades reutilizáveis personalizadas. Isso é simples de fazer. Basta definir uma classe que derive da classe Activity amplamente presente. Veja um exemplo básico:

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

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

Para que sua atividade execute algo útil, é necessário substituir o método Execute. Se estiver criando uma atividade síncrona de curta duração, simplesmente implemente a operação de sua atividade dentro desse método e retorne o status Closed.

Existem alguns problemas maiores que provavelmente as atividades do mundo real deverão considerar. Como sua atividade se comunicará com outras atividades dentro do fluxo de trabalho e do aplicativo maior que hospeda o fluxo de trabalho? Como ela acessará serviços como sistemas de bancos de dados, interação da interface do usuário e assim por diante? Para criar atividades síncronas, esses problemas são relativamente diretos.

A criação de atividades assíncronas, por outro lado, pode ser uma tarefa bem mais complexa. Felizmente, o padrão utilizado é repetido na maioria das atividades assíncronas. Na verdade, você pode capturar facilmente esse padrão em uma classe básica, como demonstrarei em breve.

Estas são as etapas básicas necessárias para a criação da maioria das atividades assíncronas:

  1. Criar uma classe que derive de Activity.
  2. Substituir o método Execute.
  3. Criar uma fila de fluxo de trabalho que possa ser usada para receber a notificação de que o evento assíncrono que você estava aguardando foi concluído.
  4. Assinar o evento QueueItemAvailable da fila.
  5. Iniciar a execução da operação de longa duração (por exemplo, enviar um email solicitando que um gerente analise uma inscrição para uma função de trabalho).
  6. Aguardar que um evento externo ocorra. Isso sinaliza efetivamente que a atividade se tornou ociosa. Você indica isso para o runtime do fluxo de trabalho retornando ExecutionActivityStatus.Executing.
  7. Quando o evento ocorrer, o método que manipula o evento QueueItemAvailable remove o item da fila, o converte no tipo de dado esperado e processa os resultados.
  8. Normalmente, isso conclui a operação da atividade. O runtime do fluxo de trabalho é então sinalizado, retornando ActivityExecutionContext.CloseActivity.

Persistência

No início deste artigo, eu disse que os dois pontos fundamentais de que precisava para atingir a independência de processos por meio do fluxo de trabalho eram as atividades assíncronas e um serviço de persistência. Você acabou de ver a parte das atividades assíncronas. Agora, vamos nos aprofundar na tecnologia por trás da persistência: serviços de fluxo de trabalho.

Os serviços de fluxo de trabalho são um ponto importante de extensibilidade do WF. O runtime do WF é uma classe que você instancia no seu aplicativo para hospedar todos os fluxos de trabalho em execução. Essa classe tem duas metas de design opostos que são atingidos simultaneamente através do conceito de serviços de fluxo de trabalho. A primeira meta é que esse runtime de fluxo de trabalho seja um objeto leve que pode ser usado em diversos locais. A segunda meta é que esse runtime forneça recursos eficientes para os fluxos de trabalho enquanto estiverem em execução. Por exemplo, ele pode fornecer a capacidade de persistir automaticamente fluxos de trabalho ociosos, acompanhar o progresso de fluxos de trabalho e dar suporte a outros recursos personalizados.

O runtime do fluxo de trabalho permanece leve por padrão, pois somente alguns desses recursos são internos. Serviços mais pesados como a persistência e o acompanhamento são instalados opcionalmente por meio do modelo de serviço. Na verdade, segundo a definição, serviço é qualquer recurso global que você deseja fornecer a seus fluxos de trabalho. Esses serviços são instalados no runtime simplesmente chamando o método AddService na classe WorkflowRuntime:

void AddService(object service)

Como AddService leva uma referência System.Object, você pode adicionar qualquer coisa que seja necessária ao seu fluxo de trabalho.

Eu vou trabalhar com dois serviços. Primeiro, usarei WorkflowQueuingService para acessar as filas de fluxo de trabalho que são fundamentais para criar atividades assíncronas. Esse serviço é instalado por padrão e não pode ser personalizado. O outro serviço é o SqlWorkflowPersistenceService. É claro, esse serviço fornecerá os recursos de persistência e ele não é instalado por padrão. Felizmente, ele é fornecido com o WF. Basta apenas adicioná-lo ao runtime.

Com um nome como SqlWorkflowPersistenceService, você pode apostar que um banco de dados será necessário em algum momento. Você pode criar um banco de dados vazio para este fim ou pode adicionar algumas tabelas a um banco de dados existente. Pessoalmente, eu prefiro usar um banco de dados dedicado, em vez de mesclar os dados de persistência de fluxo de trabalho com meus outros dados. Assim, eu crio um banco de dados vazio no SQL Server chamado WF_Persist. Eu crio o esquema de banco de dados e os procedimentos armazenados necessários executando alguns scripts. Eles são instalados como parte do Microsoft .NET Framework e, por padrão, estão localizados na pasta

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

Execute primeiro o script SqlPersistenceService_Schema.sql e depois o script SqlPersistenceService_Logic.sql. Agora, eu posso usar esse banco de dados para a persistência, passando a cadeia de conexão para o serviço de persistência:

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

wfRuntime.AddService(sqlSvc);

Esta simples chamada do método AddService é tudo o que é necessário para começar a descarregar fluxos de trabalho ociosos e armazená-los no banco de dados, e então restaurá-los quando forem novamente necessários. O runtime do WF se encarrega de todo o restante.

Tornando real

Agora que possui a base técnica suficiente, você pode reuni-la para criar um site do ASP.NET que ofereça suporte a operações de longa duração. Os três elementos principais que você verá neste exemplo são a construção de atividades assíncronas, a integração do runtime do fluxo de trabalho no aplicativo Web e a comunicação com o fluxo de trabalho a partir das páginas da Web.

Eu vou trabalhar com uma empresa de consultoria em .NET hipotética chamada Trey Research. Eles desejam automatizar o processo de recrutamento e contratação de seus consultores. Assim, vou criar um site do ASP.NET para oferecer suporte a esse processo de contratação. Vou tentar manter as coisas bem simples, mas há várias etapas para o processo:

  1. Um candidato ao trabalho visitará o site da Trey Research e comunicará seu interesse em um trabalho.
  2. Será enviado um email ao gerente informando que há um novo candidato.
  3. Esse gerente analisará a inscrição e aprovará o candidato para uma determinada função.
  4. Será enviado um email para o candidato com informações sobre a função proposta.
  5. O candidato visitará o site e aceitará ou rejeitará a função.

Esse processo é direto, mas há diversas etapas durante as quais a inscrição aguarda que uma pessoa retorne e preencha algumas outras informações. Esses pontos ociosos podem levar bastante tempo. Por isso, eles são perfeitos para demonstrar um processo de longa duração.

Esse aplicativo Web é fornecido no código-fonte do artigo. Entretanto, para ver o efeito total, será necessário criar o banco de dados de persistência e configurar o exemplo para usá-lo. Eu criei uma opção para gerenciar se o serviço de persistência está ativo ou inativo, e o defini para ficar inativo por padrão. Para ativá-lo, defina usePersistDB como true na seção AppSettings de web.config. Se desejar vê-lo em execução enquanto você lê, examine o visualizador de trabalho assíncrono em execução no meu site.

fig03.gif

Figura 3 O processo de contratação como um fluxo de trabalho

Eu começarei criando o fluxo de trabalho completamente independente do ASP.NET. Para criar o fluxo de trabalho, vou criar quatro atividades personalizadas. A primeira é uma atividade de envio de email que será uma atividade síncrona simples. As outras três representarão as etapas 1, 3 e 5 mostradas anteriormente e serão atividades assíncronas. Essas atividades são a chave para o êxito da operação de longa duração. Eu as chamarei GatherEmployeeInfoActivity, AssignJobActivity e ConfirmJobActivity, respectivamente. Em seguida, combinarei essas atividades no fluxo de trabalho reconhecidamente básico mostrado na Figura 3.

A atividade de envio de email é direta, de forma que não entrarei nos detalhes dessa atividade neste artigo. Trata-se de uma atividade síncrona como a classe MyActivity mostrada anteriormente. Examine o download do código para obter detalhes.

Assim, resta-me a criação das três atividades assíncronas. Eu economizarei bastante trabalho se puder encapsular esse processo de oito etapas para a criação de uma atividade assíncrona em uma classe básica comum. Com essa finalidade, definirei uma classe chamada AsyncActivity (veja a Figura 4). Observe que esta listagem não inclui vários métodos auxiliares internos ou a manipulação de erros presentes no código real. Esse detalhes foram deixados de lado por questões de brevidade.

Figura 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);
}

Nesta classe base, você verá que eu empacotei várias das partes monótonas e repetitivas da criação de uma atividade assíncrona. Vamos analisar essa classe de cima para baixo. Iniciando com o construtor, eu passo uma cadeia de caracteres para o nome da fila. As filas de fluxo de trabalho são pontos de entrada para o aplicativo host (as páginas da Web) passar dados em atividades, enquanto continua menos rígido. Essas filas são referenciadas por nome e instância de fluxo de trabalho, de forma que toda atividade assíncrona precisa de seu próprio nome de fila distinto.

Em seguida, eu defino o método GetQueue. Como você pode ver, acessar e criar filas de fluxo de trabalho é fácil, mas de certa forma monótono. Eu criei este método como um método auxiliar para usar dentro dessa classe e de classes derivadas.

Depois, eu defino um método chamado SubscribeToItemAvailable. Esse método encapsula os detalhes de assinatura do evento disparado quando um item chega na fila de fluxo de trabalho. Quase sempre isso representa a conclusão de um longo período de espera em que o fluxo de trabalho esteve ocioso. Assim, o caso de uso é semelhante ao seguinte:

  1. Iniciar a operação de longa duração e chamar SubscribeToItemAvailable.
  2. Informar o runtime do fluxo de trabalho que a atividade está ociosa.
  3. A instância de fluxo de trabalho é serializada para o banco de dados pelo serviço de persistência.
  4. Quando a operação é concluída, um item é enviado para a fila de fluxo de trabalho.
  5. Isso dispara a instância de fluxo de trabalho a ser restaurada do banco de dados.
  6. O método de modelo abstrato OnQueueItemAvailable é executado pelo AsyncActivity base.
  7. A atividade conclui sua operação.

Para ver essa classe AsyncActivity em ação, vamos implementar a classe AssignJobActivity. As duas outras atividades assíncronas são semelhantes e são fornecidas no download do código.

Na Figura 5, você pode ver como AssignJobActivity usa o modelo fornecido pela classe base AsyncActivity. Eu substituo Execute para fazer qualquer trabalho preliminar iniciar a atividade de longa duração, apesar de, nesse caso, não haver uma. Em seguida, eu assino o evento para quando houver mais dados disponíveis.

Figura 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;
  }
}

Existe aqui um contrato implícito de que o aplicativo host, a página da Web, enviará um novo objeto Job na fila da atividade quando tiver coletado essas informações do gerente. Isso sinalizará à atividade de que ela pode continuar. O funcionário será atualizado no banco de dados. A próxima atividade no fluxo de trabalho enviará um email para o candidato a funcionário informando-o de que essa é a tarefa proposta para sua função.

Integração com o ASP.NET

É assim que funciona dentro do fluxo de trabalho. Mas, como você inicia o fluxo de trabalho? Como a página da Web realmente coleta a oferta de trabalho do gerente? Como ela passa o Job para a atividade?

Comecemos pelo princípio: vejamos como iniciar o fluxo de trabalho. Na página de aterrissagem do site, há um link Apply Now (Inscrever-se Agora). Quando o candidato clica nesse link, ele inicia o fluxo de trabalho e a navegação pela interface do usuário em paralelo:

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

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

Eu simplesmente chamo CreateWorkflow no runtime do fluxo de trabalho e inicio a instância de fluxo de trabalho. Depois, eu acompanho a instância do fluxo de trabalho, passando o ID da instância a todas as páginas da Web subsequentes como um parâmetro de consulta.

Como eu envio dados da página da Web de volta para o fluxo de trabalho? Vamos examinar, na Figura 6, a página do trabalho atribuído, na qual um gerente escolhe um trabalho para um candidato.

Figura 6 Atribuindo um trabalho

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.";
  }
}

A página da Web de atribuição de trabalho é, em grande parte, um simples formulário de entrada. Ela possui uma lista suspensa de trabalhos disponíveis e uma caixa de texto para o salário proposto. Ela também exibe o candidato atual, mas esse código é omitido da listagem. Quando o gerente atribui uma função e um salário ao candidato, ele clicará no botão de envio e executará o código na Figura 6.

Essa página usa o ID da instância de fluxo de trabalho como um parâmetro de cadeia de caracteres de consulta para pesquisar a instância de fluxo de trabalho associada. Em seguida, um objeto Job é criado e inicializado com os valores do formulário. Finalmente, eu envio essas informações de volta à atividade, colocando o trabalho na fila dessa atividade. Esta é a etapa principal que recarrega o fluxo de trabalho ocioso e permite que ele continue sendo executado. AssignJobActivity associará esse trabalho com o funcionário coletado anteriormente e os salvará em um banco de dados.

Essas duas últimas listagens de código enfatizam com as filas de fluxo de trabalho são fundamentais para o sucesso de atividades assíncronas e a comunicação do fluxo de trabalho com o host externo. Também é importante observa que, aqui, o uso do fluxo de trabalho não tem influência sobre o fluxo de páginas. Eu também poderia usar o WF para controlar o fluxo de páginas, mas esse não é o enfoque deste artigo.

Na Figura 6, você viu que eu acessei o runtime do fluxo de trabalho através da classe de aplicativo global, da seguinte maneira:

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

Isso me leva ao ponto final da integração do Windows Workflow no nosso aplicativo Web: todos os fluxos de trabalho são executados dentro do runtime do fluxo de trabalho. Você pode ter tantos runtimes de fluxo de trabalho quantos desejar no seu AppDomain; porém, na maior parte das vezes faz sentido ter um único runtime de fluxo de trabalho. Por causa disso e devido ao fato de o objeto de runtime do WF ser thread-safe, eu o tornei uma propriedade estática pública da classe de aplicativo global. Além disso, eu inicio o runtime de fluxo de trabalho no evento de início do aplicativo e o interrompo no evento de parada do aplicativo. A Figura 7 mostra uma versão abreviada da classe de aplicativo global.

Figura 7 Iniciando o runtime de fluxo de trabalho

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.
  }
}

No evento de início do aplicativo, eu crio o runtime, instalo o serviço de persistência e inicio o runtime. E, no evento de final do aplicativo, eu interrompo o runtime. Essa é uma etapa importante. Ela será bloqueada se houver fluxos de trabalho em execução, até que eles tenham sido descarregados. Depois de interromper o runtime, eu chamo Dispose. A chamada de StopRuntime e depois de Dispose pode parecer redundante, mas não é. Você precisa chamar os dois métodos nessa ordem.

Alguns pontos a considerar

Vou colocar alguns pontos que não abordei diretamente para você pensar, apresentados no formato de pergunta e resposta. Por que eu não usei ManualWorkflowSchedulerService? Freqüentemente, quando as pessoas falam sobre a integração do WF com o ASP.NET, elas enfatizam que você deve substituir o agendador padrão para fluxos de trabalho (que usam o pool de threads) por um serviço chamado ManualWorkflowSchedulerService. Isso porque ele não é necessário e nem mesmo realmente apropriado para nossas metas de longa duração. O agendador manual é adequado quando você espera executar um único fluxo de trabalho até sua conclusão em uma determinada solicitação. Ele faz menos sentido quando o seu fluxo de trabalho será executado além dos tempos de vida dos processos, para não dizer além das solicitações.

Existe uma forma de acompanhar o progresso atual de uma determinada instância de fluxo de trabalho? Sim, há um serviço de acompanhamento completo incorporado ao WF e ele é usado de maneira semelhante ao serviço de persistência do SQL. Consulte a coluna Foundations de março de 2007, "Serviços de controle no Windows Workflow Foundation", por Matt Milner.

Resumo

Posso resumir as técnicas abordadas neste artigo em algumas etapas. Eu comecei descrevendo por que os processos de trabalho do ASP.NET e o modelo de processo em geral não são apropriados para operações de duração muito longa. Para ultrapassar essa limitação, eu tirei proveito de dois recursos do WF que são combinados para atingir a independência de processos: atividades assíncronas e persistência de fluxo de trabalho.

Como a criação de atividades assíncronas pode ser um tanto complicada, eu encapsulei os detalhes na classe base AsyncActivity apresentada neste artigo. Em seguida, eu expressei a operação de longa duração como um fluxo de trabalho sequencial criado com atividades assíncronas de forma a poder incorporá-la em um aplicativo Web e obter a independência de processos gratuitamente.

Por fim, eu demonstrei que a integração do fluxo de trabalho no ASP.NET consiste em duas partes básicas: a comunicação com as atividades através de uma fila de fluxo de trabalho e a hospedagem do runtime na classe de aplicativo global.

Agora que conheceu a integração do WF com o ASP.NET para oferecer suporte a operações de longa duração, você tem mais uma ferramenta poderosa para criar soluções sobre o .NET Framework.

Michael Kennedy é instrutor da DevelopMentor, sendo especialista em tecnologias principais do .NET, além de metodologias de desenvolvimento TDD e ágeis. Acompanhe o trabalho do Michael através de seu site e blog em michaelckennedy.net.