Desenvolvimento - WCF/WPF

WCF - Durable Services

A Microsoft introduziu na versão 3.5 do WCF uma funcionalidade chamada de Durable Services. Como o próprio nome diz, ele possibilita a criação de serviços que podem ser persistidos, sobrevivendo a eventuais reciclagens do host que o hospeda e também de reinicializações do cliente. Com este artigo vamos analisar como devemos proceder para incorporar esta funcionalidade em nossos serviços.

por Israel Aéce



Vimos nos artigos anteriores como criar, hospedar e consumir serviços com o WCF. Todas as tarefas que esses serviços disponibilizavam tinham uma duração curta, ou seja, o processo completo se resumia apenas na chamada de uma única operação. Mas é muito comum, em aplicações distribuídas, termos processos que podem durar muito mais tempo para completar toda a tarefa. Neste caso, pode sercomplicado manter ativa a instância do serviço ou do cliente por todo esse tempo.

A Microsoft introduziu na versão 3.5 do WCF uma funcionalidade chamada de Durable Services. Como o próprio nome diz, ele possibilita a criação de serviços que podem ser persistidos, sobrevivendo a eventuais reciclagens do host que o hospeda e também de reinicializações do cliente. Com este artigo vamos analisar como devemos proceder para incorporar esta funcionalidade em nossos serviços.

Independentemente do modelo de gerenciamento de instância que utilize, a classe que representa o serviço tem um tempo de vida determinado, e se por algum motivo a aplicação que hospeda o mesmo for reinicializada, todo o estadodo objetoserá perdido. Isso ocorre porque todo esse estado, que cada objeto que representao serviço mantém, é armazenado em memória, ou seja, é volátil e não conseguirá sobreviver durante possíveis reinicializações, e pode comprometer a regra de negócio, caso você dependa desta informação.

Com os Durable Services, podemos persistir as informações em algum repositório ao invés de utilizar a memória. Isso irá garantir que as informações sejam mantidas, mesmo que o processo demore dias para ser concluído. Mesmo que a sessão com o cliente seja destruída, você conseguirá restaurar o estado mais tarde. A Microsoft já disponibilizou um provider para armazenar as informações no banco de dados SQL Server, mas nada impede de você customizar, optando pela criação de um provider que armazene as informações em arquivos XML.

Para guiar os exemplos, teremos o seguinte cenário: um serviço que irá fornecer as operações necessárias de um comércio eletrônico, como por exemplo a criação de um carrinho, a inserção de novos itens, recuperação dos itens selecionados e a finalização da compra. A idéia é que os itens selecionados pelo usuário, sejam mantidos além das sessões e também do desligamento da aplicação cliente e, eventualmente, do serviço.

Os tipos que usaremos estão contidos no Assembly System.WorkflowServices.dll. Antes de efetivamente começarmos a ver os tipos disponibilizados por esse Assembly, precisamos preparar a base de dados para que ela consiga acomodar as informações. Felizmente já temos todo o script pronto, apenas será necessário executá-lo. Para isso, basta ir até o seguinte endereço: %windir%\Microsoft.Net\Framework\v3.5\SQL\EN\. Lá temos quatro arquivos, sendo dois para a criação e dois para a exclusão. No nosso caso, devemos executar os seguintes arquivos: SqlPersistenceProviderSchema.sql e SqlPersistenceProviderLogic.sql, nesta mesma ordem. O primeiro é responsável por criar a tabela InstanceData, enquanto o segundo cria as Stored Procedures com toda a lógica de inserção, exclusão e carregamento das instâncias.

Podemos serializar o estado do serviço de forma binária (o padrão) ou através de Xml, e para suportar isso, temos duas colunas na tabela InstanceData, chamadas de “instance” e “instanceXml”. A primeira é utilizada quando o estado é serializado em formato binário, enquanto a segunda apenas será utilizada quando o conteúdo for persistido em Xml. Além dessas duas colunas, ainda temos a coluna “id”, queterá o seu valorpropagado do serviço para o cliente e sendo devolvido do cliente para o serviço.Esse ID representa a instância (estado) do serviço que foi armazenada. Falaremos detalhadamente sobre ela mais tarde, ainda neste artigo.

Implementação

O contrato do serviço não sofrerá qualquer alteração. Você deve continuar decorando a interface com o atributo ServiceContractAttribute e as operações que serão expostas, com o atributo ServiceOperationAttribute. As mudanças começam a aparecer na classe que representará o serviço. O primeiro passo para a criação de um serviço durável, é decorar a classe que representa o serviço com o atributo DurableServiceAttribute. Durante o carregamento do serviço, este atributo irá garantir que o modo de gerenciamento de concorrência não esteja definido como Multiple e que o modo de gerenciamento de instância deve ser PerSession. É importante dizer que o estado deve ser de uso exclusivo de uma sessão. Se desejar que o estado seja compartilhado com todos os clientes (sessões), então você deve optar pelo modo Multiple de gerenciamento de instância. E mais um detalhe importante, temos que aplicar o atributo SerializableAttribute, assim como todas as classes que desejamos serializar.

