将数据读取器中的行转换为类型化结果

34

我正在使用一个第三方库,它返回一个数据读取器。我想要一种简单且尽可能通用的方法将其转换为对象列表。
例如,假设我有一个名为“Employee”的类,其中包含两个属性“EmployeeId”和“Name”,我希望数据读取器(其中包含员工列表)被转换为List<Employee>。
我想我别无选择,只能遍历数据读取器中的行,并将每行转换为一个Employee对象,然后将其添加到列表中。是否有更好的解决方案?我正在使用C#3.5,并且理想情况下它应该尽可能通用,以便适用于任何类(DataReader中的字段名称与各个对象的属性名称匹配)。


该死,希望我现在能够靠近编译器,我很想写这段代码!如果没有其他人让我惊叹的话,明天我会试着写一下。再加一个问题。 - Matt Howells
@MattHowells 你仍然可以写它,个人认为如果有不同的东西会很棒。 - nawfal
可能是 https://dev59.com/UnM_5IYBdhLWcg3wPAjT 的重复问题。 - nawfal
12个回答

71

你真的需要一个列表吗?还是IEnumerable就足够了?

我知道你想要它是泛型的,但更常见的模式是在目标对象类型上有一个静态工厂方法,该方法接受一个数据行(或IDataRecord)。这会看起来像这样:

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }

    public static Employee Create(IDataRecord record)
    {
        return new Employee
        {
           Id = record["id"],
           Name = record["name"]
        };
    }
}

.

public IEnumerable<Employee> GetEmployees()
{
    using (var reader = YourLibraryFunction())
    {
       while (reader.Read())
       {
           yield return Employee.Create(reader);
       }
    }
}

如果你确实需要一个列表而不是IEnumerable,那么你可以在结果上调用.ToList()。我想你也可以使用泛型+委托使这种模式的代码更具可重用性。

更新:今天我又看到了这个问题,并想写出通用代码:

public IEnumerable<T> GetData<T>(IDataReader reader, Func<IDataRecord, T> BuildObject)
{
    try
    {
        while (reader.Read())
        {
            yield return BuildObject(reader);
        }
    }
    finally
    {
         reader.Dispose();
    }
}

//call it like this:
var result = GetData(YourLibraryFunction(), Employee.Create);

@user1325179 不,这完全正确。在记录被枚举到列表中时,BuildObject函数将会被调用, 数据读取器被销毁之前。即使没有发生这种情况,dispose仅销毁非托管资源。数据读取器使用的内存仍然存在,包括IDataRecord使用的字段的最终状态。 - Joel Coehoorn
@JoelCoehoorn,感谢您的耐心等待。我知道BuildObject将在Dispose之前被调用,但一旦ToList()被调用,所有的yield命令都将被调用,并且using语句将结束,从而处理SqlDataReader。但我想关键点是即使在SqlDataReader被处理后,其数据仍然可用。我以前不知道这一点,现在在其他地方也得到了证实。谢谢。那么,调用reader.Cast<System.Data.IDataRecord>().ToList()是一个好主意吗?还是有更好的方法将SqlDataReader数据转换为List<IDataRecord>? - user1325179
@user1325179 这里的关键是不应该生成一个List<IDataRecord>。BuildObject()函数需要将数据复制到一个新对象中。如果你不这样做,那么是的:这会失败。此外,在某个地方有一个GetEnumerator()方法,使得这个方法几乎过时了。很多时候你可以直接使用它。 - Joel Coehoorn
让我们在聊天中继续这个讨论 - user1325179
1
@JoelCoehoorn,我认为,Id = record["id"]应该从object转换为int?否则会出现异常!! - Shekhar Pankaj
显示剩余11条评论

25
你可以编写一个扩展方法,例如:
public static List<T> ReadList<T>(this IDataReader reader, 
                                  Func<IDataRecord, T> generator) {
     var list = new List<T>();
     while (reader.Read())
         list.Add(generator(reader));
     return list;
}

然后像这样使用:

var employeeList = reader.ReadList(x => new Employee {
                                               Name = x.GetString(0),
                                               Age = x.GetInt32(1)
                                        });

