Custom collections data pattern

I've put together a simple pattern for when I create new data driven objects that I'd like to share. This is the method I used in the 1.x framework for easily managing data objects. This is my version of the Lightweight Business Objects (LBO) approach.

BaseDataObject Class

Let's take the situation where I have several data objects and I want to maintain all the object properties easily.  I create an abstract base class that represents a data object (public abstract class BaseDataObject). The abstract class contains a default constructor that initializes a Hashtable where data properties are stored. Then create an oveloaded constructor that has an argument for the System.Data.IDataReader:

 internal BaseDataObject(){
    _properties = new Hashtable();
}
internal BaseDataObject(IDataReader properties) : this() {
    // writes data to properties collection
    WriteData(properties);
}

The overloaded constructor calls a WriteData method which copies data from the DataReader to the Hashtable:

 void WriteData(IDataReader properties) {
    if (properties.IsClosed)
        throw new DataException(
          "The IDataReader properties must be " +
          "set to initialize a new Customer");
 
    // the name that the value is stored under
      // is the name provided from the data source
    for (int i = 0; i < properties.FieldCount; i++) {
        BaseSet(properties.GetName(i).ToLower(), properties[i]);
    }
}

I also provide methods to access the base data properties.  These are the BaseGet<T> and BaseSet methods:

 protected internal T BaseGet<T>(string name) {
    return BaseGet<T>(name, default(T));
}
protected internal T BaseGet<T>(string name, T defaultValue) {
    if (_properties[name] == null)
        return defaultValue;
    return (T)_properties[name];
}
protected virtual internal void BaseSet(string name, object value) {
    _properties[name] = value;
}

This approach makes it easy to get a properly cast object and set a property in the hashtable cleanly. This also provides a central location to create functionality around setting data properties. An example could be an IsDirty property or PropertyChanged event. Subclasses will use the BaseGet and BaseSet methods to set properties they want to expose.

Next, I attach a Save method. Because the Save method is going to be different for every object, it would normally be an abstract method; however, I take this as an opportunity to setup two events: Saving and Saved. These events can be used to cancel the data save or maybe to redirect the user to another location when the data has been saved. Here is the Save method:

 public void Save(){
    // perform an action on the data before saving
    // (possibly security validation)
    CancelEventArgs args = new CancelEventArgs();
    OnSaving(args);
    
    if (args.Cancel) return;
    
    // save the data
    SaveInternal();
    
    // provide event for after the item is saved
    OnSaved(args);
}
// this is overridden in subclasses to handle saving the object data
protected internal abstract void SaveInternal();
 
public event CancelEventHandler Saving;
public event EventHandler Saved;

Notice that instead of providing an opportunity for a developer to override the Save method, an abstract method called SaveInternal is applied.

To view the completed BaseDataObject, click here.

Example: Customer class:

Now, I will make an example Customer class and demonstrate a controller class that will populate my data object.

 public class Customer : BaseDataObject {

    public Customer() : base() { }
    internal Customer(IDataReader properties) : base(properties) { }
    
    // the property name ID differs from the database name CustomerID
    public int ID {
        get { return BaseGet<int>("customerid", 0); }
    }
 
    public string FirstName {
        get { return BaseGet<string>("firstname"); }
        set { BaseSet("firstname", value); }
    }
 
    public string LastName {
        get { return BaseGet<string>("lastname"); }
        set { BaseSet("lastname", value); }
    }
 
    protected internal override void SaveInternal() {
        // DataManager is the static data class
        // it provides CRUD data actions
        DataManager.SaveCustomer(ID, FirstName, LastName);
    }
}

CustomerManager class:

The CustomerManager class manages interactions between the CRUD data object (which I generally build using the System.Data.Common namespace) and the business logic. So examples might be to get all the customers or get a customer by ID as in the example below:

 public static class CustomerManager {
    public static List<Customer> GetCustomers() {
        List<Customer> customers = new List<Customer>();
 
        using (IDataReader data = DataManager.GetCustomers()) {
            while (data.Read()) {
                Customer customer = new Customer(data);
                customers.Add(customer);
            }
        }
        return customers;
    }
    public static Customer GetCustomer(int customerID) { 
        using (IDataReader data = DataManager.GetCustomerByID(customerID)) {
            if (data.Read()) {
                Customer customer = new Customer(data);
                return customer;
            }
        }
        return null;
    }
}

You'll notice that the data from the CustomerManager object is used to abstract the data logic from the business logic.

Now, to approach an actual case of using these objects, check out the following:

 // get all customers
List<Customer> customers = CustomerManager.GetCustomers();

// get a single customer
Customer customer = CustomerManager.GetCustomer(23);
// modify customer and save

customer.FirstName = "foo";
customer.LastName = "bar";
customer.Save();

Let me know what you like or dislike about my approach. It's nothing great, but it works well and is better than the Typed DataSets (IMO).

BaseDataObject class:

 public abstract class BaseDataObject {    private Hashtable _properties;        internal BaseDataObject(){        _properties = new Hashtable();    }    internal BaseDataObject(IDataReader properties) : this() {
        // writes data to properties collection        WriteData(properties);    }        void WriteData(IDataReader properties) {        if (properties.IsClosed)            throw new DataException("The IDataReader properties must be " +
                        "set to initialize a new Customer");         // the name that the value is stored under        // is the name provided from the data source        for (int i = 0; i < properties.FieldCount; i++) {            BaseSet(properties.GetName(i).ToLower(), properties[i]);        }    }        public void Save(){        // perform an action on the data before saving        // (possibly security validation)        CancelEventArgs args = new CancelEventArgs();        OnSaving(args);                if (args.Cancel) return;                // save the data        SaveInternal();                // provide event for after the item is saved        OnSaved(args);    }    // this is used by     protected internal abstract void SaveInternal();        public event CancelEventHandler Saving;    public event EventHandler Saved;        protected virtual void OnSaving(CancelEventArgs args){        if (Saving != null)            Saving(this, args);    }        protected virtual void OnSaved(EventArgs args){        if (Saved != null)            Saved(this, args);    }        protected internal IDictionary Properties{        get { return _properties; }    }    

    protected internal T BaseGet<T>(string name) {
        return BaseGet<T>(name, default(T));
    }

    protected internal T BaseGet<T>(string name, T defaultValue) {
        if (_properties[name] == null)
            return defaultValue;
        return (T)_properties[name];
    }

    protected virtual internal void BaseSet(string name, object value) {
        _properties[name] = value;
    }
}