Flexibilizando a requisição e resposta à serviços REST

by Israel Aece 3. August 2010 12:46

Ao criar um serviço WCF para ser acessado através do modelo REST, devemos decorar as operações que o compõem o contrato com os atributos WebGetAttribute ou WebInvokeAttribute, dependendo de como as requisições devem chegar até elas.

As requisições para estas operações podem ser enviadas e recebidas através de dois formatos: Xml ou Json, e para configurar isso, podemos recorrer à duas propriedades fornecidas pelos dois atributos listados acima. Essas propriedades são: RequestFormat e ResponseFormat, onde ambas recebem uma das opções expostas pelo enumerador WebMessageFormat.

Isso quer dizer que devemos definir, de forma estática, o formato que a mensagem será aceita e como ela será devolvida para os clientes. O grande problema desta técnica é que as vezes você pode ter um mesmo serviço sendo consumindo por clientes diferentes, que lidam melhor com um formato específico. Por exemplo, você pode ter um mesmo serviço sendo consumido por uma aplicação Silverlight, que possui um suporte melhor ao Xml, e ao mesmo tempo, o mesmo serviço sendo consumido por um código jQuery, que lida melhor com o formato Json. Para tornar o serviço flexível, tínhamos que fazer isso de forma imperativa, ou seja, dentro da implementação da operação, tínhamos que validar qual o formato desejado pelo cliente, e com isso especificar no contexto da requisição qual o formato que deve ser utilizado pelo runtime do WCF para gerar o resultado. O código abaixo ilustra de forma resumida essa condição:

public class Servico : IContrato
{
    public string Ping(string value)
    {
        WebOperationContext.Current.OutgoingResponse.Format =
            VerificarFormato(...) == "json" ? WebMessageFormat.Json : WebMessageFormat.Xml;

        return "resultado da operação";
    }
}

Geralmente o formato é fornecido por uma query string adicional ou analisando os headers da requisição HTTP, que informa o formato através do content-type. O código acima permite definirmos o formato da resposta baseando-se na preferência do usuário, mas como podemos perceber, há muito código para avaliar isso, e me obriga a misturar a minha implementação com código de infraestrutura.

Para facilitar, a Microsoft criou na versão 4.0 do WCF, uma nova propriedade na classe WebHttpBehavior, chamada AutomaticFormatSelectionEnabled. Trata-se de uma propriedade boleana, que quando definida como True, irá interpretar a requisição e gerar a resposta no mesmo formato estipulado pelo cliente, olhando para a propriedade content-type que está nos presente nos headers da requisição. Para habilitar, podemos recorrer ao seguinte código:

<?xml version="1.0"?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Service">
        <endpoint
          address=""
          binding="webHttpBinding"
          contract="IService"
          endpointConfiguration="edpConfig" />
      </service>
    </services>
    <behaviors>
      <endpointBehaviors>
        <behavior name="edpConfig">
          <webHttp automaticFormatSelectionEnabled="true" />
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Com esta opção habilitada, meu contrato fica totalmente independente do formato, ou seja, se o cliente está requisitando em formato Json, o WCF será capaz de acatar a requisição, processá-la e devolver o resultado no mesmo formato. O mesmo acontece com o Xml. Finalmente, você pode ter um único serviço sendo capaz de receber e retornar mensagens no mesmo formato do cliente, não os obrigando mais ele a trabalhar com o formato específico, que as vezes diferente daquele que é o mais comum.

Tags: , , , , ,

WCF

Tratando erros com jQuery e WCF

by Israel Aece 18. May 2010 22:03

Neste artigo eu mostrei como construir serviços WCF para serem consumidos através do jQuery. Nele falamos sobre os cuidados que devemos ter para criar e expor serviços WCF, mas não chegamos a falar sobre alguns detalhes, não menos importante, como é o caso do tratamento de erros, que é algo tão comum em qualquer tipo de aplicação.

Ao executar alguma operação, uma exceção pode ser disparada por algum motivo. Como já falamos, ao utilizar a biblioteca do jQuery para invocar algum serviço, podemos através de um callback, especificarmos um código para ser disparado quando algum problema ocorrer do lado do serviço. Para configurar isso, podemos utilizar o parâmetro error da função $.ajax. O código abaixo ilustra como podemos configurar o parâmetro error, exibindo uma mensagem informando ao usuário que algum problema ocorreu:

