Dela via


Implementing Data Transfer Object in .NET with a Typed DataSet

Retired Content

This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals who are still using these technologies. This page may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. Please see the patterns & practices guidance for the most current information.

Ff650461.ImpDTOtypedDataSet(en-us,PandP.10).png

Version 1.0.0

GotDotNet community for collaboration on this pattern

Complete List of patterns & practices

Context

You are implementing a distributed application with the .NET Framework. The client application displays a form that requires making multiple calls to an ASP.NET Web service to satisfy a single user request. Based on performance measurements you have found that making multiple calls degrades application performance. To increase performance, you would like to retrieve all the data that the user request requires in a single call to the Web service.

Background

Note: The following is the same sample application that is described in Implementing Data Transfer Object in .NET with a DataSet.

The following is a simplified Web application that communicates with an ASP.NET Web service to deliver recording and track information to the user. The Web service in turn calls a database to provide the data that the client requests. The following sequence diagram depicts the interaction among the application, the Web service, and the database for a typical page.

Ff650461.Imp_DTO_with_DataSet_Fig01(en-us,PandP.10).gif

Figure 1: Behavior of a typical user request

Figure 1 illustrates the sequence of calls needed to fulfill the entire user request. The first call retrieves the recording information, and the second call retrieves the track information for the specified recording. In addition, the Web service must make separate calls to the database to retrieve the required information.

Database Schema

The schema that is used in the example shown in Figure 2 depicts a recording record that has a one-to-many relationship with a track record.

Ff650461.Imp_DTO_with_DataSet_Fig02(en-us,PandP.10).gif

Figure 2: Schema for sample application

Implementing a DTO

One way to improve the performance of this user request is to package all the required data into a data transfer object (DTO) that can be sent with a single call to the Web service. This reduces the overhead associated with two separate calls and allows you to use a single connection with the database to retrieve both the recording and the track information. For a detailed description of how this improves performance, see the Data Transfer Object pattern.

Implementation Strategy

A typed DataSet is a generated subclass of System.Data.DataSet. You provide an XML schema file which is then used to generate a strongly-typed wrapper around the DataSet. The following two code samples illustrate the differences. The first sample is implemented with an ordinary DataSet:

 

DataTable dataTable = dataSet.Tables["recording"];
DataRow row = dataTable.Rows[0];
string artist = (string)row["artist"];
 

This sample indicates that you need to know the table and column names to access the tables and fields contained in the DataSet. You also have to know the return type of the Artist field to ensure that the correct cast is done. If you do not use the correct type, you will get a runtime error. The following is the same example implemented with a typed DataSet:

 

   Recording recording = typedDataSet.Recordings[0];
   string artist = recording.Artist; 
 

This example demonstrates the benefits that the typed interface provides. You no longer have to refer to table or column by name and you do not have to know that the return type of the Artist column is a string. A typed DataSet defines a much more explicit interface that is verifiable at compile time instead of at runtime. In addition to the strongly-typed interface a typed DataSet also can be used in all places a DataSet can be used; therefore, it also can be used as a DTO. It is loaded in a similar fashion as a DataSet and it can be serialized to and from XML. In comparison to an ordinary DataSet you do have to write and maintain an XML schema that describes the typed interface. The Microsoft Visual Studio .NET development system provides a number of tools that simplify the creation and maintenance of the schema.The rest of this implementation strategy outlines the steps required in creating a typed DataSet for the sample application just described.

Creating a Typed DataSet

A typed DataSet is generated from an XML schema. Visual Studio .NET provides a drag-and-drop tool which automates the creation of the schema (see Figure 3) and the generation of the typed DataSet classes. If you do not use Visual Studio.NET, you can write the XML schema and use a command-line tool called XSD.exe to generate the typed DataSet. For detailed instructions on both of these methods, see "Typed DataSets in ADO.NET" from the May 2001 issue of .NET Developer [Wildermuth02].

Ff650461.Imp_DTO_with_Typed_DataSet_Fig03(en-us,PandP.10).gif

Figure 3: Visual Studio .NET DataSet file type

RecordingDto.xsd

