Internals – Proxy de serviços WCF

Para aqueles que desejam consumir um serviço (WCF ou não) através da infraestrutura do WCF que existe do lado do cliente, existem duas opções para referenciá-lo na aplicação cliente, e cada uma dessas opções fornecem suas vantagens e desvantagens. A finalidade deste artigo é tentar descrever como as coisas funcionam nos bastidores, para conseguirmos tirar o melhor proveito disso.

A mais comum entre as formas de acesso um serviço, é a geração do proxy de forma automática, que através da IDE do Visual Studio ou do utilitário svcutil.exe, basta que eu informe o endereço do documento WSDL, e uma classe será criada com toda a estrutura necessária para gerenciar e invocar operações do serviço remoto. Nesta opção, todos os tipos que, eventualmente, o serviço exponha, serão automaticamente reconstruídos do lado do cliente, para que o mesmo seja capaz de enviar e/ou receber tais tipos.

Entre os tipos que são reconstruídos do lado do cliente, uma Interface é criada representando o contrato do serviço, contendo todas as operações que ele fornece. Além disso, uma classe também é criada, herdando da classe abstrata ClientBase<TChannel>. Essa classe é a que conhecemos como “proxy”. Ao herdar de ClientBase<TChannel>, o parâmetro genérico TChannel é substituído pela Interface que representa o contrato do serviço, que como vimos, foi reconstruída do lado do cliente. Além disso, a Interface do contrato do serviço também é implementada nesta classe, ou seja, ao instanciar o proxy, veremos os métodos que foram disponibilizados no serviço. Durante o tempo de execução, ao invocar esses métodos locais, o WCF encaminhará a requisição para o serviço.

Essa classe nada mais é do que um wrapper, que visa facilitar a vida de quem consome o serviço fornecendo, além dos métodos que refletem as suas operações, métodos que gerenciam a vida do canal de comunicação, tais como Open, Close, Abort, etc. Ao criar a instância desta classe ou descartá-la, uma série de tarefas custosas e complexas são realizadas, tais como: carregar as configurações do arquivo de configuração, criar toda a árvore de tipos que descreve o serviço, construção da channel stack (a complexidade depende das funcionalidades habilitadas) e o descarte dos recursos que a mesma utiliza para efetuar a comunicação.

Internamente essa classe mantém/referencia um objeto do tipo ChannelFactory<TChannel>, que é uma factory responsável por criar e configurar, efetivamente, o canal de comunicação entre o cliente e o serviço. Antes do .NET Framework 3.5 + SP1, essa classe era instanciada uma única vez, e depois é mantida viva durante a instância do proxy. Na versão mais recente, não existe mais essa afinidade, ou seja, a instância da ChannelFactory<TChannel> é mantida em nível do AppDomain, e sendo assim, mesmo que você crie e descarte o proxy a todo o momento, grande parte do processo complexo e custoso, será mantido e reutilizado independentemente de quantas instâncias do proxy você venha a criar, gerando uma espécie de cache.

Esse cache melhora consideravelmente o custo de inicialização, que na maioria das vezes, é sempre igual. Esse cache trabalha no formato MRU (most recently used), e está “limitado” à 32 objetos, e quando essa capacidade for atingida, os últimos vão sendo removidos. A cada instância do proxy, antes do WCF efetivamente criar a instância da classe ChannelFactory<TChannel> correspondente, ele interroga o cache a procura deste objeto (que atenda a essas mesmas características), e caso o encontre, evitará a criação de um novo.

Um cuidado especial que se deve ter é com relação as versões que temos no cache. O construtor do proxy possui vários overloads, e os objetos que estão colocados no cache são versionados de acordo com esses parâmetros. Aqueles overloads que não aceitam um Binding como parâmetro, seguramente, objetos que estão no cache serão reutilizados, já que os demais parâmetros são imutáveis. Quando um Binding é passado como parâmetro para um dos construtores, o caching será desabilitado, já que não há como garantir que as informações definidas na instância do binding serão sempre as mesmas. A mesma regra se aplica quando você acessa as propriedades ChannelFactory, Endpoint e ClientCredentials do proxy antes do canal de comunicação ser criado e aberto.