function RecuperarUsuario() {
    $.ajax(
    {
        type: "POST",
        url: "http://localhost/Services/ServicoDeUsuarios.svc/RecuperarUsuario",
        contentType: "application/json",
        data: '{ "nome": "", "email": "ia@israelaece.com" }', //Nome vazio causa o erro
        processData: false,
        success:
            function (resultado) {
                alert(resultado.RecuperarUsuarioResult.Nome);
                alert(resultado.RecuperarUsuarioResult.Email);
            },
        error:
            function (xhr, textStatus, errorThrown) {
                alert('Algum Problema Ocorreu!');
            }
        });
    }

Esse tipo de código funciona normalmente, até que você precise capturar a mensagem exata do erro que ocorreu. Quando executamos uma operação que está disparando alguma exceção, o WCF acaba retornando a seguinte mensagem: "Request server encountered an error processing the request. See server logs for more details". O problema é que o erro não será retornado em um formato que é facilmente entendido/preferido pelo jQuery, que é o JSON.

Ao configurar o contrato para expor via AJAX, podemos definir através do atributo WebInvokeAttribute, que a requisição (RequestFormat) e a resposta (ResponseFormat) sejam formatadas em JSON, mas isso não inclui eventuais exceções que sejam disparadas pelas respectivas operações, mesmo que esse problema esteja definido como um contrato de fault (FaultContractAttribute).

Para tentar resolver isso, podemos recorrer a alguns pontos de estensibilidade do WCF, para conseguirmos interceptar o erro que ocorreu. Ao interceptar, deveremos transformar a mensagem em algum objeto que deverá ser entendido pelo jQuery, entregando para o cliente todas as informações necessárias para que o mesmo consiga tratar o erro ocorrido.

Para descrever o problema, vamos criar uma classe que possui características que detalham o erro ocorrido. Essa classe será chamada de GenericErrorDescription, que possuirá apenas uma única propriedade (para manter a simplicidade), chamada de Message, do tipo string. Como a finalidade será transformar a exceção em formato JSON para facilitar o consumo pelo jQuery, criaremos uma classe chamada de JsonErrorHandler, e nela implementaremos a interface IErrorHandler, que é uma espécie de interceptador (mais detalhes neste artigo). O principal método fornecido por esta interface, é chamado de ProvideFault, que passa como parâmetro a instância da exceção que ocorreu e uma instância da classe Message, que corresponde a mensagem que será devolvida para o cliente.

Na implementação deste método, utilizaremos o método estático CreateMessage da classe Message, que criará a mensagem de retorno, utilizando o serializador JSON para que a mesma será formatada neste padrão, e além disso, esse serializador deverá serializar a instância da classe que representa o erro, que no nosso exemplo é a GenericErrorDescription. Na sequência configuramos a instância da classe HttpResponseMessageProperty, para customizarmos a mensagem, informando que o tipo a ser retornado será application/json, para que o jQuery consiga, facilmente, entender e interpretar esse conteúdo. Note também que mantemos o StatusCode do HTTP como 500, ou seja, um erro interno, e como sua descrição, definimos a mesma mensagem gerada pela exceção disparada pela operação.

A classe JsonErrorHandler ainda implementa a interface IEndpointBehavior, que nos permite acoplar a instância desta classe em um endpoint específico, do lado do serviço (dispatcher), na coleção de tratadores de erros. O código abaixo ilustra a classe JsonErrorHandler já devidamente implementada:

public class JsonErrorHandler : IEndpointBehavior, IErrorHandler
{
    public bool HandleError(Exception error)
    {
        return true;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        GenericErrorDescription desc = 
            new GenericErrorDescription() { Message = error.Message };

        fault = Message.CreateMessage(version, "", desc, 
            new DataContractJsonSerializer(typeof(GenericErrorDescription)));
        fault.Properties.Add(WebBodyFormatMessageProperty.Name, 
            new WebBodyFormatMessageProperty(WebContentFormat.Json));

        var msgp = new HttpResponseMessageProperty();
        msgp.Headers[HttpResponseHeader.ContentType] = "application/json";
        msgp.StatusCode = HttpStatusCode.InternalServerError;
        msgp.StatusDescription = desc.Message;

        fault.Properties.Add(HttpResponseMessageProperty.Name, msgp);
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bp) { }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime cr) { }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher ed)
    {
        ed.ChannelDispatcher.ErrorHandlers.Add(this);
    }

    public void Validate(ServiceEndpoint endpoint) { }
}

