Herança do contrato de dados

Tipos conhecidos e o resolvedor genérico

Juval Lowy

Baixar o código de exemplo

Desde o seu primeiro lançamento, os desenvolvedores do WCF (Windows Communication Foundation) têm lidado com os problemas de herança do contrato de dados, um problema chamado tipos conhecidos. Neste artigo, primeiro explicarei a origem do problema, abordarei as atenuações disponíveis no Microsoft .NET Framework 3.0 e no .NET Framework 4 e, em seguida, apresentarei minha técnica que pode eliminar completamente o problema. Você também poderá ver algumas técnicas de programação avançadas do WCF.

Por valor versus por referência

Em linguagens tradicionais orientadas a objetos, como o C++ e o C#, uma classe derivada mantém uma relação is-a com sua classe base. Isso significa que, dada essa declaração, todo objeto B também é um objeto A:

class A {...}
class B : A {...}

Graficamente, isso é semelhante ao diagrama de Venn na Figura 1, no qual cada instância B também é uma instância A (mas nem toda A é necessariamente uma B).

Figura 1 Relação is-a

Da perspectiva de uma modelagem de domínio tradicional com orientação a objetos, a relação is-a permite que você projete seu código em relação à classe base ao interagir com uma subclasse. Isso significa que você pode evoluir a modelagem de entidades de domínio ao longo do tempo e, ao mesmo tempo, minimizar o impacto no aplicativo.

Por exemplo, considere um aplicativo de gerenciamento de contatos de negócios com essa modelagem de um tipo base chamado Contact e uma classe derivada chamada Customer que especializa o contato com a adição de atributos de um cliente:

class Contact {
  public string FirstName;
  public string LastName;
}

class Customer : Contact {
  public int OrderNumber;
}

Qualquer método no aplicativo que é escrito inicialmente no tipo Contact pode aceitar objetos de Customer também, conforme mostrado na Figura 2.

Figura 2 Intercambiando referências de classes base e de subclasses

interface IContactManager {
  void AddContact(Contact contact);
  Contact[] GetContacts();
}

class AddressBook : IContactManager {
  public void AddContact(Contact contact)
  {...}
  ...
}

IContactManager contacts = new AddressBook();

Contact  contact1 = new Contact();
Contact  contact2 = new Customer();
Customer customer = new Customer();

contacts.AddContact(contact1);
contacts.AddContact(contact2);
contacts.AddContact(customer);

A razão do código da Figura 2 funcionar de qualquer modo tem a ver com a maneira como o compilador representa o estado do objeto na memória. Para dar suporte à relação is-a entre uma subclasse e sua classe base, ao alocar uma nova instância da subclasse, o compilador primeiro aloca a parte da classe base do estado do objeto e, em seguida, acrescenta diretamente a parte da subclasse, conforme mostrado na Figura 3.

Figura 3 Hierarquia do estado de objetos na memória

Quando um método que espera uma referência a um Contact recebe na verdade uma referência a um Customer, ele ainda funciona porque a referência a Customer é uma referência a Contact também. 