Voltando as formas que temos de invocar um serviço, a segunda possibilidade é utilizar o ChannelFactory<TChannel> diretamente, sem criar o proxy por algumas das ferramentas que vimos acima. Essa técnica nos permite ter um maior controle do caching, já que essa funcionalidade ficará sob nossa responsabilidade. Você pode manter a factory em algum ponto do teu código, e quando precisar de um novo canal de comunicação com as mesmas configurações, basta invocar o método CreateChannel, que ele te retorna uma instância configurada. O ponto negativo desta técnica, é que o cliente já tem que, de alguma forma, conhecer os tipos e contratos que são expostos pelo serviço, cenário comum quando utilizamos assemblies compartilhados. O código abaixo ilustra como podemos proceder para fazer isso:

ChannelFactory<IContrato> factory =
    new ChannelFactory<IContrato>(new NetTcpBinding(), “net.tcp://localhost:3922/srv”);

IContrato proxy = factory.CreateChannel();
Console.WriteLine(proxy.Ping(“Israel”));

Além de você já precisar conhecer os tipos, para invocar qualquer método que gerencie a vida do proxy (Open, Close, Abort, etc.), você precisará converter explicitamente para ICommunicationObject. E, para finalizar, quando não precisar mais da factory, tudo o que precisa fazer é também invocar o seu método Close, que descarta todos os recursos custosos que ela armazena. O exemplo abaixo ilustra como podemos proceder para encerrar o proxy e, eventualmente, a factory:

((ICommunicationObject)proxy).Close();

//Eventualmente
factory.Close();

Conclusão: Ambas técnicas tem seus pontos positivos e negativos. Muito do que vimos neste artigo, acaba sendo gerenciado de forma automática pelo WCF, mas conhecendo alguns detalhes internos como estes, nos permite identificar problemas de performance que, eventualmente, podemos estar sofrendo.

Habilitando o Performance Counter no WCF

Como eu comentei aqui, o WCF fornece vários contadores de performance que você pode utilizar para monitorar os serviços. Há vários tipos de contadores, que mensuram pontos cruciais de toda a infraestrutura do WCF. A imagem abaixo ilustra o monitoramento das instâncias (das classes que representam o serviço) que são criadas, e a quantidade de chamadas por segundo.

Ao adicionar um contador, você pode escolher monitorar esses índices para todos os serviços que rodam naquela máquina ou, se desejar, poderá monitorar esse contador para um serviço específico. Para isso, ao adicionar um contador, selecione a instância correspondente, que é representada pelo endereço onde o serviço está exposto. Se por algum motivo, quiser ler essas informações a partir de uma aplicação customizada, então você deve recorrer ao uso da classe PerformanceCounter e PerformanceCounterCategory.

Utilizando Exceptions como Faults

Quando construímos um serviço WCF, podemos utilizar o atributo FaultContractAttribute um tipo, que representará o problema que ocorreu durante o processamento daquela mensagem. O tipo especificado ali vai estar disponível para o cliente através de uma FaultException<T>, onde T representará os detalhes do erro. O parâmetro genérico T não tem nenhuma constraint, o que permite colocar qualquer tipo (desde que ele seja serializável). Para maiores detalhes, consulte essas fontes.

Como o tipo informado através do atributo FaultContractAttribute está aberto, nada impede de colocarmos ali um tipo que herde direta ou indiretamente da classe Exception. Definindo exceções que foram criadas pelo .NET Framework, não haverá problemas, já que elas existem do outro lado. As dificuldades começam quando é preciso propagar exceções customizadas, aquelas que são herdadas a partir da classe Exception. O grande problema aqui é que o serializador padrão do WCF, que é o DataContractSerializer, não serializa tipos complexos por questões de interoperabilidade. Com essa “limitação”, a classe Exception (ou uma de suas derivadas) sofrerá com isso, já que ela expõe uma propriedade chamada Data, que retorna a instância de um objeto que implementa a Interface IDictionary.

Para resolvermos isso, a boa prática é que sempre criar Faults. Dessa forma, você criará uma classe para detalhar o problema para os clientes, sem a necessidade de manipular exceções. Com isso, ao invés de disparar uma exceção customizada, você dispara uma FaultException<T>, onde T será essa classe que detalhará o problema ocorrido. Agora, se você tiver a possibilidade de compartilhar os tipos, então a exceção customizada funcionará, já que os clientes a conhecem, mas você pagará o preço da interoperabilidade.

Serialização de tipos internos

O WCF permite expor tipos complexos (classes customizadas) através de um serviço. Essas classes precisam estar decoradas com o atributo DataContractAttribute/DataMemberAttribute ou SerializableAttribute, mas se estiver utilizando .NET 3.5 + SP1, você poderá omití-los (POCO).

Esses atributos somente podem ser descartados se a classe que está expondo, tiver seu modificador de acesso definido como public. Repare que no caso abaixo, estou optando por utilizar o modelo POCO, mas a classe está definida como internal, que quer dizer que a mesma somente pode ser acessada a partir do mesmo assembly onde ela foi criada.

[ServiceContract]
internal interface IData
{
    [OperationContract]
    Cliente Ping(Cliente cliente);
}

internal class Cliente
{
    public string Nome { get; set; }
}

Ao rodar a aplicação, resultará na seguinte exeção:

Unhandled Exception: System.Runtime.Serialization.InvalidDataContractException: Type ‘Host.Cliente’ cannot be serialized. Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute.  See the Microsoft .NET Framework documentation for other supported types.

Para resolver isso, basta definir como public ou, se isso não for coerente, definindo explicitamente os atributos DataContractAttribute/DataMemberAttribute ou SerializableAttribute. Internamente, o WCF valida o tipo em um método chamado IsNonAttributedTypeValidForSerialization, que entre várias verificações, analisa se o tipo está ou não visível fora do assembly onde ele foi criado, recorrendo a propriedade IsVisible da classe Type. E como vimos, caso essa propriedade retorne False, a exceção acima será disparada. As validações para determinar se o tipo contém ou não os atributos de serialização suportados pelo WCF, também são realizadas dentro deste mesmo método.

Limites de Tamanho e Cotas do WCF

As cotas do WCF são mecanismos que utilizamos para evitar o consumo excessivo de recursos que a infraestrutura do WCF utiliza para efetuar a comunicação. Quando bem configuradas, elas evitam que eventuais ataques (DoS) sejam realizados ao teu serviço, que pode prejudicar toda a sua infraestrutura. E, assim como os timeouts, os limites de tamanho e cotas são totalmente configuráveis.

Essas configurações já vem com um valor padrão definido, e que você deve ajustar de acordo com a sua necessidade. Como grande parte dessas configurações são determinadas com números inteiros, e muitas pessoas (talvez para efeito de testes) colocam o valor máximo permitido (int.MaxValue). Provavelmente isso evitará possíveis erros que estejam ocorrendo durante os testes, mas se não se preocupar em ajustá-los de acordo com o volume que o teu serviço trabalha, você não evitará possíveis ataques.

Essas configurações são características específicas de um binding, e que podem ser realizadas de forma declarativa ou imperativa. Abaixo consta a relação dessas configurações (limites e cotas) que são comuns para todos os bindings:

  • maxBufferPoolSize (65.536): Determina, em bytes, a quantidade máxima de memória que será alocada para o processamento da mensagem.
  • maxBufferSize (65.536): Número inteiro que representa a quantidade de bytes do buffer que será usado para armazenar uma mensagem na memória.
  • maxReceivedMessageSize (65.536): Como o próprio nome diz, recebe um número inteiro que corresponde ao tamanho máximo (em bytes) da mensagem que ele (cliente ou serviço) pode receber.
  • readerQuotas: Define alguns critérios para o processamento das mensagens SOAP (XML).
    • maxDepth (32): Um número inteiro que determina a profundidade do aninhamento dos elementos (tipos). Por exemplo, você tem uma classe que possui uma propriedade de outro tipo; essa propriedade representa outro tipo, e assim por diante. Isso será transformado Xml e, consequentemente, a estrutura será representada de forma hierárquica.
    • maxStringContentLength (8.192): O tamanho máximo de uma string que será permitida. Se você tiver strings muito largas, então você precisa alterar esse parâmetro.
    • maxArrayLength (16.384): Um número inteiro que especifica a quantidade máxima de elementos que podemos ter dentro de arrays. Há situações onde o teu serviço te retorna/recebe um array, e se a quantidade de elementos dele for maior do que o valor especificado neste atributo, uma exceção será disparada.
    • maxBytesPerRead (4.096): Define a quantidade de bytes permitida para cada leitura de cada elemento do XML.
    • maxNameTableCharCount (16.384): Quantidade máxima de caracteres permitidas na TableName (XmlDictionaryReader). Todos os nomes dos elementos e atributos retornados são catalogados dentro de uma NameTable. Quando o mesmo nome é retornado várias vezes, a mesma instância da classe string será retornada, tornando o processamento mais eficiente.

Observação: Os valores que estão entre parenteses, são os valores padrão.

Atente-se para estas propriedades. Elas não são publicadas no documento WSDL e, consequentemente, não são automaticamente propagadas para o cliente. Ao fazer a referência de um serviço em uma aplicação cliente, os valores que você visualiza no arquivo de configuração são apenas os valores padrão de cada propriedade. Compete a você alterar para ajustar de acordo com a sua necessidade. Se elas não estiverem sincronizadas ou até com os valores bem dimensionados, dificilmente você conseguirá efetuar a comunicação com sucesso.

Timeouts do WCF

Como sabemos, o WCF é altamente configurável, permitindo com que grande parte das informações que ele utiliza em tempo de execução, sejam configuradas de acordo com a nossa necessidade. Entre essas centenas de configurações, temos vários tipos de timeouts espalhados pelo lado do cliente e do serviço.

timeouts que somente estarão acessíveis quando você estiver utilizando alguma funcionalidade específica, como é o caso de transações, mensagens confiáveis, etc., e também há timeouts que, independentemente das características do serviço, sempre estarão disponíveis para que possamos ajustá-los quando o valor padrão não nos atende.

Para iniciar, a classe ServiceHost possui duas propriedades, que recebem um TimeSpan, chamadas OpenTimeout e CloseTimeout. Através da propriedade OpenTimeout, podemos definir um intervalo de tempo que o runtime do WCF deve esperar ao tentar abrir o host. Invocar o método Open da classe ServiceHost, faz com que uma série de tarefas sejam efetuadas antes que o teu serviço esteja efetivamente disponível para consumo. Com o timeout de abertura, podemos determinar por quanto tempo podemos esperar até que isso seja concluída. Como já era de esperar, ao invocar o método Close da classe ServiceHost, várias tarefas serão realizadas com a finalidade de desfazer tudo aquilo que fui montado pela abertura do host. E, como o próprio nome diz, a propriedade CloseTimeout nos permite definir um intervalo de tempo que podemos esperar até que tudo isso seja encerrado.

Por padrão, o valor definido para a propriedade OpenTimeout é 00:01:00, enquanto para CloseTimeout temos 00:00:10. Felizmente também podemos configurar esses valores através do modelo declarativo, ou seja, através do arquivo de configuração. Para isso, basta utilizar o sub-elemento timeouts do elemento host, assim como podemos visualizar no código abaixo:

<system.serviceModel>
  <services>
    <service name=”GestorDeCredito”>
      <host>
        <timeouts closeTimeout=”00:01:20″ openTimeout=”00:00:20″/>
      </host>
    </service>
  </services>
</system.serviceModel>

Todos os bindings nativos do WCF e aqueles que são customizados que, herdam direta ou indiretamente da classe Binding. Essa classe fornece quatro propriedades relacionadas à timeouts, que podem ser configuradas de forma declarativa ou imperativa. As propriedades são: CloseTimeout, OpenTimeout, ReceiveTimeout e SendTimeout.

A finalidade das propriedades CloseTimeout e OpenTimeout do binding é configurar a comunicação entre o cliente/serviço. Quando você se comunica com o serviço, ou quando o serviço se comunica com o cliente, existem várias tarefas (mensagens) que são realizadas (trocadas) entre as partes, antes da mensagem da operação em si. Já através da propriedade ReceiveTimeout, podemos definir um intervalo de tempo em que, se nenhuma mensagem chegar até ele dentro deste período de tempo (aqui não é contemplado mensagens de infraestrutura), a conexão será encerrada e, consequentemente, a instância da classe que representa o serviço será descartada. A comunicação entre o cliente e o serviço envolve vários recursos custosos, e manter isso sem necessidade pode, em pouco tempo, consumir muito mais memória do que realmente é preciso. Justamente por isso que você deve avaliar cuidadosamente o intervalo necessário para manter a sua conexão ativa. Por fim, temos a propriedade SendTimeout, que por sua vez, determina o tempo em que o WCF aguarda até que uma operação seja completamente efetuada.

Ainda falando sobre timeouts, há uma outra propriedade chamada InactivityTimeout (utilizada pelas Reliable Messages). Se nenhuma mensagem for transmitida (incluindo mensagens de infraestrutura, como acknowledgements) durante o intervalo de tempo determinado por essa propriedade, a sessão será descartada e, quando omitido, o valor padrão é 10 minutos. Quando você faz uso das realiable messages, é importante que as propriedades InactivityTimeout e ReceiveTimeout estejam sincronizadas, pois do contrário, não ter a propriedade InactivityTimeout maior que ReceiveTimeout, não evitará o termino da sessão.

Atente-se para estas propriedades. Elas não são publicadas no documento WSDL e, consequentemente, não são automaticamente propagadas para o cliente. Ao fazer a referência de um serviço em uma aplicação cliente, os valores que você visualiza no arquivo de configuração são apenas os valores padrão de cada propriedade. Compete a você alterar para ajustar de acordo com a sua necessidade.

SessionId diferente com TCP

Como já disse em diversos lugares, o WCF fornece o modelo de gerenciamento de instância chamado de PerSession. Neste modelo, haverá uma instância da classe que representa o serviço para cada instância do proxy do lado do cliente. Com isso, utilizamos a propriedade SessionId, que retornará um GUID representando a identificação da instância, para que o runtime do WCF consiga correlacionar as mensagens. O código abaixo ilustra como podemos proceder para visualizar esse “Id”:

//Serviço
Debug.WriteLine(OperationContext.Current.SessionId);

//Cliente
Debug.WriteLine(proxy.InnerChannel.SessionId);

Em princípio, ao executar esses códigos, eles teriam que retornar a mesma informação. Mas há situações em que isso não acontece. Quando definimos o modo de gerenciamento de instância do serviço como PerSession e habilitamos a funcionalidade de garantia de entrega (Reliable Messages), essa combinação é referida como Reliable Sessions.

Ao utilizar o protocolo TCP em conjunto com este recurso, o cliente somente poderá capturar o “Id” após a chamada para o primeiro método ou após abrir explicitamente o proxy, através do método Open, ações que efetivamente criarão a sessão. Do contrário, a propriedade SessionId do lado do cliente sempre retornará nulo. Agora, se utilizar TCP com o recurso de sessões confiáveis desabilitado (que é o padrão), o cliente poderá acessar a propriedade SessionId antes de fazer qualquer chamada para o serviço, mas obterá um “Id” diferente daquela do serviço. Apesar de diferentes, o WCF consegue internamente lidar com isso, e correlacionar as mensagens, mas você não pode garantir a igualdade dos “Ids” em ambos os lados.

Se estiver com esse problema, o que precisa fazer é habilitar isso tanto no serviço quanto no cliente. Se utilizar o modo imperativo, pode utilizar uma versão (overload) do construtor do binding NetTcpBinding, que recebe um valor boleano indicando se a Reliable Session está ou não habilitada. Já no modo declarativo, pode utilizar o atributo enabled do elemento reliableSession, definindo-o como True. Abaixo estão os exemplos:

[ Modo Imperativo ]
NetTcpBinding tcp = new NetTcpBinding(SecurityMode.None, true);

[ Modo Declarativo ]
<reliableSession enabled=”true” ordered=”false” inactivityTimeout=”00:10:00″ />

Registrando a URL HTTP para serviços WCF

Quando você está desenvolvendo um serviço WCF, e o usuário que utiliza para rodar a aplicação de hosting não tem privilégios administrativos, provavelmente você irá se deparar com a seguinte mensagem de erro:

AddressAccessDeniedException: HTTP could not register URL http://+:8080/Servico.&nbsp; Your process does not have access rights to this namespace.

Ao rodar a aplicação que efetuará o hosting do serviço, o WCF recorre ao HTTP.sys (componente para comunicação HTTP sem uso do IIS) para criar a URL onde o serviço será exposto. Depois da URL criada, essas requisições serão encaminhadas para o respectivo serviço que efetuou o registro. No entanto, para efetuar o registro, você precisa de privilégios administrativos que, eventualmente, você não tenha.

Para resolver esse problema você pode rodar como “Administrador” (mas você já sabe a implicação disso), ou recorrer a uma ferramenta de linha de comando chamada netsh.exe. Dado a URL e o usuário, ele permite o acesso à URL à este usuário, através de ACLs. Abaixo o código que você deve executar (como “Administrador”) para conceder o acesso:

netsh http add urlacl url=http://+:80/Servico user=IANB01IsraelAece

A importância dos Namespaces em serviços

Para gerar um serviço WCF da forma politicamente correta, é necessário criarmos uma classe que representará o serviço, e implementar nela a Interface que é considerada como contrato do serviço. Você não está restrito em uma relação um para um, ou seja, você pode implementar quantas Interfaces quiser na classe que representa o serviço. Os membros expostos por essas Interfaces, somente serão propagados para o cliente se você mencioná-los durante a criação de um endpoint.

A partir do momento que você pode receber Interfaces criadas por outros membros do time, ou até mesmo por outras empresas, há a possibilidade de haver nomes de Interfaces e de membros iguais. Quando isso acontece, podemos utilizar um recurso fornecido pela própria linguagem, que é a implementação explícita da Interface, e os conflitos de nomenclatura não ocorrem.

Como essa classe trata-se de um serviço WCF, se utilizarmos esse recurso tudo compilará sem problemas, mas ao rodar o serviço, teremos uma exceção do tipo InvalidOperationException sendo disparada. Mas porque isso ocorre? Quando você invoca um serviço WCF a partir de um cliente qualquer, dentro do envelope SOAP temos a SoapAction. Essa informação determina o “alvo” da requisição, ou melhor, qual a operação que será disparada no serviço. Essa informação é uma junção entre as propriedades Namespace do atributo ServiceContractAttribute com a propriedade Name, exposta pelo atributo OperationContractAttribute. Quando essas configurações não são informadas, por padrão, ele define a propriedade Namespace para “http://tempuri.org/&rdquo;, e com isso, a SoapAction final será: “http://tempuri.org/TeuContrato/TuaOperacao&rdquo;.

Sendo assim, como temos Interfaces com o mesmo nome, e talvez até a mesma estrutura, elas terão em tempo de execução a mesma SoapAction. Com isso, o WCF não conseguirá entender para qual delas encaminhar a requisição, e neste caso antes de efetivamente abrir o host, irá disparar a exceção que falamos acima. Assim como os namespaces que utilizamos no VB.NET/C#, os namespaces do XML também servem para evitar conflitos de nomenclatura, como é o caso aqui. Configurando os contratos com os seus respectivos namespaces, evitaremos o problema. O código abaixo ilustra isso:

namespace Core
{
    [ServiceContract(Namespace = “http://www.israelaece.com/Core&#8221;)]
    public interface IContrato
    {
        [OperationContract]
        string Ping(string value);
    }
}

namespace Tools
{
    [ServiceContract(Namespace = “http://www.israelaece.com/Tools&#8221;)]
    public interface IContrato
    {
        [OperationContract]
        string Ping(string value);
    }
}

É importante não confundir o endereço do serviço com a SoapAction. O endereço do serviço é aquele que definimos durante a criação do endpoint, enquanto a SoapAction é o “alvo” da requisição. Convencionou-se utilizar uma URL na SoapAction, mas isso não é uma obrigação, ou seja, você poderia utilizar algo como “AppCore” e “AppTools” ao invés. Geralmente utiliza-se a URL quando o serviço está público, e o nome da aplicação se ele está acessível apenas internamente.

WCF Vídeos

Em uma série de 10 vídeos, eu abordei os principais tópicos para começar a dar os primeiros passos com o WCF, que é a tecnologia Microsoft para o desenvolvimento de aplicações distribuídas. A idéia foi mostrar como podemos criar, gerenciar e consumir um serviço, abordando de uma forma simples e objetiva, sem entrar em menores detalhes, justamente porque cada vídeo está relacionado a um artigo mais completo, com uma abordagem mais profunda. Abaixo temos a listagem destes vídeos, que totalizam 5 horas:

  1. 00:40:49 – Tipos de Mensagens
  2. 00:30:31 – Sincronização
  3. 00:33:30 – Tratamento de Erros
  4. 00:38:05 – Deployment

Os projetos que utilizei para demonstrar todas essas funcionalidades, podem ser baixados clicando aqui. Com isso, finalizo aqui esta primeira série de vídeos sobre WCF. Em um futuro próximo, talvez eu crie uma nova série, mas com tópicos um pouco mais avançados.