WCF Extensibility – WSDL Import (and Code Generation) Extensions

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .

Continuing on the theme of metadata extensibility, this post is the logical counterpart of the previous one – while that covered hooking on the WSDL export code path, this one shows how we can intercept (and change) the WSDL import process, where one of the WCF tools (such as svcutil or the WsdlImporter class) takes the WSDL produced by a WCF or a 3rd-party service and generates a client which is able to talk to it.

Metadata isn’t one area in WCF which I haven’t worked much, and as I was researching for this post, I ran into two new extensibility points which I hadn’t heard about: IOperationContractGenerationExtension and IServiceContractGenerationExtension – I already updated the index page to add those two (this was actually one of my goals when I started writing this series – learn more about the product I work on, and it’s been quite educative for myself). They really are very much related to the WSDL import extension (you see something in the WSDL which you want to act upon, then you use those generation extensions to alter the code generation to deal with that new information). The example in the official samples (WSDL Documentation) uses this, and the example I have here also uses the same method. So I’m combining those three extensibility points in this single post.

Interface definitions

  1. public interface IWsdlImportExtension
  2. {
  3.     void BeforeImport(ServiceDescriptionCollection wsdlDocuments, XmlSchemaSet xmlSchemas, ICollection<XmlElement> policy);
  4.     void ImportContract(WsdlImporter importer, WsdlContractConversionContext context);
  5.     void ImportEndpoint(WsdlImporter importer, WsdlEndpointConversionContext context);
  6. }

Before any WCF-specific object is created based on the service metadata, BeforeImport is called to let the extension work with the service description objects (from the WSDL namespace), XML schemas and any policy assertions made in the WSDL. Notice that none of those parameters are WCF-related: the wsdlDocuments parameter is of type System.Web.Services.Description.ServiceDescriptionCollection (in the System.Web.Services.dll), and the other two parameters are from System.Xml.dll (System.Xml.Schema.XmlSchemaSet and ICollection of System.Xml.XmlElement), so this is called really before WCF has had any chance to look at the metadata.

After WCF has a chance to initialize its objects, then ImportContract is called so the user has a chance to modify the contract or insert other generation behaviors such as IOperationContractGenerationExtension and IServiceContractGenerationExtension for each contract found in the WSDL (each <wsdl:portType> element). ImportEndpoint is called for each endpoint which is found in the WSDL (corresponding to <wsdl:services>/<wsdl:port> elements). The parameters for the two methods are similar: the instance of the WsdlImporter class, and a conversion context which contains references to both the WCF objects relative to the operation (e.g., the ContractDescription for ImportContract) and the correspondent WSDL objects (e.g., the WSDL PortType for ImportContract). The order of the call of those two operations is not defined, so you shouldn’t depend on it.

  1. public interface IServiceContractGenerationExtension
  2. {
  3.     void GenerateContract(ServiceContractGenerationContext context);
  4. }
  5. public interface IOperationContractGenerationExtension
  6. {
  7.     void GenerateOperation(OperationContractGenerationContext context);
  8. }

Both IOperationContractGenerationExtension and IServiceContractGenerationExtension interfaces are simple, with one method with a single parameter with the context for the operation. For the operation contract generation extension, the parameter to GenerateOperation is an object with references to the ServiceContractGenerator which is being used to generate the client, as well as information relative to the operation itself: the operation description, and various System.CodeDom objects representing the operation itself. For the service contract generation extension, the parameter to GenerateContract is an object with references to the same ServiceContractGenerator, and information about the contract as a whole: the contract description, and CodeDom objects for the generated contract and callback contract (in case of duplex contracts), and the list of all operations in the contract.

Public implementations in WCF

There are no public implementations of the contract generation extensions in WCF. In most of the scenarios I’ve seen, those classes are implemented as internal classes, added to the service description during the service import via some WSDL import extension. There are some public implementations of IWsdlImportExtension, which define the default behavior of WCF metadata import for various components (transport, context and message encoding binding elements, standard binding and message contract), which you can derive from and override some of the default behaviors.

How to add a WSDL import extension