Infelizmente, essa configuração intrincada é interrompida no que diz respeito ao WCF. Ao contrário da orientação a objeto tradicional ou do modelo clássico da programação CLR, o WCF passa todos os parâmetros da operação por valor, e não por referência. Embora o código pareça passar os parâmetros por referência (como no C# normal), o proxy do WCF na verdade serializa os parâmetros na mensagem. Os parâmetros são empacotados na mensagem do WCF e transferidos para o serviço, onde são desserializados para referências locais com as quais a operação do serviço trabalha.

Isso também é o que ocorre quando a operação do serviço retorna resultados ao cliente: Os resultados (ou parâmetros ou exceções de saída) são primeiro serializados em uma mensagem de resposta e, em seguida, desserializados novamente no cliente.

A forma exata da serialização que ocorre, normalmente, é um produto do contrato de dados para o qual contrato do serviço foi escrito. Por exemplo, considere estes contratos de dados:

[DataContract]
class Contact {...}

[DataContract]
class Customer : Contact {...}

Com o uso desses contratos de dados, é possível definir este contrato de serviço:

[ServiceContract]
interface IContactManager {
  [OperationContract]
  void AddContact(Contact contact);

  [OperationContract]
  Contact[] GetContacts();
}

Com aplicativos de várias camadas, o marshaling dos parâmetros por valor funciona melhor do que por referência, porque qualquer camada da arquitetura está livre para fornecer sua própria interpretação do comportamento por trás do contrato de dados. O marshaling por valor também permite chamadas remotas, interoperabilidade, chamadas na fila e fluxos de trabalho de longa execução. 

Mas, ao contrário da orientação a objeto tradicional, a operação do serviço escrita em relação à classe Contract não pode, por padrão, funcionar com a subclasse Customer. A razão é simples: Se você passar uma referência a uma subclasse para uma operação de serviço que espera uma referência a uma classe base, como o WCF poderá saber como serializar na mensagem a parte da classe derivada?

Como resultado, com as definições fornecidas até agora, haverá falha no código do WCF:

class ContactManagerClient : ClientBase<IContactManager> : 
  IContactManager{
  ...
}

IContactManager proxy = new ContactManagerClient();
Contact contact = new Customer();

// This will fail: 
contacts.AddContact(contact);

Remendos de tipos conhecidos

Com o .NET Framework 3.0, o WCF podia resolver o problema de substituição da referência a uma classe base por uma subclasse usando o KnownTypeAttribute, definido como:

[AttributeUsage(AttributeTargets.Struct|AttributeTargets.Class,
  AllowMultiple = true)]
public sealed class KnownTypeAttribute : Attribute {
  public KnownTypeAttribute(Type type);
  //More members
}

O atributo KnownType permite que você designe subclasses aceitáveis para o contrato de dados:

[DataContract]
  [KnownType(typeof(Customer))]
  class Contact {...}

  [DataContract]
  class Customer : Contact {...}

Quando o cliente passa um contrato de dados que usa uma declaração de tipo desconhecido, o formatador de mensagens do WCF testa o tipo (semelhante ao uso do operador is) e verifica se ele é o tipo conhecido esperado. Caso positivo, ele serializa o parâmetro como a subclasse e não como a classe base.

O atributo KnownType afeta todos os contratos e operações que usam a classe base em todos os serviços e pontos de extremidade, permitindo que ele aceite subclasses em vez de classes base. Além disso, o atributo inclui a subclasse nos metadados, de forma que o cliente tenha sua própria definição da subclasse e poderá passar a subclasse em vez da classe base.

Quando várias subclasses são esperadas, o desenvolvedor deve listar todas elas:

[DataContract]
[KnownType(typeof(Customer))]
[KnownType(typeof(Person))]
class Contact {...}

[DataContract]
class Person : Contact {...}

O formatador do WCF usa reflexão para coletar todos os tipos conhecidos dos contratos de dados e examina o parâmetro fornecido para ver se ele é de qualquer um dos tipos conhecidos.

Observe que você deve adicionar explicitamente todos os níveis na hierarquia de classes do contrato de dados. A adição de uma subclasse não adiciona suas classes base:

[DataContract]
[KnownType(typeof(Customer))]
[KnownType(typeof(Person))]
class Contact {...}

[DataContract]
class Customer : Contact {...}

[DataContract]
class Person : Customer {...}

Como o atributo KnownType pode ser muito amplo em escopo, o WCF também fornece o ServiceKnownTypeAttribute, que pode ser aplicado em uma operação específica ou em um contrato específico.

Finalmente, no .NET Framework 3.0, o WCF também permitia a listagem dos tipos conhecidos esperados no arquivo de configuração do aplicativo na seção system.runtime.serialization. 

Embora o uso de tipos conhecidos funcione bem tecnicamente, você deve se sentir um pouco desconfortável com ele. Na modelagem tradicional orientada a objeto, você nunca deseja acoplar a classe base em algumas subclasses específicas. A marca de uma boa classe base é precisamente esta: uma boa base é uma classe base boa para qualquer subclasse possível, e o problema de tipos conhecidos ainda a torna adequada apenas para subclasses que ela conhece. Se você fizer toda a sua modelagem com antecedência ao projetar o sistema, talvez isso não seja um obstáculo. Na realidade, ao longo do tempo, conforme o aplicativo evolui sua modelagem, você ainda encontra tipos desconhecidos que forçam você a, no mínimo, implantar o aplicativo novamente e, mais provavelmente, também a modificar suas classes base. 

Resolvedores do contrato de dados

Para minimizar o problema, no .NET Framework 4 o WCF introduziu uma maneira de resolver os tipos conhecidos em tempo de execução. Essa técnica programática, chamada de resolvedores de contrato de dados, é a opção mais avançada porque você pode estendê-la para automatizar completamente o tratamento de problemas de tipos conhecidos. Em essência, você tem uma oportunidade de interceptar a tentativa da operação de serializar e desserializar parâmetros e de resolver os tipos conhecidos em tempo de execução, tanto no cliente como no servidor.

A primeira etapa da implementação de uma resolução programática é derivar da classe abstrata DataContractResolver, definida como:

public abstract class DataContractResolver {
  protected DataContractResolver();
  
  public abstract bool TryResolveType(
    Type type,Type declaredType,
    DataContractResolver knownTypeResolver, 
    out XmlDictionaryString typeName,
    out XmlDictionaryString typeNamespace);

  public abstract Type ResolveName(
    string typeName,string typeNamespace, 
    Type declaredType,
    DataContractResolver knownTypeResolver);
}

Sua implementação de TryResolveType é chamada quando o WCF tenta serializar um tipo em uma mensagem, e o tipo fornecido (o parâmetro do tipo) é diferente do tipo declarado no contrato da operação (o parâmetro declaredType). Se desejar serializar o tipo, você deverá fornecer alguns identificadores exclusivos para servirem como chaves em um dicionário que mapeia identificadores para tipos. O WCF fornecerá essas chaves durante a desserialização de forma que você possa associar àquele tipo.

Observe que a chave do namespace não pode ser uma cadeia de caracteres vazia ou um nulo. Embora virtualmente qualquer valor de cadeia de caracteres exclusivo seja suficiente para os identificadores, é recomendável simplesmente usar o nome do tipo CLR e o namespace. Defina o nome do tipo e o namespace nos parâmetros de saída typeName e typeNamespace.

Se você retornar true de TryResolveType, o tipo será considerado resolvido como se você tivesse aplicado o atributo KnownType. Se você retornar false, o WCF não faz a chamada. Observe que o TryResolveType deve resolver todos os tipos conhecidos, mesmo os tipos que são decorados com o atributo KnownType ou estão listados no arquivo de configuração. Isso representa um risco potencial. Ele exige que o resolvedor seja acoplado a todos os tipos conhecidos no aplicativo e haverá falha na chamada da operação com outros tipos que possam surgir ao longo do tempo. Portanto, é preferível, como uma contingência de fallback, tentar resolver o tipo com o resolvedor de tipos conhecidos padrão que o WCF usaria se seu resolvedor não estivesse em uso. O parâmetro knownTypeResolver faz exatamente isso. Se sua implementação do TryResolveType não puder resolver o tipo, ele deve delegar para knownTypeResolver.

O ResolveName é chamado quando o WCF tenta desserializar um tipo de uma mensagem, e o tipo fornecido (o parâmetro do tipo) é diferente do tipo declarado no contrato da operação (o parâmetro declaredType). Nesse caso, o WCF fornece o nome do tipo e os identificadores do namespace para que você possa mapeá-los de volta para um tipo conhecido.

Como um exemplo, considere novamente estes dois contratos de dados:

[DataContract]
class Contact {...}

[DataContract]
class Customer : Contact {...}

A Figura 4 lista um resolvedor simples para o tipo Customer.

Figura 4 O CustomerResolver

class CustomerResolver : DataContractResolver {
  string Namespace {
    get {
      return typeof(Customer).Namespace ?? "global";
    }   
  }

  string Name {
    get {
      return typeof(Customer).Name;
    }   
  }

  public override Type ResolveName(
    string typeName,string typeNamespace,
    Type declaredType,
    DataContractResolver knownTypeResolver) {

    if(typeName == Name && typeNamespace == Namespace) {
      return typeof(Customer);
    }
    else {
      return knownTypeResolver.ResolveName(
        typeName,typeNamespace,declaredType,null);
    }
  }

  public override bool TryResolveType(
    Type type,Type declaredType,
    DataContractResolver knownTypeResolver,
    out XmlDictionaryString typeName,
    out XmlDictionaryString typeNamespace) {

    if(type == typeof(Customer)) {
      XmlDictionary dictionary = new XmlDictionary();
      typeName      = dictionary.Add(Name);
      typeNamespace = dictionary.Add(Namespace);
      return true;
    }
    else {
      return knownTypeResolver.TryResolveType(
        type,declaredType,null,out typeName,out typeNamespace);
    }
  }
}

O resolvedor deve ser anexado como um comportamento para cada operação no proxy ou no ponto de extremidade do serviço. A classe ServiceEndpoint tem uma propriedade chamada Contract do tipo ContractDescription:

public class ServiceEndpoint {
  public ContractDescription Contract
  {get;set;}

  // More members
}

ContractDescription tem uma coleção de descrições de operações com uma instância de OperationDescription para cada operação no contrato:

public class ContractDescription {
  public OperationDescriptionCollection Operations
  {get;}

  // More members
}
public class OperationDescriptionCollection : 
  Collection<OperationDescription>
{...}

Cada OperationDescription tem uma coleção de comportamentos de operações do tipo IOperationBehavior:

public class OperationDescription {
  public KeyedByTypeCollection<IOperationBehavior> Behaviors
  {get;}
  // More members
}

Na coleção de comportamentos, cada operação sempre tem um comportamento chamado DataContractSerializerOperationBehavior com uma propriedade DataContractResolver:

public class DataContractSerializerOperationBehavior : 
  IOperationBehavior,... {
  public DataContractResolver DataContractResolver
  {get;set}
  // More members
}

A propriedade DataContractResolver é padronizada como null, mas você pode defini-la como seu resolvedor personalizado. Para instalar um resolvedor no host, você deve iterar sobre a coleção de pontos de extremidade na descrição do serviço mantido pelo host:

public class ServiceHost : ServiceHostBase {...}

public abstract class ServiceHostBase : ... {
  public ServiceDescription Description
  {get;}
  // More members
}

public class ServiceDescription {   
  public ServiceEndpointCollection Endpoints
  {get;}
  // More members
}

public class ServiceEndpointCollection : 
  Collection<ServiceEndpoint> {...}

Suponha que você tenha a seguinte definição de serviço e esteja usando o resolvedor da Figura 4:

[ServiceContract]
interface IContactManager {
  [OperationContract]
  void AddContact(Contact contact);
  ...
}
class AddressBookService : IContactManager {...}

A Figura 5 mostra como instalar o resolvedor no host para o AddressBookService.

Figura 5 Instalando um resolvedor no host

ServiceHost host = 
  new ServiceHost(typeof(AddressBookService));

foreach(ServiceEndpoint endpoint in 
  host.Description.Endpoints) {
  foreach(OperationDescription operation in 
    endpoint.Contract.Operations) {

    DataContractSerializerOperationBehavior behavior = 
      operation.Behaviors.Find<
        DataContractSerializerOperationBehavior>();
      behavior.DataContractResolver = new CustomerResolver();
  }
}
host.Open();

No cliente, você segue etapas semelhantes, com a exceção de que precisa definir o resolvedor no único ponto de extremidade do proxy ou da fábrica de canais. Por exemplo, dada esta definição da classe proxy:

class ContactManagerClient : ClientBase<IContactManager>,IContactManager
{...}

A Figura 6 mostra como instalar o resolvedor no proxy para chamar o serviço da Figura 5 com um tipo conhecido.

Figura 6 Instalando um resolvedor no proxy

ContactManagerClient proxy = new ContactManagerClient();

foreach(OperationDescription operation in 
  proxy.Endpoint.Contract.Operations) {

  DataContractSerializerOperationBehavior behavior = 
    operation.Behaviors.Find<
    DataContractSerializerOperationBehavior>();
   
  behavior.DataContractResolver = new CustomerResolver();
}

Customer customer = new Customer();
...

proxy.AddContact(customer);

Resolvedor genérico

A criação e a instalação de um resolvedor para cada tipo, obviamente, representa muito trabalho e requer que você acompanhe meticulosamente todos os tipos conhecidos, algo que está sujeito a erros e pode ficar fora de controle em um sistema em evolução. Para automatizar a implementação de um resolvedor, criei a classe GenericResolver, definida como:

public class GenericResolver : DataContractResolver {
  public Type[] KnownTypes
  {get;}

  public GenericResolver();
  public GenericResolver(Type[] typesToResolve);

  public static GenericResolver Merge(
    GenericResolver resolver1,
    GenericResolver resolver2);
}

A GenericResolver oferece dois construtores. Um construtor pode aceitar uma matriz de tipos conhecidos a serem resolvidos. O construtor sem parâmetros adicionará automaticamente como tipos conhecidos todas as classes e estruturas no assembly de chamada e todas as classes públicas e estruturas nos assemblies referenciados pelo assembly de chamada. O construtor sem parâmetros não adicionará tipos originados em um assembly referenciado pelo .NET Framework.

Além disso, o GenericResolver oferece o método estático Merge que pode ser usado para mesclar os tipos conhecidos de dois resolvedores, retornando um GenericResolver que resolve a união de dois resolvedores fornecidos. A Figura 7 mostra a parte pertinente de GenericResolver sem refletir os tipos nos assemblies, o que não tem nada a ver com o WCF.

Figura 7 Implementando o GenericResolver (parcial)

public class GenericResolver : DataContractResolver {
  const string DefaultNamespace = "global";
   
  readonly Dictionary<Type,Tuple<string,string>> m_TypeToNames;
  readonly Dictionary<string,Dictionary<string,Type>> m_NamesToType;

  public Type[] KnownTypes {
    get {
      return m_TypeToNames.Keys.ToArray();
    }
  }

  // Get all types in calling assembly and referenced assemblies
  static Type[] ReflectTypes() {...}

  public GenericResolver() : this(ReflectTypes()) {}

  public GenericResolver(Type[] typesToResolve) {
    m_TypeToNames = new Dictionary<Type,Tuple<string,string>>();
    m_NamesToType = new Dictionary<string,Dictionary<string,Type>>();

    foreach(Type type in typesToResolve) {
      string typeNamespace = GetNamespace(type);
      string typeName = GetName(type);

      m_TypeToNames[type] = new Tuple<string,string>(typeNamespace,typeName);

      if(m_NamesToType.ContainsKey(typeNamespace) == false) {
        m_NamesToType[typeNamespace] = new Dictionary<string,Type>();
      }

      m_NamesToType[typeNamespace][typeName] = type;
    }
  }

  static string GetNamespace(Type type) {
    return type.Namespace ?? DefaultNamespace;
  }

  static string GetName(Type type) {
    return type.Name;
  }

  public static GenericResolver Merge(
    GenericResolver resolver1, GenericResolver resolver2) {

    if(resolver1 == null) {
      return resolver2;
    }

    if(resolver2 == null) {
      return resolver1;
    }

    List<Type> types = new List<Type>();

    types.AddRange(resolver1.KnownTypes);
    types.AddRange(resolver2.KnownTypes);

    return new GenericResolver(types.ToArray());
  }

  public override Type ResolveName(
    string typeName,string typeNamespace,
    Type declaredType,
    DataContractResolver knownTypeResolver) {

    if(m_NamesToType.ContainsKey(typeNamespace)) {
      if(m_NamesToType[typeNamespace].ContainsKey(typeName)) {
        return m_NamesToType[typeNamespace][typeName];
      }
    }

    return knownTypeResolver.ResolveName(
      typeName,typeNamespace,declaredType,null);
  }

  public override bool TryResolveType(
    Type type,Type declaredType,
    DataContractResolver knownTypeResolver,
    out XmlDictionaryString typeName,
    out XmlDictionaryString typeNamespace) {

    if(m_TypeToNames.ContainsKey(type)) {
      XmlDictionary dictionary = new XmlDictionary();
      typeNamespace = dictionary.Add(m_TypeToNames[type].Item1);
      typeName      = dictionary.Add(m_TypeToNames[type].Item2);
      return true;
    }
    else {
      return knownTypeResolver.TryResolveType(
      type,declaredType,null,out typeName,
      out typeNamespace);
    }
  }
}

Os membros mais importantes do GenericResolver são os dicionários m_TypeToNames e m_NamesToType. m_TypeToNames mapeia um tipo para uma tupla de seu nome e namespace. m_NamesToType mapeia o namespace e o nome de um tipo para o tipo real. O construtor que obtém a matriz de tipos inicializa esses dois dicionários. O método TryResolveType usa o tipo fornecido como uma chave no dicionário m_TypeToNames para ler o nome e o namespace do tipo. O método ResolveName usa o namespace e o nome fornecidos como chaves no dicionário m_NamesToType para retornar o tipo resolvido.

Embora você pudesse usar código tedioso semelhante ao da Figura 5 e da Figura 6 para instalar o GenericResolver, é melhor simplificá-lo com métodos de extensão. Para esse fim, use os métodos AddGenericResolver de GenericResolverInstaller, definidos como:

public static class GenericResolverInstaller {
  public static void AddGenericResolver(
    this ServiceHost host, params Type[] typesToResolve);

  public static void AddGenericResolver<T>(
    this ClientBase<T> proxy, 
    params Type[] typesToResolve) where T : class;

  public static void AddGenericResolver<T>(
    this ChannelFactory<T> factory,
    params Type[] typesToResolve) where T : class;
}

O método AddGenericResolver aceita uma matriz de tipos de parâmetros, o que significa uma lista de tipos em aberto, separados por vírgulas. Se você não especificar tipos, isso fará com que AddGenericResolver adicione como tipos conhecidos todas as classes e estruturas no assembly de chamada e as classes públicas e estruturas nos assemblies referenciados. Por exemplo, considere estes tipos conhecidos:

[DataContract]
class Contact {...}

[DataContract]
class Customer : Contact {...}

[DataContract]
class Employee : Contact {...}

A Figura 8 mostra vários exemplos do uso do método de extensão AddGenericResolver para estes tipos.

Figura 8 Instalando o GenericResolver

// Host side

ServiceHost host1 = new ServiceHost(typeof(AddressBookService));
// Resolve all types in this and referenced assemblies
host1.AddGenericResolver();
host1.Open();

ServiceHost host2 = new ServiceHost(typeof(AddressBookService));
// Resolve only Customer and Employee
host2.AddGenericResolver(typeof(Customer),typeof(Employee));
host2.Open();

ServiceHost host3 = new ServiceHost(typeof(AddressBookService));
// Can call AddGenericResolver() multiple times
host3.AddGenericResolver(typeof(Customer));
host3.AddGenericResolver(typeof(Employee));
host3.Open();

// Client side

ContactManagerClient proxy = new ContactManagerClient();
// Resolve all types in this and referenced assemblies
proxy.AddGenericResolver();

Customer customer = new Customer();
...
proxy.AddContact(customer);

Além de instalar o GenericResolver, o GenericResolverInstaller também tenta mesclá-lo com o resolvedor genérico antigo (se estiver presente). Isso significa que você pode chamar o método AddGenericResolver várias vezes. Isso é útil ao adicionar tipos genéricos limitados:

[DataContract]
class Customer<T> : Contact {...}

ServiceHost host = new ServiceHost(typeof(AddressBookService));

// Add all non-generic known types
host.AddGenericResolver();

// Add the generic types 
host.AddGenericResolver(typeof(Customer<int>,Customer<string>));

host.Open();

A Figura 9 mostra a implementação parcial do GenericResolverInstaller.

Figura 9 Implementando o GenericResolverInstaller

public static class GenericResolverInstaller {
  public static void AddGenericResolver(
    this ServiceHost host, params Type[] typesToResolve) {

    foreach(ServiceEndpoint endpoint in 
      host.Description.Endpoints) {

      AddGenericResolver(endpoint,typesToResolve);
    }
  }

  static void AddGenericResolver(
    ServiceEndpoint endpoint,Type[] typesToResolve) {

    foreach(OperationDescription operation in 
      endpoint.Contract.Operations) {

      DataContractSerializerOperationBehavior behavior = 
        operation.Behaviors.Find<
        DataContractSerializerOperationBehavior>();

      GenericResolver newResolver;

      if(typesToResolve == null || 
        typesToResolve.Any() == false) {

        newResolver = new GenericResolver();
      }
      else {
        newResolver = new GenericResolver(typesToResolve);
      }

      GenericResolver oldResolver = 
        behavior.DataContractResolver as GenericResolver;
      behavior.DataContractResolver = 
        GenericResolver.Merge(oldResolver,newResolver);
    }
  }
}

Se nenhum tipo for fornecido, o AddGenericResolver usará o construtor sem parâmetros do GenericResolver. Caso contrário, ele usará apenas os tipos especificados com a chamada de outro construtor. Observe a mesclagem com o resolvedor antigo, se presente.

Atributo do resolvedor genérico

Se, por design, seu serviço contar com o resolvedor genérico, será melhor não ficar à mercê do host e declarar sua necessidade para o resolvedor genérico na hora do design. Para esse fim, criei o GenericResolverBehaviorAttribute:

[AttributeUsage(AttributeTargets.Class)]
public class GenericResolverBehaviorAttribute : 
  Attribute,IServiceBehavior {

  void IServiceBehavior.Validate(
    ServiceDescription serviceDescription,
    ServiceHostBase serviceHostBase) {

    ServiceHost host = serviceHostBase as ServiceHost;
    host.AddGenericResolver();
  }
  // More members
}

Esse atributo conciso torna o serviço independente do host:

O GenericResolverBehaviorAttribute deriva de IServiceBehavior, que é uma interface especial do WCF e é a extensão usada mais comumente no WCF. Quando o host carrega o serviço, o host chama os métodos IServiceBehavior, principalmente, o método Validate, o que permite que o atributo interaja com o host. No caso do GenericResolverBehaviorAttribute, ele adiciona o resolvedor genérico ao host.

E isto é o que você tem: uma maneira relativamente simples de contornar os problemas da herança do contrato de dados. Utilize esta técnica em seu projeto do WCF.

Juval Lowy é arquiteto de software na IDesign e fornece treinamento e consultoria em arquitetura e em .NET. Este artigo contém trechos de seu recente livro, “Programming WCF Services, 3ª Edição” (O'Reilly, 2010). Ele também é diretor regional da Microsoft no Vale do Silício. Contate Lowy no site idesign.net.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Glenn Block e Amadeo Casas Cuadrado