Want to show your appreciation? Please to my charity.

Monday, September 08, 2008

Using WSCodeGen with Spring.Net

Since I posted the Slow Startup Performance with Spring.Net WebServiceProxyFactory about 3 weeks ago, we have made great progress towards the solution to this problem. In an earlier post, I demonstrated how to use WSCodeGen in a typical application. In this post, I'm going to discuss how we effectively used Spring.Net's configuration file to drive the WSCodeGen, a nice idea from one of my colleagues.

Using the generated client in Spring IoC container

Certainly, we can easily use the generated client in the Spring.Net to replace the WebServiceProxyFactory, but for every each Web Service client, we will have to create a definition entry in the WebServiceClient.xml file as well as an entry in the Spring.Net's XML configuration file. Taking the example in this post, we'll have something like below in the Spring.Net's XML configuration file. The WSCodeGen client can accept any property that is allowed in the ProductTemplate of the WebServiceProxyFactory, plus two dummy properties, "ServiceInterface" and "Tag", we'll use the "ServiceInterface" in the later section.

<object id="HelloWorld"
      type="Example.Client.WebService.HelloWorldClient, Example.Client.WebService">
  <property name="Timeout" value="${WebService.DefaultTimeout}" />
  <property name="EnableDecompression" value="true"/>
  <property name="Url" value="http://localhost:3586/HelloWorld.asmx"/>
</object>

It is tedious and error prone maintaining the similar configuration information in two different XML files and keeping them in sync. Wouldn't be great if we can just use the Spring.Net's configuration file only. The good news is that WSCodeGen supports any XML file as long as a proper XSLT file is also supplied to transformed it.

Eliminate the duplicated configuration

Let's take a look at the old Spring.Net's XML configuration file below that uses WebServiceProxyFactory. Thanks to the Spring's abstract object definition, I'm so glad to see that all our Web Service client definitions inherit from AbstractWebServiceClient. This abstract layer made our cut over to WSCodeGen very easy.

We have more then forty Web Service clients and I'm showing only one in the XML section below. AbstractWebServiceClient object defines an abstract template for any WebServiceProxyFactory based client. It uses AbstractWebServiceProductTemplate which in turn uses CookieContainer. The object for the "target" property of CustomViewService is an Web Service client.

Note: The AbstractXmlCarrierUnpacker is a AOP proxy we used to overcome the problem that Web Service doesn't work with ICollection<T>, IList<T> and IDictionary<T> as parameter or return type in the domain interface. Let's ignore this for now.

<object id="CookieContainer" type="System.Net.CookieContainer"/>
 
<object abstract="true" id="AbstractWebServiceProductTemplate">
  <property name="Timeout" value="${WebService.DefaultTimeout}" />
  <property name="CookieContainer" ref="CookieContainer" />
  <property name="EnableDecompression" value="true"/>
  <property name="UseDefaultCredentials" value="true"/>
  <property name="PreAuthenticate" value="true"/>
</object>
 
<object id="AbstractWebServiceClient" abstract="true"
  type="Spring.Web.Services.WebServiceProxyFactory,Spring.Services">
  <property name="ProductTemplate">
    <object parent="AbstractWebServiceProductTemplate"/>
  </property>
</object>
 
<object id="CustomViewService" parent="AbstractXmlCarrierUnpacker">
  <property name="proxyInterfaces" 
    value="Demo.Service.ICustomViewService, Demo.BizModel"/>
  <property name="target">
    <object parent="AbstractWebServiceClient">
      <property name="ServiceUri" 
        value="${WebService.IM2.BaseURL}/CustomViewService.asmx"/>
      <property name="ServiceInterface"
        value="Demo.Service.Web.ICustomViewServiceWS, Demo.BizModel"/>
    </object>
  </property>
</object>

To make the configuration change to replace WebServiceProxyFactory based client with the WSCodeGen generated client, I used WinVi32.exe, which allows me to use the regular expression as well as binary mode. I have underlined all the changes in the final XML below. The AbstractWebServiceClient becomes useless and can be deleted after all clients are converted.

<object id="CookieContainer" type="System.Net.CookieContainer"/>
 
<object abstract="true" id="AbstractWebServiceProductTemplate" 
        name="AbstractWSCodeGenClient">
  <property name="Timeout" value="${WebService.DefaultTimeout}" />
  <property name="CookieContainer" ref="CookieContainer" />
  <property name="EnableDecompression" value="true"/>
  <property name="UseDefaultCredentials" value="true"/>
  <property name="PreAuthenticate" value="true"/>
