Implementing Service-Oriented Integration with ASP.NET

 

Patterns and Practices home

Integration Patterns

Start | Previous | Next

Contents

Context

Background

Implementation Strategy

Example: Building an ASP.NET Web Service to Access the Mainframe Gateway

Resulting Context

Testing Considerations

Security Considerations

Acknowledgments

Context

You are connecting two systems by using Service-Oriented Integration so that one system can consume functions provided by the other. The service provider is implemented by using the Microsoft .NET Framework.

Background

In the Global Bank scenario, the Web application server accesses the mainframe computer through a gateway service to retrieve a customer's account balance. Figure 1 illustrates how the interaction between the two systems is implemented as Service-Oriented Integration by using Microsoft ASP.NET Web services.

Figure 1. Two applications using Service-Oriented Integration to access the gateway

This implementation describes how to expose functionality that resides on a mainframe computer as a Web service so that it can be accessed through Service-Oriented Integration. The gateway is a custom C# application that connects to the banking mainframe computer through Microsoft Host Integration Server (HIS). The mainframe computer manages the account balances for the customer accounts and provides functions such account balance retrieval, account credits, and account debits.

Note   This implementation focuses on the interaction between the Web server and the Gateway component. For a more detailed description of the interaction between the Gateway and the mainframe computer, see Implementing Gateway with Host Integration Server 2004.

The mainframe gateway exposes a number of different methods to get account balances, to credit and debit accounts, and to perform other functions. This implementation describes only the use of the GetAccountInfo method to return the name and balance of an account.

Implementation Strategy

As already mentioned, the strategy is to enable Service-Oriented Integration by exposing mainframe functionality as an ASP.NET Web service. Before discussing detailed steps, it is helpful to review the concept of Service-Oriented Integration as well as some details about ASP.NET Web services.

Service-Oriented Integration

Service-Oriented Integration connects applications through the exchange of documents, usually in the form of XML documents. Figure 2 shows Service-Oriented Integration passing documents between a service consumer and a service provider.

Figure 2. Document exchange in Service-Oriented Integration

The document exchange in Figure 2 does not imply interaction with a specific instance of a remote object. Instead, when the document is passed from the consumer to the provider, it triggers the execution of a specific function or service that is self-contained and stateless. This is an important difference between Service-Oriented Integration and Distributed Object Integration (also known as instance-based integration), which allows the client to manage the lifetime of a specific remote object instance.

The following example shows an XML document passed inside a SOAP envelope.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
               xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetAccountInfoRequest xmlns="http://msdn.microsoft.com/patterns/">
      <AccountID xmlns="">12345678</AccountID>
    </GetAccountInfoRequest>
  </soap:Body>
</soap:Envelope>
  

ASP.NET Web Services

When you use ASP.NET to implement a Web service, the function that processes the incoming document is implemented as a method of a .NET Framework class. The .NET Framework manages the instantiation of a specific instance of that class.

The .NET Framework does quite a bit of work behind the scenes when it receives an incoming SOAP request, as shown in Figure 3. Understanding the functions that the .NET Framework performs internally is not strictly necessary, but it is very helpful when designing ASP.NET Web services. It also gives you a good appreciation of the amount of functionality that resides in the .NET Framework.

Figure 3. ASP.NET Web services handling

When a document reaches a specified endpoint, the server has two main pieces of information to work with: the contents of the document and the URL of the endpoint. With those two pieces of information, the server has to complete the following steps:

  1. Determine the .NET Framework class that should handle the request.
  2. Determine the method to invoke inside that class.
  3. Instantiate data transfer objects to pass data from the incoming document to the method.
  4. Invoke the method.
  5. Return the results.

Internet Information Services (IIS) uses the file extension of the incoming URL to map requests to the appropriate Internet Services API (ISAPI) handler. ASP.NET Web services endpoints carry an .asmx extension in the URL. The .asmx extension in the URL causes IIS to map the request to the standard .NET Framework HTTP pipeline. Based on default computer configuration settings, the .NET Framework HTTP pipeline hands control over to a WebServiceHandler. The WebServiceHandler in turn determines the .NET Framework class that is associated with the URL by reading the .asmx file referenced by the URL. In Figure 4, the Gateway.asmx file specifies the class Gateway as the class servicing requests. This class is defined inside a code-behind page named Gateway.asmx.cs.