WSDL import extensions are typically used in conjunction with some tool such as svcutil. Since we can’t change the code from that tool, we can use configuration to add any extensions we define. The example below shows how a class which implements IWsdlImportExtension, called WsdlImportExtension (on namespace ImportNonReferencedTypes, in the ImportNonReferencedTypes assembly) is added to the list of extensions used by the WSDL importer used by the code. If we’re using svcutil, then this block of configuration would go into svcutil.exe.config.

  1. <system.serviceModel>
  2.   <client>
  3.     <metadata>
  4.       <wsdlImporters>
  5.         <extension type="ImportNonReferencedTypes.WsdlImportExtension, ImportNonReferencedTypes"/>
  6.       </wsdlImporters>
  7.     </metadata>
  8.   </client>
  9. </system.serviceModel>

It’s worth noting that if we’re generating the client directly (using MetadataExchangeClient, ServiceContractGenerator and WsdlImporter themselves) we can add the extension via code (using the WsdlImportExtensions property of the WsdlImporter class), but the configuration route is more often used because it can be used with tools such as svcutil as well.

How to add service and operation contract generator extensions

Similar to the WSDL export extension, service (and operation) contract generator extensions need to be added as contract (and operation) behaviors to the description. The implementation of the methods for the I[Operation/Contract]Behavior interfaces are often empty, with the interface just being used as the vehicle for the generation extension implementations into the WCF description.

  1. public class MyServiceContractGeneratorExtension : IServiceContractGenerationExtension, IContractBehavior
  2. {
  3.     // IContractBehavior methods ommitted
  4.  
  5.     public void GenerateContract(ServiceContractGenerationContext context)
  6.     {
  7.         // Change the contract here
  8.     }
  9. }

The operation contract generator extension is similar, with an IOperationBehavior instead of an IContractBehavior being the other interface implemented by the extension class.

Real world scenario: generating types not referenced in the service contract

This has popped up quite a few times in the forums, asked in different ways. Someone defines some classes in the server which are useful for the implementation, but are never referenced by the server implementation, and when they create proxies for the service, those types don’t show up. This is the design of WCF (well, if it isn’t used, it won’t be generated), but since I’ve seen it often, I decided to see if it can be accomplished via the extensibility points. And, as usual (otherwise I wouldn’t be writing about it here), there’s a way to do it in WCF, even if it’s not the easiest one.

Here’s the service I’ll use for this scenario – a calculator service for complex numbers. In this case, the complex representation is given by its “traditional” (Cartesian) coordinates (real + imaginary), but somehow we want the client to know of an alternative representation, the vectorial one (with the modulus and phase or angle defining the complex number). To help me test this sample I also added two extra types not at all relative to the scenario, but I wanted to try types in different namespaces as well.

  1. [DataContract(Name = "ComplexNumber", Namespace = "")]
  2. public class ComplexNumber
  3. {
  4.     [DataMember]
  5.     public double Real { get; set; }
  6.     [DataMember]
  7.     public double Imaginary { get; set; }
  8. }
  9.  
  10. [DataContract(Name = "ComplexNumberPolar", Namespace = "")]
  11. public class ComplexNumberPolar
  12. {
  13.     [DataMember]
  14.     public double Modulus { get; set; }
  15.     [DataMember]
  16.     public double Phase { get; set; }
  17. }
  18.  
  19. [DataContract(Name = "Person", Namespace = "https://another.type")]
  20. public class Person
  21. {
  22.     [DataMember]
  23.     public string Name { get; set; }
  24.     [DataMember]
  25.     public int Age { get; set; }
  26.     [DataMember]
  27.     public Address Address { get; set; }
  28. }
  29.  
  30. [DataContract(Name = "Address", Namespace = "https://another.type")]
  31. public class Address
  32. {
  33.     [DataMember]
  34.     public string Street { get; set; }
  35.     [DataMember]
  36.     public string State { get; set; }
  37.     [DataMember]
  38.     public string Zip { get; set; }
  39. }
  40.  
  41. [ServiceContract]
  42. public interface IComplexCalculator
  43. {
  44.     [OperationContract]
  45.     ComplexNumber Add(ComplexNumber n1, ComplexNumber n2);
  46.  
  47.     [OperationContract]
  48.     ComplexNumber Subtract(ComplexNumber n1, ComplexNumber n2);
  49.  
  50.     [OperationContract]
  51.     ComplexNumber Multiply(ComplexNumber n1, ComplexNumber n2);
  52.  
  53.     [OperationContract]
  54.     ComplexNumber Divide(ComplexNumber n1, ComplexNumber n2);
  55. }

