Controle de versão de serviço

Após a implantação inicial e, provavelmente, várias vezes durante o tempo de vida, os serviços (e os pontos de extremidade expostos) podem precisar ser alterados por diversos motivos, como a alteração das necessidades de negócios, os requisitos de tecnologia da informação ou para resolver outros problemas. Cada alteração apresenta uma nova versão do serviço. Este tópico explica como considerar o controle de versão no WCF (Windows Communication Foundation).

Quatro categorias de alterações de serviço

As alterações nos serviços necessárias podem ser classificadas em quatro categorias:

  • Alterações de contrato: por exemplo, uma operação pode ser adicionada ou um elemento de dados em uma mensagem pode ser adicionado ou alterado.

  • Alterações de endereço: por exemplo, um serviço é movido para um local diferente onde os pontos de extremidade têm novos endereços.

  • Alterações de associação: por exemplo, um mecanismo de segurança muda ou suas configurações são alteradas.

  • Alterações de implementação: por exemplo, quando uma implementação de método interno é alterada.

Algumas dessas alterações são chamadas de "interruptiva" e outras são "não interruptiva". Uma alteração é não interruptiva se todas as mensagens processadas com êxito na versão anterior forem processadas com êxito na nova versão. Qualquer alteração que não atenda a esse critério é uma alteração interruptiva.

Orientação de serviço e Controle de versão

Um dos princípios da orientação de serviço é que os serviços e os clientes são autônomos (ou independentes). Entre outras coisas, implica que os desenvolvedores de serviços não podem assumir que controlam ou sequer sabem sobre todos os clientes de serviço. Assim, elimina a opção de recompilar e reimplantar todos os clientes quando um serviço altera as versões. Este tópico pressupõe que o serviço adere a esse princípio e, portanto, deve ser alterado ou "com versão" independente de seus clientes.

Nos casos em que uma alteração interruptiva é inesperada e não pode ser evitada, um aplicativo pode optar por ignorar essa filosofia e exigir que os clientes sejam recriados e reimplantados com uma nova versão do serviço.

Controle de versão de contrato

Os contratos usados por um cliente não precisam ser iguais ao contrato usado pelo serviço. Apenas precisam ser compatíveis.

Para contratos de serviço, a compatibilidade significa que novas operações expostas pelo serviço podem ser adicionadas, mas as operações existentes não podem ser removidas ou alteradas semanticamente.

Para contratos de dados, a compatibilidade significa que novas definições de tipo de esquema podem ser adicionadas. No entanto, as definições de tipo de esquema existentes não podem ser alteradas de maneira significativa. As alterações interruptivas podem incluir a remoção de membros de dados ou a alteração do tipo de dados deles de maneira incompatível. Esse recurso permite ao serviço alguma latitude na alteração da versão de seus contratos sem interromper os clientes. As próximas duas seções explicam alterações não interruptivas e interruptivas que podem ser feitas em contratos de dados e de serviço do WCF.

Controle de versão de contrato de dados

Esta seção lida com o controle de versão de dados ao usar as classes DataContractSerializer e DataContractAttribute.

Controle de versão estrito

Em muitos cenários, quando a alteração de versões é um problema, o desenvolvedor de serviço não tem controle sobre os clientes e, portanto, não pode assumir as reações às alterações no XML ou esquema da mensagem. Nesses casos, você deve garantir que as novas mensagens serão validadas em relação ao esquema antigo, por dois motivos:

  • Os clientes antigos foram desenvolvidos com a suposição de que o esquema não será alterado. Eles podem não conseguir processar mensagens para as quais nunca foram criadas.

  • Os clientes antigos podem executar a validação real do esquema em relação ao esquema antigo antes mesmo de tentar processar as mensagens.

A abordagem recomendada nesses cenários é tratar os contratos de dados existentes como imutáveis e criar novos com nomes qualificados XML exclusivos. O desenvolvedor de serviços adicionaria novos métodos a um contrato de serviço existente ou criaria um novo contrato de serviço com métodos que usam o novo contrato de dados.

Geralmente, é o caso de um desenvolvedor de serviços que precisa escrever alguma lógica de negócios a ser executada em todas as versões de um contrato de dados, além de um código comercial específico de versão para cada versão do contrato de dados. O apêndice no final deste tópico explica como as interfaces podem ser usadas para atender a essa necessidade.