The following is the XML schema for the DTO to be used in this example. It combines both the recording table along with its associated track records in a single typed DataSet named RecordingDto:

 

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema id="RecordingDto" targetNamespace="https://msdn.microsoft.com/practices/RecordingDto.xsd"
   elementFormDefault="qualified" attributeFormDefault="qualified" xmlns="http://tempuri.org/RecordingDTO.xsd"
   xmlns:mstns="https://msdn.microsoft.com/practices/RecordingDto.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema"
   xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:codegen="urn:schemas-microsoft-com:xml-msprop">
   <xs:element name="RecordingDto" msdata:IsDataSet="true">
      <xs:complexType>
         <xs:choice maxOccurs="unbounded">
            <xs:element name="recording" codegen:typedName="Recording" codegen:typedPlural="Recordings"
               codegen:typedChildren="Track">
               <xs:complexType>
                  <xs:sequence>
                     <xs:element name="id" type="xs:long" codegen:typedName="Id" />
                     <xs:element name="title" type="xs:string" codegen:typedName="Title" />
                     <xs:element name="artist" type="xs:string" codegen:typedName="Artist" />
                  </xs:sequence>
               </xs:complexType>
            </xs:element>
            <xs:element name="track" codegen:typedName="Track" codegen:typedPlural="Tracks" codegen:typedParent="Recording">
               <xs:complexType>
                  <xs:sequence>
                     <xs:element name="id" type="xs:long" codegen:typedName="Id" />
                     <xs:element name="title" type="xs:string" codegen:typedName="Title" />
                     <xs:element name="duration" type="xs:string" codegen:typedName="Duration" />
                     <xs:element name="recordingId" type="xs:long" codegen:typedName="RecordingId" />
                  </xs:sequence>
               </xs:complexType>
            </xs:element>
         </xs:choice>
      </xs:complexType>
      <xs:unique name="RecordingDTOKey1" msdata:PrimaryKey="true">
         <xs:selector xpath=".//mstns:recording" />
         <xs:field xpath="mstns:id" />
      </xs:unique>
      <xs:unique name="RecordingDTOKey2" msdata:PrimaryKey="true">
         <xs:selector xpath=".//mstns:track" />
         <xs:field xpath="mstns:id" />
      </xs:unique>
      <xs:keyref name="recordingtrack" refer="mstns:RecordingDTOKey1">
         <xs:selector xpath=".//mstns:track" />
         <xs:field xpath="mstns:recordingId" />
      </xs:keyref>
   </xs:element>
</xs:schema>
 

This schema is not the exact file produced by Visual Studio .NET. It is annotated with a number of attributes that are prefixed from the codegen namespace. This modification is desirable because the code that is generated does not adhere to the .NET naming conventions. For example, without the modification, Visual Studio .NET would generate a track class that corresponds to the track table, whereas according to conventions used in the .NET Framework the class should be named Track. To change the name of the class that is generated, you must add the codegen:typedName attribute to the element definition in the XML schema:

 

<xs:element name="track" codegen:typedName="Track">
   
</element>
 

There are a number of other attributes besides codegen:typedName. For a detailed description of all the attributes, see "Typed DataSets in ADO.NET" from the May 2001 issue of .NET Developer [Wildermuth02].

Filling a Typed DataSet from the Database

The following code example demonstrates how to fill a typed DataSet with the data that the sample application requires. This includes the specific recording record and all of its associated track records. The difference between this code and filling an ordinary DataSet is that you do not need to explicitly define the relationship between the recording and track records.

Assembler.cs

Just as in Implementing a Data Transfer Object in .NET with a DataSet, an Assembler class maps the actual database calls into the typed DataSet:

 

using System;
using System.Data;
using System.Data.SqlClient;

using Recording;

public class Assembler
{
   public static RecordingDto CreateRecordingDto(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);

      RecordingDto dto = new RecordingDto();
      myCommand.Fill(dto, "recording");

      String trackSelect = 
         String.Format(
         "select * from Track where recordingId = {0} order by Id",
         id);

      SqlDataAdapter trackCommand = 
         new SqlDataAdapter(trackSelect, myConnection);
      trackCommand.Fill(dto, "track");

      return dto;
   }
}
 

Note: The example shown here is not meant to describe the only way to fill the typed DataSet. There are many ways to retrieve this data from the database. For example, you could use a stored procedure.

Using a Typed DataSet in an ASP.NET Page

As mentioned previously, a typed DataSet inherits from System.Data.DataSet. This means that it can be substituted for a DataSet. For example, when using the .NET user interface controls (Web Forms or Windows Forms) a typed DataSet can be used in all places you could use a DataSet. The sample application page shown in the following code example uses two DataGrid controls, RecordingGrid and TrackGrid. You can use the typed DataSet, RecordingDto when setting the DataSource properties on the controls because a typed DataSet inherits from DataSet.

 

using System;
using System.Data;
using RecordingApplication.localhost;

public class RetrieveForm : System.Web.UI.Page
{
   private RecordingCatalog catalog = new RecordingCatalog();
 
   // 

   protected void Button1_Click(object sender, System.EventArgs e)
   {
      string stringId = TextBox1.Text;
      long id = Convert.ToInt64(stringId);

      RecordingDTO dto = catalog.Get(id);
      RecordingGrid.DataSource = dto.recording;
      RecordingGrid.DataBind();

      TrackGrid.DataSource = dto.track;
      TrackGrid.DataBind();
   }
}
 

Tests

Because the typed DataSet is generated by tools in the .NET Framework, you do not need to write tests to verify that it functions correctly. In the following tests, you are testing that the Assembler class loaded the typed DataSet correctly.

AssemblerFixture.cs

 

using NUnit.Framework;
using System.Data;
using Recording;

[TestFixture]
public class AssemblerFixture
{
   private RecordingDto dto;
   private RecordingDto.Recording recording;
   private RecordingDto.Track[] tracks; 
   
   [SetUp]
   public void Init()
   {
      dto = Assembler.CreateRecordingDto(1234);
      recording = dto.Recordings[0];
      tracks = recording.GetTracks();
   }

