如何轻松地将DataReader转换为List<T>?

138

我有一个 DataReader 中的数据,我想将其转换为 List<T>。你有什么简单的解决方案吗?

比如,在 CustomerEntity 类中,我有 CustomerId 和 CustomerName 属性。如果我的 DataReader 返回这两列数据,那么我该如何将它们转换为 List<CustomerEntity>

9个回答

220

我建议为此编写一个扩展方法:

public static IEnumerable<T> Select<T>(this IDataReader reader,
                                       Func<IDataReader, T> projection)
{
    while (reader.Read())
    {
        yield return projection(reader);
    }
}

你可以使用 LINQ 的 ToList() 方法将其转换为 List<T>,如下所示:

using (IDataReader reader = ...)
{
    List<Customer> customers = reader.Select(r => new Customer {
        CustomerId = r["id"] is DBNull ? null : r["id"].ToString(),
        CustomerName = r["name"] is DBNull ? null : r["name"].ToString() 
    }).ToList();
}

我建议在Customer(或其他地方)中添加一个FromDataReader方法:

public static Customer FromDataReader(IDataReader reader) { ... }

那就剩下:

using (IDataReader reader = ...)
{
    List<Customer> customers = reader.Select<Customer>(Customer.FromDataReader)
                                     .ToList();
}

(我认为在这种情况下类型推断无法奏效,但我可能错了...)


4
扩展方法不应该是:while (drOutput.Read()),而应该是:while (reader.Read())。 - Anthony
10
请注意,这与reader.Cast<IDataReader>().Select相同。 - SLaks
1
@SLaks:确实,尽管OP从未提到使用哪种DataReader。个人而言,我非常喜欢扩展方法 :) - Jon Skeet
1
我认为你不会找到任何未继承DbDataReaderIDataReader。一旦你创建了一个扩展方法,最好让它在DataReader周围传递一个DynamicObject - SLaks
1
你可以使用 Func<dynamic, T> 并编写 d => new Customer { CustomerId = d.id } (你也可以只返回一个 IEnumerable<dynamic>)。 - SLaks
显示剩余14条评论

85

我使用以下方法编写了这个案例。

首先,添加命名空间:System.Reflection

例如:T是返回类型(类名),dr是映射DataReader的参数。

C#中,调用映射方法如下:

List<Person> personList = new List<Person>();
personList = DataReaderMapToList<Person>(dataReaderForPerson);

这是映射方法:
public static List<T> DataReaderMapToList<T>(IDataReader dr)
{
    List<T> list = new List<T>();
    T obj = default(T);
    while (dr.Read()) {
        obj = Activator.CreateInstance<T>();
        foreach (PropertyInfo prop in obj.GetType().GetProperties()) {
            if (!object.Equals(dr[prop.Name], DBNull.Value)) {
                prop.SetValue(obj, dr[prop.Name], null);
            }
        }
        list.Add(obj);
    }
    return list;
}

在VB.NET中,可以像下面这样调用映射方法:

Dim personList As New List(Of Person)
personList = DataReaderMapToList(Of Person)(dataReaderForPerson)

这是映射方法:
Public Shared Function DataReaderMapToList(Of T)(ByVal dr As IDataReader) As List(Of T)
        Dim list As New List(Of T)
        Dim obj As T
        While dr.Read()
            obj = Activator.CreateInstance(Of T)()
            For Each prop As PropertyInfo In obj.GetType().GetProperties()
                If Not Object.Equals(dr(prop.Name), DBNull.Value) Then
                    prop.SetValue(obj, dr(prop.Name), Nothing)
                End If
            Next
            list.Add(obj)
        End While
        Return list
    End Function

