O Português e o Software

by Israel Aece 28. April 2010 13:04

Artigos, preposições, pronomes, verbos, entre várias outras classes de palavras, são características de vários idiomas ao redor do mundo. Cada uma delas desempenha um papel importante, e ficaria difícil montarmos orações sem uma delas, pois coisas básicas como determinar e conectar elementos, expressar ações, etc., são por elas especificadas, e que impõem suas regras de construção.

Quando estamos criando um software, é importante que se nomeie os seus membros de forma à expressar exatamente como temos no mundo real. Isso facilita muito durante o levantamento, desenvolvimento e, principalmente, na manutenção do código, permitindo que pessoas que não conheçam exatamente os detalhes de uma linguagem de programação, olharem para o código e entenderem o que está acontecendo ali. Justamente por isso é importante que você tente manter - também - durante a criação de classes, métodos, propriedades e variáveis, os mesmos elementos gramaticais que são utilizados no mundo real.

Em um artigo anterior eu já havia comentado sobre a utilização do idioma português ao invés do inglês. Se você está criando um código de infraestrutura, como por exemplo, um repositório de logs, classes que facilitam o acesso ao registry do Windows, etc., então você pode manter isso em inglês, pois fica até mais fácil encontrar nomes mais coerentes. Já quando estamos construindo as regras de negócio, onde vamos utilizar algum modelo para o desenvolvimento (DDD, TS ou AR), devemos sim utilizar o português e nos atentarmos para manter as classes de palavras do idioma, para expressar com exatidão o negócio no mundo virtual.

Ao criar variáveis, eu considero o uso das preposições como sendo extremamente importante para conectar uma parte da variável à outra. Se vamos criar uma váriavel que irá armazenar o nome do cliente, ao invés de utilizarmos nomeCliente, é muito mais intuitivo chamarmos de nomeDoCliente. Acredito que nomear a variável como nomeCliente acaba sendo uma má interpretação do inglês, por exemplo, se fossêmos criar a mesma variável em inglês, nomearíamos para customerName, mas lembre-se de que a tradução para customer name é nome do cliente. Para elencar mais alguns exemplos práticos:

DateTime dataDeNascimento = DateTime.Now;
decimal valorTotalDoPedido = 1000.00M;
decimal? taxaDeJuros = null;
bool permiteContinuarCasoEncontreFalhasNoProcessamento = true;
Action<decimal> calculoDeSalario = (salario) => { };

O mesmo vale para métodos. O que temos em mente é que métodos devem ser representados por verbos. Isso continua sendo válido, mas você deve nomeá-lo exatamente de acordo com a sua finalidade. Geralmente você utiliza o verbo em primeiro lugar e o restante complementa de acordo com a tarefa que ele desempenha, por exemplo:

RepositorioDeClientes repositorioDeClientes = new RepositorioDeClientes();

Cliente cliente = new Cliente();
cliente.Nome = "Israel Aece";
cliente.Salario = 1000.00M;

this.CalcularLimiteDaContaCorrente(cliente);
repositorioDeClientes.Adicionar(cliente);

Depois do limite calculado, eu estou adicionando o cliente recém criado no repositório. Note que o método Adicionar é nomeado como Adicionar e não como AdicionarCliente. Como eu já estou em um repositório de clientes, eu já sei que ali eu só posso adicionar clientes. Não precisa "ratificar" isso no método. Além de desnecessário, complica a leitura.

Outro detalhe importante é quando você vai criar sobrecargas (overloads) de métodos. Na maioria destes casos, são os parâmetros (tipo e quantidade) que determinam a diferença entre as várias versões e não o nome, pois os nomes serão sempre os mesmos. Por exemplo, você tem uma classe que efetua a simulação de empréstimos pessoais, onde é necessário informar o valor a ser financiado e o CPF do cliente. Só que o cliente pode não estar com o CPF, mas tem ele na CNH ou no RG. Então você pode criar versões do método Simular, onde cada um utiliza um documento específico.

public class SimuladorDeEmprestimosPessoais
{
    public void Simular(decimal valor, RG rg)
    {
        CPF cpf = ExtrairCPFDoRG(rg);
        Simular(valor, cpf);
    }

    public void Simular(decimal valor, CNH cnh)
    {
        CPF cpf = ExtrairCPFDaCNH(cnh);
        Simular(valor, cpf);
    }

    public void Simular(decimal valor, CPF cpf)
    {
        //efetua a simulação
    }
}

Acima vemos um exemplo prático e expressivo, que olhando para os seus métodos, somos capazes de efetuar a simulação dado qualquer um dos documentos suportados (RG, CNH ou CPF) pela classe.

Como eu disse acima, com esse tipo de código é possível que pessoas que nem conheçam detalhes do C#, olhem para ele e consigam extrair e entender grande parte das funcionalidades que ele executa. Essa técnica tornará o seu trabalho e dos demais membros da equipe muito mais simples, principalmente quando precisamos efetuar algum tipo de manutenção em um código já escrito há algum tempo. A ideia é tentar praticar o que Martin Fowler já falou há algum tempo: "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.".

Tags:

General

Tecnologias que circundam o WCF

by Israel Aece 14. January 2010 06:12

Em 2006 a Microsoft lançou a versão 3.0 do .NET Framework, que nada mais era do que "grandes blocos" que foram adicionados ao 2.0. Entre esses grandes blocos, temos o WCF. Como todo mundo sabe, ele é o novo pilar para comunicação dentro da plataforma .NET. A estrutura deste framework, facilitou a entrada de novos produtos, também criados pela Microsoft, para atender cenários específicos.