Só que essa classe por si só não funciona. Precisamos efetivamente acoplar ao pipeline do WCF, e para isso, podemos criar um extension element, que nos dará a oportunidade de efetuar essa configuração de modo declarativo, ou seja, através do arquivo Web.config. Basicamente, essa classe será responsável por criar instâncias da classe que criamos acima, ou seja, do tratador de erros:

public class JsonErrorElement : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get
        {
            return typeof(JsonErrorHandler);
        }
    }

    public override object CreateBehavior()
    {
        return new JsonErrorHandler();
    }
}

Depois das classes devidamente criadas, tudo o que precisamos fazer agora é configurarmos o arquivo Web.config do serviço, acoplando o extension element ao endpoint de acesso ao serviço. A configuração final do Web.config deve ficar da seguinte forma:

<system.serviceModel>
  <services>
    <service name="ServicoDeUsuarios" behaviorConfiguration="config">
      <endpoint
        binding="webHttpBinding"
        contract="IUsuarios"
        behaviorConfiguration="edpConfig" />
    </service>
  </services>
  <behaviors>
    <serviceBehaviors>
      <behavior name="config">
        <serviceDebug includeExceptionDetailInFaults="false" />
      </behavior>
    </serviceBehaviors>
    <endpointBehaviors>
      <behavior name="edpConfig">
        <jsonErrorHandler />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <extensions>
    <behaviorExtensions>
      <add
        name="jsonErrorHandler"
        type="JsonErrorElement, WCFExtensions, Version=1.0.0.0, ..." />
    </behaviorExtensions>
  </extensions>
</system.serviceModel>

Já do lado do cliente, as mudanças são bastante ligeiras. Tudo o que precisamos fazer é uma pequena mudança no código que é disparado quando o erro ocorre. Uma vez que o interceptador de erros está acoplado do WCF, a mensagem com o erro de retorno será formatada em JSON, e se analisarmos o retorno, veremos o seguinte resultado:

{"Message": "Informe o nome do usuario\u000d\u000aParameter name: nome"}

Como a mensagem de retorno está em formato JSON, utilizaremos o método parse da classe JSON, exposta pelo JSON2. Esse método é responsável por transformar o conteúdo JSON em um objeto, respeitando as mesmas propriedades serializadas pela classe GenericErrorDescription. Com isso, o nosso código de consumo ao serviço será alterado para:

function RecuperarUsuario() {
    $.ajax(
    {
        type: "POST",
        url: "http://localhost/Services/ServicoDeUsuarios.svc/RecuperarUsuario",
        contentType: "application/json",
        data: '{ "nome": "", "email": "ia@israelaece.com" }', //Nome vazio causa o erro
        processData: false,
        success:
            function (resultado) {
                alert(resultado.RecuperarUsuarioResult.Nome);
                alert(resultado.RecuperarUsuarioResult.Email);
            },
        error:
            function (xhr, textStatus, errorThrown) {
                var erro = JSON.parse(xhr.responseText);
                alert(erro.Message);
            }
        });
    }

Se quiser levar mais detalhes do erro, você ainda tem algumas outras alternativas, mas não tão interessantes como essa. Você poderia optar por definir o atributo includeExceptionDetailInFaults do elemento serviceDebug para True, mas isso iria expor mais informações do que realmente deveria, como por exemplo a Stack Trace, algo que não deveria ultrapassar o serviço. Além dela, ainda poderia envolver toda a operação em um bloco try/catch, e caso algum problema ocorra, utilizamos a classe WebOperationContext para customizar o StatusCode e o ContentType através dela, mas isso torna a classe que representa o serviço dependente do protocolo HTTP, o que não é uma boa opção.

Conclusão: Graças a grande flexibilidade fornecida pelo WCF, podemos contornar alguma de suas "limitações" de uma forma bastante elegante, sem precisar misturar em minhas operações, códigos que estão relacionados puramente à infraestrutura.

Tags: , , , ,

