Better the devil you know - Part IV.

Roger Torres - December 19th, 2008

In Part III of this series I described the execution methods supported by the DAL. In this post I will show how to materialize data into .NET objects with generic type readers.

GENERIC READERS

One of the most common operations performed by any .NET enterprise applications is reading data records from the backing store, and passing the results back to the users in some kind of structured form. I wanted to rationalize this pattern with a simple mechanism that “materializes records into .NET type instances” via callback delegates. Again, all the boilerplate code is abstracted away from the user, a practice that not only saves development time and minimizes coding errors, but makes the framework more flexible to accept future modifications.

Let’s review the steps to read a list of employees hired after a given date as you would normally write it with classic ADO.NET:

  1. Define the Employee class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public class Employee {
    	public int EmployeeId { get; set; }
    	public string FirstName { get; set; }
    	public string LastName { get; set; }
    	public DateTime HireDate { get; set; }
     
    	public Employee() {}
    	public Employee(int id, string firstName, 
                    string lastName, DateTime hireDate) {
    		EmployeeId = id;
    		FirstName = firstName;
    		LastName = lastName;
    		HireDate = hireDate;
    	}
     
    	public override string ToString() {
    		return string.Format("{0}-{1} {2}", 
                          EmployeeId, FirstName, LastName);
    	}
    }
  2. Define the data adapter to read employees using a text command.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    public class EmployeeAdapter {
          public static IEnumerable<Employee> ReadEmployees(DateTime fromDate) {
                IList<Employee> result = new List<Employee>();
                SqlConnection cnn;
                try {
                    cnn = new SqlConnection(ConnectionString); 
                    SqlCommand cmd = new SqlCommand(
                      @"SELECT EmployeeId, FirstName, LastName, HireDate FROM Employee
                       WHERE HireDate BETWEEN @FromDate AND GETDATE()", cnn);
                    cmd.Parameters.Add(new SqlParameter("FromDate", fromDate));
    		cnn.Open();
                    SqlDataReader reader = cmd.ExecuteReader();
                    while(reader.Read()) {
                          result.Add(new Employee(
    			record.GetInt32(0),
    			record.GetString(1),
    			record.GetString(2),
    			record.GetDateTime(3)));
                    }
                    return result;
                }
                catch(Exception ex) {
                    if(cnn != null && cnn.State == ConnectionState.Open)
                        cnn.Close();
                    throw ex;
                }    
          }
    }
  3. Invoke the adapter to read the employees
    1
    2
    
    IEnumerable<Employee> result = 
          EmployeeAdapter.ReadEmployees(new DateTime(2000, 1, 1));

Lets review the same example but this time using the generic reader pattern implemented by our DAL. The framework will perform exactly the same steps, although most of the boilerplate code is now encapsulated and transparent to the developers:

  1. Define the Employee class (The Model).
    Same as above
  2. Define the Command class to read employees.
    1
    2
    3
    
    --  ReadEmployees.sql - Command Embedded Resource
    SELECT EmployeeId, FirstName, LastName, HireDate FROM Employee
    WHERE HireDate BETWEEN @FromDate AND GETDATE()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    // ReadEmployees.cs - Command Code-Behind
    public class ReadEmployees : Command {
    	public DateTime FromDate { get; set; }
     
    	public Employee MaterializeEmployee(
                    System.Data.IDataRecord record, int resultIndex) {
    		return new Employee(
    			record.GetInt32(0),
    			record.GetString(1),
    			record.GetString(2),
    			record.GetDateTime(3));
    	}
    }
  3. Invoke the command to read the employees
    1
    2
    3
    4
    
    ReadEmployees cmd = new ReadEmployees(); 
    cmd.FromDate = new DateTime(2000, 1, 1);
    IEnumerable<Employee> result = 
          cn.ExecuteReader<Employee>(cmd, cmd.MaterializeEmployee);

As expected, the command class declares a FromDate property matching the SQL parameter to feed the script before the command is executed. What’s new here is the command class implementing the core of the data adapter in a local function that accepts a data record and the index of the resultset in the internal data reader (used when multiple resultsets are returned). This is the only application specific part that developers need to be worried about, the rest is taken care by the framework. When the command is executed inside a generic reader, the framework invokes “the materializer” delegate for each returned record, storing the results in a collection or container structure that the caller can manipulate after the reading operation is completed.

In this example the materializer function has been implemented inside the command class, but this is not a requirement (in case the user decides to create a library of materializers in some other place).

AUTO MATERIALIZER

But in case you only need a simple one-to-one mapping of data records, make sure the property names of the entity class (only the ones you need to materialize) coincide with the column names coming from the script. You don’t need to provide a callback delegate in this case since the framework will create one for you automatically. Look how easy is to read the same employees, but this time we will only materialize the first and last names:

1
2
3
-- Command Script
SELECT FirstName, LastName FROM Employee
WHERE HireDate BETWEEN @FromDate AND GETDATE()
1
2
3
4
5
6
7
8
9
// Command Code-Behind
public class ReadEmployees : CoNatural.Data.Command {
	public DateTime FromDate { get; set; }
}
 
// The Controller Invoking the Command
ReadEmployees cmd = new ReadEmployees(); 
cmd.FromDate = new DateTime(2000, 1, 1);
IEnumerable<Employee> result = cn.ExecuteReader<Employee>(cmd);

THE OTHER TWO GENERIC READERS

Sometimes the developer needs to cache a keyed dictionary of entities to perform faster lookups in the application. The generic reader method ExecuteReader<K, T> follows the same pattern described above, but this time accepting an additional parameter to specify the column index in the data record where the key value of the dictionary will be found. The user must guarantee that this column returns valid (unique and not null) values of type <K>.

Finally, there is another ExecuteScalar<T> method that accepts a callback delegate to materialize only the first record in the result set returned by a data reader. This method is used as an alternative to the classic ADO.NET ExecuteScalar where all values must be returned as output parameters.

I will provide some more realistic examples later in this series (using the AdventureWorks database), but before that I will discuss the Command Binders and Batch execution modes.

Stay tuned

Leave a comment