Isso acaba facilitando bastante, já que grande parte da complexidade do WCF acaba sendo abstraída do desenvolvedor. Depois do .NET Framework 3.0, veio a versão 3.5, que incorporou novas funcionalidades, e mais tarde, no PDC 2009, a Microsoft publicou novos serviços, construídos em cima do WCF. Atualmente temos os seguintes tipos de serviços disponíveis:

  • Serviços SOAP: É o WCF em si. Possibilita a construção de serviços baseando-se em padrões de mercado, que tentam manter a interoperabilidade entre várias plataformas ou com outras tecnologias, como COM+, MSMQ, .NET Remoting, etc. Esses padrões regem transações, segurança, entre outras funcionalidades. A idéia aqui é permitir a construção de serviços orientado à operações que você precisa expor ao mundo, através dos mais diversos protocolos.
  • Serviços WebHttp: A partir da versão 3.5, a Microsoft trouxe a capacidade de construir serviços REST dentro do WCF. Usando métodos como POST, GET, PUT, etc., em conjunto URLs (onde você pode formatar do jeito que desejar), temos a flexibilidade de expor operações para serem consumidas diretamente, sem envolver essas requisições em envolopes SOAP, facilitando assim o consumo por aplicações AJAX, por exemplo.
  • Serviços para Dados: Semelhante a anterior, mas a idéia é expor via REST as informações contidas em um banco de dados. Inicialmente levava o nome de ADO.NET Data Services, mas depois do PDC foi renomeado para WCF Data Services.
  • Serviços de Workflow: Basicamente, a ideia é permitir que um workflow (construído pelo Windows Workflow Foundation (WF)) possa ser consumido e coordenado por serviços WCF. Situações onde você tem operações que possuem uma longa duração, a necessidade de manter o estado entre chamadas, esse tipo de serviço poderá ajudar.
  • Serviços RIA: WCF RIA Services estará disponível juntamente com o Silverlight 4.0, e simplificará a forma como você escreverá uma aplicação N-tier, onde o cliente será o próprio Silverlight.

Tags:

CSD | WCF

Introdução aos Commands

by Israel Aece 6. January 2010 14:53

Entre as várias novidades que o WPF introduziu, uma das que mais vieram para nos ajudar são os Commands, que é semelhante ao popular design pattern Command do GoF. A Microsoft implementou este padrão no WPF para permitir uma melhor organização do código, já que ações são executadas quando o usuário manuseia algum controle ou quando alguma tecla (ou uma combinação delas) for pressionada.

Para ilustrar o problema que temos atualmente, vamos imaginar que temos um controle ListBox com o nome dos clientes e um Button que em seu evento Click, recupera o item selecionado, e exibe os detalhes cadastrais deste em um Label mais abaixo. Além disso, há um item na ToolBar da aplicação que tem a mesma funcionalidade, mas além, ainda possibilita que pressionando as teclas Ctrl + D, também deverá ter o mesmo efeito, ou seja, de mostrar os detalhes do cliente selecionado. Basicamente o código seria qualquer coisa próximo disso:

private void butto1_Click(object sender, EventArgs e)
{
    this.ExibirDetalhesDoCliente();
}

private void menuItem1_Click(object sender, EventArgs e)
{
    this.ExibirDetalhesDoCliente();
}

private void ExibirDetalhesDoCliente()
{
    if (this.Clientes.SelectedItem != null)
    {
        this.DetalhesDoCliente.Text = this.Clientes.SelectedItem.ToString();
    }
}

A imagem abaixo ilustra o formulário já em funcionamento:



Imagine também que gostaria de algo mais interativo, como por exemplo, fazer com que o botão que exibe os detalhes do cliente altere entre o estado de habilitado e desabilitado, de acordo com a seleção (ou não) de um item do ListBox. Isso faria com que eu espalhasse pelo código, verificações e checagens para garantir este resultado. Dependendo de como faz isso, pode resultar em um código ruim e propício a muitas redundâncias. Além disso, muitas vezes você tem várias outras ações, que são executadas por um formulário, e podem ser reutilizadas por vários controles dentro do mesmo, ou até mesmo em outros formulários dentro da mesma aplicação.

Como há muitas ações que são acessadas através de vários controles do formulário, pode também ter condicionais que estão ligados a elas, que permite a execução de acordo com alguma regra (como ter um item selecionado no ListBox). Outro ponto importante é a execução em batch, onde você pode elencar várias ações, adicionando cada uma delas na medida que vai clicando ou manipulando algum controle.

Essa funcionalidade fornecida pelo WPF tem a finalidade de conseguir controlar, de uma forma mais elegante e eficaz, esse tipo de situação, que é muito comum em aplicações Windows. A ideia é que esse padrão separa a execução da ação (chamada daqui para frente de comando) daquele que a invoca, ou seja, no evento Click por exemplo, você não terá mais o código, mas o comando será vinculado ao controle que o executará. Dessa forma, esta técnica permitirá que você vincule o mesmo comando à vários controles, mas a implementação somente acontecerá uma única vez.

O primeiro tipo que vamos analisar, é a interface ICommand. Essa interface fornece três membros autoexplicativos: Execute, CanExecute e CanExecuteChanged. O que chama mais a atenção é o método CanExecute, que retorna um valor boleano indicando se o comando poderá ou não ser executado (através do método Execute). No nosso exemplo, o comando deverá avaliar se o ListBox possui algum item selecionado, para que assim conseguimos visualizar os detalhes do mesmo.

Atualmente no WPF somente existe uma única classe que implementa esta interface, que é a RoutedCommand, e por tabela, a RoutedUICommand, que herda da RoutedCommand. Esse tipo de comando leva esse nome porque trabalha de forma semelhante aos routed events, ou seja, se o controle atual não for capaz de executar o comando, ele delega para o próximo elemento da árvore, até que alguém o execute. A única diferença entre as classes RoutedCommand e RoutedUICommand é que a segunda fornece uma propriedade chamada Text, que nada mais é do que a descrição do comando, que é usado para ser exibido por algum controle de UI.

