What's this article all about?
It took me some time to understand the real power behind .NET 2.0 Generics. Until now, hearing the word "Generics" immediately gave me the association of strongly typed collections. Yes, this is a nice feature as it prevents quite expensive casting while moving on collections, but is it the only usage of Generics? Well, in most cases, yes, but I'll try to open your mind to a new set of abilities, with a few examples. This, I hope, will allow you to think about cleaner and more powerful solutions in the future.
Looks familiar?
Look at the following code:
try
{
using (IDbConnection conn = ProviderFactory.CreateConnection())
{
cmd.Connection = conn;
conn.Open();
}
}
catch (Exception e)
{
}
I bet that this pattern exists in each of your applications ... a lot more than once! Think about it, if those lines were some sort of a template, it will enable you to trace the entire traffic (SQL queries) between your application and your database in one single place! It even gives you the ability to change the type of the exception it throws, or the information it adds, in just one single place! You got the idea...
To accomplish that, we need to create a template for executing any IDbCommand
object as a Reader
, NonQuery
, Scalar
- you name it.
Refactoring time - making a template from this pattern via Generics and delegates
Step 1 - Defining a delegate as a generic handler
public delegate T CommandHandler<T>(IDbCommand cmd);
Before I run along, just to make sure you're still with me:
- A delegate is simply a pointer to a method with the same signature as the delegate itself.
T
is a generic type which means the ReaderHandler<T>
and CommandHanlder<T>
will be able to return any given <T>
type. We'll want to return int
after parsing an IDbCommand
object via the ExecuteNonQuery()
method. But, we'll probably want to return some sort of collection after parsing IDbCommand
via the ExecuteReader()
method. Generics will enable us to do just that.
(If this is still not clear, just hang on, the following samples will make it clearer.)
Step 2 - The generic "command executer" template
public static T ExecuteCommand<T>(IDbCommand cmd, CommandHandler<T> handler)
{
try
{
using (IDbConnection conn = ProviderFactory.CreateConnection())
{
cmd.Connection = conn;
DatabaseTracer.WriteToTrace(TraceLevel.Verbose, cmd,
"Data Access Layer - Query profiler");
conn.Open();
return handler(cmd);
}
}
catch (Exception e)
{
Tracer.WriteToTrace(TraceLevel.Error, e, "Data Access Layer - Exception");
throw WrapException(e);
}
}
I owe you some explanations:
- Notice the generic type
T
- This will be necessary for returning different types depending on the programmer's choice.
- Create a connection - I'm using some factory I've built in order to return a strongly typed connection depending on the selected provider in the application configuration(*.config) file.
- Trace the command (
CommandText
, parameters etc.) - The DataBaseTracer
class checks the "switch" I've declared in the .config file and traces the query only if it was requested. This will give me that ability to trace all the queries later on in the production environment (good for production debugging).
- Send the live command (the connection was opened) to the handler so it can use the command for its needs.
- Trace the exception, again, only if requested.
- Wrap the exception in
DalException
- I, as an architect, believe that the Data Access Layer should throw only DalException
exceptions.
Step 3 - A first usage of the "command executer" template
Let's use the template in order to parse a command with the ExecuteReader()
method. First, we'll declare a delegate for handling a live reader (returning a generic type, for better extendibility).
public delegate T ReaderHandler<T>(IDataReader reader);
Now, let's use the template:
public static T ExecuteReader<T>(IDbCommand cmd, ReaderHandler<T> handler)
{
return ExecuteCommand<T>(cmd,
delegate(IDbCommand liveCommand)
{
IDataReader r = liveCommand.ExecuteReader();
return handler(r);
});
}
This one is even harder to follow, but relax, it's not as bad as you might think.
- You can see that I'm using an anonymous delegate for
CommandHandler<T>
, so the delegate gets the live command object from the ExecuteCommand
method and calls ExecuteReader()
on it. Afterwards, it sends the reader to the ReaderHandler<T>
handler (given as a parameter).
Step 4 - Real life example, using ExecuteReader<T> to parse a reader into a List<Person> and string
public static List<Person> GetPersonsList()
{
IDbCommand cmd = ProviderFactory.CreateCommand();
cmd.CommandText = "SELECT Name,Age,Email FROM Persons";
cmd.CommandType = CommandType.Text;
return DalServices.ExecuteReader<List<Person>>(cmd,
delegate(IDataReader r)
{
List<Person> persons = new List<Person>();
while (r.Read())
{
Person person = new Person(r["Name"].ToString(),
Convert.ToInt32(r["Age"]),
r["Email"].ToString());
persons.Add(person);
}
return persons;
});
}
public static string GetPersonsXml()
{
IDbCommand cmd = ProviderFactory.CreateCommand();
cmd.CommandText = "SELECT Name,Age,Email FROM Persons";
cmd.CommandType = CommandType.Text;
return DalServices.ExecuteReader<string>(cmd,
delegate(IDataReader r)
{
StringBuilder builder = new StringBuilder(500);
builder.Append("<Persons>");
while (r.Read())
{
Person person = new Person(r["Name"].ToString(),
Convert.ToInt32(r["Age"]),
r["Email"].ToString());
builder.Append(person.ToXml());
}
builder.Append("</Persons>");
return builder.ToString();
});
}
The first method returns a strongly typed collection of Person
objects, while the other method returns an XML representation of the results. This (strongly typed return values) is possible only by Generics.
Step 5 - Leveraging the command executer template
Now that we understand the template, let's wrap some more execution "modes". You can add to it later on, according to your needs.
public static int ExecuteNonQuery(IDbCommand cmd)
{
return ExecuteCommand<int>(cmd,
delegate(IDbCommand liveCommand)
{
return liveCommand.ExecuteNonQuery();
});
}
public static T ExecuteScalar<T>(IDbCommand cmd)
{
return ExecuteCommand<T>(cmd,
delegate(IDbCommand liveCommand)
{
return (T)liveCommand.ExecuteScalar();
});
}
Conclusions
I hope I managed to show you another kind of usage for Generics and delegates through a real world refactoring I've made in my infrastructure. Prior to Generics, this had to be done via interfaces, which cost us performance for each casting we had to make.
Can you now think about further usages of Generics??
Updates
- [01/03/2006] - I've fixed some of the method signatures and attached a demo project. I hope this will make the article a bit more approachable and easier to understand.