The dataset is an integral part of Microsoft’s new data access model ADO.NET. It introduces a simple offline method for retrieving and updating data. Before using the dataset, you should have an understanding of the Microsoft .NET platform and a basic understanding of the dataset model.
Key concepts in datasets
The dataset is an in-memory representation of a subset of the database. It can have tables, rows, constraints and relationships between tables. Though the dataset is capable of handling relational data efficiently, it fails if you try to extend its capabilities.
In good object-oriented design, the data and the methods used on the data should be encapsulated. But with the dataset in .NET 1.0 and 1.1, users had to pass the dataset to business components to perform certain functions. There are a few limitations to this strategy, since the data and the methods that work on the data are not encapsulated as a single, logical unit.
Before looking at extending the capabilities of the .NET 2.0 dataset with business functionality, it is important to look at typed datasets.
Typed datasets can be strongly typed with table and column names to make them less error-prone and to provide a simple syntax for accessing data. Typed datasets create an auto-generated class inherited from the dataset base class. With typed datasets, the syntax for accessing data is simplified compared to untyped datasets. For example:
Untyped dataset syntax (C#):
1 |
DataSet ds = new DataSet();//load the data into the datasetstring s = ds.Tables["InvoiceHeader"].Rows[0]["InvoiceId"].ToString(); |
Typed dataset syntax (C#):
1 |
Invoice ds = new Invoice();//load the data into the datasetstring s = ds.InvoiceHeader[0].InvoiceId; |
The typed dataset has a very simple syntax for accessing data. The column and table names are properties of the typed dataset. In the untyped dataset, this information has to be passed as an index or string, making it more error-prone. If you misspell the table name, for example, the compiler will not catch the error, but at runtime your program will fail.
Although the typed dataset makes dataset functions easy to use, it is difficult to add business functions into it. Let’s take a simple scenario in which we have a dataset that resembles an invoice with two datatables, one for the invoice header and one for invoice details. The invoice header contains fields such as invoice id, customer name, mailing address and contact number. The invoice detail contains invoice id, item code, unit price and ordered quantity. This simple structure is mapped into a typed dataset as shown in Figure 1:
Figure 1 Typed Invoice Dataset
The typed dataset is ideal for representing the structure of the invoice, but what if we want to add functionality to it? Let’s say we want to apply a 10 percent discount to any invoice detail line in which the subtotal is greater that $1,000, and calculate the resulting amount. We’ll use partial classes to add that functionality to our typed dataset.
Partial classes
Partial classes are classes defined across one or more source files. These multiple definitions are put into one class by the compiler. Partial classes are extremely useful when two or more people with different concerns need to work on a class definition.
In our example, the typed dataset we create will generate a few partial classes to hold the information on the tables and columns. The person who generates the code for the typed dataset is limited to generating the source for the relational representation of our invoice. The other person, the business developer, focuses on adding business functionality to the invoice class.
Implementing business-aware datasets with typed datasets
Each typed dataset created will generate a partial class for the dataset, a partial nested class for each table in the dataset (with the name <table name>DataTable), a partial nested class to map a row for each table in the dataset (with the name <table name>Row). The code below shows the partial class definitions generated for the typed dataset in Figure 1.
C# Code
1 |
[System.Serializable()][System.ComponentModel.DesignerCategoryAttribute("code")][System.ComponentModel.ToolboxItem(true)][System.Xml.Serialization.XmlSchemaProviderAttribute("GetTypedDataSetSchema")][System.Xml.Serialization.XmlRootAttribute("Invoice")][System.ComponentModel.Design.HelpKeywordAttribute("vs.data.DataSet")][System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Usage", "CA2240:ImplementISerializableCorrectly")]public partial class Invoice : System.Data.DataSet{//More code here//... |
1 |
[System.Serializable()][System.Xml.Serialization.XmlSchemaProviderAttribute("GetTypedTableSchema")]public partial class InvoiceHeaderDataTable : System.Data.DataTable, System.Collections.IEnumerable{//More code here//...} |
1 |
[System.Serializable()][System.Xml.Serialization.XmlSchemaProviderAttribute("GetTypedTableSchema")]public partial class InvoiceDetailDataTable : System.Data.DataTable, System.Collections.IEnumerable{//More code here//...} |
1 |
public partial class InvoiceHeaderRow : System.Data.DataRow{//More code here//...} |
1 |
public partial class InvoiceDetailRow : System.Data.DataRow{//More code here//...} |
1 |
//More code here//...} |
When you analyze the code, you can see there is a partial class generated for the dataset named Invoice inheriting from DataSet. So another partial class definition for the Invoice class can be created to add more functionality at the dataset level. Similarly, you can add functionality at the data table level by providing partial class definitions to the classes InvoiceHeaderDataTable and InvoiceDetailDataTable, or at the data row level by providing partial class definitions for InvoiceHeaderRow and InvoiceDetailRow.
It’s very important to add the business functionality to the correct partial class and at the correct level. If, for example, you want to calculate the discounted amount of an invoice, it has to be on a row level on the invoice header using a collection of invoice detail rows to calculate the invoice total. So an ideal place to implement this is in the InvoiceHeaderRow class.
C# Code
1 |
public partial class Invoice{public partial class InvoiceHeaderRow{public double DiscountedAmount{get{double discountedAmt = 0;foreach (InvoiceDetailRow row in this.GetInvoiceDetailRows()){double rowSubTotal = row.UnitPrice * row.OrderedQty;if (rowSubTotal > 1000){discountedAmt += (rowSubTotal * 90 / 100);}else{discountedAmt += rowSubTotal;}}return discountedAmt;}}} |
Even though we are providing a partial class implementation for InvoiceHeaderRow class, we still need to nest it inside the Invoice partial class definition. When the partial classes generated from the typed dataset and the partial class definitions we created are compiled, the InvoiceHeaderRow objects have a new read-only property called DiscountedAmount that calculates the invoice amount after applying the discount as per the business rules.
But couldn’t we have implemented this property directly inside the generated source, since we have the source for the typed dataset we created? That is not a viable approach because any changes made on the typed dataset will regenerate the code, and handwritten code inside the generated source will be lost.
Although it is possible to implement business functionality into typed datasets by inheriting the auto-generated class from the typed dataset, it might be overkill to have both the base classes and the child classes for each dataset exposed.
Conclusion
Datasets are an important construct used in data-driven applications. This article shows one approach to making your datasets more intelligent by incorporating business functionality using partial classes as part of .NET 2.0.
Load comments