Vererbung von Datenverträgen

Bekannte Typen und der allgemeine Konfliktlöser

Juval Lowy

Beispielcode herunterladen.

Seit der ersten Version mussten sich Windows Communication Foundation (WCF)-Entwickler häufig mit den Schwierigkeiten bei der Vererbung von Datenverträgen in WCF auseinandersetzen, die dem Begriff "Bekannte Typen" umschrieben werden. In diesem Artikel erkläre ich zunächst den Ursprung des Problems, bespreche die verfügbaren Abhilfen in Microsoft .NET Framework 3.0 und .NET Framework 4, und zeige Ihnen anschließend mein Verfahren, mit dem dieses Problem vollständig beseitigt werden kann. Ich werde Ihnen auch einige erweiterte WCF-Programmiertechniken zeigen.

Nach Wert versus nach Verweis

In traditionellen objektorientierten Sprachen wie C++ und C# bewahrt eine abgeleitete Klasse eine Ist-Ein-Beziehung mit der Basisklasse. Das bedeutet, dass jedes B-Objekt auch ein A-Objekt ist:

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

Grafisch sieht dies wie das Venn-Diagram in Abbildung 1 aus, in dem jede B-Instanz auch eine A-Instanz ist (aber nicht jedes A notwendigerweise ein B ist).

image: Is-A Relationship

Abbildung 1 Ist-Ein-Beziehung

Aus der Sicht der traditionellen objektorientierten Domänenmodellierung ermöglicht Ihnen die Ist-Ein-Beziehung das Entwerfen des Codes auf der Grundlage der Basisklasse, bei Interaktion mit einer Unterklasse. Das bedeutet, dass Sie die Modellierung von Domänenentitäten über die Zeit entwickeln und gleichzeitig die Auswirkungen auf die Anwendung reduzieren können.

Betrachten Sie zum Beispiel eine Anwendung für die Verwaltung von Geschäftskontakten mit der folgenden Modellierung eines Basistyps namens Contact und einer abgeleiteten Klasse namens Customer, die den Kontakt spezifiziert, indem sie diesem die Eigenschaften eines Kunden hinzufügt:

class Contact {
  public string FirstName;
  public string LastName;
}
class Customer : Contact {
  public int OrderNumber;
}

Jede Methode in der Anwendung, die anfangs auf der Grundlage des Contact-Typs erstellt wurde, kann auch die Customer-Objekte empfangen, wie in Abbildung 2 gezeigt.

Abbildung 2 Austausch von Basisklasse und Unterklassenverweisen

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

Der Grund dafür, dass der Code in Abbildung 2 funktioniert, hat mit der Art und Weise zu tun, in der der Compiler den Objektstatus im Arbeitsspeicher darstellt. Um die Ist-Ein-Beziehung zwischen einer Unterklasse und der Basisklasse zu unterstützen, teilt der Compiler bei der Zuteilung einer neuen Unterklasseninstanz zunächst den Basisklassenteil des Objektstatus zu und hängt unmittelbar danach den Unterklassenteil an, wie in Abbildung 3 gezeigt.

image: Object State Hierarchy in Memory

Abbildung 3 Objektstatushierarchie im Arbeitsspeicher

Wenn eine Methode, die einen Verweis auf einen Kontakt erwartet, stattdessen einen Verweis auf einen Kunden erhält, funktioniert sie weiter, da der Kundenverweis auch ein Verweis auf den Kontakt ist. 