1
实际上,您的DataReaderMapToList的内部可以很好地作为Jon Skeet上面答案的默认投影。 - Steven Wilber
2
这个很好用。我对 C# 代码有一个小建议:将以 prop.SetValue 开头的那一行改为 prop.SetValue(obj, Convert.ChangeType(dr[prop.Name], prop.PropertyType), null);。这样可以让代码适用于除字符串以外的其他类型。 - Brad W
如果一个列是可空的,尝试将 Nullable<> 转换为类型会抛出异常。这里提供了解决方案:https://dev59.com/aWMl5IYBdhLWcg3w7qlq#18015612 - Niklas
1
可能看起来很明显,但是在SELECT命令中的字段名必须与数据对象中的字段名匹配。 - Starjumper Tech SL
谢谢!我根据Brad W的建议补充了解决方案,还可以在while循环后添加一个语句**dr.Close();**。 - Wilson

54

我曾经看到一些系统使用反射和属性或字段上的属性,将DataReader映射到对象中(有点像LinqToSql所做的)。它们可以节省一些编码时间,并且在处理DBNull等情况时可以减少错误。一旦缓存生成的代码,它们可能比大多数手写代码更快,因此如果您经常这样做,请考虑“高路”。

请参见“.NET中反射的防御”以获取其中的一个示例。

然后,您可以编写如下代码:

class CustomerDTO  
{
    [Field("id")]
    public int? CustomerId;

    [Field("name")]
    public string CustomerName;
}

...

using (DataReader reader = ...)
{    
   List<CustomerDTO> customers = reader.AutoMap<CustomerDTO>()
                                    .ToList();
}

(AutoMap(),是一个扩展方法)


@Stilgar,感谢您的精彩评论

如果您能够的话,最好使用NHibernate、EF或Linq to Sql等。然而,在旧项目中(或出于其他(有时合理的)原因,例如“非创造性地在这里”,“对存储过程的喜爱”等),不总是可能使用ORM,因此轻量级系统可以作为“备选方案”很有用。

如果您曾经需要编写大量的IDataReader循环,您会看到减少编码(和错误)的好处,而无需更改系统架构。这并不意味着这是一个好的起始架构......

我假设CustomerDTO不会超出数据访问层,而复合对象等将由数据访问层使用DTO对象构建。


在我写下这个答案的几年后,Dapper进入了.NET世界,它可能是编写您自己的AutoMapper的一个很好的起点,也许它完全消除了您无需这样做的需要。


5
这种方法的问题在于,一旦开始使用复合对象,你就会遇到很多麻烦。如果客户与公司相关联,则需要一个公司属性。你可以进行递归处理,但公司可能具有List<Customer>属性,然后你必须遍历图形。为此,你需要一个表示关联的属性。这就是LINQ to SQL和Entity Framework所做的,但它们是大型产品,你不能轻松地开发内部解决方案。而且,如果你要这样做,为什么不改用EF呢? - Stilgar
4
为什么我没有 AutoMap() 函数? - saber tabatabaee yazdi
6
我需要添加哪些引用才能使[Field("id")]起作用? - Drew Chapin
5
使用EF对于一个已有“非EF规则”的数据库进行操作是不使用EF的一个非常有力的理由...(并且,不,这条评论并不晚六年,因为EF仍然存在着当时的问题)。 - user2864740
3
你们是否有 AutoMap 的实现? - nawfal
显示剩余2条评论

41

最简单的解决方案:

var dt = new DataTable();
dt.Load(myDataReader);
List<DataRow> rows = dt.AsEnumerable();
var customers = rows.Select(dr=>new Customer(...)).ToList();

1
@coolcake:添加 using System.Data;,这是一个扩展方法。 - Mohsen
2
这是一个很棒的解决方案+1,但请记住DataTable解决方案会带走数据读取器最大的优势,即在加载时的需求。DataTable首先将整个数据读入内存。顺便说一下,AsEnumerable扩展方法位于System.Data.DataSetExtensions程序集中(我必须说这是一个奇怪的程序集名称,听起来更像命名空间)。 - nawfal
这行代码的意思是“DataTable不包含一个不带参数的构造函数”。因此,我不能像第一行那样实例化数据表。这在核心中可行吗? - Sam
2
我在这里漏掉了什么吗?这个解决方案并不会产生一个 List(Of {MyType}),而是会产生一个 List(Of DataRow),这与 Op 请求的解决方案有很大的区别。 - SteveCinq
1
给他一个编程奥斯卡。 - Yousha Arif
显示剩余5条评论