Controle de versão de Lax

Em muitos outros cenários, o desenvolvedor de serviços pode assumir que adicionar um novo membro opcional ao contrato de dados não interrompe os clientes existentes. Assim, exige que o desenvolvedor de serviços investigue se os clientes existentes não estão executando a validação de esquema e que ignoram membros de dados desconhecidos. Nesses cenários, é possível aproveitar os recursos do contrato de dados para adicionar novos membros de forma não interruptiva. O desenvolvedor de serviço pode fazer essa suposição com confiança se os recursos do contrato de dados para controle de versão já foram usados para a primeira versão do serviço.

O WCF, ASP.NET Serviços Web e muitas outras pilhas de serviço Web dão suporte ao controle de versão lax: ou seja, não geram exceções para novos membros de dados desconhecidos nos dados recebidos.

É fácil cometer o erro de acreditar que adicionar um novo membro não interrompe os clientes existentes. Se você não tiver certeza de que todos os clientes podem lidar com o controle de versão lax, a recomendação é usar as diretrizes de controle de versão estritas e tratar os contratos de dados como imutáveis.

Para obter diretrizes detalhadas sobre o controle de versão lax e estrito dos contratos de dados, consulte Práticas Recomendadas: Controle de Versão do Contrato de Dados.

Distinguindo entre tipos .NET e contrato de dados

Uma classe ou estrutura .NET pode ser projetada como um contrato de dados aplicando o atributo DataContractAttribute à classe. O tipo .NET e suas projeções de contrato de dados são duas questões distintas. É possível ter vários tipos .NET com a mesma projeção de contrato de dados. Essa distinção é útil para que você altere o tipo .NET mantendo o contrato de dados projetado e também a compatibilidade com clientes existentes mesmo no sentido estrito da palavra. Há duas coisas que você sempre deve fazer para manter essa distinção entre o tipo .NET e o contrato de dados:

  • Especifique Name e Namespace. Você sempre deve especificar o nome e o namespace do contrato de dados para impedir que o nome e o namespace do tipo .NET sejam expostos no contrato. Dessa forma, se você decidir alterar posteriormente o namespace do .NET ou o nome do tipo, seu contrato de dados permanecerá igual.

  • Especifique Name. Você sempre deve especificar o nome dos membros de dados para impedir que o nome do membro .NET seja exposto no contrato. Dessa forma, se você decidir alterar posteriormente o nome do membro do .NET, seu contrato de dados permanecerá igual.

Alterando ou removendo membros

A alteração do nome ou do tipo de dados de um membro ou a remoção de membros de dados é uma alteração interruptiva, mesmo que o uso de um controle de versão flexível seja permitido. Se for necessário, crie um novo contrato de dados.

Se a compatibilidade do serviço for de grande importância, você pode ignorar membros de dados não utilizados em seu código e deixá-los vigentes. Se você estiver dividindo um membro de dados em vários membros, pode considerar deixar o membro existente vigente como uma propriedade que pode executar a divisão e a reagregação necessárias para clientes de nível inferior (clientes que não são atualizados para a versão mais recente).

Da mesma forma, as alterações no nome ou namespace do contrato de dados são alterações interruptivas.

Round-Trips de dados desconhecidos

Em alguns cenários, há a necessidade de "round-trip" de dados desconhecidos provenientes de membros adicionados em uma nova versão. Por exemplo, um serviço "versionNew" envia dados com alguns membros recém-adicionados a um cliente "versionOld". O cliente ignora os membros recém-adicionados ao processar a mensagem, mas reenvia os mesmos dados, incluindo esses membros novamente ao serviço versionNew. O cenário típico são as atualizações de dados em que os dados são recuperados do serviço, alterados e retornados.

Para habilitar o round-trip de um tipo específico, o tipo deve implementar a interface IExtensibleDataObject. A interface contém uma propriedade ExtensionData que retorna o tipo ExtensionDataObject. A propriedade armazena todos os dados de versões futuras do contrato de dados desconhecido na versão atual. Esses dados são opacos para o cliente, mas quando a instância é serializada, o conteúdo da propriedade ExtensionData é gravado com o restante dos dados dos membros do contrato de dados.

É recomendável que todos os seus tipos implementem essa interface para acomodar novos e desconhecidos membros futuros.

Bibliotecas de Contrato de Dados