Para começarmos a modificar o código acima, aquele para exibição de detalhes do cliente, vamos criar uma instância da classe RoutedUICommand, configurando em seu construtor o nome do comando e o tipo da classe onde ela é criada. Note que o modelo de criação segue mais ou menos a mesma forma de criação das dependency properties e dos routed events. Logo após a criação, note que estamos adicionando a possibilidade de utilizar uma combinação de teclas para executar o comando, através da coleção de gestures:

public static class MeusComandos
{
    public static readonly RoutedUICommand ExibirDetalhesDoCliente;

    static MeusComandos()
    {
        ExibirDetalhesDoCliente = 
            new RoutedUICommand("Exibir Cliente", "ExibirDetalhesDoCliente", typeof(MeusComandos));
        ExibirDetalhesDoCliente.InputGestures.Add(new KeyGesture(Key.D, ModifierKeys.Control));
    }
}

É importante dizer que o controle por si só não faz nada. Depois de decidir onde irá usá-lo, você precisa determinar o que ele irá fazer e se ele pode ou não executar esse comando. Para vincular o comando à algum controle, você precisará fazer uso dos CommandBindings. CommandBindings é uma espécie de listeners que aguardam um determinado comando ser executado, e utilizará os métodos já conhecidos (CanExecute e Execute) quando este comando for acionado. Repare no código abaixo, que em momento nenhum o código relacionado ao comando está vinculado ao controle; o que vinculamos no(s) controle(s) é o comando que ele deverá executar, através de um command binding.

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
        this.ConfigurarComandos();
    }

    private void ConfigurarComandos()
    {
        this.CommandBindings.Add(
            new CommandBinding(
                MeusComandos.ExibirDetalhesDoCliente,
                cb_Executed,
                cb_CanExecute));

        this.button1.Command = MeusComandos.ExibirDetalhesDoCliente;
        this.menuItem1.Command = MeusComandos.ExibirDetalhesDoCliente;
    }

    private void cb_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        this.label1.Content =
            ((ListBoxItem)this.listBox1.SelectedItem).Content.ToString();
    }

    private void cb_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = this.listBox1.SelectedItem != null;
    }
}

A classe CommandBinding fornece vários eventos, e entre eles os eventos CanExecute e Executed, que é exatamente onde colocamos a regra para determinar se o comando pode ser executado, e o código referente ao comando propriamente dito, que no nosso caso, é mostrar os detalhes. Note que vinculamos a instância no comando criado (ExibirDetalhesDoCliente) na propriedade Command (exposta pela interface ICommandSource) do controle Button e do Menu, que por sua vez, recebe a instância de alguma classe que implemente a interface ICommand. Repare que não estamos nos vinculamos ao evento Click dos controles, pois internamente, depois deste evento (Click) disparado, o WPF avalia se a propriedade Command está definida com algum comando, e estando, o executará, extraindo dos command bindings quais são os eventos correspondentes ao comando. Como estamos lidando com eventos aqui, a propriedade CanExecute defina no parâmetro CanExecuteRoutedEventArgs, é que recebe essa resposta e encaminha para o WPF avaliar, e se estiver True, o executará. O código acima fica mais simples de ler, centraliza as regras e facilita a manutenção.

Algo interessante a se notar é que os controles que estão com o comando vinculado, monitoram o retorno do método/evento CanExecute, definindo a sua respectiva propriedade Enabled com o resultado deste método, ou seja, deixará o controle desabilitado até que ele retorna True. A imagem abaixo ilustra esse comportamento. Do lado esquerdo, a imagem está com os dois controles (Menu e Button) desabilitados até o momento que seleciono um dos itens do ListBox, que é a imagem da direita. Ao clicar no Menu, no Button ou pressionar Ctrl + D, o nome selecionado aparecerá no Label.



É interessante dizer também que os command bindings podem ser definidos de forma declarativa, através do XAML, eliminando assim todo o código que está definido dentro do método ConfigurarComandos.

<Window ... xmlns:local="clr-namespace:WpfApplication1">
    <Window.CommandBindings>
        <CommandBinding
            Command="local:MeusComandos.ExibirDetalhesDoCliente"
            CanExecute="b_CanExecute"
            Executed="b_Executed" />
    </Window.CommandBindings>

    <Grid>
        <Button Command="local:MeusComandos.ExibirDetalhesDoCliente" ... />
        <Menu Name="menu1">
            <MenuItem Header="Cadastro">
                <MenuItem
                    Name="menuItem1"
                    Command="local:MeusComandos.ExibirDetalhesDoCliente" />
            </MenuItem>
        </Menu>
    </Grid>
</Window>

Comandos Predefinidos

Há alguns comandos que já são bastante conhecidos, como é caso do Copiar, Colar, Recortar, etc., e a Microsoft mapeou eles e já embutiu no WPF, distribuindo-os em cinco categorias: ApplicationCommands, ComponentCommands, MediaCommands, NavigationCommands e EditingCommands. Cada categoria nada mais é do que uma classe estática e, consequentemente, todos os comandos também são declarados como estático, o que siginifica que haverá apenas uma única instância compartilhada de cada um com toda a aplicação.

Pelo fato dos controles serem estáticos (mesmo aquele que criamos acima), quem o "traz" para o controle atual é o CommandBinding, evitando conflitos entre outras regiões que fazem uso deste mesmo comando. Abaixo temos um exemplo simples de como podemos utilizar o comando Copy/Paste, expostos pela classe ApplicationCommands:

<Window ...>
    <Window.CommandBindings>
        <CommandBinding
            Command="ApplicationCommands.Copy"
            CanExecute="CommandBinding_CanExecute" />
    </Window.CommandBindings>

    <Grid>
        <TextBox Name="textBox1" />

        <Button Name="button1" Command="ApplicationCommands.Copy" />
        <Button Name="button2" Command="ApplicationCommands.Paste" />
    </Grid>
</Window>

E no C#, você precisa apenas testar se há algo digitado no TextBox e se ele está selecionado:

private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = !string.IsNullOrEmpty(this.textBox1.SelectedText);
}