12

我已经开始使用Dapper了。要使用你的示例,可以这样写(从记忆中写出):

public List<CustomerEntity> GetCustomerList()
{
    using (DbConnection connection = CreateConnection())
    {
        return connection.Query<CustomerEntity>("procToReturnCustomers", commandType: CommandType.StoredProcedure).ToList();
    }
}

CreateConnection()会处理访问数据库并返回连接。

Dapper可以自动将数据字段映射到属性。它还支持多种类型和结果集,并且非常快速。

查询返回IEnumerable,因此使用ToList()方法。


这是一个非常棒的答案!!!我刚刚下载了Dapper,它运行得非常好,为我节省了很多时间和烦恼!谢谢 - Funky

10

显然@Ian Ringrose的中心论点是你应该使用一个库来处理这个问题,这是这里最好的单一答案(因此+1),但为了最小化丢弃或演示代码,这里提供了@SLaks@Jon Skeet更细致的评论(+1)的具体说明:

public List<XXX> Load( <<args>> )
{
    using ( var connection = CreateConnection() )
    using ( var command = Create<<ListXXX>>Command( <<args>>, connection ) )
    {
        connection.Open();
        using ( var reader = command.ExecuteReader() )
            return reader.Cast<IDataRecord>()
                .Select( x => new XXX( x.GetString( 0 ), x.GetString( 1 ) ) )
                .ToList();
    }
}

就像 @Jon Skeet 的回答中所说,

            .Select( x => new XXX( x.GetString( 0 ), x.GetString( 1 ) ) )

位可以提取到一个帮助器中(我喜欢将它们转储到查询类中):

    public static XXX FromDataRecord( this IDataRecord record)
    {
        return new XXX( record.GetString( 0 ), record.GetString( 1 ) );
    }

并被用作:

            .Select( FromDataRecord )

更新于3月9日13:另请参见此答案中拆分样板代码的一些优秀技术细节



9

你不能直接将数据读取器转换为列表。

你需要循环遍历数据读取器中的所有元素,并插入到列表中。

以下是示例代码:

using (drOutput)   
{
            System.Collections.Generic.List<CustomerEntity > arrObjects = new System.Collections.Generic.List<CustomerEntity >();        
            int customerId = drOutput.GetOrdinal("customerId ");
            int CustomerName = drOutput.GetOrdinal("CustomerName ");

        while (drOutput.Read())        
        {
            CustomerEntity obj=new CustomerEntity ();
            obj.customerId = (drOutput[customerId ] != Convert.DBNull) ? drOutput[customerId ].ToString() : null;
            obj.CustomerName = (drOutput[CustomerName ] != Convert.DBNull) ? drOutput[CustomerName ].ToString() : null;
            arrObjects .Add(obj);
        }

}

5
我在一个个人项目中涵盖了这个内容,你可以使用你想要的。请注意,ListEx实现了IDataReader接口。

people = new ListExCommand(command)
.Map(p=> new ContactPerson()
{
  Age = p.GetInt32(p.GetOrdinal("Age")),
  FirstName = p.GetString(p.GetOrdinal("FirstName")),
  IdNumber = p.GetInt64(p.GetOrdinal("IdNumber")),
  Surname = p.GetString(p.GetOrdinal("Surname")),
  Email = "z.evans@caprisoft.co.za"
})
.ToListEx()
.Where("FirstName", "Peter");

或者像以下示例中使用对象映射。

people = new ListExAutoMap(personList)
.Map(p => new ContactPerson()
{
    Age = p.Age,
    FirstName = p.FirstName,
    IdNumber = p.IdNumber,
    Surname = p.Surname,
    Email = "z.evans@caprisoft.co.za"
})
.ToListEx()
.Where(contactPerson => contactPerson.FirstName == "Zack");

请看:

请查看http://caprisoft.codeplex.com


