Implementing Service Interface in .NETVersion 1.0.0 GotDotNet community for collaboration on this pattern Complete List of patterns & practices
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.
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:
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; } }
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
Liabilities
Related Patterns Acknowledgments [Microsoft02-1] Microsoft Corporation. "XML Web Services Overview." .NET Framework Developer's Guide. Available from the MSDN Library at: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconwebservicesoverview.asp. [Fowler03] Fowler, Martin. Enterprise Application Architecture Patterns. Addison-Wesley, 2003. |