É interessante notar que em momento nenhum eu escrevi código para mandar o conteúdo selecionado para o ClipBoard ou para ler o conteúdo de lá. Esse código já está criado em outro lugar (no próprio sistema operacional), e o que o WPF faz é uma forma de interceptar e me permitir decidir de devo ou não continuar.

Conclusão: Eis mais uma grande funcionalidade que facilita muito a escrita de código no desenvolvimento de aplicações Windows. Além disso, torna o código mais fácil para dar manutenção, já que a centralização é um dos principais trunfos desta técnica.

Tags: , ,

WPF

MEF - Managed Extensibility Framework

by Israel Aece 17. December 2009 09:37
Revista Mundo .NET - 018 Toda aplicação que deseja suportar “plugins”, terá que se preocupar, também, em criar toda a infraestrutura necessária para suportá-los. Como essa preocupação está cada vez mais popular nos dias de hoje, a Microsoft trabalha em um projeto chamado MEF – Managed Extensibility Framework. Ao invés de explicitamente referenciar os componentes na aplicação, o MEF permitirá efetuar o descobrimento desses componentes de forma implícita, através de composição, gerenciando tudo o que for preciso para manter essas extensões, possibilitando que sua aplicação fique dependente de uma abstração, e não de uma implementação.

Atualmente o MEF encontra-se em desenvolvimento, e a Microsoft tem disponibilizado versões CTPs (Community Technology Preview), para avaliações do produto. A versão atual é a CTP 8, que já vem incorporada no Beta 2 do Visual Studio .NET 2010 e .NET Framework 4.0 e, provavelmente, fará parte da versão final do .NET Framework 4.0.

Este artigo tem a finalidade de abordar a API do MEF, mostrando todas as suas funcionalidades e como podemos utilizá-las em nossas aplicações.

Tags: , ,

.NET Framework

Granularidade de Serviços

by Israel Aece 29. October 2009 15:03

Independentemente de qual tecnologia estamos utilizando, uma das grandes perguntas que nos fazemos ao criar serviços, é o que realmente devemos disponibilizar em cada um deles, que ao meu ver, vai muito além os parâmetros e do resultado que cada operação recebe e/ou retorna. Existe uma série de aspectos que devemos nos atentar ao projetar ou construir um conjunto de serviços, que muitas vezes atenderão as aplicações que rodam dentro dessa mesma companhia.

O primeiro aspecto que temos que verificar é a questão da granularidade dos serviços, que é utilizada para mensurar a profundidade de abstração que foi aplicado. A granularidade pode ser dividida em duas partes, sendo: granularidade fina (fine-grained) e granularidade grossa (coarse-grained), onde granularidade fina determina que precisamos de muitos "grãos", enquanto na granularidade grossa, teremos poucos "grãos", bem maiores.

O que eu quero mostrar com o parágrafo acima, é que com a granularidade fina, teremos serviços com poucas operações, mas dividiremos essas operações por vários serviços. Já com a granularidade grossa, isso se inverte, ou seja, teremos poucos serviços, mas cada um deles conterá uma porção bem maior de operações. Cada uma das técnicas tem suas vantagens e desvantagens, e ao meu ver, quando temos uma granularidade fina, temos pequenos "blocos" de funcionalidades bem específicas e muitas vezes independentes, e que ficam bem mais fáceis de serem atualizadas, distribuídas e gerenciadas, mas isso pode se tornar complexo demais para aqueles que consomem os serviços, já que terão que compor e sincronizar suas operações, para que atinja um determinado objetivo. Por outro lado, a granularidade grossa pode tornar os serviços mais auto-suficientes, mas o problema disso é que cada serviço poderá, acidentalmente, fazer muito mais trabalho do que ele realmente deveria, e que muitas vezes precisará recorrer à outros serviços, para que, também, atinjam o seu objetivo, aumentando assim o acoplamento.

Acoplamento é um outro problema que pode ocorrer, que nada mais é do que a - forte - dependência que um serviço tem de outro. Isso infringe um dos princípios do SOA, que diz que os serviços precisam ser autônomos, ou seja, não depender de outros serviços. Mas em algumas situações isso pode ser benéfico, principalmente em um ambiente de composição. Imagine que você queira criar um serviço para resolver um problema maior, com uma complexidade muito grande. Ao invés de todos os consumidores acessarem esses serviços e ficar sob responsabilidade de cada um organizar isso, você poderá criar um serviço para compor essas tarefas que, como dissemos acima, precisarão recorrer à outros serviços.

Esses conceitos que vimos acima não são novidades. Eles já são (ou deveriam ser) aplicados na programação orientada à objetos, exatamente para termos os mesmos benefícios. Essas características não são exclusividades da computação, pois podemos adotar esses mesmos princípios no nosso dia-à-dia.

Outro grande ponto a ser considerado na construção de serviços, é a criação de serviços com interfaces CRUD (Create, Read, Update e Delete). A proposta destes tipos de serviços é permitir, na maioria das vezes, a manipulação de registros dentro de uma determinada base de dados. Nestes casos, o serviço será apenas uma espécie de wrapper para os dados, não fazendo nada além do que as operações básicas que todo banco de dados possui (INSERT, SELECT, UPDATE e DELETE).

Quando você trabalha com uma aplicação data-centric, onde a toda a regra se concentra em manipular a base de dados, talvez esses tipos de serviços sejam úteis. Considere aqui o uso do ADO.NET Data Services, que evitará conhecer e criar toda a estrutura necessária para expor via WCF.

Mas o ideal é não ter isso em mente ao construir serviços. Interfaces (contratos) CRUD induzem à granularidade fina, onde cada serviço será responsável por manipular uma determinada entidade/tabela. Como vimos acima, a granularidade fina em si não é o problema. A questão aqui é que muitas vezes, um cadastro de um cliente não consiste apenas em um INSERT na base de dados, ao contrário, vai muito além disso.

