I stumbled across a little ADO.NET gotcha the other day...
By and large, ADO.NET's SQL client library presents a good, consistent exception interface when something goes wrong: errors and warnings are reported as instances of SqlException and there is lots of context wrapped up in these exception objects enabling exception handling code to distinguish between the various error conditions and implement a suitable failure or recovery strategy. This is fortunate, because lots of things can go wrong when calling into a database across a network connection, many of them being transient issues which call for retry strategies of one sort or another. So a common pattern in a data access layer involves catching and mapping SqlException instances to a specific exception handling implementation, following every call to the database:
try
{
using (SqlConnection connection = ...)
{
...
connection.Open();
... use the connection to call the database
}
}
catch (SqlException sqlException)
{
HandleException(sqlException); // Application-specific mapping function
}
This is so much nicer than using APIs which have a list as long as your arm of different exception types which might be thrown depending on the error condition.
I was disappointed to discover that ADO.NET (1.1) is not perfect in this respect either. A system under heavy load suddenly threw this exception at us, neatly circumventing all our carefully crafted database exception handling and requiring operator intervention for a condition which was transient and eminently suitable for an automated approach to recovery:
System.InvalidOperationException: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
at System.Data.SqlClient.SqlConnectionPoolManager.GetPooledConnection(SqlConnectionString options, Boolean& isInTransaction)
at System.Data.SqlClient.SqlConnection.Open()
at ... (our code)
What made this even more disappointing was that a brief foray with Reflector immediately showed that this was not the side effect of some catch-all exception handling which happened to throw InvalidOperation as a default choice, but must have been the result of a conscious decision to throw InvalidOperation for a very specific timeout condition (the exception thrown is even instantiated in its own dedicated utility method System.Data.SqlClient.SQL.PooledOpenTimeout()). I would love to know what the thought process behind that design decision was. InvalidOperation is supposed to mean that the method call was invalid for the current state of the object on which it is made. Er... excuse me... I just called Open() an a freshly instantiated SqlConnection. And the GetPooledConnection method where it was actually thrown is static.
InvalidOperationException unfortunately provides exactly zero additional context data beyond what it inherits from its base types System.SystemException and System.Exception, which leaves us having to parse the Message property to distinguish this particular timeout condition from any other InvalidOperationException which we might catch. Not very nice.
Thankfully, I see that the implementation behind SqlConnection.Open has changed substantially in Framework 2.0, and SqlConnectionPoolManager.GetPooledConnection() no longer exists, so let's hope this has been fixed.