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.



See http://forums.asp.net/p/1220006/2300244.aspx
Robert Chaplin (Posted on 4/15/2008 8:09:00 PM)
Pingback (Posted on 6/3/2010 11:19:00 AM)
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)