The Web service handler still has to determine the method to invoke for the incoming document. By default, it determines the method to invoke based on the value of the SOAPAction field that is part of the HTTP header. The following is a sample of that part of an HTTP header.

SOAPAction: "http://msdn.microsoft.com/patterns/GetAccountInfo"
  

After the Web service handler has determined the method to invoke, it parses the incoming XML document and creates an instance of the matching .NET Framework objects to be passed to the method. This step is referred to as deserialization and is performed by the .NET Framework XML Serializer. The method performing the function does not even have to know that the parameters were originally passed as an XML document wrapped inside a SOAP envelope. Lastly, the Web service handler creates an instance of the class that implements the service function and invokes the appropriate method, passing instances of the applicable data transfer objects.

For a more detailed description of ASP.NET Web service internals, see "How ASP.NET Web Services Work" on MSDN (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwebsrv/html/howwebmeth.asp) [Skonnard03].

Building an ASP.NET Web Service

XML Web services expose the service contract to potential consumers as a Web Services Description Language (WSDL) document. To simplify development of Web services, the .NET Framework offers a variety of capabilities, including the automatic generation of WSDL documents based on a service implementation. As a result, you have a number of options when creating a new service interface:

  • Develop the code first. Write the service implementation code first, and then let the .NET Framework create the WSDL document from the code.
  • Specify the XML schemas first. Develop message schemas as XML Schema definition language (XSD) schemas first, generate code from the schemas, and then let the .NET Framework create the WSDL document.
  • Develop the WSDL first. Develop the WSDL definition first, and then let the .NET Framework generate skeleton code matching the definition.

Each approach has advantages and limitations. Generally, the choice is among the amount of control you need, the effort required, and the ease of integrating with other platforms.

Developing the Code First

The easiest way to expose code as a Web service is to label a .NET Framework class with the [WebService] attribute and to label the affected method with a [WebMethod] attribute. The .NET Framework then takes care of all the details. For example, it generates the WSDL document to give potential clients a description of the service. To perform this conversion without any additional information, the .NET Framework derives the names of SOAP body elements from the name of the method and its arguments. The advantage of this approach is clearly its simplicity. With two lines of extra "code," a method can become a Web service that can be invoked remotely. The limitation lies in the fact that the service interface exactly resembles the code, including variable names. The .NET Framework allows you to override the default naming choices by specifying additional attributes, but the specification for the service still resides in application code. This can be undesirable, for example, if an enterprise wants to keep a global repository of all message definitions independent of the platform that implements a particular service. Another potential pitfall, however, is that this approach can generate a service that will not function properly. For example, if a method returns a non-serializable object, any service invocation will fail even though the code compiled without errors.

Specifying XML Schemas First

A Web services call typically requires two separate messages: a request message and a response message. These messages play roles similar to the roles played by the parameters and the return value of a regular method. Just as defining the signature of a method is an important first step in the creation of a new method, defining the request and return message formats is a logical first step in the creation of a Web services interface. After the message format has been specified, it can be compiled into a C# classes automatically without incurring additional effort. At run time, the .NET Framework still renders the WSDL document and eliminates the need for hand-coding.

XML Web services use XSD documents to define message formats. Defining these documents by using a standard format such as XSD has advantages over generating them from the code. XSD documents are platform-independent and can be used to define message formats independent of the technology implementing the service, whether it is the .NET Framework, the Java 2 Platform, Java 2 Enterprise Edition (J2EE), or any in a range of other technologies. This makes it feasible to keep a global repository of all message types in an enterprise. Creating XML schemas also gives the interface designer exact control over the look of SOAP messages that are sent to and from a particular service.

Potential disadvantages of developing the XSD schemas first include the fact that a new specification language (XML Schema) and tool has to be used. However, many modern development environments, including Microsoft Visual Studio .NET, include powerful and easy-to-use XML Schema editors. Creating XSD documents first also requires additional development and build steps because the XSD document first has to be converted into C# classes. This step has to be repeated whenever the XSD document changes, presenting the risk that the document and the implementation of the service can get out of synchronization.

Developing WSDL First

