Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Improving the ASP.NET Webservice Help Generator to Reflect Inheritance

3.63/5 (5 votes)
17 Aug 2008CPOL2 min read 1   445  
Apply an XSL transformation to the service description of an ASP.NET webservice to generate better interface documentation.

Introduction

XML webservices can be developed very easily within the .NET Framework, and moreover it provides us with a nice help generator so you can automatically show your clients how they have to talk with your service.

Unfortunately, the generated help doesn't reflect inherited data structures, so it becomes quite useless as soon as such structures are being used to build the interface. Inheritance is of course a very useful technique when authoring XML webservices. So, for instance, you may require login data for each request or you may want to reply some status data with each response. Thus every request structure may inherit a generic one and every response may inherit a generic response structure.

Background

Data exchange between an XML webservice and its consumers is defined by a WSDL contract. The .NET Framework creates it automatically from the structures defined by the webservice author. Such a contract is represented externally by an XML Schema document and internally by an instance of the System.Web.Services.Description.ServiceDescription class. In either case, the WSDL reflects exactly any inheritance structure using the XML Schema extension element pointing to the base structure. However, the default help rendering algorithm doesn't follow these extension references, therefore base structures remain unseen.

Solution

You can write your own help generator in order to generate any kind of documentation pages you like. The location of the WsdlHelpGenerator can be defined in the web.config.

But I really don't want to rewrite or change the default rendering algorithm, so my solution is to modify the WSDL data slightly for it, using a simple XSL transformation that replaces any extension reference by the appropriate base structure. This can be done by handling the PreLoad event of the WsdlHelpGenerator, so no code needs to be changed at all, apart from adding two methods to the WsdlHelpGenerator class.

Three Steps to Solve the Problem

  1. Get the installed default description generator DefaultWsdlHelpGenerator.aspx (on my computer, it's in C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG) and save it as WsdlHelpGenerator.aspx in the web directory of your webservice. Open your web.config and put...

    XML
    <webServices>
      <wsdlHelpGenerator href="WsdlHelpGenerator.aspx" />
    </webServices>
    

    ... inside the <system.web> section.

  2. Open WsdlHelpGenerator.aspx and add these two methods directly below the Page_Load method:

    C#
    protected override void OnPreLoad(EventArgs e) {
       base.OnPreLoad(e);
    
       // transform any service description stored within HttpContext
       // cf. Page_Load: try "wsdlsWithPost" first and fall back to "wsdls"
       string key = Context.Items["wsdlsWithPost"] != null ?
                   "wsdlsWithPost" : "wsdls";
    
       serviceDescriptions = (ServiceDescriptionCollection)Context.Items[key];
       TransformServiceDescriptions(ref serviceDescriptions);
       Context.Items[key] = serviceDescriptions;
     }
    
    void TransformServiceDescriptions(ref ServiceDescriptionCollection descriptions) {
    
       // modify each description by an XSLT processor
       ServiceDescriptionCollection transformed = new ServiceDescriptionCollection();
       System.Xml.Xsl.XslCompiledTransform xslt =
           new System.Xml.Xsl.XslCompiledTransform();
       xslt.Load(Server.MapPath("WsdlHelp.xsl"));
    
       foreach (ServiceDescription desc in descriptions)
       {
         // load original WSDL data
         MemoryStream ms1 = new MemoryStream(), ms2 = new MemoryStream();
         desc.Write(ms1);
    
         // process WSDL data using WsdlHelp.xsl
         ms1.Position = 0;
         xslt.Transform(new System.Xml.XPath.XPathDocument(ms1), null, ms2);
    
         // replace current WSDL data with the transformed stream
         ms2.Position = 0;
         transformed.Add(ServiceDescription.Read(ms2));
    
         ms1.Dispose();
         ms2.Dispose();
       }
       descriptions = transformed;
    }
    
  3. Finally, to get this code working, put the transformation file WsdlHelp.xsl into the web directory of your webservice. It may look as follows:

    XML
    <?xml version="1.0" encoding="utf-8"?>
    <xsl:stylesheet version="1.0"
        xmlns:xsl=http://www.w3.org/1999/XSL/Transform 
    		xmlns:s="http://www.w3.org/2001/XMLSchema">
      <xsl:output
        method="xml"
        indent="no"
        encoding="utf-8"
        omit-xml-declaration="no"
      />
      <!-- recursively dissolve any schema extension elements to the base structure -->
    
      <xsl:template match="/" xml:space="default">
        <xsl:apply-templates />
      </xsl:template>
    
      <xsl:template match="*" priority="0.5" xml:space="default">
        <xsl:copy>
          <xsl:copy-of select="attribute::*" />
          <xsl:choose>
            <xsl:when test="child::*" />
            <xsl:otherwise>
              <xsl:value-of select="." />
            </xsl:otherwise>
          </xsl:choose>
          <xsl:apply-templates select="child::*" />
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="s:complexType" priority="1.0">
        <xsl:element name="s:complexType" namespace="http://www.w3.org/2001/XMLSchema">
          <xsl:copy-of select="attribute::*" />
          <xsl:element name="s:sequence">
            <xsl:copy-of select=".//s:sequence/*" />
            <xsl:if test="./s:complexContent/s:extension">
              <xsl:comment> schema extension expanded: <xsl:value-of
                select="./s:complexContent/s:extension/@base"/> </xsl:comment>
              <xsl:call-template name="fetch-sequence">
                <xsl:with-param name="typename"
                  select="substring-after(./s:complexContent/s:extension/@base,':')" />
              </xsl:call-template>
            </xsl:if>
          </xsl:element>
        </xsl:element>
      </xsl:template>
    
      <xsl:template name="fetch-sequence">
        <xsl:param name="typename" />
        <xsl:copy-of select="//s:complexType[@name = $typename]//s:sequence/*" />
        <xsl:if test="//s:complexType[@name = $typename]/s:complexContent/s:extension">
          <xsl:call-template name="fetch-sequence">
            <xsl:with-param name="typename"
              select="substring-after(//s:complexType[@name = $typename]
    				/s:complexContent/s:extension/@base,':')" />
          </xsl:call-template>
        </xsl:if>
      </xsl:template>
    
    </xsl:stylesheet>

Points of Interest

Once you have your own WsdlHelpGenerator, you can also easily modify the appearance, add your own stylesheets, your company logo, etc.

If you don't want to have your webservice method list sorted alphabetically, replace the SortedList methodsTable within the Page_Load method of the WsdlHelpGenerator by an unsorted list.

You may also have a look at the excellent article Externalizing Web Service Documentation and apply XSL transformation there. Just put the C# code above into the ServiceDescriptionGenerator.aspx.cs file.

History

  • 17th August, 2008: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)