Monday, June 7, 2010

Using T4 to generate ‘Specified’ properties for a WCF proxy

Visual Studio 2008 makes it very easy to generate a set of WCF proxy classes: just add a service reference.  Sometimes though, you’ll want to alter the behaviour of the generated classes – especially since the WCF Proxy Generator has some shortcomings.

I’m going to outline a simple way to do this using Visual Studio’s built-in code generator (T4) and a bit of XPath.

Here’s a typical problem that you might encounter with the proxy generator; let’s say our WSDL contains a PayrollNumber element:

  1. <xs:simpleType name="String1">
  2.   <xs:restriction base="xs:string">
  3.     <xs:minLength value="1" />
  4.   </xs:restriction>
  5. </xs:simpleType>
  6. ...
  7. <xs:element minOccurs="0" name="PayrollNumber" type="cc:String1"/>

Some things to note about this element:

  1. It’s a string
  2. It’s optional (minOccurs=“0”)
  3. If specified, it has a minimum length of 1

The WCF Proxy Generator will give you something like this:

  1. [Serializable, XmlType]
  2. public partial class MyClass
  3. {
  4.     [XmlElement]
  5.     public string PayrollNumber { get; set; }

If you leave this property null and call the WCF service, it will send an empty element, leading to an invalid XML schema.  Running a schema validator will give you a big fat  XMLSchemaValidationException:

The value ‘’ is invalid according to its datatype ... The actual length is less than the MinLength value.

What we actually want is for the element to be left out of the request (it's optional after all).  This is a known issue with the proxy generator, and one workaround is to manually add a ‘Specified’ property to the class, which will be picked up by the XML serializer:

  1. [XmlIgnore]
  2. public bool PayrollNumberSpecified
  3. {
  4.     get { return PayrollNumber != null; }
  5. }

Since the proxy classes are partial, we can add this property to our own partial class of the same name.  Even better, we can auto-generate these partial classes using a T4 script that parses the WSDL.

To do this, create a new file in Visual Studio called [ServiceName].tt, and paste in the following (you’ll need to change the namespace etc):

Update (30/7): download the latest version.

  1. <#@ hostspecific="true" language="C#" #>
  2. <#@ name="System.Core" #>
  3. <#@ name="System.Xml" #>
  4. <#@ namespace="System.Xml" #>
  5. <#@ namespace="System.Collections.Generic" #>
  6. <#@ namespace="System.IO" #>
  7. #pragma warning disable 1591
  8. // <autogenerated>
  9. // This code was generated by a tool. Any changes made manually will be lost
  10. // the next time this code is regenerated.
  11. // </autogenerated>
  12.    using System.Xml.Serialization;
  13.  
  14. namespace MyNamespace.MyService
  15. {<#
  16.     XmlDocument doc = new XmlDocument();
  17.     string absolutePath = Host.ResolvePath(@"..\Service References\\cf8MyService\\cf8service.wsdl");
  18.     doc.Load(absolutePath);
  19.    
  20.     XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);
  21.     ns.AddNamespace("xs", "http://www.w3.org/2001/XMLSchema");
  22.    
  23.     foreach(XmlNode ndClass in doc.SelectNodes(@"//xs:complexType[@name]", ns))
  24.     {
  25.         bool createdClass = false;
  26.         foreach(XmlNode ndProp in ndClass.SelectNodes(@"./xs:sequence/xs:element[starts-with(@type, 'cc:String') and @minOccurs='0' and not(@maxOccurs)]", ns))
  27.         {
  28.             if (!createdClass) {
  29. #>
  30.    
  31.     public partial class <#= ndClass.Attributes["name"].InnerText #>
  32.     {
  33. <#
  34.                 createdClass = true;
  35.             }
  36.        
  37.             string fieldName = ndProp.Attributes["name"].InnerText;
  38. #>
  39.         [XmlIgnore]
  40.         public bool <#= fieldName #>Specified { get { return !string.IsNullOrEmpty(<#= fieldName #>); } }
  41.        
  42. <#        }
  43.         if (createdClass) {
  44. #>    }
  45. <#
  46.         }   
  47.     } #>
  48. }
  49. #pragma warning restore 1591

A brief explanation:

  • Uses Host.ResolvePath to get the path of the WSDL file
  • Uses XPath to select all string elements with minOccurs=‘0’
  • Generates partial classes with ‘Specified’ properties for each corresponding element

When you hit Save, a new file called [ServiceName].cs will be generated, containing a list of partial classes.