Imagine um novo cliente que deseja abrir uma conta bancária em um determinado banco, com um cartão de crédito vinculado. O processo de cadastro do cliente consistirá em:

  1. Validar os dados, como idade, renda, endereço, etc.;
  2. Consultar outras instituições financeiras para se certificar de que ele é um bom pagador;
  3. Definir o limite que ele terá no cheque especial;
  4. Inserir o cliente na base de dados;
  5. Criar a conta corrente para este cliente;
  6. Efetuar o lançamento da taxa de cadastro/abertura na conta corrente recém criada;
  7. Comunicar com o serviço de cartões de crédito, para que ele gere um novo cartão para este cliente;
  8. Notificar outros departamentos do banco de que uma nova conta foi aberta, para oferecimento de novos produtos.

Como podemos perceber, o cadastro de um novo cliente não consiste apenas em adicioná-lo na base de dados. Há muito mais do que isso. Se modelarmos nossos serviços orientado à dados, eu teria um serviço que manipula os clientes, outro serviço que manipula a conta corrente, outro de notificação e por aí vai. Quanto mais entidades/tabelas você tiver envolvidas em uma mesma tarefa, mais complicado ficará para gerenciar tudo isso, principalmente do ponto de vista daquele que consumirá esses serviços.

O consumo de serviço é um processo caro para se fazer à todo momento, e neste cenário, para efetuar o cadastro de um cliente, eu precisarei chamar, no mínimo, quatro serviços e tudo o que eu precisarei passar para eles, é exatamente os dados do cliente que eu desejo avaliar/cadastrar. Outro ponto importante é com relação ao fluxo de informações. Neste caso, fica sempre sob responsabilidade do cliente que consome esses serviços, configurar a ordem de chamadas, e em um ambiente onde múltiplas aplicações podem incluir clientes, eventualmente uma delas poderá alterar essa ordem, fazendo com que o processo fique em um estado inválido, comprometendo assim a veracidade e consistência das informações. Nada impedirá que uma pessoa maliciosa invoque apenas o serviço de cadastro de cliente diretamente, sem passar pelas políticas de validação necessárias.

Quando temos várias "sub-tarefas" que se juntam para algo maior, em muitos casos queremos garantir a atomicidade, que garantirá que todos os passos sejam efetuados com sucesso, ou tudo falhará. O que garante a atomicidade são as transações, e transações distribuídas são caras, e todas as chamadas para esses serviços devem estar envolvidas dentro dessa transação, que será, também, coordenada pelo cliente, que será o responsável por avaliar se tudo deu certo. Se sim, ele efetivará (Commit), do contrário, irá desfazer (Rollback).

Um segundo cenário que também ilustra isso: você possui clientes e cada um deles possui um flag que determina a situação dele dentro da sua empresa: Ativo, Bloqueado, EmProcessoJuridico, etc. Da mesma forma que vimos antes, alterar situação dele vai muito além de um simples comando de UPDATE na base de dados. Quando eu mover um determinado cliente para a situação de EmProcessoJuridico, eu terei que inserir um item no histórico deste cliente, desativar algumas opções que o mesmo tem site, e alterar a sua situação na tabela do banco de dados. Se movê-lo para Bloqueado, terei que inserir um item no seu histórico, efetuar um lockdown em todas as contas de acesso desse cliente no site, notificar o gerente responsável e, finalmente, efetuar o UPDATE da coluna onde armazeno a situação atual na tabela de clientes.

Como podemos perceber, interfaces CRUD definem os serviços como sendo data-centric, que modelando dessa forma, nós perderemos o contexto de negócio que será executado pelo cliente, tornando bem mais complicado de se entender o processo como um todo. Ao modelar os serviços, o ideal seria pensar em task-centric, ou seja, o serviço fornecerá operações que englobam grande parte do processo, não sendo o consumidor o responsável por isso. Nos dois cenários que vimos acima, teríamos um serviço de cliente, e que me forneceria uma operação para criar um novo cliente dentro do banco (IncluirNovoCliente), outra operação para bloquear o cliente (Bloquear), outro para criar um processo contra este cliente (AbrirProcessoJuridico), que o moverá para a situação EmProcessoJuridico, e assim por diante.

Note que as operações que serão expostas pelo serviço expressarão claramente o negócio, fazendo internamente tudo o que for necessário para atingir o respectivo objetivo. O interessante é que neste caso, não temos o overhead de chamar N serviços, problemas de fluxo e, principalmente, evitando transações distrubuídas.

Claro que poderá haver situações em que, mesmo que você utilize o modelo task-centric, os teus serviços estarão, coincidentemente, alinhados à interfaces CRUD, mas o importante é que isso apenas seja uma coincidência e não uma regra. Isso muitas vezes acontece quando as regras não estão tão aparentes. Por exemplo, se você mantém um cadastro de cidades e permite a alteração delas, provavelmente você poderá ter: 1 - Valinhos e 2 - Campinas. Os clientes cadastrados na sua base de dados e que são de Valinhos guardam o número 1, e os de Campinas, o número 2. Se agora, você diz que Valinhos será o 2 e Campinas o 1, você corromperá todos os clientes que fazem uso dessas cidades. Poderia haver aqui uma regra que, ao efetuar a alteração da cidade (swap), você atualizasse todos os clientes relacionados.

É importante dizer que serviços task-centric, em algum momento, precisarão efetuar operações de CRUD dentro da base de dados. Com isso, podemos criar duas categorias de serviços: Task Service e Entity Service. Task Services são os tipos de serviços que vimos acima, que serão responsáveis por orquestrar toda a regra de validação, fluxo, manipulação e persistência das informações. É neste ponto que entra em cena os Entity Services, que são responsáveis por gerenciar a vida/estado de uma entidade específica, incluindo seus respectivos relacionamentos, tendo esses serviços, uma interface semelhante à interface CRUD.

