Partager via


Héritage de contrat de données

Types connus et le solveur générique

Juval Lowy

Télécharger l'exemple de code

Depuis sa première version, les développeurs de Windows Communication Foundation (WCF) ont dû gérer les problèmes d'héritage de contrats de données, un problème appelé types connus. Dans cet article, j'explique d'abord l'origine du problème, j'aborde les atténuations disponibles dans Microsoft .NET Framework 3.0 et .NET Framework 4, puis je présente ma technique pour éliminer entièrement le problème. Vous verrez également des techniques de programmation WCF de niveau avancé.

Différences entre « par valeur » et « par référence »

Dans les langages traditionnels orientés objet tels que C++ et C#, une classe dérivée entretient une relation « est- un(e) » avec sa classe de base. Cela signifie qu'avec cette déclaration, chaque objet B est aussi un objet A :

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

De façon graphique, cela ressemble au diagramme de Venn à la figure 1, dans lequel chaque instance de B est aussi une instance de A (mais tous les A ne sont pas nécessairement des B).

image: Is-A Relationship

Figure 1 Relation « est-un(e) »

Du point de vue de la modélisation de domaine orientée objet traditionnelle, la relation « est-un(e) » vous permet de concevoir votre code par rapport à la classe de base tout en interagissant avec une sous-classe. Cela signifie qu'à terme, vous pouvez faire évoluer la modélisation des entités de domaine en réduisant son impact sur l'application.

Par exemple, imaginez une application de gestion des contacts professionnels avec une modélisation de type de base appelée Contact et une classe dérivée appelée Customer (client) qui spécifie le contact en y ajoutant les attributs d'un client.

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

class Customer : Contact {
  public int OrderNumber;
}

Chaque méthode de l'application qui est écrite initialement par rapport au type de Contact peut aussi accepter les objets Customer comme indiqué à la figure 2.

Figure 2 Utilisation interchangeable de la classe de base et des références de sous-classe

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);

Le fonctionnement du code de la figure 2 est fonction de la manière dont le compilateur représente l'état de l'objet en mémoire. Pour prendre en charge la relation « est-un(e) » entre une sous-classe et sa classe de base pendant l'attribution d'une nouvelle instance de sous-classe, le compilateur attribue d'abord la partie de la classe de base de l'état de l'objet, puis y ajoute directement la partie de la sous-classe, comme le montre la figure 3.

image: Object State Hierarchy in Memory

Figure 3 Hiérarchie de l'état des objets en mémoire

Lorsqu'une méthode qui attend une référence à un Contact reçoit en fait une référence à un Customer (client), elle fonctionne comme prévu parce que la référence Customer est également une référence à un Contact. 

Malheureusement, cela provoque des problèmes d'installation lorsqu'il s'agit de WCF. Contrairement à l'orientation d'objet traditionnelle ou au modèle de programmation classique CLR, WCF passe tous les paramètres d'opérations selon la valeur et non selon la référence. Même si le code donne l'impression que les paramètres sont passés par référence (comme dans C#), le proxy WCF sérialise en fait les paramètres dans le message. Les paramètres sont intégrés dans le message WCF et transférés au service où ils sont alors désérialisés en références locales pour être utilisés par l'opération de service.

C'est également ce qui arrive lorsque l'opération de service renvoie des résultats au client : les résultats (ou paramètres sortants, ou encore exceptions) sont d'abord sérialisés en un message de réponse, puis désérialisé vers le côté client.

La forme exacte de la sérialisation qui a lieu est généralement le fruit du contrat de données pour lequel est écrit le contrat de service. Prenons par exemple ces contrats de données :

[DataContract]
class Contact {...}

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

En utilisant ces contrats de données, vous pouvez définir le contrat de service suivant :

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

  [OperationContract]
  Contact[] GetContacts();
}

Avec des applications multiniveaux, le marshaling des paramètres par valeur fonctionne mieux que par référence, car chaque couche de l'architecture peut fournir sa propre interprétation du comportement motivant le contrat de données. Le marshaling par valeur permet également les appels à distance, l'interopérabilité, les appels en attente et les workflows longs. 