Leider funktioniert dieses detaillierte Konzept im Fall von WCF nicht. Anders als bei der traditionellen Objektorientierung oder dem klassischen CLR-Programmiermodell übergibt WCF alle Operationsparameter nach Wert und nicht nach Verweis. Auch wenn der Code anscheinend die Parameter nach Verweis übergibt (wie in normalem C#), serialisiert der WCF-Proxy die Parameter in die Meldung. Die Parameter werden in der WCF-Meldung verpackt und an den Dienst weitergeleitet. Dort werden sie für den entsprechenden Dienstprozess zu lokalen Verweisen deserialisiert.

Dies geschieht auch, wenn der Dienstprozess die Ergebnisse an den Client zurückgibt: Die Ergebnisse (bzw. die ausgehenden Parameter oder Ausnahmen) werden zuerst in eine Antwortmeldung serialisiert und anschließend auf dem Client deserialisiert.

Die genaue Art, wie die Serialisierung stattfindet, besteht normalerweise in einem Produkt des Datenvertrags, auf dessen Basis der Dienstvertrag erstellt wurde. Betrachten Sie beispielsweise die folgenden Datenverträge:

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

Mittels dieser Datenverträge können Sie den folgenden Dienstvertrag definieren:

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

Bei mehrschichtigen Anwendungen funktioniert die Ausführung der Parameter nach Wert besser als nach Verweis, da jede Schicht in der Architektur eine eigene Interpretation des Verhaltens hinter dem Datenvertrag bereitstellen kann. Die Ausführung nach Wert ermöglicht außerdem Remoteaufrufe, Interoperabilität, Aufrufwarteschlangen sowie langfristig ausgeführte Workflows. 

Anders als im Fall der traditionellen Objektorientierung kann der Dienstprozess, der auf der Basis der Contact-Klasse erstellt wurde, per Voreinstellung nicht mit der Customer-Unterklasse funktionieren. Der Grund ist einfach: Wenn Sie einen Unterklassenverweis an einen Dienstprozess übergeben, der einen Basisklassenverweis erwartet, wie kann WCF wissen, wie der abgeleitete Klassenteil in die Meldung serialisiert werden soll?

Daher wird dieser WCF-Code zu einem Fehler führen:

class ContactManagerClient : ClientBase<IContactManager> : 
  IContactManager{
  ...
}
IContactManager proxy = new ContactManagerClient();
Contact contact = new Customer();
// This will fail: 
contacts.AddContact(contact);

Die Abhilfen für bekannte Typen

Mit .NET Framework 3.0 konnte WCF das Problem des Ersetzens eines Basisklassenverweises durch eine Unterklasse mittels KnownTypeAttribute lösen, definiert als:

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

Das KnownType-Attribut ermöglicht Ihnen auch die Bestimmung akzeptabler Unterklassen für den Datenvertrag:

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

Wenn der Client einen Datenvertrag übergibt, der eine Deklaration für bekannte Typen verwendet, testet der WCF-Meldungsformatierer den Typ (als ob mit Verwendung des IS-Operators) und überprüft, ob es sich um den erwarteten bekannten Typ handelt. Wenn dies der Fall ist, serialisiert er den Parameter als die Unterklasse und nicht als die Basisklasse.

Das KnownType-Attribut wirkt sich auf alle Verträge und Prozesse aus, die die Basisklasse verwenden, und für alle Dienste und Endpunkte. Dadurch können Unterklassen anstelle von Basisklassen akzeptiert werden. Außerdem schließt es die Unterklasse in die Metadaten ein, sodass der Client über eine eigene Definitio der Unterklasse verfügt und die Unterklasse anstelle der Basisklasse übergeben kann.

Wenn mehrere Unterklassen erwartet werden, muss der Entwickler alle Unterklassen auflisten:

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

Der WCF-Formatierer verwendet Reflektion, um alle bekannten Typen der Datenverträge zu sammeln. Anschließend überprüft er die angegebenen Parameter, um festzustellen, ob es sich um einen bekannten Typ handelt.

Beachten Sie, dass Sie explizit alle Stufen in der Klassenhierarchie des Datenvertrags hinzufügen müssen. Das Hinzufügen einer Unterklasse fügt nicht deren Basisklasse hinzu:

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

Da die Funktionalität des KnownType-Attributs zu umfangreich sein kann, stellt WCF außerdem ServiceKnownTypeAttribute bereit, das Sie auf einen spezifischen Prozess oder einen spezifischen Vertrag anwenden können.

Schließlich ermöglichte WCF in .NET Framework 3.0 auch das Auflisten der erwarteten bekannten Typen in der config-Datei der Anwendung im system.runtime.serialization-Abschnitt. 

Obwohl die Verwendung bekannter Typen technisch gesehen gut funktioniert, sollten Sie sich nicht zu sehr darauf verlassen. In der traditionellen objektorientierten Modellierung möchten Sie die Basisklasse nicht mit spezifischen Unterklassen verbinden. Das Kennzeichen einer guten Basisklasse ist genau dies: Eine gute Basis ist eine gute Basisklasse für jede mögliche Unterklasse. Das Problem der bekannten Typen macht sie jedoch nur für Unterklassen geeignet, die sie kennt. Wenn Sie die gesamte Modellierung im Voraus durchführen, wenn Sie das System entwerfen, ist dies möglicherweise kein Problem. In der Praxis jedoch, wenn die Modellierung für die Anwendung über die Zeit entwickelt wird, wird es bis dahin unbekannte Typen geben, die Sie mindestens dazu zwingen, die Anwendung neu bereitzustellen. Wahrscheinlicher müssen Sie jedoch die Basisklassen modifizieren. 

Auflösungsprogramme für Datenverträge

Um das Problem zu lösen, führte WCF in .NET Framework 4 eine Möglichkeit ein, die bekannten Typen zur Laufzeit aufzulösen. Diese Programmiertechnik namens Auflösungsprogramm für Datenverträge stellt die leistungsfähigste Option dar, da Sie sie erweitern können, um die Probleme mit bekannten Typen vollständig automatisch zu lösen. Im Wesentlichen erhalten Sie die Möglichkeit, den Versuch des Prozesses abzufangen, Parameter zu serialisieren und zu deserialisieren. Sie können die bekannten Typen zur Laufzeit auflösen, sowohl für den Client als für den Dienst.

Der erste Schritt bei der Implementierung einer programmatischen Auflösung besteht in der Ableitung von der abstrakten DataContractResolver-Klasse, die wie folgt definiert ist:

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

Ihre Implementierung von TryResolveType wird aufgerufen, wenn WCF versucht, einen Typ in eine Meldung zu serialisieren, und der bereitgestellte Typ (Typparameter) sich von dem Typ unterscheidet, der im Prozessvertrag deklariert ist (deklarierter Typparameter). Wenn Sie den Typ serialisieren möchten, müssen Sie einige eindeutige Identifizierer angeben, die als Schlüssel zu einem Dictionary dienen, in dem die Identifizierer Typen zugeordnet werden. WCF stellt diese Schlüssel während der Deserialisierung bereit, sodass Sie auf der Basis dieses Typs binden können.

Beachten Sie, dass der Namespaceschlüssel keine leere Zeichenfolge oder Null sein darf. Obwohl praktisch jede eindeutige Zeichenfolge für die Identifizierer geeignet ist, empfehle ich, einfach den Typnamen und Namespace der CLR zu verwenden. Setzen Sie den Typnamen und Namespace in die ausgehenden typeName- und typeNamespace-Parameter.

Wenn TryResolveType den Wert "true" zurückgibt, wird der Typ als aufgelöst betrachtet, so als ob Sie das KnownType-Attribut angewendet hätten. Wenn "false" zurückgegeben wird, kann WCF den Aufruf nicht durchführen. Beachten Sie, dass TryResolveType alle bekannten Typen auflösen muss, auch die Typen, die mit dem KnownType-Attribut ausgestattet oder in der config-Datei aufgelistet sind. Dies führt zu einem möglichen Problem. Das Auflösungsprogramm muss mit allen bekannten Typen in der Anwendung verbunden sein. Der Prozessaufruf kann nicht mit anderen Typen ausgeführt werden, die mit der Zeit hinzugefügt worden sind. Daher ist dieses Verfahren bevorzugt eine Fallbacklösung, um zu versuchen, den Typ mittels des voreingestellten Auflösungsprogramms für bekannte Typen aufzulösen, den WCF verwendet hätte, wenn Ihr eigenes Auflösungsprogramm nicht verwendet würde. Dies ist genau der Zweck, für den der knownTypeResolver-Parameter vorgesehen ist. Wenn Ihre Implementierung von TryResolveType den Typ nicht auflösen kann, sollte das Problem an knownTypeResolver übergeben werden.

ResolveName wird aufgerufen, wenn WCF versucht, einen Typ aus einer Meldung zu deserialisieren, und der bereitgestellte Typ (Typparameter) sich von dem Typ unterscheidet, der im Prozessvertrag deklariert ist (deklarierter Typparameter). In diesem Fall stellt WCF die Identifizierer für den Typnamen und den Namespace bereit, sodass Sie diese einem bekannten Typ zuordnen können.

Betrachten Sie beispielsweise diese beiden Datenverträge:

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

Abbildung 4 zeigt ein einfaches Auflösungsprogramm für den Customer-Typ.

Abbildung 4 Das Customer-Auflösungsprogramm

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

Das Auflösungsprogramm muss jedem Prozess auf dem Proxy oder dem Dienstendpunkt als Verhalten angefügt sein. Die ServiceEndpoint-Klasse hat eine Eigenschaft namens Contract des Typs ContractDescription:

public class ServiceEndpoint {
  public ContractDescription Contract
  {get;set;}
  // More members
}

ContractDescription enthält eine Sammlung von Prozessbeschreibungen, mit einer Instanz von OperationDescription für jeden Prozess auf dem Vertrag:

public class ContractDescription {
  public OperationDescriptionCollection Operations
  {get;}
  // More members
}
public class OperationDescriptionCollection : 
  Collection<OperationDescription>
{...}

Jede OperationDescription hat eine Sammlung von Prozessverhaltensweisen des Typs IOperationBehavior:

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

In der Sammlung von Verhaltensweisen hat jeder Prozess stets ein Verhalten namens DataContractSerializerOperationBehavior mit einer Eigenschaft DataContractResolver:

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

Die Eigenschaft DataContractResolver ist voreingestellt Null, Sie können sie jedoch als das angepasste Auflösungsprogramm festlegen. Um ein Auflösungsprogramm auf dem Host zu installieren, müssen Sie über die Sammlung von Endpunkten in der Dienstbeschreibung iterieren, die auf dem Host gewartet wird:

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

Nehmen Sie an, Sie haben die folgende Dienstdefinition und verwenden das Auflösungsprogramm aus Abbildung 4.

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

Abbildung 5 zeigt die Installation des Auflösungsprogramms auf dem Host für AddressBookService.

Abbildung 5 Installieren eines Auflösungsprogramms auf dem 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();

Auf dem Client führen Sie ähnliche Schritte durch, außer dass Sie das Auflösungsprogramm auf den einzelnen Endpunkt des Proxys oder der Kanalfactory festlegen. Zum Beispiel sieht dies bei der folgenden Proxyklassendefinition folgendermaßen aus:

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

Abbildung 6 zeigt die Installation des Auflösungsprogramms auf dem Proxy, um den Dienst aus Abbildung 5 mit einem bekannten Typ aufzurufen.

Abbildung 6 Installieren eines Auflösungsprogramms auf dem 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);