Cada operação que irá compor o serviço durável, deverá ser decorada com o atributo DurableOperationAttribute. Esse atributo indica ao runtime que ao completar cada operação, o estado do serviço deverá ser persistido fisicamente. Essa classe possui duas propriedades, que por padrão são sempre False: CanCreateInstance e CompletesInstance. A primeira delas indica se uma nova instância do serviço deve ser criada ao executar a respectiva operação. Já a segunda propriedade, indica se a instância será removida da memória e excluída do repositório quando a operação for executada. A classe abaixo exibe como configurar esses atributos, com a implementação omitida para poupar espaço:

[Serializable]
[DurableService]
public class ServicoDeComercioEletronico : IComercioEletronio
{
private List<ItemDaCompra> _produtos;
private string _usuario;

[DurableOperation(CanCreateInstance = true)]
public void CriarCarrinho(string usuario) { }

[DurableOperation]
public void AdicionarItem(ItemDaCompra item) { }

[DurableOperation]
public ItemDaCompra[] RecuperarItensDaCompra() { }

[DurableOperation(CompletesInstance = true)]
public void FinalizarCompra() { }
}

Não há nenhuma mudança drástica na implementação do serviço, apenas temos que nos atentar ao estado dos membros internos, que serão mantidos entre as chamadas (lembre-se de que essa persistência sobreviverá mesmo após o host ou o cliente ser encerrado).

Mudanças mais significativas são realizadas para expor o serviço. Isso se deve ao fato da necessidade de propagar o ID que representa o estado o serviço. O ID em questão é o mesmo que é gerado durante a inserção do registro na tabela acima mencionada. Quando o runtime encontra uma operação que possui o atributo DurableOperationAttribute e com a propriedade CanCreateInstance definida como True, ele irá criar um registro na tabela InstanceData, capturar o ID gerado e devolver para o cliente. Todas as operações subsequentes devem embutir esse ID.

Antes da operação ser efetivamente executada, o runtime irá extrair a instância da base de dados, abastecer os membros privados previamente serializados, e depois disso irá executar a operação; ao retornar, o runtime devolve os dados para a base de dados, com as informações atualizadas. Finalmente, quando o runtime encontra uma operação com o atributo DurableOperationAttribute e com a propriedade CompletesInstance definida como True, o respectivo registro que representa o estado do serviço é excluído da base de dados.

Como podemos perceber, todo o processo acontece utilizando o ID gerado durante a primeira requisição, e a necessidade de mantê-lo durante as requisições futuras se faz necessário, caso queira manter o estado. Visando essa manutenção do ID, a Microsoft criou três novos bindings: NetTcpContextBinding, BasicHttpContextBinding e WSHttpContextBinding. Cada um deles herda diretamente dos bindings tradicionais (NetTcpBinding, BasicHttpBinding e WSHttpBinding), apenas trazendo o suporte necessário para gerenciar o ID de persistência.

Cada um destes bindings sobrescrevem o método CreateBindingElements, criando uma instância da classe ContextBindingElement. Este elemento é responsável por gerenciar como o ID será propagado entre o serviço e o cliente (ou vice-versa). Em seu construtor, recebe uma das seguintes opções expostas pelo enumerador ContextExchangeMechanism:

- ContextSoapHeader: O ID será enviado através de um header na mensagem SOAP. É o valor padrão.
- HttpCookie: O ID será definido em um cookie.

Cada um dos bindings utiliza um mecanismo diferente. O binding NetTcpContextBinding utiliza a primeira opção, mesmo porque não é possível utilizar cookiesatravés deTCP. Já o BasicHttpContextBinding utiliza cookies para manter o ID, e irá disparar uma exceção caso não haja suporte aos mesmos. Finalmente, o binding WSHttpContextBinding utiliza cookies quando suportado, e SOAP Headers quando não há tal suporte. Com isso, não precisamos nos preocupar como o ID será propagado entre as partes, apenas teremos a responsabilidade de armazenar o ID para conseguir carregar uma instância previamente criada. O trecho de código abaixo ilustra como proceder para configurar a classe ServiceHost, utilizando o binding NetTcpContextBinding:

using (ServiceHost host =
new ServiceHost(typeof(ServicoDeComercioEletronico),
new Uri[] { new Uri("net.tcp://localhost:3832") }))
{
host.Description.Behaviors.Add(ConfigurarPersistencia());
host.AddServiceEndpoint(typeof(IComercioEletronio), new NetTcpContextBinding(), "srv");

host.Open();
Console.ReadLine();
}