Mais contrairement à l'orientation objet traditionnelle, l'opération de service rédigée pour la classe Contact ne peut fonctionner par défaut avec la sous-classe client. La raison est simple : si vous passez une référence de sous-classe à une opération de service qui attend une référence de classe de base, comment WCF peut-il sérialiser la partie de classe dérivée dans le message ?

C'est pourquoi, compte tenu des définitions présentées jusqu'ici, ce code WCF va échouer :

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

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

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

Les supports des types connus

Avec le .NET Framework 3.0, WCF a pu résoudre le problème de substitution d'une référence de classe de base avec une sous-classe en utilisant le KnownTypeAttribute, défini comme suit :

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

L'attribut KnownType permet de désigner des sous-classes acceptables pour le contrat de données :

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

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

Lorsque le client passe un contrat de données qui utilise une déclaration de type connu, le module de formatage de message WCF teste le type (ce qui revient à utiliser l'opérateur « est ») et voit s'il est le type connu attendu. Si c'est le cas, il sérialise le paramètre en tant que sous-classe plutôt que comme classe de base.

L'attribut KnownType affecte tous les contrats et opérations qui utilisent la classe de base, dans tous les services et à tous les points de terminaison, permettant ainsi d'autoriser à accepter les sous-classes à la place des classes de base. De plus, il inclut la sous-classe dans les métadonnées de sorte que le client aura sa propre définition de la sous-classe et pourra passer la sous-classe au lieu de la classe de base.

Lorsque plusieurs sous-classes sont attendues, le développeur doit en faire la liste :

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

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

Le module de formatage WCF utilise la réflexion pour collecter tous les types connus des contrats de données, puis examine le paramètre fourni pour voir s'il appartient à un type connu.

Notez que vous devez explicitement ajouter tous les niveaux dans la hiérarchie de classe du contrat de données. L'ajout d'une sous-classe ne permet pas d'ajouter ses classes de base :

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

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

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

Parce que l'attribut KnownType peut être trop large, WCF propose également ServiceKnownTypeAttribute que vous pouvez utiliser dans une opération ou un contrat spécifiques.

Enfin, dans le .NET Framework 3.0, WCF permet également de faire la liste des types connus attendus dans le fichier de configuration de l'application dans la partie system.runtime.serialization. 

Même si l'utilisation des types connus fonctionne bien sur le plan technique, vous devriez ressentir une certaine gêne. Dans la modélisation orientée objet, il n'est pas souhaitable d'associer la classe de base avec des sous-classes spécifiques. La caractéristique d'une bonne base est précisément cela : une bonne classe de base pour chaque sous-classe possible. Pourtant la question des types connus ne la rend intéressante que pour les sous-classes auxquelles elle peut s'attendre. Si vous faites toute votre modélisation à l'avance lors de la conception du système, cela peut ne pas être un obstacle. En fait, au fil du temps, au fur et à mesure que la modélisation de l'application évolue, vous ferez face à des types encore inconnus qui vous forceront, tout au moins, à redéployer votre application et, de façon plus probable, à modifier également vos classes de base. 

Programmes de résolution de contrats de données

Pour atténuer ce problème, WCF a introduit dans le .NET Framework 4 une manière de résoudre les types connus pendant l'exécution. Cette technique de programmation, appelée programmes de résolution de contrat de données, est l'option la plus puissante parce que vous pouvez l'étendre afin d'automatiser complètement la gestion des types connus. En bref, vous avez la chance de pouvoir intercepter la tentative de sérialisation et de désérialisation des paramètres et de résoudre les types connus lors de l'exécution côté client et côté services.

La première étape de l'implémentation d'une résolution de programmation consiste à partir de la classe abstraite DataContractResolver, définie comme suit :

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);
}

L'implémentation de TryResolveType est appelée lorsque WCF essaie de sérialiser un type dans un message et que le type proposé (le paramètre de type) est différent du type déclaré dans le contrat d'opération (le paramètre declaredType). Si vous voulez sérialiser le type, vous devez fournir des identificateurs uniques pouvant servir de clés dans un dictionnaire qui met en correspondance les identificateurs et les types. WCF va fournir ces clés pendant la désérialisation pour que vous puissiez lier ce type.