If we host this service and create a proxy for it, only the ComplexNumber type will be generated. If we use the ServiceKnownType attribute to declare the other three data contract types to be known to the service, those types will actually be part of the schemas in the service metadata, but the WCF tools won’t emit them, because they aren’t used anywhere in the service contract.

In this sample, I wanted to add an annotation to those “unreferenced” types to indicate that they should be generated on the client, regardless of whether WCF wanted them to be. So I defined another attribute (UnreferencedServiceKnownType) as a WSDL Export extension (covered in last post) that would add those types to the service metadata and annotate them appropriately

  1. [ServiceContract]
  2. [UnreferencedServiceKnownType(typeof(ComplexNumberPolar), typeof(Person), typeof(Address))]
  3. public interface IComplexCalculator
  4. {
  5.     [OperationContract]
  6.     ComplexNumber Add(ComplexNumber n1, ComplexNumber n2);

And before long, the usual disclaimer: this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). There are many things which can be improved, including not needing to add all the types (the code could infer that Address was needed based on Person) to the contract. Also, I don’t have a lot of experience with metadata, so it’s possible that I’m doing something which could be accomplished easier (possibly with some classes from the XML or Schema namespaces, or even WCF ones). And as usual, I’ve kept error checking to a minimum.

To implement the WSDL export extension, I’m using the XsdDataContractExporter class inside the ExportContract method. For all types which are passed to the attribute constructor, we search for them in the exporter schemas and add an annotation to be read on the client for each. The rest of the class can be found in the code gallery sample.

  1. public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
  2. {
  3.     XsdDataContractExporter xsdExporter = new XsdDataContractExporter(exporter.GeneratedXmlSchemas);
  4.     if (xsdExporter.CanExport(this.types))
  5.     {
  6.         xsdExporter.Export(this.types);
  7.         foreach (XmlSchema schema in exporter.GeneratedXmlSchemas.Schemas())
  8.         {
  9.             foreach (DictionaryEntry schemaType in schema.SchemaTypes)
  10.             {
  11.                 foreach (Type unreferencedType in this.types)
  12.                 {
  13.                     XmlQualifiedName qName = xsdExporter.GetRootElementName(unreferencedType);
  14.                     if (qName.Equals(schemaType.Key))
  15.                     {
  16.                         XmlSchemaComplexType complexType = schemaType.Value as XmlSchemaComplexType;
  17.                         if (complexType != null)
  18.                         {
  19.                             AddAnnotation(complexType);
  20.                         }
  21.                     }
  22.                 }
  23.             }
  24.         }
  25.     }
  26. }

Now for the import extension. As usual, the implementation is quite trivial – simply add one of the contract generator extensions to the contract behavior, and let it do the work when the metadata is being imported.

  1. public class WsdlImportExtension : IWsdlImportExtension
  2. {
  3.     public void BeforeImport(System.Web.Services.Description.ServiceDescriptionCollection wsdlDocuments, XmlSchemaSet xmlSchemas, ICollection<XmlElement> policy)
  4.     {
  5.     }
  6.  
  7.     public void ImportContract(WsdlImporter importer, WsdlContractConversionContext context)
  8.     {
  9.         var schemas = importer.WsdlDocuments[0].Types.Schemas;
  10.         context.Contract.Behaviors.Add(new ServiceContractGeneratorExtension(schemas));
  11.     }
  12.  
  13.     public void ImportEndpoint(WsdlImporter importer, WsdlEndpointConversionContext context)
  14.     {
  15.     }
  16. }

