【问题标题】:How can I easily convert DataReader to List<T>? [duplicate]如何轻松将 DataReader 转换为 List<T>? [复制]
【发布时间】:2009-09-23 09:14:58
【问题描述】:

我在 DataReader 中有数据,我想将其转换为 List&lt;T&gt;。 对此有什么可能的简单解决方案?

例如在 CustomerEntity 类中,我有 CustomerId 和 CustomerName 属性。如果我的 DataReader 将这两列作为数据返回,那么如何将其转换为 List&lt;CustomerEntity&gt;

【问题讨论】:

    标签: c# datareader generic-list


    【解决方案1】:

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

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

    然后,您可以根据需要使用 LINQ 的 ToList() 方法将其转换为 List&lt;T&gt;,如下所示:

    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();
    }
    

    我实际上建议将FromDataReader 方法放在Customer(或其他地方):

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

    那会离开:

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

    (我不认为类型推断在这种情况下会起作用,但我可能是错的......)

    【讨论】:

    • 扩展方法不应该是:while (reader.Read()) 而不是 while (drOutput.Read())
    • 优秀。特别是因为这种方法(稍作调整)也可以用于匿名类型,这大大简化了即席查询。
    • 注意这里和reader.Cast&lt;IDataReader&gt;().Select一样。
    • @Jon: DbDataRader 确实如此。 msdn.microsoft.com/en-us/library/…
    • @SLaks:是的,尽管 OP 从未提及正在使用哪种类型的 DataReader。不过,我个人非常喜欢扩展方法:)
    【解决方案2】:

    我用这个案例写了下面的方法。

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

    例如:T 是返回类型(ClassName),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
    

    【讨论】:

    • 实际上,您的 DataReaderMapToList 的内部结构可以很好地作为上面 Jon Skeets 答案的默认投影。
    • 这很好用。我对 C# 代码有一个小建议:将以 prop.SetValue 开头的行更改为 prop.SetValue(obj, Convert.ChangeType(dr[prop.Name], prop.PropertyType), null);。这将使代码适用于字符串以外的类型。
    • 如果列可以为空,这将引发尝试将Nullable&lt;&gt; 转换为类型的异常。这是stackoverflow.com/a/18015612/3956100的解决方案
    • 可能看起来很明显,但是 SELECT 命令中的字段名称必须与数据对象中的字段名称匹配
    【解决方案3】:

    我见过使用反射和属性或字段上的属性将 DataReader 映射到对象的系统。 (有点像 LinqToSql 所做的。)它们节省了一些输入,并且在为 DBNull 等编码时可以减少错误的数量。一旦你缓存了生成的代码,它们也可以比大多数手写代码更快,所以 如果您经常这样做,请考虑“大路”。

    请参阅"A Defense of Reflection in .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 or Linq to Sql, etc 但是在旧项目(或出于其他(有时有效)原因,例如“不是在这里发明”、“喜欢存储procs”等)并不总是可以使用 ORM,因此轻量级的系统对于“袖手旁观”很有用

    如果您每个人都需要编写大量 IDataReader 循环,您将看到减少编码(和错误)的好处无需更改您正在处理的系统的架构。这并不是说它是一个很好的架构。..

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


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

    【讨论】:

    • 这种方法的问题是一旦开始使用复合对象就会遇到很多麻烦。如果客户与公司相关联,则您需要公司属性。您可以递归,但 Company 可能具有 List 属性,然后您必须遍历图表。为此,您需要一个表示关联的属性。这就是 LINQ to SQL 和 Entity Framework 所做的,但它们是大型产品,您无法轻松开发内部解决方案。如果你打算这样做,为什么不使用 EF 呢?
    • 为什么我没有 AutoMap()
    • 我需要添加哪些引用才能使[Field(“id”)] 工作?
    • @Stilgar 对具有“非 EF 规则”的现有数据库使用 EF 是 使用 EF 的一个非常令人信服的原因 ..(不,此评论不是 6晚了几年,因为 EF 仍然存在与当时相同的问题)
    • 你在某个地方有 AutoMap 的实现吗?
    【解决方案4】:

    最简单的解决方案:

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

    【讨论】:

    • 我没有在 dt 的枚举器上找到 ToList。这段代码有效吗?
    • @coolcake:添加using System.Data;,它是一个扩展方法。
    • 这是一个很酷的解决方案 +1,但请记住 DataTable 解决方案带走了数据读取器的最大优势,即负载需求。 DataTable 首先将整个数据读取到内存中。顺便说一句,AsEnumerable 扩展方法在 System.Data.DataSetExtensions 程序集中(我必须说的程序集的名称很奇怪,听起来更像命名空间)。
    • 我在这里遗漏了什么吗?此解决方案不会产生List(Of {MyType}),它会产生List(Of DataRow),这与操作员请求的解决方案有很大不同。
    • 给他一个编程奥斯卡奖。
    【解决方案5】:

    我会(并且已经)开始使用Dapper。使用你的例子就像(从记忆中写的):

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

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

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

    查询返回IEnumerable,因此返回ToList()

    【讨论】:

    • 这是一个很棒的答案!!!我刚刚下载了 Dapper,它运行良好,为我节省了很多时间和头痛!谢谢
    【解决方案6】:

    显然@Ian Ringrose 的中心论点是您应该为此使用库是这里最好的单一答案(因此是 +1),但对于最少的一次性或演示代码,这里是@SLaks 的具体说明对@Jon Skeet 的更细化(+1'd)答案的微妙评论:

    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 ) ) )
    

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

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

    并用作:

                .Select( FromDataRecord )
    

    13 年 3 月 9 日更新:另见 Some excellent further subtle coding techniques to split out the boilerplate in this answer

    【讨论】:

      【解决方案7】:

      您不能简单(直接)将数据读取器转换为列表。

      你必须遍历datareader中的所有元素并插入到列表中

      示例代码下方

      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);
              }
      
      }
      

      【讨论】:

        【解决方案8】:

        我已经在一个宠物项目中介绍了这一点。 使用你想要的。

        请注意,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

        【讨论】:

        • 这不是和 Jon Skeet 的 Func&lt;IDataReader, T&gt; 方法完全一样吗?是的。并且通过引入您自己的ICommandIConnections,它更难在各种ADO.NET 提供程序之间进行互操作。我不明白为什么首先需要它。
        • 在你的ListExCommand 类中,如果我们必须手动提供映射器,为什么还要使用反射来绑定属性?在您的ListExAutoMap 类中,我们在哪里将IEnumerable&lt;T&gt; 传递给构造函数,因为我们只剩下IEnumerable (DbDataReader)。如果我们必须手动在其上添加foreach(例如reader.Cast&lt;IDataRecord&gt;()),那么这会使您在类中的内部foreach 循环变得多余,总体而言使这种方法非常缓慢。 +1 的努力..
        【解决方案9】:

        我知道这个问题很老,并且已经回答了,但是......

        既然 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 IDataReader 不继承 IEnumerable。 System.Data 命名空间中的 .NET 4 Framework 中的至少标准版本不在我的机器上。
        • 你是对的......不是继承......实现:msdn.microsoft.com/en-us/library/…
        • 我是对的,因为错误的原因,我很抱歉我查看的是 IDataReader 接口而不是 DataReader 本身。对工具和继承的挑剔实际上是无意的。
        猜你喜欢
        • 2020-10-24
        • 1970-01-01
        • 2016-09-28
        • 1970-01-01
        • 1970-01-01
        • 2015-09-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多