Typed DataSets in ADO.NET
In Chapter 5, I extolled the virtues of setting up your DataSets like in-memory databases. Unfortunately, I asked you to write quite a bit of code to do all the work. I was only teasing you. This chapter will show you how to use Typed DataSets to make that job a lot easier, while at the same time creating type-safety at compilation time.
6.1 What Are Typed DataSets?
Typed DataSets are a different animal than most of what we have discussed so far. They are not a set of classes in the framework, but instead, they are a set of generated classes that inherit directly from the DataSet family of classes. Figure 6.1 contains the class diagram from Chapter 5, which shows how the elements of the DataSet are related.
Figure 6.1: The structure of a DataSet
In contrast, the Typed DataSet derives from these classes. The class diagram looks like that shown in Figure 6.2.
Figure 6.2: The structure of a Typed DataSet
But why are they called Typed DataSets? In Chapter 5 we saw that we could create DataColumns for our DataTables to specify what type of data could be stored in each column. This enforces runtime type-safety, but on most occasions we would like to know that our DataSets are type-safe when we write the code. Typed DataSets generate classes that expose each object in a DataSet in a type-safe manner. With a DataSet, our code would look like Listing 6.1.
Listing 6.1: Using a DataSet
... // Create a DataAdapter for each of the tables we're filling SqlDataAdapter daCustomers = new SqlDataAdapter("SELECT * FROM CUSTOMER;", conn); // Create the Invoice DataAdapter SqlDataAdapter daInvoices = new SqlDataAdapter("SELECT * FROM INVOICE", conn); // Create your blank DataSet DataSet dataSet = new DataSet(); // Fill the DataSet with each DataAdapter daCustomers.Fill(dataSet, "Customers"); daInvoices.Fill(dataSet, "Invoices"); // Show the customer name Console.WriteLine(dataSet.Tables["Customers"]. Rows["FirstName"].ToString()); Console.WriteLine(dataSet.Tables["Customers"]. Rows["LastName"].ToString()); Console.WriteLine(dataSet.Tables["Customers"]. Rows["HomePhone"].ToString()); // Change an invoice number with a string // this shouldn't work because InvoiceNumber // expects an integer dataSet.Tables["Invoices"]. Rows["InvoiceNumber"] = "15234";
We need to use indexers with the DataSet to get each piece of the hierarchy until we finally get down to the row level. At any point you can misspell any name and get an error when this code is executed. In addition, the last line of the example shows us attempting to set the invoice number using a string. The DataSet knows that this column can only hold integers so we will get a runtime error enforcing that rule. In Listing 6.2, we do the same thing with a Typed DataSet.
Listing 6.2: Using a Typed DataSet
// Create a DataAdapter for each of the tables we're filling SqlDataAdapter daCustomers = new SqlDataAdapter("SELECT * FROM CUSTOMER;", conn); // Create the invoice DataAdapter SqlDataAdapter daInvoices = new SqlDataAdapter("SELECT * FROM INVOICE", conn); // Create your blank DataSet CustomerTDS dataSet = new CustomerTDS(); // Fill the DataSet with each DataAdapter daCustomers.Fill(dataSet, "Customers"); daInvoices.Fill(dataSet, "Invoices"); // Show the customer name Console.WriteLine(dataSet.Customer.FirstName); Console.WriteLine(dataSet.Customer.LastName); Console.WriteLine(dataSet.Customer.HomePhone); Console.WriteLine(DataSet.Customer.FullName); // This will not compile because InvoiceNumber expects // an integer dataSet.Invoice.InvoiceNumber = "12345";
There are a few things to notice in this example. First, we create our Typed DataSet much like we created a DataSet in the first examplethe difference is that the schema already exists in our Typed DataSet. Second, even though this is a Typed DataSet, the CustomerTDS class directly derives from the DataSet class. Therefore, when we call the DataAdapters to fill our DataSet, it will accept our Typed DataSet. In fact, the Typed DataSet is a DataSet . . . a specialized DataSet. Next, you should notice that the syntax to get at tables and fields is much more straightforward with Typed DataSets. Each DataTable is now referenced with a property of CustomerTDS. Likewise, each field is a property of a row. Not only is this syntax more straightforward, but you will get compiler errors if you misspell any of the elements. Lastly, when we try to set our invoice number with a string we also get a compiler error, because our generated class knows that invoice numbers are integers.
In addition to normal columns, you can set up expression columns in our Typed DataSets to make sure that expressions can be returned in a type-safe manner. For instance, in the above example, we can retrieve the FullName property from Customer. FullName is just an expression field that puts our customer's first and last names together in a convenient form. Because it is part of the Typed DataSet, this expression is returned as a string.
Lastly, as we will see in this chapter, using Typed DataSets as the basis for data object or business object layers is a powerful tool. By deriving directly from Typed DataSets we can eliminate much of the tedium of writing these layers, while at the same time achieving the type-safety we want.