Pattern: resource management using callbacks
In C# in order to manage resources, IDisposable interface is used. But what if resource creating is complicated and requires a dedicated function? Lets say we have a system with multiple databases and complicated logic deciding which database should be used. We want to encapsulate this knowledge into a function:
class ConnectionWrapper {
IDbConnection NewConnection() {
string connectionString
... // whatever application specific logic
... // constructing connection string
return new SqlConnection(connectionString);
}
}
There is something bothering me in this code. We return new Connection object without guarantee that it will be disposed. We assume that the caller will utilize "using" statement. So we create resource but we do not have control over it correct usage.
Now, do you remember that when creating Data Reader by calling IDbCommand.ExecuteReader, we can provide a parameter "CommandBehavior" and one of the options is "CommandBehavior.CloseConnection". Why is that? It is exactly for the reason I've just described: sometimes we create connection in one place but we create a reader in another place. So we can not coordinate their life time. We can not wrap Connection into "using" because most likely we will close connection before data reader will have chance to finish reading. That's where "CommandBehavior.CloseConnection" helps. We create connection without worrying (or shall I say "with a hope") and we always create reader with "CommandBehavior.CloseConnection" and wrap reader into "using".
Needless to say, it is fragile. May be not really terrible, but you have to be careful and remember about all those little agreements you have in your application. Of course, you never forget things but when you write a framework to be used by others 10 programmers, it is just matter of time, when somebody will start leaking half-committed transactions... you will have a lot of fun trying to figure it out.
So, what new and shiny C#-2.0 (or even better 3.0) gives us? Callbacks. Or should I say simple way to write callbacks.
Lets see, what we want is a guarantee that resource allocated is not leaked. So we need to use "using" statement.
class ConnectionWrapper {
IDbConnection NewConnection() {
string connectionString
... // whatever application specific logic
... // constructing connection string
using(var connection = new SqlConnection(connectionString))
return connection;
}
}
}
This is what we want. But it will not work, because connection will be disposed before returning it to the caller and caller will get disposed connection. So here is the trick:
class ConnectionWrapper {
void NewConnection(Action<IDbConnection> callback) {
string connectionString
... // whatever application specific logic
... // constructing connection string
using(var connection = new SqlConnection(connectionString))
callback(connection);
}
}
}
We do not return connection object now. Instead caller must provide a callback code.
And notice important detail: connection object is wrapped into "using" statement, so we achieved our goal: we have guarantee that it will not leak.
Here is how caller will look like:
ConnectionWrapper.NewConnection(connection=>{
IDbCommand cmd = connection.CreateCommand();
cmd.CommandText = "select ...";
using(IDataReader reader = cmd.ExecuteReader()) {
while(reader.Read())
...
}
});
Notice, that caller does not have to use "using" on connection object. We simplified caller and improved reliability the same time.
But now we look suspiciously at the remaining "using" statement: if we managed to get rid of one, can we do the same thing to the another? What if callback will return reader instead of connection string?
class ConnectionWrapper {
void NewConnection(string sql, Action<IDataReader> callback) {
string connectionString
... // whatever application specific logic
... // constructing connection string
using(var connection = new SqlConnection(connectionString))
IDbCommand cmd = connection.CreateCommand();
cmd.CommandText = sql;
using(IDataReader reader = cmd.ExecuteReader()) {
while(reader.Read())
callback(reader);
}
}
}
}
// Caller:
var names = new List<string>();
ConnectionWrapper.NewConnection("select ...", reader=>{
names.Add((string)reader["Name"]);
});
Wow! Caller is now just two lines of code. And at the same time we keep Connection and Reader object inside "using".
But what if I want some parameters into my Command? Or you want to set command timeout? By now you should get used that the answer is going to be... right, callback :)
class ConnectionWrapper {
void NewConnection(Action<IDbCommand> cmdCallback, Action<IDataReader> callback) {
string connectionString
... // whatever application specific logic
... // constructing connection string
using(var connection = new SqlConnection(connectionString))
IDbCommand cmd = connection.CreateCommand();
cmdCallback(cmd);
using(IDataReader reader = cmd.ExecuteReader()) {
while(reader.Read())
callback(reader);
}
}
}
}
// Caller:
var names = new List<string>();
ConnectionWrapper.NewConnection(
cmd => {cmd.CommandText = "select ..."; cmd.Parameters.Add(...)},
reader=>{names.Add((string)reader["Name"]);
});
Now the framework call two callbacks: one to give the caller opportunity to set up Command properties, and another callback for Reader which will be called for each row in Reader.
Conclusion.
This article demonstrates how to use callback to keep resources life time under control without burdening callers.