The specification of a service contract depends on factors other than just the format of inbound and outbound messages. For example, to be accessible, the service has to be bound to a port that can be addressed by using a specified Uniform Resource Identifier (URI). A WSDL document specifies these additional contract terms, such as ports and binding. Starting with the creation of a WSDL document ensures that all details of the service contract (at least to the extent that they are supported by WSDL) can be specified directly and in a technology-neutral manner. The biggest drawback of starting with a WSDL document is that the verboseness of WSDL makes the manual creation tedious.

Example: Building an ASP.NET Web Service to Access the Mainframe Gateway

As described in the Global Bank scenario, the Web application server accesses the mainframe computer through a gateway service to retrieve a customer's account balance. This example creates an ASP.NET Web service that accesses the gateway to the mainframe.

The ASP.NET page framework supports the development of both service providers and service consumers. This implementation begins with the design and development of the service provider. In the example, the mainframe gateway is the service provider. Fortunately, most of the work that the .NET Framework performs is hidden from the developer of an ASP.NET Web service. The developer's main task is to create the interface definition of the service and to fill out the implementing code with the correct attributes to configure the behavior of the ASP.NET internals.

As mentioned in the previous section, there are several approaches to developing a Web service by using ASP.NET. This implementation begins by defining XSD documents for the request and response messages. This approach gives you good control over the published service interface while sparing you from having to hand-code full-blown WSDL documents.

To expose an existing function as an ASP.NET Web service, follow these steps:

  1. Develop an XSD document for the request and the response message.
  2. Generate data transfer classes from the schema.
  3. Define the operations that the service exposes.
  4. Connect the service interface to the service implementation.
  5. Build and run the service.
  6. Create a test client that invokes the Web service.

Figure 4 illustrates this process. Each numbered step is explained in detail in the sections that follow.

Figure 4. Building an ASP.NET Web service

Step 1: Develop XSD Documents

The first step is to define the format of the request and response messages by creating two XSD files. The following are the XSD files for the GetAccountInfo method generated using the Microsoft Visual Studio .NET XSD Editor. To ensure maximum interoperability between the gateway service and the consumers, this implementation uses only standard data types and avoids using .NET Framework–specific data types such as DataSet.

The XML schema for the request message (GetAccountInfoRequest.xsd) looks like the following example. (To improve readability, the namespace declarations have been omitted.)

<xs:schema … >
  <xs:element name="GetAccountInfoRequest">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="AccountID" type="xs:string" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
  

You will notice that the document for the request is quite simple; it contains only a single string data element named <AccountID>.

The following sample is what the XML schema response (GetAccountInfoResponse.xsd) looks like.

<xs:schema … >
  <xs:element name="GetAccountInfoResponse">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="AccountID" type="xs:string" />
        <xs:element name="Balance" type="xs:decimal" />
        <xs:element name="Name" type="xs:string" />
        <xs:element name="Description" type="xs:string" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
  

After creating the XSD files, the next step is to generate the data transfer classes.

Step 2: Generate Data Transfer Classes

The XML Schema Definition tool (xsd.exe) in the .NET Framework can create a .NET Framework class from an XSD file. To create the data transfer files, run the following commands from the command prompt in the directory where the XSD files reside:

xsd /n:GatewayWS /c GetAccountInfoRequest.xsd
xsd /n:GatewayWS /c GetAccountInfoResponse.xsd
  

These commands generate the GetAccountInfoRequest.cs and GetAccountInfoResponse.cs files. Using the /namespace option (or the short form, /n) enables you to specify the namespace to be used for the generated class. Otherwise, the global namespace is used as the default namespace, which could lead to conflicting namespaces.

The generated class file GetAccountInfoRequest.cs is shown in the following sample.

