- What Are Typed DataSets?
- Generating Typed DataSets
- Using Typed DataSets
- Simplification of Business Object Layers
- Conclusion
6.4 Simplification of Business Object Layers
I have spent more of my adult life writing business object layers than any other work I have done. The work always involved several tasks:
Mapping our relational model to a hierarchical model.
Reading and writing those objects to and from the database.
Adding business logic to handle our business needs for the data.
Mapping the relational model to the hierarchical model and the database manipulation code were the most tedious aspects of this work. Luckily, by using Typed DataSets you can eliminate the need to write this code yourself. When you create a Typed DataSet with multiple tables and relationships, you already have your relational-to-object mapping. By viewing a single row in a single table as the top of an object graph, you can navigate the relationships to get a tree of items. This is similar to what was done when business object layers were written with database joins across multiple tables to get an object graph in a database result. In the DataSet and Typed DataSet, the difference is that if you navigate the relationships, you are navigating to only the related rows in the related table. In this way, the Typed DataSet relieves you from writing the relational-to-object mapping, because it is inherent in the way the DataSet works.
The next job of the database programmer in this situation is usually to write the database manipulation code. Because ADO.NET has abstracted the DataSet from the database manipulation code (primarily in the managed providers), you will not have to write much code to make the database manipulation work. You may still have to write the stored procedures to handle the CRUD (Create, Read, Update, Delete) operations to the database and tie those stored procedures to the DataAdapter (see Chapter 8 for more information on how this is done), but in the larger picture ADO.NET does a lot of the heavy lifting.
That leaves only the business logic or rules to write, which in my experience is usually the easiest of the code to write. What is this business logic exactly? In some systems, business logic is as simple as data validation, whereas in other systems it is as complicated as integrating various systems to keep them in sync. Business logic is really any logic that needs to be added to the raw data in the database. So in this model where we are using Typed DataSets to get around writing business logic layers, where do we put our business logic? I recommend deriving from your generated Typed DataSets to put this logic in place.
6.4.1 Hooking Up Your Business Logic
The code generated from the .XSD file is still just a class, so you can inherit from it very simplybut how do you hook up your business logic? There are two approaches, and both are appropriate in the right circumstances. Both approaches begin by inheriting directly from the generated Typed DataSet.
6.4.1.1 Event-Driven DataSet
In the case where you do not have much business logic, you can decide to derive from the Typed DataSet and register for event notification to do your business logic. Within the DataSet, you register for notification from many different events, but the ones that make the most sense for most business logic are the RowChanging and RowChanged events. Listing 6.5 is an example of that solution.
Listing 6.5: Capturing Events from a Typed DataSet
public class CustomersObject : CustomerTDS { public CustomersObject() : base() { Register(); } protected CustomersObject(SerializationInfo info, StreamingContext context) : base(info, context) { Register(); } private void Register() { Invoice.InvoiceRowChanging += new InvoiceRowChangeEventHandler(InvoiceChanging); } private void InvoiceChanging(object source, InvoiceRowChangeEvent args) { if (args.Action == DataRowAction.Add || args.Action == DataRowAction.Change) { if (args.Row.InvoiceDate > DateTime.Today) { throw new Exception("Cannot Create Invoices" + "in the Future"); } } } }
We create a new class that derives from our Typed DataSet (CustomerTDS) and create two constructors. The first of these is for normal construction, and the other is for XML deserialization. The constructor that is used for deserialization must be implemented or there will be no support for XML serialization. Other than calling the base class's constructors, the only other thing we do here is call our new Register method, which is used to register for the events we are interested in. In this case, we have registered for the InvoiceRowChangingEvent. This is fired before the change to the row actually takes place. In our handler method (InvoiceChanging), we check to see whether the changed row was added or changed. If either of these actions occurred, we make sure the invoice date does not occur in the future. If it does, we throw an exception. We would use our new Typed DataSet as in Listing 6.6.
Listing 6.6: Testing Our Event-driven Typed DataSet Business Logic
... // 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 an instance of our inherited Typed DataSet CustomersObject dataset = new CustomersObject(); // Use the DataAdapters to fill the DataSet daCustomers.Fill(dataset, "Customer"); daInvoices.Fill(dataset, "Invoice"); // This will throw an exception because we're creating // an invoice date in the future CustomerTDS.InvoiceRow invoice; Invoice = dataset.Invoice.AddInvoiceRow(Guid.NewGuid(), DateTime.Now + new TimeSpan(4,0,0,0), "", "", "", dataset.Customer[0]);
The use of our derived Typed DataSet is identical to how the original Typed DataSet is used. But because we have registered for events, we will be notified during certain types of operations to implement our business logic. In this case, when we add a new invoice, we are creating it with an invoice date four days in the future so the event will throw an exception to let the user know he did something wrong.
6.4.1.2 Deriving from Typed DataSets
The other approach is not only to derive from the Typed DataSet, but also to derive from the Typed DataTable and Typed DataRow classes. This allows us to override any behavior to implement our business logic. We do not have to derive from each and every DataTable or DataRow, just the ones that need specific business logic. If we are going to need to derive from the DataRow, we will need to derive from its parent DataTable.
For this example, we want to put some logic into the Invoice table to check for credit before we allow a new invoice to be added to the table. Ultimately, we want our logic to look something like Listing 6.7.
Listing 6.7: AddInvoiceRow Method
public void AddInvoiceRow(InheritedInvoiceRow row) { if (DoesCustomerHaveCredit()) { base.AddInvoiceRow(row); } else { throw new Exception( "Customer Invoice cannot be created, " + "no credit available"); } }
To get to the point where we can make this change, we will have to start by inheriting from the Typed DataTable, as shown in Listing 6.8.
Listing 6.8: Inheriting from a Typed DataSet
public class InheritedTDS : FixedCustomerTDS { ... public class InheritedInvoiceDataTable : InvoiceDataTable { internal InheritedInvoiceDataTable() : base() { } internal InheritedInvoiceDataTable(DataTable table) : base(table) { } ... } ... }
Inheriting from the Typed DataTable requires that we support two constructors again. This time, the second constructor takes a DataTable. This second constructor is used for XML serialization as well. Creating the InheritedInvoiceDataTable was easy; the hard part is getting the Typed DataSet to use this class for its InvoiceDataTable. The generated code makes it hard on us because it is creating the entire schema (including our DataTable) during the base class's construction. This means that in order to replace the old DataTable with our inherited class we need to re-create much of the schema that is in the constructor. To get around this we could edit the generated code in a few small ways. First we could add a new virtual method that is called during construction to build the DataTable we want to replace, as shown in Listing 6.9.
Listing 6.9: Adding a Create Table Method
// This is the generated class public class FixedCustomerTDS : DataSet { ... protected virtual InvoiceDataTable CreateInvoiceDataTable(DataTable table) { if (table == null) return new InvoiceDataTable(); else return new InvoiceDataTable(table); } ... }
This method will return a Typed DataTable object. This is important because when we inherit from the Typed DataSet, we will need to override this method and return the same Typed DataTable. It ensures that our inherited DataTable actually does inherit from their Typed DataTable. The generated code will have code that depends on the DataTable being typed and by writing the method this way, we will guarantee not to break the generated code.
Next, we need to override this method when we inherit from the Typed DataSet. Because the method is virtual (or overridable in VB .NET), the Typed DataSet will call our version of this method when it constructs the DataTables. To hook this up to our Typed DataSet, we will need to inherit from the Typed DataSet and override the creation method, as shown in Listing 6.10.
Listing 6.10: Calling the Create Table Method
public class InheritedTDS : FixedCustomerTDS { public InheritedTDS() : base() { } protected InheritedTDS(SerializationInfo info, StreamingContext context) : base(info, context) { } protected override InvoiceDataTable CreateInvoiceDataTable(DataTable table) { if (table == null) { return new InheritedInvoiceDataTable() as InvoiceDataTable; } else { return new InheritedInvoiceDataTable(table) as InvoiceDataTable; } } ... }
This should look very much like the method in the base class, except that it creates our derived DataTable, but returns it as an instance of the base DataTable.
Next, we need to modify the generated code to replace all calls to construction of our DataTable to use this method (see Listing 6.11).
Listing 6.11: Changing Default Behavior of the Typed DataSet
// This is the generated class public class FixedCustomerTDS : DataSet { ... private void InitClass() { ... /* Originally this.tableInvoice = new InvoiceDataTable(); */ // New this.tableInvoice = CreateInvoiceDataTable(null); ... } ... protected FixedCustomerTDS(SerializationInfo info, StreamingContext context) { ... /* Originally this.Tables.Add( new InvoiceDataTable(ds.Tables["Invoice"])); */ // New this.Tables.Add( CreateInvoiceDataTable(ds.Tables["Invoice"])); ... } ... }
We need to replace two different styles of construction. Each happens a couple times per Typed DataSet. The first style is new NameDataTable(). We want to replace this with CreateNameDataTable(). The second style is new NameDataTable(SomeDataTable). We want to replace this with CreateNameDataTable(SomeDataTable). In the above example, both styles are shown as they originally looked and as they looked after we changed them.
The last piece that will help make our derived Typed DataSet useful is to create a new property to hide the base class's DataTable property, as shown in Listing 6.12.
Listing 6.12: Hiding the DataTable Property
public class InheritedTDS : FixedCustomerTDS { ... public new InheritedInvoiceDataTable Invoice { get { return (InheritedInvoiceDataTable)base.Invoice; } } ... }
By creating a new Invoice property, we are not only hiding the base class's Invoice property, but also making sure all code that references the Invoice property is dealing with our derived version of the DataTable.
With all of this in place, we can put some business logic into our inherited DataTable, as shown in Listing 6.13.
Listing 6.13: Adding Our Business Logic to the Derived Class
public class InheritedTDS : FixedCustomerTDS { ... public class InheritedInvoiceDataTable : InvoiceDataTable { ... public void AddInvoiceRow(InheritedInvoiceRow row) { if (DoesCustomerHaveCredit()) { base.AddInvoiceRow(row); } else { throw new Exception( "Customer Invoice cannot be created, " + "no credit available"); } } ... } ... }
Now, when a user attempts to call the DataTable and add a new invoice for a customer, we will make sure that the customer can have a new invoice; otherwise, we can throw an exception. This should allow us to put business logic at the table level, but that may not be enough. We might want to control some different behavior at the row level. To ensure that none of our users create invoices that are accidentally dated in the future, we want the following business object added to the DataRow's InvoiceDate property (see Listing 6.14).
Listing 6.14: Protecting the Invoice Date
public new DateTime InvoiceDate { get { return base.InvoiceDate; } set { if (value > DateTime.Today) { return new StrongTypingException("Invoice Date" + "cannot be in the" + "future", null); } else { base.InvoiceDate = value; } } }
To accomplish this, Listing 6.15 shows how we need to derive from the Typed DataRow as well.
Listing 6.15: Enabling Deriving from the DataRow
public class InheritedTDS : FixedCustomerTDS { ... public class InheritedInvoiceRow : InvoiceRow { public InheritedInvoiceRow(DataRowBuilder builder) : base(builder) { } ... } ... }
In the case of deriving from the Typed DataRow, the only constructor that is required is to support one that takes a DataRowBuilder. This constructor is used by the DataTable.NewRowFromBuilder() method. This method is called by the DataTable when a user asks for a new row for the DataTable. The base class calls the method to create rows that are type-safe. To make this work, Listing 6.16 shows how we need to override the NewRowFromBuilder() and GetRowType() methods in our DataTable.
Listing 6.16: Allowing Creation of New Derived DataRows
public class InheritedTDS : FixedCustomerTDS { ... public class InheritedInvoiceDataTable : InvoiceDataTable { ... protected override DataRow NewRowFromBuilder(DataRowBuilder builder) { return new InheritedInvoiceRow(builder); } protected override System.Type GetRowType() { return typeof(InheritedInvoiceRow); } ... } ... }
This ensures that all new rows created from this DataTable will be of our derived type (such as InheritedInvoiceRow). We need our DataTable to be handing out our new DataRows to users instead of the base class's implementation. To do this, we create a couple of new methods, as shown in Listing 6.17.
Listing 6.17: Overriding DataRow Creation
public class InheritedTDS : FixedCustomerTDS { ... public class InheritedInvoiceDataTable : InvoiceDataTable { ... public new InheritedInvoiceRow this[int index] { get { return ((InheritedInvoiceRow)(this.Rows[index])); } } public new InheritedInvoiceRow AddInvoiceRow(Guid InvoiceID, DateTime InvoiceDate, string Terms, string FOB, string PO, CustomerRow parentCustomerRowByCustomerInvoice) { ... } public new InheritedInvoiceRow NewInvoiceRow() { ... } ... } ... }
We first create a new indexer to return our new DataRow class to support returning our new DataRow, instead of the base class's indexer, which returns the base class. Next, we create new AddInvoiceRow() and NewInvoiceRow() methods (created in the Typed DataSet) to return our new DataRow class as well. Now our derived class should be type-safe and completely inherited.
Now that all the right plumbing is there, we can put our business logic in our DataRow class, as shown in Listing 6.18.
Listing 6.18: Adding Our Business Logic
public class InheritedTDS : FixedCustomerTDS { ... public class InheritedInvoiceRow : InvoiceRow { ... public new DateTime InvoiceDate { get { return base.InvoiceDate; } set { if (value > DateTime.Today) { throw new StrongTypingException( "Invoice Date Cannot be in the future", null); } else { base.InvoiceDate = value; } } } ... } ... }
In this business logic, we wanted to check to make sure that whenever an invoice date is set, it is not set in the future. Putting this logic into the property makes sure users cannot set the invoice date incorrectly, but since the DataRow is ultimately the base class, we will need to make sure that users cannot set our invoice date by using the indexer (which would skip calling our property). One way we can do this is by overriding the indexer to make it read-only (see Listing 6.19).
Listing 6.19: Protecting Our Business Logic
public class InheritedTDS : FixedCustomerTDS { ... public class InheritedInvoiceRow : InvoiceRow { ... // Only allow read by the indexer public new object this[int index] { get { return base[index]; } } ... } ... }
By overriding the indexer, users will have to use the properties to set the individual values. This way we do not have to duplicate our business logic or route the calls through our properties.
In our example, I derived from the invoice DataTable and the invoice DataRow. Our Typed DataSet also has type-safe classes for the customer's DataTable and DataRow. We did not derive from these because we did not need to add any business logic. So you can see that when you inherit from a Typed DataSet you do not need to derive from every DataTable, only the ones you want to add business logic into.
In this section we were able to derive from the Typed DataSet by manually editing the generated code. Because the generated code could change, it is generally a bad idea to edit it. To allow you to inherit without editing the generated code, Chris Sells and I have made a Visual Studio Add-in that will replace the standard Typed DataSet generator to make these changes for you in the generated code. You can download this code at my Web site (www.adoguy.com/book/AGDataSetGenerator).