The contract generator extension initializes itself by going through all of the schemas passed to it by the WSDL import extension, and adding to its XmlSchemaSet field the types which have the annotation indicating that they need to be generated on the client. The schemas are processed and since the default way that WCF exports metadata is via a modular WSDL (with the schemas being imported from the “main” document), the code deals with processing the schema import statements and loading them as well. For each “real” schema which it encounters, it calls the AddTypes method which will filter the schemas for only types which are marked with the appropriate annotation.

  1. public ServiceContractGeneratorExtension(XmlSchemas schemas)
  2. {
  3.     this.loadedSchemas = new List<string>();
  4.     this.schemaSet = new XmlSchemaSet();
  5.     foreach (XmlSchema schema in schemas)
  6.     {
  7.         ProcessSchema(schema);
  8.     }
  9. }
  10.  
  11. private void ProcessSchema(XmlSchema schema)
  12. {
  13.     this.AddTypes(schema);
  14.     foreach (XmlSchemaObject entry in schema.Includes)
  15.     {
  16.         XmlSchemaImport import = entry as XmlSchemaImport;
  17.         if (import != null)
  18.         {
  19.             ProcessSchemaImport(import);
  20.         }
  21.     }
  22. }
  23.  
  24. private void ProcessSchemaImport(XmlSchemaImport import)
  25. {
  26.     if (import.Schema == null)
  27.     {
  28.         if (!this.loadedSchemas.Contains(import.SchemaLocation))
  29.         {
  30.             WebClient c = new WebClient();
  31.             byte[] schemaBytes = c.DownloadData(import.SchemaLocation);
  32.             using (MemoryStream ms = new MemoryStream(schemaBytes))
  33.             {
  34.                 import.Schema = XmlSchema.Read(ms, null);
  35.             }
  36.  
  37.             this.loadedSchemas.Add(import.SchemaLocation);
  38.         }
  39.     }
  40.  
  41.     if (import.Schema != null)
  42.     {
  43.         this.ProcessSchema(import.Schema);
  44.     }
  45. }

The implementation of the AddTypes method is the piece of code which I think there may be some clever way of accomplishing, but this worked well fine for me. Basically the code writes out the schema, then performs a XML search for the appropriate annotation. If it found any, it will remove all elements which are not related to the annotated types, then save the schema for future processing.

  1. private void AddTypes(XmlSchema schema)
  2. {
  3.     bool hasType = false;
  4.     MemoryStream ms = new MemoryStream();
  5.     schema.Write(ms);
  6.     ms.Position = 0;
  7.     XmlDocument doc = new XmlDocument();
  8.     doc.Load(ms);
  9.     XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
  10.     nsManager.AddNamespace("xs", "https://www.w3.org/2001/XMLSchema");
  11.     List<string> typeNames = new List<string>();
  12.     foreach (XmlNode docNode in doc.SelectNodes("//xs:schema/xs:complexType/xs:annotation/xs:documentation", nsManager))
  13.     {
  14.         if (docNode.InnerText == UnreferencedServiceKnownTypeAttribute.GenerateOnClientText)
  15.         {
  16.             typeNames.Add(docNode.ParentNode.ParentNode.Attributes["name"].Value);
  17.             hasType = true;
  18.         }
  19.     }
  20.  
  21.     if (hasType)
  22.     {
  23.         List<XmlNode> toRemove = new List<XmlNode>();
  24.         foreach (XmlNode node in doc.DocumentElement.ChildNodes)
  25.         {
  26.             bool shouldRemove = true;
  27.             if (node.NodeType == XmlNodeType.Element)
  28.             {
  29.                 if (node.LocalName == "complexType")
  30.                 {
  31.                     if (typeNames.Contains(node.Attributes["name"].Value))
  32.                     {
  33.                         shouldRemove = false;
  34.                     }
  35.                 }
  36.                 else if (node.LocalName == "element")
  37.                 {
  38.                     if (typeNames.Contains(node.Attributes["type"].Value))
  39.                     {
  40.                         shouldRemove = false;
  41.                     }
  42.                 }
  43.             }
  44.  
  45.             if (shouldRemove)
  46.             {
  47.                 toRemove.Add(node);
  48.             }
  49.         }
  50.  
  51.         foreach (XmlNode node in toRemove)
  52.         {
  53.             node.ParentNode.RemoveChild(node);
  54.         }
  55.  
  56.         ms.SetLength(0);
  57.         XmlWriter w = XmlWriter.Create(ms);
  58.         doc.WriteTo(w);
  59.         w.Flush();
  60.         ms.Position = 0;
  61.         this.schemaSet.Add(XmlSchema.Read(ms, null));
  62.     }
  63. }