namespace GatewayWS {
    using System.Xml.Serialization;
    …    
    public class GetAccountInfoRequest {
       [System.Xml.Serialization.XmlElementAttribute
             (Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
        public string AccountID;
    }
}
  

The attribute in front of the AccountID field instructs the XML Serializer that no namespace qualifier is required in the XML string for this element.

The generated class has no functionality, but rather it is a simple data holder class that acts as a Data Transfer Object [Trowbridge03] between the .NET Framework and the service implementation. This class is the .NET Framework representation of the XML document that is embedded in a SOAP request to the gateway service. As described previously, at run time the XML Serializer parses the incoming SOAP document, creates an instance of this object, and populates all fields with the values from the incoming SOAP XML document.

Step 3: Define the Operations That the Service Exposes

Now that you have data transfer objects, you can define the methods and operations that the service is going to expose. As described in "ASP.NET Web Services" earlier in this pattern, an ASP.NET Web service endpoint is created from the combination of an .asmx file and an associated code-behind page that contains the actual class definition. Because the Visual Studio .NET tool set generates the .asmx file for you, you can focus on the Gateway class itself, which is contained in the file Gateway.asmx.cs. This implementation follows the Service Interface [Trowbridge03] approach and separates the public service interface from the implementation.

The class Gateway inherits from the base class WebService. To keep things simple, the class exposes only a single method, GetAccountInfo, as a service.

namespace GatewayWS
{
    [WebService(Namespace="http://msdn.microsoft.com/patterns/")]
    public class Gateway : System.Web.Services.WebService
    {
    …
        [WebMethod]
        [SoapDocumentMethod(ParameterStyle=SoapParameterStyle.Bare)]
        public GetAccountInfoResponse GetAccountInfo(
            GetAccountInfoRequest GetAccountInfoRequest)
        {
            return null;
        }
    }
}
  

For now, leave the method body empty and only return a null value. You will tie this method to the service implementation in the next step.

Note that both the method and the class are encoded with special attributes. The [WebMethod] attribute of the GetAccountInfo method makes the method accessible as part of the Web service. The additional [SoapDocumentMethod(…)]attribute customizes the way the XML Serializer parses incoming SOAP messages. By default, the XML Serializer expects method parameters to be wrapped inside an additional element, which is in turn contained in the <soap:Body> element. Changing the ParameterStyle setting to SoapParameterStyle.Bare makes these method parameters appear immediately under the <soap:Body> element, rather than encapsulated in an additional XML element.

The following example shows a SOAP message that causes the GetAccountInfo method of the Gateway.asmx Web service to be invoked.

<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
               xmlns:xsd="http://www.w3.org/2001/XMLSchema"
               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetAccountInfoRequest xmlns="http://msdn.microsoft.com/patterns">
      <AccountID xmlns="">1234567</AccountID>
    </GetAccountInfoRequest>
  </soap:Body>
</soap:Envelope>
  

The <soap:Body> element contains a <GetAccountInfoRequest> element. This element corresponds to the single parameter that the GetAccountInfo method receives.

Without the ParameterStyle setting, the SOAP request for the same method would look like the following sample. Note that an additional GetAccountInfo node beneath the <soap:Body> element wraps the method parameters.

<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
               xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetAccountInfo xmlns="http://msdn.microsoft.com/patterns/">
      <GetAccountInfoRequest xmlns="http://msdn.microsoft.com/patterns">
        <AccountID xmlns="">1234567</AccountID>
      </GetAccountInfoRequest>
    </GetAccountInfo>
  </soap:Body>
</soap:Envelope>
  

Because the method receives all the necessary data elements inside a single data transfer object, the additional wrapping is not required and makes the SOAP message unnecessarily verbose. Therefore, set the ParameterStyle to Bare.

Step 4: Connect the Service Interface to the Service Implementation

Now that you have built the service interface and have encoded it with the necessary Web service attributes, you need to link the still-empty GetAccountInfo method to the actual service implementation. One option is to insert the code that implements the service into the GetAccountInfo method of the Gateway class. However, this approach has a number of drawbacks.

First, the Gateway class inherits from the WebService base class. That means that the class cannot be part of a separate inheritance tree that the service implementation may require.

Second, tying together the Web service interface and the implementation makes it harder to test the implementation outside the Web services context.

Third, the functions that the service implementation provides may not exactly match the service interface definition. For example, the service implementation may require multiple calls to fine-grained methods, whereas the Web service interface should expose coarse-grained functions. Or, the service implementation may use .NET Framework–specific data types, such as DataSets, that you are seeking to avoid in the public Web service interface. As a result, the service may need to contain logic to arbitrate between the public Web service interface and the internal implementation.

Finally, tying the Web service directly to the implementation means that the Web service can be functional only when the service implementation is running and is available. That may not always be the case if the actual service implementation resides in an existing system. For example, many mainframe systems have offline times when they are not available for online requests. As the Web service is weaved into a larger solution, these outages could hinder testing. For testing purposes, it would be very convenient to be able to replace the service implementation with a dummy implementation without affecting the service interface.

All these problems can be solved with a combination of well-known design patterns that is shown in Figure 5. The first step is to separate the interface of the service functionality from the implementation—for example, the mainframe access. You can do so by applying the Separated Interface pattern [Fowler03]. The interface IGlobalBank is a generic interface that represents the functions provided by the mainframe system, but it has no dependency on the server running HIS. The class GlobalHIS implements the methods specified in this interface by connecting to the mainframe through HIS.

Figure 5. Separating the service implementation from the service interface

After you have separated the interface from the implementation, you can create a Service Stub [Fowler03]. A service stub is a dummy implementation of an external service that reduces external dependencies during testing. The GlobalStub service stub implements the same IGlobalBank interface but does not actually connect to the mainframe computer. Instead, it simulates the mainframe functions internally.

Now that you have two implementations, you have to decide which one to use. You want to be able to switch between the dummy implementation and the real implementation without having to change any code or having to recompile. Therefore, this example uses a Plugin [Fowler03]. Plugin links classes during configuration rather than compilation. You implement the Plugin inside the GlobalPlugInFactory class. The factory class reads the name of the implementing class from a configuration file so that you can switch between GlobalStub and GlobalHIS at run time by changing the application configuration file.

What is left for the Gateway class to do? It has to call the GlobalPlugInFactory class to obtain a reference to an implementation of the IGlobalBank interface. Next, it has to invoke the appropriate method in the interface. The names and types of the parameters of the service implementation may differ from the XML schemas that you created, so the Gateway class may have to perform simple mapping functions between the two data representations.

Even though the implementation of these extra classes is not strictly necessary to create a working Web service, these patterns simplify testing tremendously and are well worth the additional coding effort. It turns out that the implementation of each class is actually quite simple. The implementation involves the following steps:

  1. Create an interface.
  2. Create service implementations.
  3. Create the plug-in factory.
  4. Implement the Web service method.

Let's walk through these steps one by one.

Step 4.1: Create an Interface

First, create an interface for the service implementation. The following code is from the IGlobalBank.cs file. The code references the AccountInfo class. The AccountInfo class is used by the service implementation. Note that this interface and the data transfer class have no dependency on a specific service implementation.

public interface IGlobalBank
{
    // Throws ArgumentException if account does not exist.
    AccountInfo GetAccountInfo (string AccountID);
}

public class AccountInfo 
{
    public string accountID;
    public string name;
    public string description;
    public decimal balance;

    public AccountInfo(string accountID, string name, 
                       string description, decimal balance)
    {
        this.accountID = accountID;
        this.name = name;
        this.description = description;
        this.balance = balance;
    }
}
  

Step 4.2: Create the Service Implementations

Next, create two implementations of the interface as shown. Create one that is a simple stub, and create another one that connects to the mainframe system through HIS.

The two implementation classes are named GlobalHIS and GlobalStub. GlobalHIS connects to the external system. In the example, the mainframe gateway is the external system. The class implements the IGlobalBank interface.

public class GlobalHIS : IGlobalBank
{
    …  
    public AccountInfo GetAccountInfo(string accountID)
    {
       decimal balance = 0.00m; 
       string name = "";
       object [] contextArray = null;

       TCP_LinkTRM_NET.GlobalBank bank = new TCP_LinkTRM_NET.GlobalBank ();
       bank.cedrbank (ref name ,ref accountID ,ref balance, 
                      ref contextArray);
       AccountInfo info = new AccountInfo(accountID, "","", balance);
       return info;
    }     
}
  

The GlobalStub class provides another implementation of the IGlobalBank interface but is a simple stub without any dependency on external systems. It uses an internal accounts collection to simulate account balances. For testing purposes, the class constructor initializes this collection by using hard-coded values.

public class GlobalStub : IGlobalBank
{
   static IDictionary accounts = (IDictionary) new Hashtable();

