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( ); }
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.
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.
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 T
s, 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.
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( ); }
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( ); } } }
18.119.142.232