'Return SqlDataReader with closed connection

I have created a Sql helper class which handles most of my needs.

Amongst those, I have a function which executes a SQL statement and returns a SqlDataReader as follows:

public static SqlDataReader ExecuteCommand(string cmdText, bool useParameters)
{
    using (var sqlConnection = new SqlConnection(_connectionString.ConnectionString))
    {
        sqlConnection.Open();
        using (var sqlCommand = new SqlCommand(cmdText, sqlConnection))
        {
            if (useParameters && SqlParameterCollection.Count > 0)
            {
                sqlCommand.Parameters.AddRange(SqlParameterCollection.ToArray());
            }

            using (var sqlDataReader = sqlCommand.ExecuteReader())
            {
                return sqlDataReader;
            }
        }
    }
}

The problem with this is, obviously, the fact that it returns a sqlDataReader which requires an open connection and the fact that the connection is closed.

I have looked at returning a SqlDataAdapter instead, but after reading the following thread SqlDataAdapter vs SqlDataReader it doesn't sound like such a good idea as a general function to be used in every single scenario when you have absolutely no idea of the amount of data it is supposed to load.

So... What's a good alternative?

The only thing I can think of off the top of my head is to cycle through the rows in the SqlDataReader and do a yield return IEnumerable<IDataRecord>.

Is there any better way of achieving this or is this pretty much it?



Solution 1:[1]

You could use CommandBehavior.CloseConnection, which offloads closure to the caller - but this then makes your code dependent on the caller using the reader correctly.

Personally, I would try to minimize this type of dependency - and move the materialization code as close to the DB code as possible. In reality, there aren't that many cases when the caller needs a raw reader - or at least, there shouldn't be. I would strongly advise using tools like "dapper" or ORMs, so that you can just do something like:

return connection.Query<T>(cmdText, args).ToList();

which then doesn't leave many places for the caller to make a mess of things.

Solution 2:[2]

You can create a wrapper for your connection and reader, which implements System.IDisposable. The wrapper becomes responsible for closing the connection:

public class DbReaderWrapper : IDbReaderWrapper
    {
        public DbDataReader Reader { get; private set; }
        private readonly DbConnection _connection;
        private bool _disposed = false;

        public DbReaderWrapper(DbConnection connection, DbDataReader reader)
        {
            Reader = reader;
            _connection = connection;
        }

        ~DbReaderWrapper()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if(_disposed)
            {
                return;
            }

            if(disposing)
            {
                _connection.Dispose();
            }

            _disposed = true;
        }
    }

Your ExecuteCommand() function would look something like this:

public static SqlDataReader ExecuteCommand(string cmdText, bool useParameters)
{
    var sqlConnection = new SqlConnection(_connectionString.ConnectionString)
    sqlConnection.Open();
    var sqlCommand = new SqlCommand(cmdText, sqlConnection)

    if (useParameters && SqlParameterCollection.Count > 0)
    {
        sqlCommand.Parameters.AddRange(SqlParameterCollection.ToArray());
    }

    var sqlDataReader = sqlCommand.ExecuteReader()
    return new DbReaderWrapper(sqlConnection, sqlDataReader);
}

And you can call this method with the using statement:

using var readerWrapper = sqlHelper.ExecuteCommand(query, true) {
    // Do something with the reader
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Marc Gravell
Solution 2 Axel Köhler