Context
Your application is deployed on the Microsoft Windows operating system. You have decided to expose a piece of your application's functionality as an ASP.NET Web Service. Interoperability is a key issue so you cannot use complex data types that are present only in the Microsoft .NET Framework.
Background
When you insert an audio compact disc (CD) into your computer often the program that you use to play the CD informs you of various pieces of information regarding the recording. This information might include track information, cover art, reviews, and so on. To demonstrate an implementation of the Service Interface pattern, this is implemented as an ASP.NET Web service.
Implementation Strategy
Service Interface describes a separation of interface mechanics and application logic. The interface is responsible for implementing and enforcing the contract for a service that is being exposed and the application logic is responsible for the business functionality that the interface uses in a particular way. This example uses an ASP.NET Web service to implement the service interface.
Note: The application logic that is shown here is an example of the Table Data Gateway pattern. In a typical application, there would be some additional business functionality that the implementation would provide. To focus on Service Interface, such additional business functionality is omitted from this example.
Service Interface Implementation
An ASP.NET Web Service is used to implement Service Interface. Implementing this as a Web Service makes this piece of functionality accessible to any number of disparate systems using Internet standards, such as XML, SOAP, and HTTP. Web services depend heavily upon the acceptance of XML and other Internet standards to create an infrastructure that supports application interoperability.
Because the focus is on interoperability between the consumer and the provider you cannot rely on complex types that may or may not be present on different platforms. This leads you to define a contract that provides interoperability. The approach described below involves defining a data transfer object using an XML schema, generating the data transfer object using platform specific tools and then relying on the platform to implement the service interface code that uses the data transfer object. This is not the only approach that will work. The .NET Framework generates all the pieces of functionality for you. However, there are cases in which it generates service interfaces that are not easily interoperable. On the other hand, you could specify the interface using Web Services Description Language (WSDL) and XML schema and then use the wsdl.exe utility to generate service interfaces for your application..
Contract
As described in Service Interface a contract exists which allows providers of a service and consumers to interoperate. There are three aspects to this contract when implementing it as an ASP.NET Web service:
Specify XML schema. The definition of the data that is transferred between the consumer and the provider is specified using an XML schema. The input to the service is a simple variable of the type long; therefore a schema is not needed for this scenario because simple types are built into the SOAP specification. However, the return type of the Web service is not a simple type, so the type must be specified using an XML schema. In this example, the schema is contained in the Recording.xsd file.
Data transfer object. The .NET framework has a tool called xsd.exe which, given an XML schema, can generate a data transfer object to be used by the code that implements the Web service. In this example, the name of the data transfer object is Recording and it is contained in the Recording.cs file.
Service Interface implementation. A class that inherits from System.Web.Services.WebService and specifies at least one method that is marked with the [WebMethod] attribute. In this example, the class is called RecordingCatalog and it is contained in the RecordingCatalog.asmx.cs file. This class is responsible for making the call to the service implementation and also for translating the output of the service implementation into the format that the Web service will use. The functionality to translate the data is encapsulated in a class called RecordingAssembler and contained in the RecordingAssembler.cs file. This class is an example of an assembler, which is a variant of the Mapper pattern. [Fowler03]
The following diagram depicts the relationship of the classes that implement the service interface.
Recording.xsd
The definition of the information that will be transferred to the client is specified using an XML schema. The following schema defines two complex types; Recording and Track.
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:tns="http://msdn.microsoft.com/practices" elementFormDefault="qualified"
targetNamespace="http://msdn.microsoft.com/patterns" xmlns:
xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Recording" type="tns:Recording" />
<xs:complexType name="Recording">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:long" />
<xs:element minOccurs="1" maxOccurs="1" name="title" type="xs:string" />
<xs:element minOccurs="1" maxOccurs="1" name="artist" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="unbounded" name="Track" type="tns:Track" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Track">
<xs:sequence>
<xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:long" />
<xs:element minOccurs="1" maxOccurs="1" name="title" type="xs:string" />
<xs:element minOccurs="1" maxOccurs="1" name="duration" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:schema>
The Recording type has an ID, artist, title, and an unbounded number of Track types. A Track type also has ID, title, and duration elements.
Recording.cs
As mentioned earlier, the .NET Framework has a xsd.exe command-line tool, which takes as input an XML schema and outputs a class that can be used in your program. The generated class is used as the return value of the Web service. The command that was used to generate the Recording.cs class is as follows:
xsd /classes Recording.xsd
The output that was produced by running this command is shown below:
//------------------------------------------------------------------------------
// <autogenerated>
// This code was generated by a tool.
// Runtime Version: 1.0.3705.288
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </autogenerated>
//------------------------------------------------------------------------------
//
// This source code was auto-generated by xsd, Version=1.0.3705.288.
//
using System.Xml.Serialization;
/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://msdn.microsoft.com/practices")]
[System.Xml.Serialization.XmlRootAttribute(Namespace=http://msdn.microsoft.com/practices,
IsNullable=false)]
public class Recording {
/// <remarks/>
public long id;
/// <remarks/>
public string title;
/// <remarks/>
public string artist;
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("Track")]
public Track[] Track;
}
/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://msdn.microsoft.com/practices")]
public class Track {
/// <remarks/>
public long id;
/// <remarks/>
public string title;
/// <remarks/>
public string duration;
}
RecordingCatalog.asmx.cs
After the types are defined, you need to implement the actual Web service implementation. This class encapsulates all of the Service Interface behavior. The service that is being exposed is defined explicitly by using the [WebMethod] attribute.
[WebMethod]
public Recording Get(long id)
{ /* */ }
The Get method takes as input an id and returns a Recording object. As described in the XML schema a Recording may also include a number of Track objects. The following is the implementation.
using System.ComponentModel;
using System.Data;
using System.Web.Services;
namespace ServiceInterface
{
[WebService(Namespace="http://msdn.microsoft.com/practices")]
public class RecordingCatalog : System.Web.Services.WebService
{
private RecordingGateway gateway;
public RecordingCatalog()
{
gateway = new RecordingGateway();
InitializeComponent();
}
#region Component Designer generated code
//
#endregion
[WebMethod]
public Recording Get(long id)
{
DataSet ds = RecordingGateway.GetRecording(id);
return RecordingAssembler.Assemble(ds);
}
}
}
The Get method makes a call to the RecordingGateway to retrieve a DataSet. It then makes a call to the RecordingAssembler.Assemble method to translate the DataSet into the generated Recording and Track objects.
RecordingAssembler.cs
The reason this class is part of the service interface is because of the need to translate the output of the application logic into the objects that are being sent out over the Web service. The RecordingAssembler class is responsible for translating the return type of the service implementation, in this case an ADO.NET DataSet, into the Recording and Track types that were generated in a previous step.
using System;
using System.Collections;
using System.Data;
public class RecordingAssembler
{
public static Recording Assemble(DataSet ds)
{
DataTable recordingTable = ds.Tables["recording"];
if(recordingTable.Rows.Count == 0) return null;
DataRow row = recordingTable.Rows[0];
Recording recording = new Recording();
recording.id = (long)row["id"];
string artist = (string)row["artist"];
recording.artist = artist.Trim();
string title = (string)row["title"];
recording.title = title.Trim();
ArrayList tracks = new ArrayList();
DataTable trackTable = ds.Tables["track"];
foreach(DataRow trackRow in trackTable.Rows)
{
Track track = new Track();
track.id = (long)trackRow["id"];
string trackTitle = (string)trackRow["title"];
track.title = trackTitle.Trim();
string duration = (string)trackRow["duration"];
track.duration = duration.Trim();
tracks.Add(track);
}
recording.Track = (Track[])tracks.ToArray(typeof(Track));
return recording;
}
}
Assembler classes in general are somewhat ugly. Their job is to translate from one representation to another so they are usually straightforward but always depend on both representations. These dependencies make them susceptible to changes from both representations.
Although assemblers are useful, you may not always want to create one yourself if there are readily available alternatives that meet your needs. As an alternative in this case, you could use XML serialization to create an instance of an XMLDataDocument, associate it with the DataSet and return the XML instead. For details on this approach, see the "DataSets, Web Services, DiffGrams, Arrays, and Interoperability" article on MSDN: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnservice/html/service02112003.asp?frame=true.
Application Logic
The application logic in this example is probably too simple for most enterprise applications. The reasoning for this that the pattern focuses on the Service Interface so the implementation portion is shown more for completeness instead of being a representative example. This implementation uses a Table Data Gateway to retrieve data from a database. The Table Data Gateway class, called RecordingGateway, retrieves the recording record and the track records associated with the recording. The result is returned in a single DataSet. For a detailed discussion of the database schema used and of DataSet, see Implementing Data Transfer Object in .NET with a DataSet.
RecordingGateway.cs
This class fills a DataSet with two results sets: recording and track. The client passes in the ID of the recording record that is desired. The class performs two queries against the database to fill the DataSet. The last thing it does is to define the relationship between the recording and its track records.
using System;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
public class RecordingGateway
{
public static DataSet GetRecording(long id)
{
String selectCmd =
String.Format(
"select * from recording where id = {0}",
id);
SqlConnection myConnection =
new SqlConnection(
"server=(local);database=recordings;Trusted_Connection=yes");
SqlDataAdapter myCommand =
new SqlDataAdapter(selectCmd, myConnection);
DataSet ds = new DataSet();
myCommand.Fill(ds, "recording");
String trackSelect =
String.Format(
"select * from Track where recordingId = {0} order by Id",
id);
SqlDataAdapter trackCommand =
new SqlDataAdapter(trackSelect, myConnection);
trackCommand.Fill(ds, "track");
ds.Relations.Add("RecordingTracks",
ds.Tables["recording"].Columns["id"],
ds.Tables["track"].Columns["recordingId"]);
return ds;
}
}
Note: The example shown here is not meant to describe the only way to fill a DataSet. There are many ways to retrieve this data from the database. For example, you could use a stored procedure.
Tests
The unit tests focus on testing the internal aspects of the implementation. One unit test tests the retrieval of information from the database (RecordingGatewayFixture) and the other tests the conversion of a DataSet into Recording and Track objects (RecordingAssemblerFixture).
RecordingGatewayFixture
The RecordingGatewayFixture class tests the output of the RecordingGateway, which is a DataSet. This verifies that, given an ID, a proper DataSet is retrieved from the database with both recording and track information.
using NUnit.Framework;
using System.Data;
[TestFixture]
public class RecordingGatewayFixture
{
private DataSet ds;
private DataTable recordingTable;
private DataRelation relationship;
private DataRow[] trackRows;
[SetUp]
public void Init()
{
ds = RecordingGateway.GetRecording(1234);
recordingTable = ds.Tables["recording"];
relationship = recordingTable.ChildRelations[0];
trackRows = recordingTable.Rows[0].GetChildRows(relationship);
}
[Test]
public void RecordingCount()
{
Assertion.AssertEquals(1, recordingTable.Rows.Count);
}
[Test]
public void RecordingTitle()
{
DataRow recording = recordingTable.Rows[0];
string title = (string)recording["title"];
Assertion.AssertEquals("Up", title.Trim());
}
[Test]
public void RecordingTrackRelationship()
{
Assertion.AssertEquals(10, trackRows.Length);
}
[Test]
public void TrackContent()
{
DataRow track = trackRows[0];
string title = (string)track["title"];
Assertion.AssertEquals("Darkness", title.Trim());
}
[Test]
public void InvalidRecording()
{
DataSet ds = RecordingGateway.GetRecording(-1);
Assertion.AssertEquals(0, ds.Tables["recording"].Rows.Count);
Assertion.AssertEquals(0, ds.Tables["track"].Rows.Count);
}
}
RecordingAssemblerFixture
The second fixture tests the RecordingAssembler class by testing the conversion of a DataSet into Recording and Track objects:
using NUnit.Framework;
using System.Data;
using System.IO;
using System.Xml;
[TestFixture]
public class RecordingAssemblerFixture
{
private static readonly long testId = 1234;
private Recording recording;
[SetUp]
public void Init()
{
DataSet ds = RecordingGateway.GetRecording(1234);
recording = RecordingAssembler.Assemble(ds);
}
[Test]
public void Id()
{
Assertion.AssertEquals(testId, recording.id);
}
[Test]
public void Title()
{
Assertion.AssertEquals("Up", recording.title);
}
[Test]
public void Artist()
{
Assertion.AssertEquals("Peter Gabriel", recording.artist);
}
[Test]
public void TrackCount()
{
Assertion.AssertEquals(10, recording.Track.Length);
}
[Test]
public void TrackTitle()
{
Track track = recording.Track[0];
Assertion.AssertEquals("Darkness", track.title);
}
[Test]
public void TrackDuration()
{
Track track = recording.Track[0];
Assertion.AssertEquals("6:51", track.duration);
}
[Test]
public void InvalidRecording()
{
DataSet ds = RecordingGateway.GetRecording(-1);
Recording recording = RecordingAssembler.Assemble(ds);
Assertion.AssertNull(recording);
}
}
After running these tests you have confidence that the retrieval of information from the database works correctly and you can translate the database output into the data transfer objects. However, the tests do not address end-to-end functionality nor do they test all of the service interface code. The following example tests the full functionality. It is referred to as a functional or acceptance test since it verifies that the whole interface works as expected. The approach described below retrieves a DataSet from the RecordingGateway. It then makes a call using the web service to retrieve the exact same Recording. After it is received it simply compares the two results. If they are the equal then Service Interface works correctly.
Note: Only a sample of possible acceptance tests are shown here. You should also note that there are also other ways to do this type of testing. This is just one way of performing the tests.
AcceptanceTest.cs
The following are some sample acceptance tests for the service interface:
using System;
using System.Data;
using NUnit.Framework;
using ServiceInterface.TestCatalog;
[TestFixture]
public class AcceptanceTest
{
private static readonly long id = 1234;
private DataSet localData;
private DataTable recordingTable;
private RecordingCatalog catalog = new RecordingCatalog();
private ServiceInterface.TestCatalog.Recording recording;
[SetUp]
public void Init()
{
// get the recording from the database
localData = RecordingGateway.GetRecording(id);
recordingTable = localData.Tables["recording"];
// get the same recording from the web service
recording = catalog.Get(id);
}
[Test]
public void Title()
{
DataRow recordingRow = recordingTable.Rows[0];
string title = (string)recordingRow["title"];
Assertion.AssertEquals(title.Trim(), recording.title);
}
[Test]
public void Artist()
{
DataRow recordingRow = recordingTable.Rows[0];
string title = (string)recordingRow["artist"];
Assertion.AssertEquals(title.Trim(), recording.artist);
}
// continued
}
Resulting Context
The following are the benefits and liabilities related to using an ASP.NET Web service as an implementation of Service Interface:
Benefits
Separation of concerns. The separation of the service interface and application logic is important because they are likely to vary independently. Implementing the interface portion as an ASP.NET Web service facilitates the separation.
Interoperability. Basing the interface on Internet standards, such as XML and SOAP, allow for different clients to access the Web service, no matter which operating system they are using.
ASP.NET Web services and Microsoft Visual Studio.NET. The environment makes working with Web services very straightforward. The xsd.exe tool demonstrated in this example provides a tool to translate an XML schema into a C# or Microsoft Visual Basic .NET class. To create the Web service, this example used a predefined template in the Microsoft Visual Studio .NET development system and generated the majority of the RecordingCatalog.asmx.cs file.
Liabilities
Data Transformation. In many cases, there must be a data transformation from the application logic representation to the representation that is being used by the service interface. This transformation is always problematic due to the dependencies introduced by having a class that depends on both representations. In this example, the RecordingAssembler class depends on the DataSet returned by the RecordingGateway as well as the generated Recording and Track classes.
Synchronization. Keeping the schema and the generated code both updated is not automatic. Therefore, any change to the schema requires that you rerun the xsd.exe tool to regenerate the Recording.cs class.
Related Patterns
Table Data Gateway [Fowler03]. The RecordingGateway shown here is an example of this pattern.
Mapper [Fowler03] The RecordingAssembler shown here is a variant of the Mapper pattern, which is often referred to as an assembler.
Implementing Data Transfer Object in .NET with a DataSet. This pattern describes the database schema that is used in this example.