1
这不就是 Jon Skeet 的“Func<IDataReader, T>”方法吗?是的。通过引入自己的ICommandIConnection,在各种 ADO.NET 提供程序之间实现互操作性更加困难。我不明白为什么一开始就需要它。 - nawfal
1
在你的ListExCommand类中,如果我们必须手动提供映射器,为什么要使用反射来绑定属性?在你的ListExAutoMap类中,我们从哪里获取IEnumerable<T>以传递给构造函数,因为我们只剩下IEnumerableDbDataReader)。如果我们必须手动进行foreach(例如reader.Cast<IDataRecord>()),那么这将使类中的内部foreach循环变得多余,从而使这种方法非常缓慢。对于你的努力点个赞。 - nawfal

2

我知道这个问题很老了,已经有答案了,但是...

既然SqlDataReader已经实现了IEnumerable,为什么还需要创建一个记录循环呢?

我一直在使用下面的方法,没有任何问题,也没有性能问题:到目前为止,我已经测试过IList、List(Of T)、IEnumerable、IEnumerable(Of T)、IQueryable和IQueryable(Of T)。

Imports System.Data.SqlClient
Imports System.Data
Imports System.Threading.Tasks

Public Class DataAccess
Implements IDisposable

#Region "   Properties  "

''' <summary>
''' Set the Query Type
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property QueryType() As CmdType
    Set(ByVal value As CmdType)
        _QT = value
    End Set
End Property
Private _QT As CmdType

''' <summary>
''' Set the query to run
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property Query() As String
    Set(ByVal value As String)
        _Qry = value
    End Set
End Property
Private _Qry As String

''' <summary>
''' Set the parameter names
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property ParameterNames() As Object
    Set(ByVal value As Object)
        _PNs = value
    End Set
End Property
Private _PNs As Object

''' <summary>
''' Set the parameter values
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property ParameterValues() As Object
    Set(ByVal value As Object)
        _PVs = value
    End Set
End Property
Private _PVs As Object

''' <summary>
''' Set the parameter data type
''' </summary>
''' <value></value>
''' <remarks></remarks>
Public WriteOnly Property ParameterDataTypes() As DataType()
    Set(ByVal value As DataType())
        _DTs = value
    End Set
End Property
Private _DTs As DataType()

''' <summary>
''' Check if there are parameters, before setting them
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Private ReadOnly Property AreParams() As Boolean
    Get
        If (IsArray(_PVs) And IsArray(_PNs)) Then
            If (_PVs.GetUpperBound(0) = _PNs.GetUpperBound(0)) Then
                Return True
            Else
                Return False
            End If
        Else
            Return False
        End If
    End Get
End Property

''' <summary>
''' Set our dynamic connection string
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Private ReadOnly Property _ConnString() As String
    Get
        If System.Diagnostics.Debugger.IsAttached OrElse My.Settings.AttachToBeta OrElse Not (Common.CheckPaid) Then
            Return My.Settings.DevConnString
        Else
            Return My.Settings.TurboKitsv2ConnectionString
        End If
    End Get
End Property

Private _Rdr As SqlDataReader
Private _Conn As SqlConnection
Private _Cmd As SqlCommand

#End Region

#Region "   Methods "

''' <summary>
''' Fire us up!
''' </summary>
''' <remarks></remarks>
Public Sub New()
    Parallel.Invoke(Sub()
                        _Conn = New SqlConnection(_ConnString)
                    End Sub,
                    Sub()
                        _Cmd = New SqlCommand
                    End Sub)
End Sub

''' <summary>
''' Get our results
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Function GetResults() As SqlDataReader
    Try
        Parallel.Invoke(Sub()
                            If AreParams Then
                                PrepareParams(_Cmd)
                            End If
                            _Cmd.Connection = _Conn
                            _Cmd.CommandType = _QT
                            _Cmd.CommandText = _Qry
                            _Cmd.Connection.Open()
                            _Rdr = _Cmd.ExecuteReader(CommandBehavior.CloseConnection)
                        End Sub)
        If _Rdr.HasRows Then
            Return _Rdr
        Else
            Return Nothing
        End If
    Catch sEx As SqlException
        Return Nothing
    Catch ex As Exception
        Return Nothing
    End Try
End Function

''' <summary>
''' Prepare our parameters
''' </summary>
''' <param name="objCmd"></param>
''' <remarks></remarks>
Private Sub PrepareParams(ByVal objCmd As Object)
    Try
        Dim _DataSize As Long
        Dim _PCt As Integer = _PVs.GetUpperBound(0)

        For i As Long = 0 To _PCt
            If IsArray(_DTs) Then
                Select Case _DTs(i)
                    Case 0, 33, 6, 9, 13, 19
                        _DataSize = 8
                    Case 1, 3, 7, 10, 12, 21, 22, 23, 25
                        _DataSize = Len(_PVs(i))
                    Case 2, 20
                        _DataSize = 1
                    Case 5
                        _DataSize = 17
                    Case 8, 17, 15
                        _DataSize = 4
                    Case 14
                        _DataSize = 16
                    Case 31
                        _DataSize = 3
                    Case 32
                        _DataSize = 5
                    Case 16
                        _DataSize = 2
                    Case 15
                End Select
                objCmd.Parameters.Add(_PNs(i), _DTs(i), _DataSize).Value = _PVs(i)
            Else
                objCmd.Parameters.AddWithValue(_PNs(i), _PVs(i))
            End If
        Next
    Catch ex As Exception
    End Try
