Consuming a DataReader with LINQ

Consuming a DataReader with LINQ

Last night at the Atlanta MS Pros meeting, I gave the first my new talks on LINQ Migration Strategies. If you missed it, you can catch me at the Raleigh, NC and Huntsville, AL code camps. Baring that, check out the presentation slides. This talk focuses on how you can extend existing structures using the power of LINQ. Among other things, this includes LINQ to DataSets and the XML Bridge classes.

During the presentation, fellow MVP, Keith Rome asked a question that I couldn't let sit. Is it possible to us LINQ with a data reader? I answered that it should be possible if you combine the power of C# (sorry, VB doesn't have this yet) iterators with the concept of using the fields collection in Datasets. Essentially an untyped dataset is an array of arrays. The first array is consumed by LINQ through the iterator.

The challenge here is that LINQ works on anything that implements IEnumerable, but the DataReader doesn't implement that; at least not natively. Here's where the fun of Extension Methods comes to play. With a C# extension method, we can expose an IEnumerable pattern as we iterate over the rows that we read.

To create an extension method in C#, we create a static method in a static class. We then decorate the first parameter of the method with the "this" keyword to indicate that we are extending that type. In this sample, I wanted to expose the results as an IEnumerable<IDataRecord>, but I couldn't figure out how to get a handle on the current record to yield it as we are iterating. I did find that you can push a row's data into a object collection, so that's what I did in this example. I welcome other recommendations to keep things more strongly typed. Here's the extension method implementation.

public static class DataReaderExtension
{
    public static IEnumerable<Object[]> DataRecord(this System.Data.IDataReader source)
    {
        if (source == null)
            throw new ArgumentNullException("source");
 
        while (source.Read())
        {
            Object[] row = new Object[source.FieldCount];
            source.GetValues(row);
            yield return row;
        }
    }
}

With this extension method, we can now create a data reader and query it using LINQ to Objects as follows:

Using cn As New System.Data.SqlClient.SqlConnection(MyConnectionString)
    cn.Open()
    Using cm = cn.CreateCommand
        cm.CommandType = Data.CommandType.Text
        cm.CommandText = "Select IsApproved, EnteredDate, Creator from Comments"
       
Using dr = cm.ExecuteReader
            Me.Listbox1.DataSource = _
                From row In dr.DataRecord _
                Where CBool(row(0)) _
                Order By CDate(row(1)) _
                Select CStr(row(2)) Distinct
           
Listbox1.DataBind()
        End Using
   
End Using
End Using

I am not happy about the excessive casting to and from object in this implementation. As a result of the extra casting, I suspect that it doesn't perform as well as more native implementations even though we are consuming a data reader, but I haven't had the chance to actually run performance comparisons on the alternatives. Alternative solutions are welcome.

Posted on 2/5/2008 11:08:00 PM - Comments(3)
Categories: C# LINQ VB
Comments:
  • Gravatar Actually, SqlDataReader DOES implement IEnumerable via DbDataReader, which it extends.



    See http://forums.asp.net/p/1220006/2300244.aspx


    Robert Chaplin (Posted on 4/15/2008 8:09:00 PM)
  • Gravatar Pingback from http://topsy.com/trackback?utm_source=pingback&utm_campaign=L2&url=http://www.thinqlinq.com/Post.aspx/Title/Consuming-a-DataReader-with-LINQ
    Pingback (Posted on 6/3/2010 11:19:00 AM)
  • Gravatar If you change the extension method above to be named AsEnumerable LINQ will apply that automatically in the query without have to call the extension method within the query. Also, you may want the return type to be IEnumerable<IDataReader> and just return the reader rather than getting all the values in the row. This will give you the normal methods one would expect when working with a DataReader (GetString, GetBytes, etc.). For example, you may have a column you want to access in a streaming manner using GetBytes. If you want to prevent someone from messing with the state of the reader (.read) then make the return type IEnumerable<IDataRecord>



    Although we do not have iterators (yield) in VB, we can still create a wrapper class that implements IEnumerable/IEnumerator and return a new instance of that class through an AsEnumerable extension method, making the reader LINQ queryable.



    The one thing I am not sure of is whether the IDisposable implementation should dispose the underlying reader. I have not had issues in it with my code but that does not mean I cover every use case.



    The wrapper class is:



    Imports System.Data

    ''' <summary>

    ''' Wraps an IDataReader in an IEnumerable so that the DataReader can be used by LINQ or simply iterated over.

    ''' </summary>

    ''' <remarks>Does not implement the reset method since a DataReader only allows forward reading.</remarks>

    Public Class DataReaderEnumerator

    Implements IEnumerator(Of IDataReader), IEnumerable(Of IDataReader), IDisposable



    Private _isReadable As Boolean



    Public Sub New(ByVal reader As IDataReader)

    _reader = reader

    End Sub



    Private _reader As IDataReader

    Public Property DataReader() As IDataReader

    Get

    Return _reader

    End Get

    Set(ByVal value As IDataReader)

    _reader = value

    End Set

    End Property



    Public ReadOnly Property Current() As System.Data.IDataReader Implements System.Collections.Generic.IEnumerator(Of System.Data.IDataReader).Current

    Get

    If (_reader IsNot Nothing AndAlso Not _reader.IsClosed AndAlso _isReadable) Then

    Return _reader

    Else

    Return Nothing

    End If

    End Get

    End Property



    Public ReadOnly Property Current1() As Object Implements System.Collections.IEnumerator.Current

    Get

    If (_reader IsNot Nothing AndAlso Not _reader.IsClosed AndAlso _isReadable) Then

    Return _reader

    Else

    Return Nothing

    End If

    End Get

    End Property



    Public Function MoveNext() As Boolean Implements System.Collections.IEnumerator.MoveNext

    _isReadable = _reader.Read()

    Return _isReadable

    End Function



    Public Sub Reset() Implements System.Collections.IEnumerator.Reset

    Throw New NotSupportedException("DataReaders implement forward only reading and cannot be reset.")

    End Sub



    Private disposedValue As Boolean ' To detect redundant calls



    ' IDisposable

    Protected Overridable Sub Dispose(ByVal disposing As Boolean)

    If Not Me.disposedValue Then

    If disposing Then

    _reader.Dispose()

    End If

    _reader = Nothing

    End If

    Me.disposedValue = True

    End Sub



    #Region " IDisposable Support "

    ' This code added by Visual Basic to correctly implement the disposable pattern.

    Public Sub Dispose() Implements IDisposable.Dispose

    ' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.

    Dispose(True)

    GC.SuppressFinalize(Me)

    End Sub

    #End Region



    Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of System.Data.IDataReader) Implements System.Collections.Generic.IEnumerable(Of System.Data.IDataReader).GetEnumerator

    Return Me

    End Function



    Public Function GetEnumerator1() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator

    Return Me

    End Function

    End Class



    The extension method is:

    <Extension()> _

    Public Function AsEnumerable(ByVal reader As IDataReader) As IEnumerable(Of IDataReader)

    Return New DataReaderEnumerator(reader)

    End Function



    And now we can do queries like:

    Dim command As DbCommand = _db.GetSqlStringCommand(query)

    dim reader As IDataReader = _db.ExecuteReader(command)

    Dim results As IEnumerable(Of String) = _

    From record In reader _

    Select record.GetString(0)




    Richard Collette (Posted on 2/25/2011 11:53:00 AM)