   [Test]
   public void RecordingCount()
   {
      Assert.Equals(1, dto.Recordings.Rows.Count);
   }

   [Test]
   public void RecordingTitle()
   {
      Assert.Equals("Up", recording.Title.Trim());
   }


   [Test]
   public void RecordingChild()
   {
      Assert.Equals(10, tracks.Length);

      foreach(RecordingDto.Track track in tracks)
      {
         Assert.Equals(recording.Id, track.RecordingId);
      }
   }

   [Test]
   public void TrackParent()
   {
      RecordingDto.Track track = tracks[0];
      RecordingDto.Recording parent = track.Recording;
      Assert.Equals("Up", parent.Title.Trim());
   }

   [Test]
   public void TrackContent()
   {
      RecordingDto.Track track = tracks[0];
      Assert.Equals("Darkness", track.Title.Trim());
   }

   [Test]
   public void InvalidRecording()
   {
      RecordingDto dto = Assembler.CreateRecordingDto(-1);
      Assert.Equals(0, dto.Recordings.Rows.Count);
      Assert.Equals(0, dto.Tracks.Rows.Count);
   }
}
 

These tests describe how to access the individual elements of the DataSet. Because of the use of a typed DataSet, the test code does not require the actual column names and does not require the return type to be cast. Comparing these tests with the ones described in Implementing Data Transfer Object in .NET with a DataSet reveals the differences between using a strongly-typed interface and a generic interface. The strongly-typed interface is easier to use and understand. It also provides the added benefit of compile-time checking on return types.

Resulting Context

Implementing DTO with a typed DataSet shares a number of the same benefits and liabilities as implementing DTO with a DataSet; however, certain benefits and liabilities are unique to a typed-DataSet implementation.

Benefits

The typed DataSet shares the following benefits with a DataSet when used as a DTO:

Development tool support. Because the DataSet class is implemented in ADO.NET, there is no need to design and implement the DTO. There is also extensive support in Visual Studio for automating the creation and filling of DataSet and typed-DataSet objects.

Integration with controls. A DataSet works directly with the built-in controls in Windows Forms and Web Forms, making it a logical choice as a DTO.

Serialization. The DataSet comes complete with the ability to serialize itself into XML. Not only is the content serialized, but the schema for the content is also present in the serialization.

Disconnected database model. The DataSet represents a snapshot of the current contents of the database. This means that you can alter the contents of the DataSet and subsequently use the DataSet as the means to update the database.

An additional benefit that might persuade you to use a typed DataSet as opposed to an ordinary DataSet is the strongly-typed interface of the typed DataSet. A typed DataSet, as described here, generates classes that can be used to access the contained data. The classes present an interface which defines how the class is to be used in a more explicit manner. This removes the need for casting which was present in the DataSet implementation.

Liabilities

The typed DataSet shares the following liabilities with a DataSet when used in the context of a DTO:

Interoperability. Because the DataSet class is part of ADO.NET, it is not the best choice for a DTO in cases requiring interoperability with clients that are not running the .NET Framework.. You can still use DataSet, however, the client will be forced to parse the XML and build its own representation. If interoperability is a requirement, see Implementing Data Transfer Object in .NET with Serialized Objects.

Stale data. The typed DataSet, like a DataSet, is disconnected from the database. It is filled with a snapshot of the data in the database when it is constructed. This implies that the actual data in the database may be different from what is contained in the typed DataSet. For reading primarily static data, this is not a major issue. If the data is constantly changing, however, using any kind of DataSet is not recommended.

Potential for performance degradation. Instantiating and filling a DataSet can be an expensive operation. Serializing and deserializing a DataSet can also be very time consuming. A good rule of thumb for using a DataSet is that a DataSet is a good choice when you are using more than one table or relying on the capability of the DataSet to update the database. If you are displaying the results from a single table, then using a DataReader with strongly-typed objects may offer better performance. For more information, see Implementing Data Transfer Object in .NET with Serialized Objects.

The following are additional liabilities when using a typed DataSet as opposed to an ordinary DataSet:

A typed DataSet is still a DataSet. A typed DataSet can be substituted at runtime with a DataSet. This means that even though the strongly-typed interface exists, programmers can still access the data without the typed interface. A possible result of doing this is that there could be parts of the code which couple the application tightly to the DataSet table and column names.

The need for an XML schema. When using a typed DataSet you have to create and maintain an XML schema to describe the strongly-typed interface. Visual Studio .NET provides a number of tools to assist in this process, but nevertheless you still have to maintain an additional file.

For more information, see the following related patterns:

Implementing Data Transfer Object in .NET with a DataSet.

Implementing Data Transfer Object in .NET with Serialized Objects.

Assembler. In Enterprise Application Architecture Patterns, Fowler defines Assembler as a specialized instance of the Mapper pattern [Fowler03].

Acknowledgments

[Beau02] Beauchemin, Bob. Essential ADO.NET. Addison-Wesley, 2002.

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

[Wildermuth01] Wildermuth, Shawn. "Typed DataSets in ADO.NET." .NET Developer. May 2001.

patterns & practices Developer Center