Joel的建议很好。你可以选择返回IEnumerable<T>。将上述代码转换为这样很容易:

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

如果你想要自动将列映射到属性,代码的思路是相同的。你只需要用一个函数替换上面代码中的generator函数,这个新函数应该会调查typeof(T)并通过反射读取匹配的列,在对象上设置属性。不过,我个人更喜欢定义一个工厂方法(就像Joel的回答中提到的那样),并将它的代理传递给这个函数:

 var list = dataReader.GetEnumerator(Employee.Create).ToList();

非常好的答案。Linq 很酷。因为你终于能够读懂这样的代码并且欣赏它而感到自豪,这算是傲慢吗? - rp.
1
“0 票反对” 不应该存在!我已经投了赞成票。复制粘贴的魔鬼把我带到这里。 - rp.
当我复制并粘贴时,出现了“找不到类型或命名空间名称'T'”。我可能错过了一些明显的东西。有什么想法吗? - Anthony
为什么要使用生成器?问题指定属性映射到列。 - Matt Howells
Anthony: 哎呀,我忘记了泛型参数。已修复。 - Mehrdad Afshari
马特:我不理解你的陈述。你是指一些能够通过反射自动将列映射到属性的函数吗? - Mehrdad Afshari

3

虽然我不建议在生产代码中使用,但您可以使用反射和泛型自动完成此操作:

public static class DataRecordHelper
{
    public static void CreateRecord<T>(IDataRecord record, T myClass)
    {
        PropertyInfo[] propertyInfos = typeof(T).GetProperties();

        for (int i = 0; i < record.FieldCount; i++)
        {
            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                if (propertyInfo.Name == record.GetName(i))
                {
                    propertyInfo.SetValue(myClass, Convert.ChangeType(record.GetValue(i), record.GetFieldType(i)), null);
                    break;
                }
            }
        }
    }
}

public class Employee
{
    public int Id { get; set; }
    public string LastName { get; set; }
    public DateTime? BirthDate { get; set; }

    public static IDataReader GetEmployeesReader()
    {
        SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString);

        conn.Open();
        using (SqlCommand cmd = new SqlCommand("SELECT EmployeeID As Id, LastName, BirthDate FROM Employees"))
        {
            cmd.Connection = conn;
            return cmd.ExecuteReader(CommandBehavior.CloseConnection);
        }
    }

    public static IEnumerable GetEmployees()
    {
        IDataReader rdr = GetEmployeesReader();
        while (rdr.Read())
        {
            Employee emp = new Employee();
            DataRecordHelper.CreateRecord<Employee>(rdr, emp);

            yield return emp;
        }
    }
}

您可以使用 CreateRecord<T>() 根据数据读取器中的字段来实例化任何类。

<asp:GridView ID="GvEmps" runat="server" AutoGenerateColumns="true"></asp:GridView>

GvEmps.DataSource = Employee.GetEmployees();
GvEmps.DataBind();

你不会推荐它用于生产环境吗? - Anthony
1
因为它“感觉”不对。从数据读取器自动设置属性意味着您对错误检查的控制较少,并且使用反射是昂贵的。我觉得这种方法不够健壮,尽管它应该能工作。然而,如果您真的想要使用这种技术,那么最好考虑使用适当的ORM映射解决方案,例如LinqToSql、Entity Framework、nHibernate等。 - Dan Diplo
@DanDiplo 没错!这是大多数映射器喜欢做的事情,只有我们程序员知道它有多脆弱。我们应该考虑领域对象,元编程应该是最后的手段! - nawfal
在编程中,使用一些缓存来提高反射性能,如此所示这里。基本思路是不要在循环内部使用反射。 - nawfal
我不确定我赞同反射的易碎性。我在许多生产环境中使用它来开发应用程序,并取得了巨大的成功。当然,反射带来很大的开销,但缓存繁重工作的结果可以消除很多开销。就自动设置属性而言,我也不同意这里的观点。假设您使用泛型并传递对象,您可以使用反射来调用特定的构造函数等。 - pim

2

注意:这是.NET Core代码

一个性能非常出色的选项,如果你不介意使用外部依赖(优秀的Fast Member nuget包):