Utilizar um dos bindings que vimos acima não é o bastante. Ainda precisamos configurar a persistência das informações, e como já era de se esperar, isso será feito através de um behavior de serviço, chamado PersistenceProviderBehavior. Note que há um método customizado chamado de “ConfigurarPersistencia”, que é o responsável por criar e retornar a instância do provider que fará a persistência das informações.

O construtor da classe PersistenceProviderBehavior recebe como parâmetro uma instância da classe PersistenceProviderFactory. Essa classe abstrata serve como base para todos os providers, inclusive aquele que a Microsoft já disponibilizou para efetuar a persistência no SQL Server. Caso você queira criar o seu próprio provider, então será necessário criar duas classes: o provider em si (herdando da classe PersistenceProvider) e a factory responsável por criar e gerir as instâncias do respectivo provider (herdando de PersistenceProviderFactory).

Como comentado acima, utilizaremos o provider para SQL Server, chamado SqlPersistenceProviderFactory, que está contido no namespace System.ServiceModel.Persistence. Para utilitizá-lo, é importante que você prepare a sua base de dados, rodando os scripts mencionados acima. Como estou utilizando a configuração imperativa, então vou criar a instância da classe SqlPersistenceProviderFactory, que em seu construtor receberá a string de conexão com a base de dados. Note que no código abaixo, além da string de conexão, ainda é passado um valor boleano, indicando como será efetuado a persistência, onde True indica que será serializada em Xml e False em formato binário (padrão).

static PersistenceProviderBehavior ConfigurarPersistencia()
{
return new PersistenceProviderBehavior(
new SqlPersistenceProviderFactory(
ConfigurationManager.ConnectionStrings["SqlConnString"].ConnectionString, true));
}

Consumindo o Serviço

Para referenciar e invocar operações que compõem serviços duráveis, não há diferenças em relação ao que já conhecemos.Apenas devemos ter duas preocupações: utilizar o binding correspondente ao binding utilizado pelo serviço e tambémcomo e onde armazenar o ID que representa a instância remota, que por sua vez, deverá ser devolvido do cliente para o serviço, afim de carregar o respectivo estado do mesmo.

O método “CriarCarrinho”, responsável por criar a instância, deve somente ser invocado uma única vez. Se você não se atentar a isso e chamá-lo sempre, uma nova instância será criada, perdendo todo o sentido da funcionalidade fornecida pelos serviços duráveis. Como também já foi falado acima, sempre quando o método retorna, o repositório é acionado para armazenar o estado atual do objeto, que eventualmente a operação alterou. Se você não invocar o método “FinalizarCompra” (responsável pela exclusão do registro do repositório), a instância sobreviverá a eventuais reinicializações do host ou daaplicação cliente, podendo iniciar uma tarefa e finalizá-la mais tarde, sem a preocupação de perder todo o trabalho realizado até aquele momento.

Toda a mensagem que é enviada do cliente para o serviço, deverá conter o ID que irá relacionar a mensagem a uma determinada instância. O desafio aqui é manter esse ID entre as chamadas, mas lembrando que elas podem ser feitas dias depois, e com isso, utilizar a memória não resolve o nosso problema. O que precisamos é persistir esse ID fisicamente, para que em eventuais reinicializações, sejamos capazes de restaurar o mesmo, e reenviar novas requisições para serem relacionadas aquela instância específica. Utilizaremos as classes já conhecidas do namespace System.IO para efetuar essa tarefa, juntamente com o serializador binário que o .NET disponibiliza (BinaryFormatter).

O segredo é como extrair o ID que foi enviado/gerado pelo serviço do lado do cliente. O proxy gerado durante a referência do serviço, herda diretamente da classe ClientBase<TChannel>. Essa classe possui uma propriedade chamada InnerChannel do tipo IClientChannel. A finalidade desta propriedade é expor a funcionalidade básica de comunicação e informações contextuais. Entre os membros fornecidas pela interface IClientChannel, temos o método GetProperty<T>. Este método genérico, recebe um objeto tipado que o método utilizará para efetuar a busca dentro da channel stack. Caso o objeto seja encontrado, ele é retornado; caso contrário, ele encaminha a busca para a próxima layer.

Utilizaremos este método para extrair uma classe que implementa a interface IContextManager. Como o próprio nome diz, ela representa o gerenciador do contexto do canal atual (contexto relacionado aos serviços duráveis), permitindo você ler ou definir um contexto, cominformações específicas. Para isso, ela fornece dois simples métodos: GetContext e SetContext. O primeiro retorna uma cópia do contexto atual, representado por um dicionário de dados (onde a chave e o valor são do tipo string) com os itens que foram enviados pelo serviço. Já o segundo método, SetContext, recebe um dicionário de dados (do mesmo tipo anterior), com as informações que devem ser enviadas do cliente para o serviço. Para facilitar, criei uma classe chamada “GerenciadorDeEstado”, que tem como finalidade gerir o ID que é informado pelo serviço, e reenviado para ele. Por questões de espaço, alguns membros foram omitidos:

internal static class GerenciadorDeEstado
{
private const string ARQUIVO_COM_CHAVE = "InstanceId.bin";

public static void Salvar(IClientChannel channel)
{
IContextManager context = channel.GetProperty<IContextManager>();
if (context != null)
using (FileStream fs = File.Create(ARQUIVO_COM_CHAVE))
new BinaryFormatter().Serialize(fs, context.GetContext());
}

public static void Carregar(IClientChannel channel)
{
if (JaExisteArquivo)
{
IContextManager context = channel.GetProperty<IContextManager>();
if (context != null)
using (FileStream fs = File.Open(ARQUIVO_COM_CHAVE, FileMode.Open))
context.SetContext((IDictionary<string, string>)new BinaryFormatter().Deserialize(fs));
}
}

//Outros Membros
}

Note que no método “Salvar” invocamos o método GetContext, enquanto no método “Carregar” utilizamos o método SetContext. No nosso exemplo, esse dicionário irá conter apenas uma única entrada, chamada de “instanceId”, que é justamente o GUID gerado pela inserção do registro no SQL Server.

O que irá determinar se existe ou não uma instância em aberto para esse cliente, é a existência do arquivo com o respectivo ID. Se notarmos o código abaixo, ele irá verificar a existência do arquivo. Caso não exista, então ele invoca o método “CriarCarrinho” e salvará o contexto atual (ID); caso contrário, ele apenas carregará o contexto (ID) existente, para que as chamadas para as operações sejam encaminhadas para a instância previamente criada. Na sequência, você inclui itens dentro do carrinho e, finalmente, será perguntado se deseja ou não finalizar a compra. Se disser não, então você poderá reabrir a aplicação cliente, que os produtos adicionados ainda estarão disponíveis. Se optar por finalizar a compra, então você deve invocara operação“FinalizarCompra”, que como já sabemos, é responsável por remover o registro da base de dados.

using (ComercioEletronioClient proxy = new ComercioEletronioClient())
{
if (!GerenciadorDeEstado.JaExisteArquivo)
{
proxy.CriarCarrinho("Israel Aece");
GerenciadorDeEstado.Salvar(proxy.InnerChannel);
}
else
{
GerenciadorDeEstado.Carregar(proxy.InnerChannel);
}

//Incluir Itens

Console.WriteLine("\nDeseja finalizar a compra? (S)im ou (N)ão");
if (Console.ReadLine() == "S")
{
//Finaliza a Compra e remove o registro da base de dados.
proxy.FinalizarCompra();
GerenciadorDeEstado.Excluir();

Console.WriteLine("A compra foi finalizada com sucesso.");
}
}

Durante a execução, ou depois do término da aplicação cliente sem finalizar a compra, ao analisarmos a tabela InstaceData no SQL Server, notaremos um registro adicionado, onde a coluna “id” representa o ID que foi propagado para o cliente e está armazenado no arquivo “InstanceId.bin”, e a coluna "instanceXml” com os membros privados devidamente serializados em formato Xml. Abaixo temos informação formatada que está nesta coluna:

<ServicoDeComercioEletronico xmlns="http://schemas.datacontract.org/2004/07/Host"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/"
z:Id="1"
z:Type="Host.ServicoDeComercioEletronico"
z:Assembly="Host, Version=1.0.0.0">
<_produtos z:Id="2">
<_items z:Id="3"
z:Size="4">
<ItemDaCompra z:Id="4">
<NomeDoProduto z:Id="5">Mouse Microsoft</NomeDoProduto>
<Quantidade>10</Quantidade>
<Valor>100.00</Valor>
</ItemDaCompra>
<ItemDaCompra z:Id="6">
<NomeDoProduto z:Id="7">Celular Motorola</NomeDoProduto>
<Quantidade>10</Quantidade>
<Valor>40.0</Valor>
</ItemDaCompra>
<ItemDaCompra i:nil="true" />
<ItemDaCompra i:nil="true" />
</_items>
<_size>2</_size>
<_version>2</_version>
</_produtos>
<_usuario z:Id="8">Israel Aece</_usuario>
</ServicoDeComercioEletronico>

Conclusão: Através deste artigo podemos compreender a finalidade e como implementar serviços duráveis. Notamos que não há nenhuma mudança muito radical em relação ao que conhecemos para a construção de serviços em WCF, mas há alguns detalhes importantes, que se não nos atentarmos, este recurso não funcionará como o esperado. Esse tipo de serviço permite enriquecer ainda mais a experiência com o usuário, não obrigando o mesmo a finalizar a tarefa naquele momento, podendo persistir e restaurar mais tarde.

WCFDurableServices.zip (73.37 kb)

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.