WCF

Auditando serviços WCF

by Israel Aece 3. March 2010 09:50

Como já comentei antes, o WCF fornece uma série de formas de autenticação e autorização, e mais recentemente, também já dá suporte ao WIF, assunto qual abordarei no futuro. Uma vez que a segurança esteja habilitada, uma necessidade que se tem é justamente como auditar os processos de autenticação que são realizados, independentemente do modelo de credencial que esteja sendo fornecido pelo cliente.

O WCF fornece vários pontos de estensibilidade, mas utilizá-lo há dois problemas, onde o primeiro é a necessidade de escrever o código necessário para isso, e o segundo, e mais complicado, é que a maioria (talvez todos) os pontos para acoplar algum código customizado, sempre acontece depois que o processo de autenticação já tenha sido efetuado. Já quando você possuir um modelo de autenticação customizado, você pode interceptar a validação do usuário, e caso ele não seja válido, você cataloga isso em algum lugar. Mas e quando o modelo de autenticação é Windows ou qualquer outra forma que o WCF já entende?

Apesar de informações bem simplistas, o WCF já traz nativamente um behavior que podemos utilizar em nível de serviço para auditar a autenticação. Como tudo no WCF, este behavior pode ser configurado de forma imperativa ou declarativa, e nos fornece quatro propriedades para a sua configuração, sendo elas:

  • AuditLogLocation: Todas as informações geradas pela auditoria são armazenadas no log do Windows. Esta opção permite especificar o local onde essas informações serão colocadas. Temos três possíveis opções, fornecidas pelo enumerador AuditLogLocation:
    • Default: Utiliza o log padrão do sistema operacional.
    • Application: Armazena as informações no event log Application.
    • Security: Armazena as informações no event log Security.
  • SuppressAuditFailure: Propriedade do tipo boleana, que permite especificar se eventuais falhas que aconteçam no momento da gravação do log sejam enviadas para a aplicação. Se a aplicação não se preocupa com as falhas que possam acontecer durante este momento, então é importante que se defina esta campo para True, caso contrário, as exceções comprometerão o funcionamento dela. Mas definir isso como True, faz com que possíveis exceções que seriam disparadas não sejam propagadas para a aplicação, e você nunca saberá se há algum problema, e quando realmente precisar recorrer aos logs de auditoria para saber se alguém acessou em um determinado dia e hora, as informações não estarão lá.
  • ServiceAuthorizationAuditLevel: Este nível de auditoria consiste em catalogar as informações de autorização que são realizadas em nível de serviço, utilizando a mesma política de segurança para todos os métodos.
  • MessageAuthenticationAuditLevel: Neste nível, a auditoria monitora os eventos que são gerados durante uma mensagem específica, que por sua vez, executará uma única operação.

As propriedades ServiceAuthorizationAuditLevel e MessageAuthenticationAuditLevel recebem uma das opções expostas pelo enumerador AuditLevel: None, Success, Failure ou SuccessOrFailure. A ideia aqui é permitir ao desenvolvedor catalogar somente aqueles eventos importantes para ele, pois talvez ele esteja somente interessado nas falhas que ocorreram. É importante dizer também que isso influencia no tamanho do log, e se você especificou um limite máximo, dependendo do volume de requisições que chegam para este serviço, em pouco tempo podemos exceder esse tamanho, e a partir daí, exceções podem começar a acontecer. Eis aqui um dos motivos para a existência da propriedade SuppressAuditFailure.