Das generische Auflösungsprogramm

Das Erstellen und Installieren eines Auflösungsprogramms für die einzelnen Typen bedeutet offensichtlich viel Arbeit. Sie müssen alle bekannten Typen genauestens dokumentieren. Dies ist fehleranfällig und kann bei einem schnell wachsenden System aus der Kontrolle geraten. Um die Implementierung eines Auflösungsprogramms zu automatisieren, habe ich die Klasse GenericResolver geschrieben, die folgendermaßen definiert ist:

public class GenericResolver : DataContractResolver {
  public Type[] KnownTypes
  {get;}
  public GenericResolver();
  public GenericResolver(Type[] typesToResolve);
  public static GenericResolver Merge(
    GenericResolver resolver1,
    GenericResolver resolver2);
}

GenericResolver enthält zwei Konstruktoren. Ein Konstruktor kann ein Array bekannter Typen zur Auflösung annehmen. Der parameterlose Konstruktor fügt automatisch alle Klassen und Strukte in der aufrufenden Assembly und alle öffentlichen Klassen und Strukte in Assemblys, auf die von der aufrufenden Assembly verwiesen wird, als bekannte Typen hinzu. Der parameterlose Konstruktor fügt keine Typen hinzu, die aus einer durch .NET Framework-referenzierten Assembly stammen.