Pode haver bibliotecas de contratos de dados em que um contrato é publicado em um repositório central, e os implementadores de serviço e tipo implementam e expõem contratos de dados desse repositório. Nesse caso, quando você publica um contrato de dados no repositório, não há controle sobre quem cria os tipos que o implementam. Portanto, não é possível modificar o contrato depois de publicado, tornando-o efetivamente imutável.

Usando XmlSerializer

Os mesmos princípios de controle de versão se aplicam ao usar a classe XmlSerializer. Quando for necessário o controle de versão estrito, trate os contratos de dados como imutáveis e crie novos contratos de dados com nomes exclusivos e qualificados para as novas versões. Quando tiver certeza de que o controle de versão lax pode ser usado, você pode adicionar novos membros serializáveis em novas versões, mas não alterar ou remover membros existentes.

Observação

O XmlSerializer usa os atributos XmlAnyElementAttribute e XmlAnyAttributeAttribute para dar suporte ao round-tripping de dados desconhecidos.

Controle de versão do contrato de mensagem

As diretrizes para o controle de versão do contrato de mensagem são muito semelhantes aos contratos de dados de controle de versão. Se o controle de versão estrito for necessário, você não deve alterar o corpo da mensagem, mas criar um novo contrato de mensagem com um nome qualificado exclusivo. Se você souber que pode usar o controle de versão lax, pode adicionar novas partes do corpo da mensagem, mas não alterar ou remover as existentes. Essa orientação se aplica tanto a contratos de mensagens básicas quanto encapsuladas.

Cabeçalhos de mensagem sempre podem ser adicionados, mesmo que o controle de versão estrito esteja em uso. O sinalizador MustUnderstand pode alterar o controle de versão. Em geral, o modelo de controle de versão para cabeçalhos no WCF é conforme descrito na especificação SOAP.

Controle de versão do contrato de serviço

Semelhante ao controle de versão do contrato de dados, o controle de versão do contrato de serviço também envolve adicionar, alterar e remover operações.

Especificando nome, namespace e ação

Por padrão, o nome de um contrato de serviço é o nome da interface. Seu namespace padrão é http://tempuri.org e a ação de cada operação é http://tempuri.org/contractname/methodname. É recomendável que você especifique explicitamente um nome e um namespace para o contrato de serviço e uma ação para cada operação para evitar o uso de http://tempuri.org e para impedir que nomes de interface e método sejam expostos no contrato do serviço.

Adicionando parâmetros e operações

A adição de operações de serviço expostas pelo serviço é uma alteração sem interrupção porque os clientes existentes não precisam se preocupar com essas novas operações.

Observação

A adição de operações a um contrato do retorno de chamada duplex é uma alteração interruptiva.

Alterando o parâmetro de operação ou tipos de retorno

A alteração de parâmetros ou tipos de retorno geralmente é uma alteração interruptiva, a menos que o novo tipo implemente o mesmo contrato de dados implementado pelo tipo antigo. Para fazer essa alteração, adicione uma nova operação ao contrato de serviço ou defina um novo contrato de serviço.

Removendo operações

A remoção de operações também é uma alteração interruptiva. Para fazer essa alteração, defina um novo contrato de serviço e coloque-o em um novo ponto de extremidade.

Contratos de falha

O atributo FaultContractAttribute permite que um desenvolvedor de contrato de serviço especifique informações sobre falhas que podem ser retornadas das operações do contrato.

A lista de falhas descrita no contrato de um serviço não é considerada exaustiva. A qualquer momento, uma operação pode retornar falhas que não são descritas em seu contrato. Portanto, a alteração do conjunto de falhas descritas no contrato não é considerada falha. Por exemplo, adicionar uma nova falha ao contrato usando FaultContractAttribute ou removendo uma falha existente do contrato.

Bibliotecas de Contrato de Serviço

As organizações podem ter bibliotecas de contratos em que um contrato é publicado em um repositório central e os implementadores de serviço implementam contratos desse repositório. Nesse caso, quando você publica um contrato de serviços no repositório, não há controle sobre quem cria os serviços que o implementam. Portanto, não é possível modificar o contrato de serviço depois de publicado, tornando-o efetivamente imutável. O WCF dá suporte à herança contratual, que pode ser usada para criar um novo contrato que se estende aos contratos existentes. Para usar esse recurso, defina uma nova interface de contrato de serviço herdada da interface antiga do contrato de serviço e adicione métodos à nova interface. Em seguida, você altera o serviço que implementa o contrato antigo para implementar o novo contrato e alterar a definição de ponto de extremidade "versionOld" para usar o novo contrato. Para clientes "versionOld", o ponto de extremidade continua a aparecer como expondo o contrato "versionOld"; para clientes "versionNew", o ponto de extremidade aparece para expor o contrato "versionNew".

