Home > Articles > Programming > Windows Programming

This chapter is from the book

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 simply—but 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).

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020