Finally, when GenerateContract is caled, we use the counterpart of the XsdDataContractExporter which we used in the WSDL export, the (aptly named) XsdDataContractImporter class to import the schemas we saved into a CodeCompileUnit object. When it’s done, we find out which types don’t appear in the generated code, and add those ourselves.

  1. public void GenerateContract(ServiceContractGenerationContext context)
  2. {
  3.     CodeCompileUnit codeCompileUnit = new CodeCompileUnit();
  4.     XsdDataContractImporter importer = new XsdDataContractImporter(codeCompileUnit);
  5.     importer.Import(this.schemaSet);
  6.  
  7.     foreach (CodeNamespace ns in codeCompileUnit.Namespaces)
  8.     {
  9.         CodeNamespace targetNamespace = context.ServiceContractGenerator.TargetCompileUnit.Namespaces
  10.             .OfType<CodeNamespace>().Where(x => x.Name == ns.Name).FirstOrDefault();
  11.  
  12.         if (targetNamespace == null)
  13.         {
  14.             context.ServiceContractGenerator.TargetCompileUnit.Namespaces.Add(ns);
  15.         }
  16.         else
  17.         {
  18.             foreach (CodeTypeDeclaration type in ns.Types)
  19.             {
  20.                 if (targetNamespace.Types.OfType<CodeNamespace>().Where(x => x.Name == type.Name).Count() == 0)
  21.                 {
  22.                     targetNamespace.Types.Add(type);
  23.                 }
  24.             }
  25.         }
  26.     }
  27. }

Now, as usual, a client to test the code. The code was “borrowed” from the Custom WSDL Publication sample, and it generates a proxy client which should contain the unreferenced types from the service as well.

  1. Uri metadataAddress = new Uri("https://" + Environment.MachineName + ":8000/Service?wsdl");
  2. MetadataExchangeClient mexClient = new MetadataExchangeClient(metadataAddress, MetadataExchangeClientMode.HttpGet);
  3. mexClient.ResolveMetadataReferences = true;
  4. MetadataSet metaDocs = mexClient.GetMetadata();
  5.  
  6. WsdlImporter importer = new WsdlImporter(metaDocs);
  7. ServiceContractGenerator generator = new ServiceContractGenerator();
  8.  
  9. Collection<ContractDescription> contracts = importer.ImportAllContracts();
  10. importer.ImportAllEndpoints();
  11.  
  12. foreach (ContractDescription contract in contracts)
  13. {
  14.     generator.GenerateServiceContractType(contract);
  15. }
  16.  
  17. if (generator.Errors.Count != 0)
  18. {
  19.     throw new ApplicationException("There were errors during code compilation.");
  20. }
  21.  
  22. // Write the code dom
  23. CodeGeneratorOptions options = new CodeGeneratorOptions();
  24. options.BracingStyle = "C";
  25. CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("C#");
  26. using (StreamWriter sw = File.CreateText(fileName))
  27. {
  28.     IndentedTextWriter textWriter = new IndentedTextWriter(sw);
  29.     codeDomProvider.GenerateCodeFromCompileUnit(generator.TargetCompileUnit, textWriter, options);
  30.     textWriter.Close();
  31.     Console.WriteLine("File saved to {0}", Path.GetFullPath(fileName));
  32. }

Notice that I only created the proxy using the helper classes myself to make the sample self-contained. I also tried it using svcutil (creating a svcutil.exe.config and registering the extension) and it worked out just as well.

Coming up

The last post of the series about metadata extensibility, about policy import / export extensions.

[Code in this post]

[Back to the index]