Data Sets and Tables

One of the most common types of data contract exchanged between clients and services involves data that originates in or is destined for a database. In .NET, a common way of interacting with databases is via ADO.NET's data set and data table types. Applications can use the raw DataSet and DataTable types, or use the data access management tools in Visual Studio 2008 to generate type-safe derivatives.

The raw DataSet and DataTable types are serializable (i.e., marked with the Serializable attribute):

[Serializable]
public class DataSet : ...
{...}

[Serializable]
public class DataTable : ...
{...}

This means that you can define valid service contracts that accept or return data tables or data sets:

[DataContract]
struct Contact
{...}

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   void AddContact(Contact contact);

   [OperationContract]
   void AddContacts(DataTable contacts);

   [OperationContract]
   DataTable GetContacts(  );
}

You can also use the type-safe subclasses of DataSet and DataTable in your contracts. For example, suppose in your database you have a table called Contacts that contains your contacts, with columns such as FirstName and LastName. You can use Visual Studio 2008 to generate a type-safe data set called MyDataSet that has a nested class called ContactsDataTable, as well as a type-safe row and type-safe data adapter, as shown in Example 3-10.

Example 3-10. Type-safe data set and data table

[Serializable]
public partial class MyDataSet : DataSet
{
   public ContactsDataTable Contacts
   {get;}

   [Serializable]
   public partial class ContactsDataTable : ...
   {
      public void AddContactsRow(ContactsRow row);
      public ContactsRow AddContactsRow(string FirstName,string LastName);
      //More members
   }

   public partial class ContactsRow : DataRow
   {
      public string FirstName
      {get;set;}

      public string LastName
      {get;set;}
      //More members
   }
   //More members
}
public partial class ContactsTableAdapter : Component
{
   public virtual MyDataSet.ContactsDataTable GetData(  );
   //More members
}

You can then use the type-safe data table in your service contract:

[DataContract]
struct Contact
{...}

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   void AddContact(Contact contact);

   [OperationContract]
   void AddContacts(MyDataSet.ContactsDataTable contacts);

   [OperationContract]
   MyDataSet.ContactsDataTable GetContacts(  );
}

Warning

The data row itself is not serializable, so you cannot use it (or its type-safe subclass) in operations, like this:

//Invalid definition
[OperationContract]
void AddContact(MyDataSet.ContactsRow contact);

The type-safe data table will be part of the published metadata of the service. When importing it to the client, Visual Studio 2008 is smart enough to regenerate the type-safe data table, and the proxy file will include not just the data contract, but the code itself. If the client already has a local definition of the type-safe table, you can remove the definition from the proxy file.

Using Arrays Instead of Tables

ADO.NET and the Visual Studio tools make it trivial for both WCF clients and services to use DataSet and DataTable and their type-safe derivatives. However, these data access types are specific to .NET. While they are serializable, their resulting data contract schema is so complex that trying to interact with it on other platforms is impractical. There are additional drawbacks to using a table or a data set in a service contract: you may be exposing your internal data structure to the world, and the clients are unlikely to care about internal data storage artifacts such as IDs, keys, and foreign key relationships. The client may also care about only a subset of the information in the table. Furthermore, future changes to the database schema will break your clients, so any service that defines a service contract in terms of its underlying data table must hold that table schema as immutable (something you will find difficult to do). Unless you are designing a data access service, in general it is better to expose operations on the data as opposed to the data itself. In short, sending the data table across the service boundary is rarely a good idea.

If you do need to pass around the data itself, it is best to do so using a neutral data structure such as an array, and transform the individual rows into some data contract that encapsulates the original schema.

Converting legacy data tables

All versions of Visual Studio up until Visual Studio 2008 generated a type-safe data table that derived from the DataTable class, without a type-safe way of enumerating over the table or converting it to an array:

[Serializable]
public partial class ContactsDataTable : DataTable,IEnumerable
{...}

To streamline the task of converting a data table to an array, you can use my DataTableHelper class, defined as:

public static class DataTableHelper
{
   public static T[] ToArray<R,T>(this DataTable table,Func<R,T> converter)
                                                              where R : DataRow;
}

DataTableHelper defines the DataTable extension method ToArray( ).

All DataTableHelper requires is a converter from a data row in the table to the data contract. DataTableHelper also adds some compile-time and runtime type-safety verification. Example 3-11 demonstrates using DataTableHelper.

Example 3-11. Using DataTableHelper

[DataContract]
struct Contact
{
   [DataMember]
   public string FirstName;

   [DataMember]
   public string LastName;
}
[ServiceContract]
interface IContactManager
{
   [OperationContract]
   Contact[] GetContacts(  );
   ...
}
class ContactManager : IContactManager
{
   public Contact[] GetContacts(  )
   {
      ContactsTableAdapter adapter = new ContactsTableAdapter(  );
      MyDataSet.ContactsDataTable contactsTable = adapter.GetData(  );

      Func<MyDataSet.ContactsRow,Contact> converter = (row)=>
                                                      {
                                                          return new Contact(  )
                                                                     {
                                                       FirstName = row.FirstName,
                                                       LastName = row.LastName
                                                                     };
                                                      };
      return contactsTable.ToArray(converter);
   }
   //Rest of the implementation
}