End Sub

#End Region

#Region "IDisposable Support"

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
        End If
        Try
            Erase _PNs : Erase _PVs : Erase _DTs
            _Qry = String.Empty
            _Rdr.Close()
            _Rdr.Dispose()
            _Cmd.Parameters.Clear()
            _Cmd.Connection.Close()
            _Conn.Close()
            _Cmd.Dispose()
            _Conn.Dispose()
        Catch ex As Exception

        End Try
    End If
    Me.disposedValue = True
End Sub

' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
Protected Overrides Sub Finalize()
    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
    Dispose(False)
    MyBase.Finalize()
End Sub

' 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

End Class

强类型类

Public Class OrderDCTyping
    Public Property OrderID As Long = 0
    Public Property OrderTrackingNumber As String = String.Empty
    Public Property OrderShipped As Boolean = False
    Public Property OrderShippedOn As Date = Nothing
    Public Property OrderPaid As Boolean = False
    Public Property OrderPaidOn As Date = Nothing
    Public Property TransactionID As String
End Class

使用方法

Public Function GetCurrentOrders() As IEnumerable(Of OrderDCTyping)
    Try
        Using db As New DataAccess
            With db
                .QueryType = CmdType.StoredProcedure
                .Query = "[Desktop].[CurrentOrders]"
                Using _Results = .GetResults()
                    If _Results IsNot Nothing Then
                        _Qry = (From row In _Results.Cast(Of DbDataRecord)()
                                    Select New OrderDCTyping() With {
                                        .OrderID = Common.IsNull(Of Long)(row, 0, 0),
                                        .OrderTrackingNumber = Common.IsNull(Of String)(row, 1, String.Empty),
                                        .OrderShipped = Common.IsNull(Of Boolean)(row, 2, False),
                                        .OrderShippedOn = Common.IsNull(Of Date)(row, 3, Nothing),
                                        .OrderPaid = Common.IsNull(Of Boolean)(row, 4, False),
                                        .OrderPaidOn = Common.IsNull(Of Date)(row, 5, Nothing),
                                        .TransactionID = Common.IsNull(Of String)(row, 6, String.Empty)
                                    }).ToList()
                    Else
                        _Qry = Nothing
                    End If
                End Using
                Return _Qry
            End With
        End Using
    Catch ex As Exception
        Return Nothing
    End Try
End Function

IDataReader并没有继承IEnumerable。至少在我的机器上,位于System.Data命名空间下的.NET 4 Framework标准库中的IDataReader并没有继承IEnumerable。 - Joshua Enfield
你是对的...不是继承....而是实现:http://msdn.microsoft.com/zh-cn/library/system.data.sqlclient.sqldatareader(v=vs.71).aspx - Kevin
1
我之前的想法是对的,但原因错误了。我道歉,我之前看错了接口IDataReader,而不是DataReader本身。实现和继承的问题实际上是无意中出现的小问题。 - Joshua Enfield

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接