Außerdem enthält GenericResolver die MergeStatic-Methode, die Sie für die Zusammenführung der bekannten Typen aus zwei Auflösungsprogrammen verwenden können. Sie gibt eine GenericResolver-Klasse zurück, die das Gesamtergebnis der beiden angegebenen Auflösungsprogramme auflöst. Abbildung 7 zeigt den relevanten Teil von GenericResolver, ohne die Typen in den Assemblys zu reflektieren, da dies nichts mit WCF zu tun hat.

Abbildung 7 Implementieren von GenericResolver (teilweise)

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

Die wichtigsten Mitglieder von GenericResolver sind die Dictionarys m_TypeToNames und m_NamesToType. m_TypeToNames ordnet einen Typ einem Tupel seines Namens und seines Namespace zu. m_NamesToType ordnet einen Typnamespace und einen Typnamen dem tatsächlichen Typ zu. Der Konstruktor, der das Typenarray annimmt, initialisiert die beiden Dictionarys. Die TryResolveType-Methode verwendet den bereitgestellten Typ als Schlüssel für das m_TypeToNames-Dictionary, um den Namen und den Namespace des Typs zu lesen. Die ResolveName-Methode verwendet den bereitgestellten Namespace und Namen als Schlüssel für das m_NamesToType-Dictionary, um den aufgelösten Typ zurückzugeben.