Os Entity Services levam o nome de uma entidade, como por exemplo: Cliente, ContaCorrente, PoliticasDeValidacao, etc., não fazendo nada além do que o nome diz, ou seja, disponibilizará apenas as informações referentes à respectiva entidade, não conhecendo nada sobre negócios. Já os nomes dos Task Services são voltados para o negócio em si: AdministracaoDeClientes, GestorDeCredito, CobrancaDeTitulos, etc. E ainda, os Task Services poderão utilizar um ou vários Entity Services para executar uma determinada tarefa, atentanto-se sempre aos conceitos que vimos acima, como é o caso do baixo/alto acoplamento e a granularidade.

Tudo o que foi falado aqui poderá, em alguns cenários, não ser a melhor opção, mas utilizar esse tipo de visão para a construção de serviços, ajudará a ter e criar uma representação muito mais consolidada de sua estrutura, e que será relativamente fácil de gerenciar, mas que refletirá, virtualmente, o seu negócio.

Tags:

CSD

WCF – Roteamento de Mensagens

by Israel Aece 5. August 2009 22:10

Ao desenvolver um serviço WCF, disponibilizamos um endpoint de acesso ao mesmo, permitindo que clientes o consumam diretamente. Independentemente da tarefa que ele venha a desempenhar, fica sob responsabilidade do mesmo, através de algum ponto de extensibilidade ou até mesmo em sua implementação, efetuar alguma customização ou reutilização em termos de infraestrutura, como segurança, caching, etc.

Esse tipo de customização visa centralizar alguns processos, facilitando a reutilização e gerenciamento dessas tarefas. Outro ponto importante no desenvolvimento de serviço, é a questão do balanceamento de carga. Ao publicar um serviço e muitos clientes passarem a consumí-lo, provavelmente o servidor onde ele ficar hospedado não comportará esse aumento. Neste caso, cria-se um segundo servidor para distribuir a execução do serviço, e através de algum software ou hardware, faz a configuração necessária para direcionar as requisições de acordo com a sua capacidade/disponibilidade.

Esses são alguns dos típicos cenários para o uso de roteamento de mensagens. A ideia do roteador é receber uma mensagem e encaminhá-la, podendo ou não fazer alguma verificação. Até a versão atual do WCF (3.5), é necessário uma grande quantidade de código para a criação deste roteador, enquanto na versão 4.0, que está por vir, já trará esse serviço nativamente, e é o que veremos no decorrer deste artigo.

Todos os tipos necessários para criarmos este roteador estão abaixo de um novo namespace, chamado System.ServiceModel.Routing (assembly System.ServiceModel.Routing.dll). Assim como a implementação manual que existia antes da versão 4.0, o roteador será disponibilizado como um serviço WCF qualquer, mas implementando alguns contratos (Interfaces) específicos, que determinarão como as mensagens serão encaminhadas para o respectivo serviço. A classe responsável por representar o serviço de roteamento é chamada de RoutingService, que por sua vez, implementa as seguintes Interfaces: ISimplexDatagramRouter, ISimplexSessionRouter, IRequestReplyRouter e IDuplexSessionRouter.

Cada uma dessas Interfaces descrevem as funcionalidades suportadas pelo serviço de roteamento. A primeira delas, ISimplexDatagramRouter, traz suporte ao processamento assíncrono de uma mensagem, suportando tipos “one-way”; já a Interface ISimplexSessionRouter, possibilita o processamento de mensagens que requerem sessões; a Interface IRequestReplyRouter possibilita ao serviço de roteamento, processar mensagens do tipo requisição-resposta, podendo ou não suportar sessões e, finalmente, a Interface IDuplexSessionRouter, que permite ao roteador processar mensagens “duplex” (aquelas que suportam callbacks).

Todas essas Interfaces estão implementadas na classe RoutingService, podendo ela tratar qualquer requisição, para os mais variados tipos de mensagens. A idéia de ter isso tudo isolado em Interfaces é que, eventualmente, você possa vir a criar um serviço de roteamento que suporte apenas um dos tipos.

A implementação e a forma como criamos e hospedamos um serviço não muda em nada. Continuamos criando os contratos, criação da classe que representa o serviço e o hosting do mesmo, com os respectivos endpoints. Em princípio, as aplicações que consomem o serviço também não mudam em nada. A única – grande – diferença é que entre essas duas partes haverá um intermediário, que como vimos acima, será o responsável por encaminhar as mensagens do cliente para o serviço e as mensagens do serviço para o cliente.

Uma vez que o serviço estiver construído, é necessário criarmos um serviço que servirá como roteador. Como vimos acima, a classe que representa isso é a RouterService, e podemos hospedá-la em qualquer hosting suportado pelo WCF. Já as Interfaces, que também foram comentadas acima, serão utilizadas para construir os endpoints do roteador, nos obrigando a escolher a Interface correta, em sincronia com o tipo de mensagem exposto pelo serviço efetivo.

Como o roteador será um serviço qualquer, também temos que configurar o(s) endpoint(s) e behavior(s). Como sabemos, uma das características do endpoint é o endereço, e neste caso, ele será utilizado pelos clientes para enviar a mensagem para qualquer um dos serviços que estão atrás do roteador, que por sua vez, se baseará em filtros para encaminhar a mensagem ao serviço correto.

Para o exemplo teremos dois serviços: um responsável pelo gerenciamento dos usuários e outro pelo gerenciamento de clientes, e ambos estarão acessíveis através do roteador. Um dos serviços (“ServicoDeUsuarios”) foi criado e disponibilizado utilizando o binding BasicHttpBinding, e as mensagens são do tipo requisição-resposta. Já o segundo serviço (“ServicoDeClientes”) possuirá apenas um método do tipo “one-way”, sendo disponibilizado através do binding WSHttpBinding. Dessa forma, o roteador será criado com dois endpoints distintos, onde o primeiro deles é configurado com o binding BasicHttpBinding e com o contrato IRequestReplyRouter, enquanto o segundo, utilizará o binding WSHttpBinding e o contrato definido como ISimplexDatagramRouter. Abaixo temos a configuração parcial do roteador:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="System.ServiceModel.Routing.RoutingService"
               behaviorConfiguration="routerConfig">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:9997/Router"/>
          </baseAddresses>
        </host>
        <endpoint address="rr"
                  binding="basicHttpBinding"
                  contract="System.ServiceModel.Routing.IRequestReplyRouter" />
        <endpoint address="ow"
                  binding="wsHttpBinding"
                  contract="System.ServiceModel.Routing.ISimplexDatagramRouter" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="routerConfig">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
          <routing filterOnHeadersOnly="false" 
                       routingTableName="RouterMapping" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <!-- Outras Configurações -->
  </system.serviceModel>