public static T ConvertToObject<T>(this SqlDataReader rd) where T : class, new()
{

    Type type = typeof(T);
    var accessor = TypeAccessor.Create(type);
    var members = accessor.GetMembers();
    var t = new T();

    for (int i = 0; i < rd.FieldCount; i++)
    {
        if (!rd.IsDBNull(i))
        {
            string fieldName = rd.GetName(i);

            if (members.Any(m => string.Equals(m.Name, fieldName, StringComparison.OrdinalIgnoreCase)))
            {
                accessor[t, fieldName] = rd.GetValue(i);
            }
        }
    }

    return t;
}

使用方法:
public IEnumerable<T> GetResults<T>(SqlDataReader dr) where T : class, new()
{
    while (dr.Read())
    {
        yield return dr.ConvertToObject<T>());
    }
}

2
我们已经实现了以下解决方案,并且觉得它运行得非常好。它相当简单,需要比映射器更多的布线。然而,有时手动控制很好,而且你只需一次布线就可以完成。
简而言之:我们的领域模型实现了一个接口,该接口具有一个方法,该方法接受一个IDataReader并从中填充模型属性。然后,我们使用泛型和反射来创建模型实例并调用其Parse方法。
我们考虑使用构造函数并将IDataReader传递给它,但是我们进行的基本性能检查似乎表明,接口始终更快(即使只有一点)。此外,接口路线通过编译错误提供即时反馈。
我喜欢的其中一件事情是,您可以在下面的示例中像Age属性一样利用private set,并直接从数据库设置它们。
public interface IDataReaderParser
{
    void Parse(IDataReader reader);
}

public class Foo : IDataReaderParser
{
    public string Name { get; set; }
    public int Age { get; private set; }

    public void Parse(IDataReader reader)
    {
        Name = reader["Name"] as string;
        Age = Convert.ToInt32(reader["Age"]);
    }
}

public class DataLoader
{
    public static IEnumerable<TEntity> GetRecords<TEntity>(string connectionStringName, string storedProcedureName, IEnumerable<SqlParameter> parameters = null)
                where TEntity : IDataReaderParser, new()
    {
        using (var sqlCommand = new SqlCommand(storedProcedureName, Connections.GetSqlConnection(connectionStringName)))
        {
            using (sqlCommand.Connection)
            {
                sqlCommand.CommandType = CommandType.StoredProcedure;
                AssignParameters(parameters, sqlCommand);
                sqlCommand.Connection.Open();

                using (var sqlDataReader = sqlCommand.ExecuteReader())
                {
                    while (sqlDataReader.Read())
                    {
                        //Create an instance and parse the reader to set the properties
                        var entity = new TEntity();
                        entity.Parse(sqlDataReader);
                        yield return entity;
                    }
                }
            }
        }
    }
}

要调用它,只需提供类型参数即可。

IEnumerable<Foo> foos = DataLoader.GetRecords<Foo>(/* params */)

1
我喜欢使用接口的解决方案。也许将new()用作T的where子句会更好。这样,您就不需要像Activator类那样进行任何反射操作。 - Sebi
@Sebi非常感谢你的绝妙建议!不知怎么回事,我忘记了泛型的这种能力!请注意,您需要将new()作为泛型约束添加进去。感谢您友善地表达建议,没有使用讽刺或点踩的方式! - Airn5475
@Aim5475 感谢您的回答,但它不是与这个非常相似吗?https://dev59.com/SnM_5IYBdhLWcg3w2XBg#1202973 - Anthony
1
@Anthony,我认为在仔细查看“更新”后,我同意你的观点,所以对那个解决方案不表示不敬。在我看来,我的方法更加简洁,因为我不需要将两个函数传递给“GetRecords”方法,只需要一个类型参数即可。#有多种方法可以达到同样的目的 - Airn5475
1
这个解决方案仍然有效!刚刚尝试了一下 .NET Core 3.1,与第一个答案中的两个函数相比,它更容易隔离和重用数据库和代码。 - Philip
@Airn5475 是否可以将异步/等待任务集成到同一解决方案中?这样做会有什么好处吗? - Philip