   public GlobalStub()
   {
       if (accounts.Count == 0) 
       {
           accounts.Add("123", 
             new AccountInfo("123", "TestAccount", "TestDescription", 777.12m));
       }
   }

   public AccountInfo GetAccountInfo(string accountID)
   {
       if (!accounts.Contains(accountID)) 
           throw new ArgumentException("Account does not exist"); 
       return (AccountInfo)accounts[accountID];
   }
}
  

Step 4.3: Create the Plug-in Factory

Now that you have created two implementations of the IGlobalBank interface, you need to decide the implementation that you want to use at run time. This is the purpose of the GlobalBankPlugInFactory class. The class reads the name of the class to be used from the configuration file and then creates an instance of that class. It returns a reference to the newly created object to the service interface, but the reference is cast to the IGlobalStub interface. This way the service interface is not dependent on the implementation that was chosen.

public class GlobalBankPlugInFactory
{
    static string globalBankImplName = System.Configuration.ConfigurationSettings.AppSettings["GlobalBankImpl"];

    static public GlobalBank.IGlobalBank GetGlobalBankImpl()
    {
        Type globalBankImplType = Type.GetType(GetImplementationClassName());
        if (globalBankImplType == null)
            throw new TypeLoadException("Cannot load type " + globalBankImplName);
        GlobalBank.IGlobalBank bank = (GlobalBank.IGlobalBank)Activator.CreateInstance(globalBankImplType);
        return bank;
    }

    static public string GetImplementationClassName()
    {
        if (globalBankImplName == null)
            globalBankImplName = "GlobalBank.GlobalStub";
        return globalBankImplName;
    }
}
  

For the Plugin class to be functional, you need to add the following entry to the Web.config file.

<appSettings>
      <add key="GlobalBankImpl" value="GlobalBank.GlobalStub"/>
</appSettings>
  

Step 4.4: Implement the Web Service Method

Now that you have defined the implementation classes, you can finally fill in the implementation code to the GetAccountInfo method of the Gateway.asmx Web service as follows.

 [WebMethod]
[SoapDocumentMethod(ParameterStyle=SoapParameterStyle.Bare)]
public GetAccountInfoResponse GetAccountInfo(
            GetAccountInfoRequest GetAccountInfoRequest)
        {
            GlobalBank.IGlobalBank bank = GlobalBank.GlobalBankPlugInFactory.GetGlobalBankImpl();

            GlobalBank.AccountInfo globalAccountInfo = bank.GetAccountInfo(GetAccountInfoRequest.AccountID);

            return BuildAccountInfo(globalAccountInfo);
        }

private GetAccountInfoResponse BuildAccountInfo(GlobalBank.AccountInfo globalAccountInfo)
        {
            GetAccountInfoResponse response = new GetAccountInfoResponse();
            response.AccountID = globalAccountInfo.accountID;
            response.Balance = globalAccountInfo.balance;
            response.Name = globalAccountInfo.name;
            response.Description = globalAccountInfo.description;
            return response;
        } 
  

The preparation you have done pays off. The implementation of the Web service interface method now consists of three easy steps:

  1. Obtain a reference to the IGlobalBank interface.
  2. Invoke the service implementation by using the reference.
  3. Construct the correct response format to return to the consumer.

Each step can be implemented in a single line of code. Step 3 is implemented in a BuildAccountInfoResponse private helper method. In this contrived example, it might appear unnecessary to use separate structures for GetAccountInfoResponse and GlobalBank.AccountInfo because they are essentially identical. However, each structure is likely to undergo a different change cycle over time. Including this translation step allows the gateway to accommodate changes to either the HIS interface or to the gateway interface without affecting both interfaces.

Step 5: Build and Run the Web Service

Now you are ready to build and run the Web service. In Visual Studio .NET, click the Build menu, and then click Build Solution. Visual Studio then compiles the Web service. Browse the Web service by typing the URL in the Microsoft Internet Explorer Address bar. For the example, the URL is http://localhost/GatewayWS/Gateway.asmx.

A key aspect of a service is the service contract. XML Web services use WSDL documents to describe the contract between a service consumer and the provider. A WSDL document is quite comprehensive. Fortunately, the .NET Framework automatically renders the WSDL document that describes the service. The following example shows the definitions section of the WSDL document.

<?xml version="1.0" encoding="utf8"?>
<definitions …>
    <types>…</types>
    <message name="GetAccountInfoSoapIn">
        <part name="GetAccountInfoRequest" element="s0:GetAccountInfoRequest"/>
    </message>
    <message name="GetAccountInfoSoapOut">
        <part name="GetAccountInfoResult" element="s0:GetAccountInfoResult"/>
    </message>
    <portType name="GatewaySoap">
        <operation name="GetAccountInfo">
            <input message="tns:GetAccountInfoSoapIn"/>
            <output message="tns:GetAccountInfoSoapOut"/>
        </operation>
    </portType>
    <binding>…</binding>
    <service name="Gateway">
        <port name="GatewaySoap" binding="tns:GatewaySoap">
            <soap:address location="http://localhost/GatewayWS/Gateway.asmx"/>
        </port>
    </service>
</definitions>
  

The other major sections of the WSDL document include the following:

  • Types
  • Messages
  • Bindings
  • Service
Note   For the sake of clarity, some sections of the document have been condensed here; they are discussed in more detail later in this chapter.

Types

The <types> element specifies the request and response messages. The content of the element reflects the XSD documents that you created in step 1.

<types>
  <s:schema elementFormDefault="qualified"
            targetNamespace="http://msdn.microsoft.com/patterns">
    <s:element name="GetAccountInfoRequest" type="s0:GetAccountInfoRequest"/>
    <s:complexType name="GetAccountInfoRequest">
      <s:sequence>
        <s:element minOccurs="0" maxOccurs="1" form="unqualified" 
                   name="AccountID" type="s:string"/>
      </s:sequence>
    </s:complexType>
    <s:element name="GetAccountInfoResult" type="s0:GetAccountInfoResponse"/>
    <s:complexType name="GetAccountInfoResponse">
      <s:sequence>
        <s:element minOccurs="0" maxOccurs="1" form="unqualified" 
                   name="AccountID" type="s:string"/>
        <s:element minOccurs="1" maxOccurs="1" form="unqualified" 
                   name="Balance" type="s:decimal"/>
        <s:element minOccurs="0" maxOccurs="1" form="unqualified" 
                   name="Name" type="s:string"/>
        <s:element minOccurs="0" maxOccurs="1" form="unqualified" 
                   name="Description" type="s:string"/>
      </s:sequence>
    </s:complexType>
  </s:schema>
</types>
  

Messages

The next section of the WSDL document specifies the operations that the service supports. For each operation, the request and response message format is specified through the types declared in the <types> section of the WSDL document. As you can see in the condensed WSDL listing, the Web service you are building provides a single operation named GetAccountInfo. The operation takes a GetAccountInfoRequest as an argument and returns a message of type GetAccountInfoResult. The .NET Framework derives the name of the operation directly from the name of the method that implemented it.

Bindings

The third major section of the WSDL document defines the binding of the operation to a transport protocol and a message format. The style attribute of the <soap:binding> element is set to document, indicating that the operation is document-oriented and not remote procedure call–oriented. The [soapAction] attribute of the <soap:operation> element specifies the action string to be used in the SOAPAction HTTP header. As explained in "ASP.NET Web Services," the .NET Framework uses this string to determine the method to execute.

The soap:body elements describe that the message format will be literal, meaning that the body portion of an incoming SOAP request looks exactly as specified in the types section of the document without any additional wrapping or encoding. The specified style of SOAP message exchange is also referred to as doc/literal.

<binding name="GatewaySoap" type="tns:GatewaySoap">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http"
                  style="document"/>
    <operation name="GetAccountInfo">
        <soap:operation style="document" 
              soapAction="http://msdn.microsoft.com/patterns/GetAccountInfo" />
        <input>
            <soap:body use="literal"/>
        </input>
        <output>
            <soap:body use="literal"/>
        </output>
    </operation>
</binding>
  

Service

The last part of the WSDL document specifies an address for the service. The service in the example is available through the URL http://localhost/GatewayWS/Gateway.asmx.

As you can see, you can save a significant amount of work by having the .NET Framework generate the WSDL document for you rather than coding it manually.

Step 6: Create a Test Client

Now that the service is available online and it is properly described by a service contract in the form of a WSDL document, you are ready to create a client application that consumes the service.

To create a test client that accesses the Web service, create a new ASP.NET project. Use the Add Web Reference Wizard in Visual Studio .NET Solution Explorer to create a reference to the gateway Web service. You can also use the Web Services Description Language tool (Wsdl.exe). This command-line tool creates proxy classes from WSDL. Compiling this proxy class into an application and then calling the method of this proxy class causes the proxy class to package a SOAP request across HTTP and to receive the SOAP-encoded response.

You can use these proxy classes later to generate automated test cases by using test tools such as NUnit (http://www.nunit.org).

Resulting Context

Evaluate the following benefits and liabilities to decide whether you should implement and use Service-Oriented Integration with ASP.NET.

Benefits

  • Efficient. ASP.NET does a lot of the work involved in exposing functions as Web services.
  • Flexible. If you want, you can still control the exact behavior declaratively through the use of attributes. This approach provides a good combination of simplicity without being unnecessarily restrictive. If you need even finer-grained control, you can replace the standard WebServiceHandler with a custom class.

Liabilities

  • Geared towards custom applications. Even though the .NET Framework does a lot of the Web service coding and configuration, you still have to code the service implementation in C# or Visual Basic .NET by hand. ASP.NET does not offer built-in support for process modeling and orchestrations that might be needed for the creation of more complex composite services. If this type of functionality is required, consider using Implementing Service-Oriented Integration with BizTalk Server 2004 instead.
  • Synchronous interaction. By default, this approach supports only synchronous interaction. The client has to wait until the service completes. In distributed service-oriented environments, asynchronous communication is often the preferred approach.

Testing Considerations

Two design decisions significantly improve your ability to test the service provider in this implementation:

  • The separation of the service interface from the service implementation
  • The use of a plug-in to dynamically substitute a service stub for the implementation

Service Interface Separation

The separation of the service interface from the service implementation makes it easy to test the service implementation without having to deal with the Web service aspect of the gateway component. You can create unit test cases as you would for any application. Do this by creating test cases for the service implementation and by circumventing the service interface, as shown in Figure 6).

Figure 6. Unit testing of Service-Oriented Integration

Automating the unit test cases has a very desirable side effect: you can apply the same test cases to both the real implementation and the dummy implementation. Applying the same test cases ensures that the GlobalStub class is a true rendering of the mainframe functionality. It also allows you to use the stub implementation to run all functional tests with confidence.

Service Stub

The service stub, in combination with the plug-in factory, allows you to switch between the real service implementation and a dummy implementation at run time by changing a configuration file. This allows you to test consumers of the service more easily, reliably, and quickly because you eliminate any dependencies to the mainframe service (see Figure 7).

Figure 7. Replacing the implementation of the gateway for testing

You can be assured that the switch does not affect the behavior of the gateway because both the dummy (stub) implementation and the Host Integration Server interface have been unit-tested by using the same suite of unit tests.

Security Considerations

The Web service implementation presented here does not incorporate any security considerations. This may be undesirable because the Web service exposes sensitive account data that resides in the bank's mainframe computer. Even though the Web service resides behind the firewall, you would not want anyone with Visual Studio .NET and a little programming skill to create a client to this service to retrieve customers' account balances.

Security has been widely recognized as a critical element for the success of Web services. Microsoft has partnered with IBM and VeriSign to develop the Web Services Security (WS-Security) specification. This specification describes how SOAP messages can be augmented with security certificates. The WS-Security specification is currently implemented by Web Services Enhancements (WSE) for Microsoft .NET.

Acknowledgments

[Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.

[Skonnard03] Skonnard, Aaron. "How ASP.NET Web Services Work." MSDN Library, May 2003. Available at: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwebsrv/html/howwebmeth.asp.

[Trowbridge03] Trowbridge, David, et al. Enterprise Solution Patterns Using Microsoft .NET. Microsoft Press, 2003. Also available on the MSDN Architecture Center at: http://msdn.microsoft.com/architecture/patterns/default.aspx?pull=/library/en-us/dnpatterns/html/Esp.asp.

Start | Previous | Next