Notez que la clé de l'espace de nom ne peut être une chaîne nulle ou vide. Alors que virtuellement chaque valeur de chaîne unique peut fonctionner avec les identificateurs, je conseille de simplement utiliser le nom et l'espace de noms de type CLR. Configurez le nom de type et l'espace de noms en tant que paramètres de sortie typeName et typeNamespace.

Si le TryResolveType renvoie une réponse positive, le type est considéré comme résolu, comme si vous aviez appliqué l'attribut KnownType. Si la réponse est négative, WCF fait échouer l'appel. Notez que TryResolveType doit résoudre tous les types connus, même ceux qui sont affublés d'un attribut KnownType ou qui sont répertoriés dans le fichier de configuration. Cela présente un risque potentiel : cela nécessite que le programme de résolution soit associé avec tous les types connus de l'application et qu'il fasse échouer l'appel d'opération avec d'autres types qui peuvent émerger au fil du temps. Par conséquent, il est préférable, en tant que plan de secours, d'essayer de résoudre le type en utilisant le programme de résolution des types connus par défaut que WCF aurait utilisé si votre programme de résolution n'était pas utilisé. C'est exactement la raison d'être du paramètre knownTypeResolver. Si votre implémentation de TryResolveType ne peut résoudre le type, il devrait déléguer à knownTypeResolver.

ResolveName est appelé lorsque WCF essaie de désérialiser un type à partir d'un message et que le type proposé (le paramètre de type) est différent de celui qui est déclaré dans le contrat d'opération (le paramètre declaredType). Dans ce cas, WCF propose des identificateurs de nom de type et d'espace de noms afin que vous puissiez les mettre en correspondance avec un type connu.

Comme exemple, regardez une fois encore ces deux contrats de données :

[DataContract]
class Contact {...}

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

La figure 4 établit un programme de résolution simple pour le type Customer (client).

Figure 4 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);
    }
  }
}

Le programme de résolution doit être attaché en tant que comportement pour chaque opération sur le proxy ou au point de terminaison du service. La classe ServiceEndpoint a une propriété appelée Contract du type ContractDescription :

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

  // More members
}

ContractDescription possède un ensemble de description d'opérations, avec une instance d'OperationDescription pour chaque opération du contrat :

public class ContractDescription {
  public OperationDescriptionCollection Operations
  {get;}

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

Chaque OperationDescription possède une collection de comportements d'opérations du type IOperationBehavior :

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

Dans sa collection de comportements, chaque opération a toujours un comportement appelé DataContractSerializerOperationBehavior avec une propriété DataContractResolver :

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

La propriété DataContractResolver est nulle par défaut, mais vous pouvez la configurer dans votre programme de résolution personnalisé. Pour installer un programme de résolution du côté hôte, vous devez effectuer une itération sur la collection des points de terminaison dans la description du service entretenue par l'hôte :

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> {...}

Imaginez que vous avez la définition de service suivante et que vous utilisez le programme de résolution en figure 4 :

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

La figure 5 illustre comment installer le programme de résolution sur l'hôte pour l'AddressBookService.

Figure 5 Installation d'un programme de résolution sur l'hôte

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();

Côte client, il faut suivre les mêmes étapes, à la différence qu'il faut configurer le programme de résolution sur le seul point de terminaison du proxy ou de la fabrique de canaux. Par exemple, à partir de cette définition de classe proxy :

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

La figure 6 illustre comment installer le programme de résolution sur le proxy en vue d'appeler le service de la figure 5 avec un type connu.

Figure 6 Installation d'un programme de résolution sur le 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);

Le programme de résolution générique

L'écriture et l'installation d'un programme de résolution pour chaque type suppose évidemment beaucoup de travail qui vous oblige à traquer méticuleusement tous les types connus, une opération sujette aux erreurs et qui peut rapidement vous échapper dans un système évolutif. Pour automatiser l'implémentation d'un programme de résolution, j'ai défini la classe GenericResolver comme suit :

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

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

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

GenericResolver propose deux constructeurs. Un constructeur peut accepter un tableau de types connus à résoudre. Le constructeur sans paramètre va automatiquement ajouter en tant que types connus toutes les classes et structures de l'assembly appelant et toutes les classes publiques et structures des assemblys référencées par l'assembly appelant. Le constructeur sans paramètre ne va pas ajouter les types provenant d' un assembly référencé dans le .NET Framework.

En outre, GenericResolver propose la méthode statique Merge que vous pouvez utiliser pour fusionner les types connus de deux programmes de résolution, en renvoyant un GenericResolver qui résout l'union des deux programmes de résolution proposés. La figure 7 illustre la partie pertinente de GenericResolver sans montrer les types des assemblys qui n'ont rien à voir avec WCF.

Figure 7 Implémentation de GenericResolver (partielle)

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);
    }
  }
}