Controle de versão de endereço e associação

As alterações à associação e ao endereço do ponto de extremidade são alterações interruptivas, a menos que os clientes possam descobrir dinamicamente a nova associação ou o novo endereço do ponto de extremidade. Um mecanismo para implementar essa funcionalidade é usar um registro UDDI (Descrição e Integração Universal de Descoberta) e o Padrão de Invocação de UDDI em que um cliente tenta se comunicar com um ponto de extremidade e, após falha, consulta um registro UDDI conhecido para os metadados do ponto de extremidade atual. Em seguida, o cliente usa o endereço e a associação desse metadados para se comunicar com o ponto de extremidade. Se essa comunicação for bem-sucedida, o cliente armazenará em cache o endereço e as informações de associação para uso futuro.

Serviço de roteamento e controle de versão

Se as alterações feitas a um serviço forem alterações interruptivas e você precisar ter duas ou mais versões diferentes de um serviço em execução simultaneamente, você poderá usar o WCF Routing Service para rotear mensagens para a instância de serviço apropriada. O Serviço de Roteamento do WCF usa roteamento baseado em conteúdo, ou seja, usa informações dentro da mensagem para determinar para onde rotear a mensagem. Para obter mais informações sobre o Serviço de Roteamento do WCF, consulte Serviço de Roteamento. Para obter um exemplo de como usar o Serviço de Roteamento do WCF para controle de versão de serviço, consulte Como controlar a versão de serviço.

Apêndice

As diretrizes gerais de controle de versão do contrato de dados, quando é necessário o controle de versão estrito, tratam os contratos de dados como imutáveis e criam novos quando forem necessárias alterações. Uma nova classe precisa ser criada para cada novo contrato de dados. Portanto, um mecanismo é necessário para evitar pegar o código existente que foi escrito nos termos da classe de contrato de dados antiga e reescrevê-lo nos termos da nova classe de contrato de dados.

Um desses mecanismos é usar interfaces para definir os membros de cada contrato de dados e escrever o código de implementação interno nos termos das interfaces em vez das classes de contrato de dados que implementam as interfaces. O código a seguir para a versão 1 de um serviço mostra uma interface IPurchaseOrderV1 e um PurchaseOrderV1:

public interface IPurchaseOrderV1  
{  
    string OrderId { get; set; }  
    string CustomerId { get; set; }  
}  
  
[DataContract(  
Name = "PurchaseOrder",  
Namespace = "http://examples.microsoft.com/WCF/2005/10/PurchaseOrder")]  
public class PurchaseOrderV1 : IPurchaseOrderV1  
{  
    [DataMember(...)]  
    public string OrderId {...}  
    [DataMember(...)]  
    public string CustomerId {...}  
}  

Embora as operações do contrato de serviço sejam gravadas em termos de PurchaseOrderV1, a lógica de negócios real seria em termos de IPurchaseOrderV1. Em seguida, na versão 2, haveria uma nova interface IPurchaseOrderV2 e uma nova classe PurchaseOrderV2, conforme mostrado no seguinte código:

public interface IPurchaseOrderV2  
{  
    DateTime OrderDate { get; set; }  
}

[DataContract(
Name = "PurchaseOrder",  
Namespace = "http://examples.microsoft.com/WCF/2006/02/PurchaseOrder")]  
public class PurchaseOrderV2 : IPurchaseOrderV1, IPurchaseOrderV2  
{  
    [DataMember(...)]  
    public string OrderId {...}  
    [DataMember(...)]  
    public string CustomerId {...}  
    [DataMember(...)]  
    public DateTime OrderDate { ... }  
}  

O contrato de serviço seria atualizado para incluir novas operações que são gravadas em termos de PurchaseOrderV2. A lógica de negócios existente em termos de IPurchaseOrderV1 continuar a funcionar PurchaseOrderV2 e uma nova lógica de negócios que precise da propriedade OrderDate seria escrita em termos de IPurchaseOrderV2.

Confira também