In Example 3-11, the GetContacts( ) method uses the type-safe table adapter ContactsTableAdapter (listed in Example 3-10) to get the records from the database in the form of the type-safe table MyDataSet.ContactsDataTable. GetContacts( ) then defines a lambda expression that converts an instance of the type-safe data row MyDataSet.ContactsRow to a Contact instance. Finally, GetContacts( ) calls the ToArray( ) extension, providing it with the table and the converter.

Example 3-12 shows the implementation of DataTableHelper.ToArray( ).

Example 3-12. The DataTableHelper class

public static class DataTableHelper
{
   public static T[] ToArray<R,T>(this DataTable table,Func<R,T> converter)
                                                                 where R : DataRow
   {
      //Verify [DataContract] or [Serializable] on T
      Debug.Assert(IsDataContract(typeof(T)) || typeof(T).IsSerializable);

      if(table.Rows.Count == 0)
      {
         return new T[]{};
      }

      //Verify table contains correct rows
      Debug.Assert(MatchingTableRow<R>(table));

      return table.Rows.Cast<R>().Select(converter).ToArray();
   }
   static bool IsDataContract(Type type)
   {
      object[] attributes =
                     type.GetCustomAttributes(typeof(DataContractAttribute),false);
      return attributes.Length == 1;
   }
   static bool MatchingTableRow<R>(DataTable table)
   {
      if(table.Rows.Count == 0)
      {
         return true;
      }
      return table.Rows[0] is R;
   }
}

DataTableHelper.ToArray() first uses the Cast() LINQ extension method to convert each row (which is an object) into an R, then it uses a Select() query to convert that collection of rows to a collection of Ts, and finally converts that collection into an array.

DataTableHelper adds some type safety. At compile time, it constrains the type parameter R to be a data row. At runtime, DataTableHelper verifies that the type parameter T is decorated either with the DataContract attribute or the Serializable attribute. Verifying the presence of the DataContract attribute is done via the helper method IsDataContract(), which uses reflection to look up the attribute. Verifying the Serializable attribute is done by checking whether the IsSerializable bit is set on the type. The method returns an empty array if the table is empty. Finally, ToArray() verifies that the provided table has the rows specified with the type parameter R. This is done via the MatchingTableRow() helper method, which gets the first row and verifies its type.

Converting new data tables

Visual Studio 2008 introduced support for strongly typed queries and LINQ. When using Visual Studio 2008 to generate a table, the table derives from TypedTableBase<T>:

[Serializable]
public partial class ContactsDataTable : TypedTableBase<ContactsRow>
{...}

This makes it easier to use LINQ to convert the table directly into an array. Using the same definitions as Example 3-11, you can now write:

public Contact[] GetContacts(  )
{
   Func<CustomersDataSet.ContactsRow,Contact> converter = (row)=>
                                                          {
                                                             return new Contact(  )
                                                                        {
                                                        FirstName = row.FirstName,
                                                        LastName = row.LastName
                                                                        };
                                                          };
   return contactsTable.Select(converter).ToArray(  );
}

Or even:

public Contact[] GetContacts(  )
{
   return contactsTable.Select(row => new Contact(  ){
                    FirstName = row.FirstName,LastName = row.LastName}).ToArray(  );
}

Using LINQ to SQL

Instead of using tables, you can use LINQ to SQL and Visual Studio 2008 to generate the code for a strongly typed data context. However, the strongly-typed object collection on the context is not decorated with either the Serializable or the DataContract attribute, forcing you to rely on the inferred data contract, as shown in Example 3-13.

Example 3-13. Using LINQ to SQL with inferred data contract

public partial class ContactsDataContext : DataContext
{
   public Table<Contact> Contacts
   {get;}
   //More members
}

public partial class Contact
{
   public string FirstName
   {get;set;}

   public string LastName
   {get;set;}
}

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   Contact[] GetContacts(  );
   ...
}
class ContactManager : IContactManager
{
   public Contact[] GetContacts(  )
   {
      using(ContactsDataContext context = new ContactsDataContext(  ))
      {
         return context.Contacts.ToArray(  );
      }
   }
}

Instead of an inferred data contract (or changing the machine-generated code), you can convert the strongly typed machine-generated type into a data contract you manage, as shown in Example 3-14, using the same definitions as in Example 3-13.

Example 3-14. Using LINQ to SQL without inferred data contract

[DataContract(Name = "Contact")]
class MyContact
{
   [DataMember]
   public string FirstName
   {get;set;}

   [DataMember]
    public string LastName
   {get;set;}
}

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   MyContact[] GetContacts(  );
   ...
}

class ContactManager : IContactManager
{
   public MyContact[] GetContacts(  )
   {
      using(ContactsDataContext context = new ContactsDataContext(  ))
      {
         return context.Contacts.Select(row => new MyContact(  ){
                    FirstName = row.FirstName,LastName = row.LastName}).ToArray(  );
      }
   }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.142.232