2

像魔术一样

我个人非常讨厌在构造函数中手动映射,也不喜欢自己写反射。因此,在这里提供另一种解决方案,感谢出色且广泛使用的Newtonsoft JSON库。

它只能在属性名称与数据读取器列名称完全匹配时才能工作,但对我们来说效果很好。

...假设您有一个名为“yourDataReader”的数据读取器...

        var dt = new DataTable();
        dt.Load(yourDataReader);
        // creates a json array of objects
        string json = Newtonsoft.Json.JsonConvert.SerializeObject(dt);
        // this is what you're looking for right??
        List<YourEntityType> list = 
Newtonsoft.Json.JsonConvert
.DeserializeObject<List<YourEntityType>>(json);

1
C#/.NET5 的最新版本提供了一个称为“源代码生成器”的新功能,我敦促大家去探索。简而言之,它允许您在编译时生成 C# 代码,这些代码将为您执行手动映射。它明显比任何反射(即使使用缓存和类似的速度技巧)快20-30倍,因为它实际上只是将属性分配给 IDataReader 字段。由于源代码生成器仍然有点复杂(它应该驻留在单独的程序集中,您还需要学习 Roslyn API 等),因此我无法在此处分享短代码片段,但我确信它是一个值得探索的很棒的功能,适用于 ORM 领域以及周围的所有人。我开始尝试源代码生成器并喜欢它:https://github.com/jitbit/MapDataReader 随意从我的存储库中“窃取”一些代码。

声明:我分享了链接到自己的 Github 存储库


0

最简单的解决方案:

var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();

然后选择它们,以便将它们映射到任何类型。


0

对于 .NET Core 2.0:

这里有一个扩展方法,可与 .NET CORE 2.0 一起使用,执行原始 SQL 并将结果映射到任意类型的列表中:

用法:

 var theViewModel = new List();
 string theQuery = @"SELECT * FROM dbo.Something";
 theViewModel = DataSQLHelper.ExecSQL(theQuery,_context);

 using Microsoft.EntityFrameworkCore;
 using System.Data;
 using System.Data.SqlClient;
 using System.Reflection;

public static List ExecSQL(string query, myDBcontext context)
 {
 using (context)
 {
 using (var command = context.Database.GetDbConnection().CreateCommand())
 {
 command.CommandText = query;
 command.CommandType = CommandType.Text;
 context.Database.OpenConnection();
                using (var result = command.ExecuteReader())
                {
                    List<T> list = new List<T>();
                    T obj = default(T);
                    while (result.Read())
                    {
                        obj = Activator.CreateInstance<T>();
                        foreach (PropertyInfo prop in obj.GetType().GetProperties())
                        {
                            if (!object.Equals(result[prop.Name], DBNull.Value))
                            {
                                prop.SetValue(obj, result[prop.Name], null);
                            }
                        }
                        list.Add(obj);
                    }
                    return list;

                }
            }
        }
    }

0

我的版本

用法:

var Q = await Reader.GetTable<DbRoom>("SELECT id, name FROM rooms");

PgRoom是一个

public class DbRoom
{
    [Column("id")]
    public int Id { get; set; }

    [Column("name")]
    public string Name { get; set; }
}

Reader.GetTable 包含:

            using (var R = await Q.ExecuteReaderAsync())
            {
                List<T> Result = new List<T>();

                Dictionary<int, PropertyInfo> Props = new Dictionary<int, PropertyInfo>();

                foreach (var p in typeof(T).GetProperties())
                {
                    for (int i = 0; i < R.FieldCount; i++)
                    {
                        if (p.GetCustomAttributes<ColumnAttribute>().FirstOrDefault(t => t.Name == R.GetName(i)) != null
                            && p.PropertyType == R.GetFieldType(i))
                        {
                            Props.Add(i, p);
                        }
                    }
                }

                while (await R.ReadAsync())
                {
                    T row = new T();

                    foreach (var kvp in Props)
                    {
                        kvp.Value.SetValue(row, R[kvp.Key]);
                    }

                    Result.Add(row);
                }

                return Result;
            }

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