O código abaixo exibe a utilização deste behavior utilizando o modelo declarativo, através de um arquivo de configuração (App.config ou Web.config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Host.Servico" behaviorConfiguration="srvBehaviorConfig">
        <!-- endpoints -->
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="srvBehaviorConfig">
          <serviceSecurityAudit
            auditLogLocation="Application"
            messageAuthenticationAuditLevel="SuccessOrFailure"
            serviceAuthorizationAuditLevel="SuccessOrFailure"
            suppressAuditFailure="false" />
          <!-- configurações de segurança -->
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Abaixo temos o resultado que foi colocado no Event Log:

---------------------------------------------------------------------------------
Service authorization succeeded.
Service: http://localhost:8778/srv
Action: http://www.israelaece.com/IContrato/RecuperarDados
ClientIdentity: IsraelAece
AuthorizationContext: uuid-019ae4a4-2cb5-4414-9806-1a4e22a4bf79-2
ActivityId: <null>
ServiceAuthorizationManager: XmlAuthorizationManager
---------------------------------------------------------------------------------

Tags: , ,

WCF

UseRequestHeadersForMetadataAddressBehavior

by Israel Aece 18. January 2010 21:03

Há algum tempo, eu comentei aqui sobre um problema que é comum no WCF, quando publicamos um serviço no IIS, que o endereço público (do domínio) não aparece, mantendo o nome da máquina como endereço para o serviço.

A partir da versão 4.0 do WCF, a Microsoft adicionou um novo behavior de serviço chamado UseRequestHeadersForMetadataAddressBehavior. Ao utilizá-lo, o WCF irá automaticamente gerar os endereços (do documento WSDL e das URIs do seu interior) baseando-se no header da requisição. Abaixo vemos como podemos proceder para habilitar esse recurso, e em seguida, as imagens com o antes e depois desta funcionalidade configurada.

<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior>
        <useRequestHeadersForMetadataAddress />
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

Além da configuração declarativa como vemos acima, podemos optar também pela forma imperativa. E ainda, este behavior também está disponível para o WCF 3.5 + SP1, a partir deste endereço.

Tags: ,

WCF

Propagando exceções para o Silverlight

by Israel Aece 23. September 2009 09:56

O WCF é uma das formas que temos para estabelecer a comunicação entre uma aplicação Silverlight e a aplicação (ASP.NET) que a hospeda. Com o WCF podemos permitir que o Silverlight se conecte e envie e/ou receba informações, pois como sabemos, ele roda dentro do navegador do usuário. Apesar de você não ter acesso a maior parte dos recursos do WCF, o básico você consegue fazer.

Antes da versão 3 do Silverlight, há uma grande dificuldade em propagar as exceções que esse serviço, eventualmente, disparasse. Qualquer exceção que ocorra dentro do WCF, a resposta é enviada ao cliente definindo o código de resposta do HTTP (Http Status Code) como 500, o que indica um erro interno do servidor e impede o navegador de ler qualquer resposta. Esse problema pode ser facilmente contornado, criando um message inspector, verificando antes de enviar a resposta para o cliente, se a mensagem é ou não uma fault. Caso seja, você altera o status do HTTP para 200 (OK) ao invés de 500. O código abaixo ilustra como criar esse behavior customizado:

public class SLFaultMessageInspector : IDispatchMessageInspector
{
    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        if (reply.IsFault)
            reply.Properties[HttpResponseMessageProperty.Name] =
                new HttpResponseMessageProperty() { StatusCode = HttpStatusCode.OK };
    }

    //....
}

Isso ainda poderia ser feito na versão 2 do Silverlight, mas o cliente não conseguia interpretar a fault, ou melhor, traduzí-la para uma exceção, e permitir que o desenvolvedor trate o erro da forma tradicional. Já a versão 3, incorpora a capacidade ao Silverlight de processar as faults, podendo especificar os contratos de faults ou optar por não especificá-los, para assim propagar ao cliente os objetos que representam e trazem maiores informações sobre o problema que ocorreu do outro lado.

Tags: , , ,

WCF

WCF Vídeo - Sincronização

by Israel Aece 1. September 2009 16:14
Sincronização Sincronização

Dependendo do modo de gerenciamento de instância que você utilize, provavelmente outro problema entra em cena: a sincronização de objetos compartilhados. Este vídeo mostrará as opções que temos para controlar isso. Para maiores detalhes, consulte este artigo.

Formato: WMV - Duração: 00:30:31 - Tamanho: 23MB

Tags: , ,

WCF

WCF Vídeo - Gerenciamento de Instâncias

by Israel Aece 31. August 2009 08:15
Gerenciamento de Instâncias Gerenciamento de Instâncias

O modo gerenciamento de instância influencia diretamente em quantas instâncias da classe que representa o serviço serão criadas. Este vídeo irá mostrar como configurar e comentar um pouco sobre os pontos positivos e negativos de cada um deles. Para maiores detalhes, consulte este artigo.

Formato: WMV - Duração: 00:18:38 - Tamanho: 22MB

Tags: , ,

WCF

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

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