3.8. Data Sets and TablesOne of the most common types of data contracts exchanged between clients and services is data that originates in or is destined to a database. In .NET, the native 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 to generate type-safe derivatives. The raw DataSet and DataTable types are serializable, 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( ); } When importing the definition of this service contract above, the generated proxy file will contain the definition of the DataTable data contractonly the schema of DataTable, without any of the code. You can freely remove this definition from the file and reference ADO.NET instead. You can also use the type-safe subclasses of DataSet and DataTable in your contract. For example, suppose you have a table in a database called ContactsDataTable, containing your contacts, with columns such as FirstName and LastName. You can use Visual Studio to generate a type-safe data set called MyDataSet, which 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
You can 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 type-safe data table will be part of the published metadata of the service. When importing it to the client, SvcUtil and Visual Studio are 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. 3.8.1. Arrays Instead of TablesADO.NET and the Visual Studio tools make it trivial for both a WCF client and service 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 for using a table or a data set in a service contract: you may be exposing your internal data structure to the world. Also, future changes to the database schema may affect your clients. While inside an application it may be permissible to pass the data table, sending the data table across an application or public service boundary is rarely a good idea. It is better in general to expose operations on the data as opposed to the data itself. 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. 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>(DataTable table,Converter<R,T> converter) where R : DataRow; } 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
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 an anonymous method that converts an instance of the type-safe data row MyDataSet.ContactsRow to a Contact instance. GetContacts( ) then calls DataTableHelper.ToArray( ), providing it with the table and the converter. Example 3-12 shows the implementation of DataTableHelper.ToArray( ). Example 3-12. The DataTableHelper class
In essence, all DataTableHelper.ToArray( ) does is use the myCollection helper class to invoke the converter on every row in the table, converting every row to a single Contact and returning the resulted array. DataTableHelper adds some type safety. At compile time, it places constraints on the type parameter R to be a data row. At runtime, the ToArray( ) method returns an empty array if the table is empty. It verifies that the type parameter T is decorated either with the DataContract attribute or the Serializable attribute. Verifying 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 last verification done by ToArray( ) is to ensure 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. |