</configuration>

Analisando essa primeira parte do arquivo de configuração do serviço de roteamento, podemos notar que o serviço que está sendo exposto é o RoutingService. Na sua definição vemos o baseAddress e dois endpoints. O endereço especificado no baseAddress, é o endereço que os clientes utilizarão para efetuar a comunicação com o roteador. Logo em seguida temos dois endpoints, onde o primeiro define o binding BasicHttpBinding e o contrato IRequestReplyRouter, ou seja, aceitará requisição através deste binding, suportando o tipo de mensagem requisição-resposta. Já o segundo, utiliza o binding WSHttpBinding com o contrato ISimplexDatagramRouter, ou seja, suporte à operações do tipo “one-way”.

Podemos reparar também que o serviço está referenciando um behavior chamado “routerConfig”. Dentro desta seção de configuração, além das opções comuns, como a disponibilidade de metadados, exceções, temos um novo behavior, chamado de RoutingBehavior (representando pelo elemento <routing />). Esse elemento possui apenas dois atributos: filterOnHeadersOnly e routingTableName. O primeiro atributo recebe um valor boleano indicando se poderemos ou não utilizar o corpo da mensagem para aplicar um determinado filtro (veremos mais sobre isso abaixo). O segundo atributo, define o nome de uma seção (que deve estar no mesmo arquivo de configuração), onde definiremos todos os filtros necessários para avaliar e, consequentemente, efetuar o encaminhamento da mensagem para o respectivo serviço.

Antes de falarmos efetivamente sobre os filtros, há uma seção muito importante e que é necessário efetuarmos a configuração da forma correta. Esta seção, delimitada pelo elemento <client />, muitas vezes é utilizada do lado de aplicações consumidoras, para especificar o endpoint que será utilizado por ela para efetuar a comunicação com o serviço. Neste contexto, esse elemento tem uma finalidade diferente, ou seja, de especificar o nome, endereço, binding e contrato dos serviços para os quais, eventualmente, o roteador encaminhará as mensagens. Abaixo podemos visualizar como fica a configuração dele:

<client>
  <endpoint name="Servico1"
            address="http://localhost:9998/usuarios"
            binding="basicHttpBinding"
            contract="*" />
  <endpoint name="Servico2"
            address="http://localhost:9999/clientes"
            binding="wsHttpBinding"
            contract="*" />
</client>

Na configurações destes endpoints, elencamos o endereço de cada serviço para qual o roteador enviará a mensagem, e a única e principal diferença em relação a uma configuração tradicional, é a presença o caracter “*” como contrato. Isso quer dizer que o serviço poderá receber a mensagem de qualquer contrato, obviamente, desde que passe pelos critérios que serão estabelecidos nos filtros.

A terceira e última parte do arquivo de configuração do roteador, consiste na definição dos filtros e como eles serão avaliados. No trecho de código abaixo, o elemento <routing /> agrupa dois sub-elementos que compõem o sistema de filtragem. O primeiro deles é a seção <filters />. Como o próprio nome diz, é uma coleção de filtros, onde cada filtro é represetado pelo elemento <filter />, que por sua vez, possui três atributos: name, filterType e filterData. O atributo name é autoexplicativo; já o atributo filterType especifica como será analisado o filtro. No exemplo abaixo, estou verificando a propriedade Action no header da mensagem. Finalmente, o atributo filterData é o valor a ser comparado com o qual foi extraído da mensagem.

O segundo sub-elemento é chamado de <routingTables />. Este elemento também possui uma coleção de entradas, onde ele relaciona um filtro à um endpoint cadastrado previamente (através do elemento <client />), por exemplo, se o “FiltroServico1” for avaliado como verdadeiro, a mensagem será encaminhada para o endpoint “Servico1”, e o mesmo acontecerá para o endpoint “Servico2” se o filtro “FiltroServico2” for atendido.

<routing>
  <filters>
    <filter name="FiltroServico1"
            filterType="Action"
            filterData="http://tempuri.org/IUsuarios/Adicionar" />
    <filter name="FiltroServico2"
            filterType="Action"
            filterData="http://tempuri.org/IClientes/Notificar" />
  </filters>
  <routingTables>
    <table name="RouterMapping">
      <entries>
        <add filterName="FiltroServico1" endpointName="Servico1" />
        <add filterName="FiltroServico2" endpointName="Servico2" />
      </entries>
    </table>
  </routingTables>
</routing>

Observação: Se existir dois endpoints do tipo “one-way” ou “duplex”, e que apontam para um mesmo filtro, que por sua vez, foi atendido, a mensagem será encaminhada para ambos endpoints.

Action é um dos filtros possíveis. Você pode utilizar o filtro do tipo XPath, para que através de uma query XPath, você consiga efetuar validações/consultas mais complexas, podendo inclusive analisar o corpo da mensagem. Com essa opção, você terá uma grande flexibilidade, já que conseguirá extrair mais informações e ter maior precisão na hora de avaliar/aplicar o filtro. Outro tipo de filtro é o MatchAll, que como o próprio nome já diz, acatará todas as mensagens. Basicamente, um filtro nada mais é do que uma classe que herda de MessageFilter, e sendo assim, você pode criar os teus próprios filtros, controlando como eles serão aplicados.