Sie können zwar auch langwierigen Code ähnlich dem in Abbildung 5 und Abbildung 6 gezeigten Code verwenden, um GenericResolver zu installieren. Es ist jedoch am besten, dies mittels Erweiterungsmethoden durchzuführen. Dazu verwenden Sie meine AddGenericResolver-Methoden von GenericResolverInstaller, die folgendermaßen definiert sind:

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

Die AddGenericResolver-Methode nimmt ein params-Typenarray an. Dabei handelt es sich um eine offene, durch Kommas getrennte Liste von Typen. Wenn Sie keine Typen angeben, fügt AddGenericResolver alle Klassen und Strukte in der aufrufenden Assembly und alle öffentlichen Klassen und Strukte in den Assemblys, auf die von der aufrufenden Assembly verwiesen wird, als bekannte Typen hinzu. Betrachten Sie beispielsweise die folgenden bekannten Typen:

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

Abbildung 8 zeigt verschiedene Beispiele für die Verwendung der AddGenericResolver-Erweiterungsmethode für diese Typen.

Abbildung 8 Installieren von 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 installiert nicht nur GenericResolver, sondern versucht auch, diese mit dem alten generischen Auflösungsprogramm (wenn vorhanden) zusammenzuführen. Das bedeutet, dass Sie die AddGenericResolver-Methode mehrmals aufrufen können. Dies ist praktisch, wenn Sie eingeschlossene generische Typen hinzufügen:

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

Abbildung 9 zeigt eine teilweise Implementierung von GenericResolverInstaller.

Abbildung 9 Implementieren von 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);
    }
  }
}

Wenn keine Typen angegeben werden, verwendet AddGenericResolver den parameterlosen Konstruktor von GenericResolver. Andernfalls verwendet sie nur die angegebenen Typen, indem der andere Konstruktor aufgerufen wird. Beachten Sie die Zusammenführung mit dem alten Auflösungsprogramm, wenn vorhanden.

Das GenericResolver-Attribut

Wenn Ihr Dienst vom generischen Auflösungsprogramm abhängig ist, sollten Sie sich nicht auf den Host verlassen und den Bedarf für das generische Auflösungsprogramm zur Entwurfszeit deklarieren. Zu diesem Zweck habe ich GenericResolverBehaviorAttribute geschrieben:

[AttributeUsage(AttributeTargets.Class)]
public class GenericResolverBehaviorAttribute : 
  Attribute,IServiceBehavior {
  void IServiceBehavior.Validate(
    ServiceDescription serviceDescription,
    ServiceHostBase serviceHostBase) {
    ServiceHost host = serviceHostBase as ServiceHost;
    host.AddGenericResolver();
  }
  // More members
}

Mithilfe dieses kompakten Attributs wird der Dienst vom Host unabhängig:

GenericResolverBehaviorAttribute ist von IServiceBehavior abgeleitet, einer speziellen WCF-Schnittstelle, und ist die am häufigsten verwendete Erweiterung in WCF. Wenn der Host den Dienst lädt, ruft er die IServiceBehavior-Methoden, insbesondere die Validate-Methode, auf. Dadurch kann das Attribut mit dem Host interagieren. Im Fall von GenericResolverBehaviorAttribute wird dem Host das generische Auflösungsprogramm hinzugefügt.

Und dies ist die Lösung: eine vergleichsweise einfache und flexible Möglichkeit, die Probleme der Datenvertragsvererbung zu umgehen. Nutzen Sie diese Programmiertechnik für Ihr nächstes WCF-Projekt.

Juval Lowy ist als Softwarearchitekt bei IDesign tätig. Er bietet Schulungen und Beratung zu .NET und Architektur an. Dieser Artikel enthält Auszüge aus seinem aktuellen Buch "Programming WCF Services" (Programmieren von WCF-Diensten), (O'Reilly, 3. Aufl. 2010). Er ist außerdem Microsoft Regional Director für Silicon Valley. Sie erreichen Juval Lowy unter idesign.net.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Glenn Block und Amadeo Casas Cuadrado