</object>
 
<object id="AbstractWebServiceClient" abstract="true"
  type="Spring.Web.Services.WebServiceProxyFactory,Spring.Services">
  <property name="ProductTemplate">
    <object parent="AbstractWebServiceProductTemplate"/>
  </property>
</object>
 
<object id="CustomViewService" parent="AbstractXmlCarrierUnpacker">
  <property name="proxyInterfaces" 
    value="Demo.Service.ICustomViewService, Demo.BizModel"/>
  <property name="target">
    <object parent="AbstractWSCodeGenClient"
      type="Demo.WSClient.CustomViewService, Demo.WSClient">
      <property name="Url" value="${BaseURL}/CustomViewService.asmx"/>
      <property name="ServiceInterface"
        value="Demo.Service.Web.ICustomViewServiceWS, Demo.BizModel"/>
    </object>
  </property>
</object>

If you are still with me so far, you may noticed that the "ServiceInterface" property of the WSCodeGen client. Since the client was generated at compile time, what is point of setting the implemented interface at the runtime time again? Good question!

Dummy properties of the generated client class

There are two dummy properties, "ServiceInterface" and "Tag", being added to each Web Service client that is created by the WSCodeGen. They are called dummy properties because their setters do absolutely nothing. The purpose of those properties is to allow us adding additional information about the client in the Spring.Net's XML configuration file so we can keep the configuration information in one place. An XSLT transformer can then easily extract those information to create the XML document needed by the WSCodeGen.

Now let's add this XSLT file that picks up all the objects inherited from AbstractWSCodeGenClient and transform them to another XML document that is understood by the WSCodeGen.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                xmlns:spring="http://www.springframework.net">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:param name="namespace">Demo.WSClient</xsl:param>
  <xsl:template match="/">
    <web-service xmlns="urn:web-service-client-configuration-1.0">
      <client-group namespace="{$namespace}" xml-namespace="http://demo.com/1.0">
        <xsl:for-each select="//spring:object[@parent='AbstractWSCodeGenClient']">
          <client class-name="{substring(substring-before(@type,','), string-length($namespace)+2)}" 
                  xml-namespace="#">
            <interface>
              <xsl:value-of select="spring:property[@name='ServiceInterface']/@value"/>
            </interface>
          </client>
        </xsl:for-each>
      </client-group>
    </web-service>
  </xsl:template>
</xsl:stylesheet>

Finally, we need to tell the VS.Net to take the Spring.Net's configuration file and transform it using the XSLT file to generate the source code. The following line should be used in the after build event of the Model project.

..\..\..\dependency\WSCodeGen\net\2.0\Debug\WebServiceClientGenerator.exe ..\..\..\src\Demo.SmartClient\app.config ..\..\..\src\Demo.WSClient\WSClient.cs ..\..\..\src\Demo.WSClient\WSClient.xsl

Your mileage may vary

The above was exactly how we integrated the WSCodeGen into our real world project. Indeed the examples are real except the true domain name is replaced by "demo.com". The XSLT was created based on our conventions illustrated below with examples.

  1. Web Services are named in PascalCase. For example, CustomViewService.
  2. Web Services are exposed as <name>.asmx. For example, CustomViewService.asmx. We actually, uses "CustomViewService.asmx" literally as object ID in our Spring.Net configuration file. Take a look at this post of how we made this possible to avoid naming conflicts.
  3. XML namespace of every Web Service starts with "http://demo.com/1.0" and followed by the name of the Web Service. For CustomViewService, it is http://demo.com/1.0/CustomViewService.
  4. Uses the Web Service name as the class name of the generated client. This is to ensure that the client name matches with the XML namespace.

The name match between 3 and 4 is essential for the XSLT to work because we use xml-namespace="#" to indicate that class name is used to suffix the XML namespace inherited from the group configuration. If you have no control to the XML namespace of the Web Services that clients are connecting to. You should consider to make user the the dummy property "Tag" of the client to embed that information in the Spring.Net configuration file.

Q&A

Questions can be posted to http://www.codeplex.com/WSCodeGen/Thread/List.aspx, or leave a comment here.

No comments:

Post a Comment