Les membres les plus importants de GenericResolver sont les dictionnaires m_TypeToNames et m_NamesToType. m_TypeToNames mappe un type vers un tuple de ses nom et espace de noms. m_NamesToType mappe un espace de noms et un nom de type vers le type réel. Le constructeur qui prend en charge un tableau de types initialise ces deux dictionnaires. La méthode TryResolveType utilise le type fourni comme une clé du dictionnaire m_TypeToNames pour lire le nom et l'espace de noms du type. La méthode ResolveName utilise l'espace de noms et le nom fournis comme des clés du dictionnaire m_NamesToType pour renvoyer le type résolu.

Même s'il est possible d'utiliser un code fastidieux comme celui présenté aux figures 5 et 6 pour installer GenericResolver, il vaut mieux le simplifier avec des méthodes d'extension. À cette fin, utilisez mes méthodes AddGenericResolver de GenericResolverInstaller, qui sont définies comme suit :

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;
}

La méthode AddGenericResolver accepte un tableau de paramètres de types, c'est-à-dire une liste de types ouverte et séparée par des virgules. Si vous ne spécifiez pas les types, AddGenericResolver va ajouter en tant que types connus toutes les classes et structures de l'assembly appelant plus les classes publiques et structures des assembly référencés. Prenons par exemple ces types connus :

[DataContract]
class Contact {...}

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

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

La figure 8 montre plusieurs exemples d'utilisation de la méthode d'extension AddGenericResolver pour ces types.

Figure 8 Installation de 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);

GenericResolverInstaller installe non seulement le GenericResolver, mais il essaie également de le fusionner avec l'ancien programme de résolution générique (s'il existe). Cela signifie que vous pouvez faire appel à la méthode AddGenericResolver plusieurs fois. C'est pratique lorsque vous ajoutez des types génériques limités.

[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();

La figure 9 présente une implémentation partielle de GenericResolverInstaller.

Figure 9 Implémentation de 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);
    }
  }
}

Si aucun type n'est fourni, AddGenericResolver va utiliser le constructeur sans paramètre de GenericResolver. Sinon, il va uniquement utiliser les types spécifiés en faisant appel à l'autre constructeur. Notez la fusion avec l'ancien programme de résolution, s'il est présent.

L'attribut du programme de résolution générique

Si votre service s'appuie sur le programme de résolution générique de par sa conception, il vaut mieux ne pas être à la merci de l'hôte et de faire appel au programme de résolution générique au moment de la conception. Dans ce but, j'ai écrit le 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
}

Cet attribut concis rend le service indépendant de l'hôte :

GenericResolverBehaviorAttribute est dérivé de IServiceBehavior, qui est une interface WCF particulière et qui est également l'extension la plus couramment utilisée dans WCF. Lorsque l'hôte charge le service, il fait appel aux méthodes IServiceBehavior, en particulier à la méthode Validate, qui permet une interaction entre l'attribut et l'hôte. Dans le cas de GenericResolverBehaviorAttribute, il ajoute le programme de résolution à l'hôte.

Et voilà : un moyen relativement simple et flexible qui permet d'éviter les problèmes d'héritage de contrat de données. Utilisez cette technique dans votre futur projet WCF.

Juval Lowy est un architecte logiciel chez IDesign, spécialiste des formations et du conseil en architecture .NET. Cet article contient des extraits de son récent livre intitulé « Programming WCF Services, 3rd Third Edition » (O'Reilly, 2010). Il est également le directeur régional Microsoft pour la Silicon Valley. Vous pouvez contacter Lowy à l'adresse idesign.net.

Merci aux experts techniques suivants d'avoir relu cet article : Glenn Block et Amadeo Casas Cuadrado