É importante dizer que os filtros são avaliados de acordo com a prioridade. Para determiná-la, podemos utilizar o atributo priority na coleção de filtros do elemento <routingTables />. Para o exemplo deste artigo, isso não faz muito sentido, já que temos dois filtros e cada um deles lidará com um tipo de mensagem específico, mas em um cenário onde você conseguir detectar a frequência de acesso, você pode determinar a prioridade para tirar melhor proveito em termos de performance, evitando que ele gaste tempo na avaliação dos outros filtros. Ao encontrar um filtro que atenda a requisição, a mensagem é encaminhada para o endpoint correspondente, caso contrário, uma exceção será disparada.

Outras Funcionalidades

Ainda há alguns outras funcionalidades que estão diretamente ligadas ao sistema de roteamento de mensagens. A primeira delas é a capacidade de aplicar filtros no corpo da mensagem. Como falado acima, tudo o que você precisa fazer é definir o atributo filterOnHeadersOnly para False e vasculhar o corpo da mensagem em busca dos parâmetros necessários para avaliar/aplicar o filtro, e para isso, você pode utilizar a seção <namespaceTable />. É através dela que conseguimos estabelecer a relação de namespaces com seus respectivos prefixos, para que assim, consiga encontrar e navegar pelos elementos que estão dentro da mensagem.

Como vimos acima, relacionamos um filtro à um determinado endpoint, e caso esse filtro seja atendido, a mensagem será encaminhada para o endpoint correspondente. Mas e se houver alguma falha de comunicação, como por exemplo, timeout? Para isso, a Microsoft também disponibilizou um elemento chamado <alternateEndpoints />. Através dele, podemos relacionar uma lista de endpoints, e caso a mensagem falhe, automaticamente o WCF tentará reencaminhar para o outro endpoint desta lista, até que algum deles processe com sucesso. Além deste elemento, tudo o que precisamos fazer é relacionar a lista de endpoints ao filtro, através do atributo alternateEndpoints, como vemos abaixo:

<routing>
  <!-- Outras Configurações -->
  <routingTables>
    <table name="RouterMapping">
      <entries>
        <add filterName="FiltroServico1"
             endpointName="Servico1"
             alternateEndpoints="alternateEndpoints" />
      </entries>
    </table>
  </routingTables>
  <alternateEndpoints>
    <list name="alternateEndpoints">
      <endpoints>
        <add endpointName="Servico1Servidor2"/>
        <add endpointName="Servico1Servidor3"/>
      </endpoints>
     </list>
  </alternateEndpoints>
</routing>

Para finalizar, outra funcionalidade que temos é a capacidade que o serviço de roteamento tem para trocar o binding que está sendo utilizado na comunicação entre o cliente e o roteador e entre o roteador e o cliente. Isso quer dizer que você pode definir que a comunicação com que o cliente terá com o roteador seja através do binding WSHttpBinding, enquanto a mensagem será encaminhada para o serviço através do binding NetTcpBinding.

Conclusão: Baseando-se nos problemas que vimos no início deste artigo, um roteador pode ajudar imensamente a resolvê-los e, felizmente, com este recurso disponível a partir da versão 4.0 do WCF, trará novas capacidades e funcionalidades para incorporarmos em nossas aplicações, tornando-as muitos mais poderosas, e resumindo alguns processos que antes eram complexos de serem realizados, em configurações extremamente simples.

RoteamentoDeMensagens.zip (38.73 kb)

Tags: , , ,

WCF

WCF - Durable Services

by Israel Aece 23. June 2009 12:24

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 ser complicado 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 estado do objeto será perdido. Isso ocorre porque todo esse estado, que cada objeto que representa o 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”, que terá o seu valor propagado 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 cookies através de TCP. 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ém como 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 da aplicaçã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, com informaçõ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 invocar a 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)

Tags: , ,

CSD | WCF

Serviços CRUD

by Israel Aece 4. May 2009 15:09

Muitas pessoas utilizam ou já utilizaram o WCF (ou até mesmo os antigos ASP.NET Web Services (ASMX)), para servir como um wrapper de uma base de dados. Basicamente era criado um serviço para cada entidade desta base, onde cada um deles apenas define em sua interface as operações de CRUD, que nada mais são do que as operações básicas com uma determinada tabela relacional.

Se você ainda precisa criar algum tipo que serviço que exponha essas funcionalidades, então acredito que seria uma boa alternativa considerar o uso do ADO.NET Data Services. Este framework é construído em cima do próprio WCF, fornecendo a possibilidade de efetuar as operações CRUD em cima de contexto de dados do Entity Framework ou qualquer outra fonte de dados que implemente a interface IQueryable. Todas as funcionalidades são baseadas no padrão REST, que utiliza URIs predefinidas em conjunto com os verbos HTTP, para executar cada uma dessas operações. Já a serialização do resultado pode ser emitida em ATOM (Xml) ou até mesmo JSON, permitindo assim, que qualquer cliente HTTP (como um navegador) consuma o serviço.

Um detalhe importante é que a Microsoft incluiu no .NET Client Library, Silverlight e no AJAX tudo o que é necessário para efetuar a comunicação com serviços baseados no ADO.NET Data Services.

É importante dizer que o ADO.NET Data Services não é ideal para todos os cenários. Há muitas ocasiões onde a customização pode ser muito grande, e utilizando-o pode tornar o processo muito mais trabalhoso do que produtivo e, sendo assim, será mais viável utilizar o WCF, podendo inclusive, expor as funcionalidades do serviço utilizando o padrão REST, se assim desejar. Agora, se tudo o que precisa são as simples operações de CRUD, então criar serviços baseados no ADO.NET Data Services será uma opção boa e bastante produtiva.

Tags:

CSD | Data | WCF

Powered by BlogEngine.NET 1.5.0.0
Theme by Mads Kristensen

Sobre

Meu nome é Israel Aece e sou especialista em tecnologias de desenvolvimento Microsoft, atuando como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. [ Mais ]

Twitter

Host