将SqlDataReader的结果快速映射到对象的最快方法

34

我正在比较Dapper和ADO.NET之间的物化时间,并且Dapper通常比ADO.NET更快,尽管给定提取查询第一次执行的时间比ADO.NET慢。一些结果显示,Dapper比ADO.NET稍微快一点(几乎所有结果都表明它们是可比较的)。
因此,我认为我在将SqlDataReader的结果映射到对象方面使用了低效的方法。
这是我的代码

var sql = "SELECT * FROM Sales.SalesOrderHeader WHERE SalesOrderID = @Id";
        var conn = new SqlConnection(ConnectionString);
        var stopWatch = new Stopwatch();

        try
        {
            conn.Open();
            var sqlCmd = new SqlCommand(sql, conn);

            for (var i = 0; i < keys.GetLength(0); i++)
            {
                for (var r = 0; r < keys.GetLength(1); r++)
                {
                    stopWatch.Restart();
                    sqlCmd.Parameters.Clear();
                    sqlCmd.Parameters.AddWithValue("@Id", keys[i, r]);
                    var reader = await sqlCmd.ExecuteReaderAsync();
                    SalesOrderHeaderSQLserver salesOrderHeader = null;

                    while (await reader.ReadAsync())
                    {
                        salesOrderHeader = new SalesOrderHeaderSQLserver();
                        salesOrderHeader.SalesOrderId = (int)reader["SalesOrderId"];
                        salesOrderHeader.SalesOrderNumber = reader["SalesOrderNumber"] as string;
                        salesOrderHeader.AccountNumber = reader["AccountNumber"] as string;
                        salesOrderHeader.BillToAddressID = (int)reader["BillToAddressID"];
                        salesOrderHeader.TotalDue = (decimal)reader["TotalDue"];
                        salesOrderHeader.Comment = reader["Comment"] as string;
                        salesOrderHeader.DueDate = (DateTime)reader["DueDate"];
                        salesOrderHeader.CurrencyRateID = reader["CurrencyRateID"] as int?;
                        salesOrderHeader.CustomerID = (int)reader["CustomerID"];
                        salesOrderHeader.SalesPersonID = reader["SalesPersonID"] as int?;
                        salesOrderHeader.CreditCardApprovalCode = reader["CreditCardApprovalCode"] as string;
                        salesOrderHeader.ShipDate = reader["ShipDate"] as DateTime?;
                        salesOrderHeader.Freight = (decimal)reader["Freight"];
                        salesOrderHeader.ModifiedDate = (DateTime)reader["ModifiedDate"];
                        salesOrderHeader.OrderDate = (DateTime)reader["OrderDate"];
                        salesOrderHeader.TerritoryID = reader["TerritoryID"] as int?;
                        salesOrderHeader.CreditCardID = reader["CreditCardID"] as int?;
                        salesOrderHeader.OnlineOrderFlag = (bool)reader["OnlineOrderFlag"];
                        salesOrderHeader.PurchaseOrderNumber = reader["PurchaseOrderNumber"] as string;
                        salesOrderHeader.RevisionNumber = (byte)reader["RevisionNumber"];
                        salesOrderHeader.Rowguid = (Guid)reader["Rowguid"];
                        salesOrderHeader.ShipMethodID = (int)reader["ShipMethodID"];
                        salesOrderHeader.ShipToAddressID = (int)reader["ShipToAddressID"];
                        salesOrderHeader.Status = (byte)reader["Status"];
                        salesOrderHeader.SubTotal = (decimal)reader["SubTotal"];
                        salesOrderHeader.TaxAmt = (decimal)reader["TaxAmt"];
                    }

                    stopWatch.Stop();
                    reader.Close();
                    await PrintTestFindByPKReport(stopWatch.ElapsedMilliseconds, salesOrderHeader.SalesOrderId.ToString());
                }
我在可空列中使用了as关键字进行强制类型转换,这样做是正确的吗?
而且这是Dapper的代码。
using (var conn = new SqlConnection(ConnectionString))
        {
            conn.Open();
            var stopWatch = new Stopwatch();

            for (var i = 0; i < keys.GetLength(0); i++)
            {
                for (var r = 0; r < keys.GetLength(1); r++)
                {
                    stopWatch.Restart();
                    var result = (await conn.QueryAsync<SalesOrderHeader>("SELECT * FROM Sales.SalesOrderHeader WHERE SalesOrderID = @Id", new { Id = keys[i, r] })).FirstOrDefault();
                    stopWatch.Stop();
                    await PrintTestFindByPKReport(stopWatch.ElapsedMilliseconds, result.ToString());
                }
            }
        }

如果您删除异步代码并进行比较,会发生什么? - Tim Schmelter
7
“最终,Dapper 往往比 ADO.NET 更快”这句话不合理;Dapper是建立在 ADO.NET 之上的;它不能比它所使用的东西更“快”,而且 ADO.NET 没有提供你需要的服务... 你能具体说明一下你的意思吗? - Marc Gravell
看起来你的主要问题是在进行大量查询;你考虑过使用INNER JOIN或多个SELECT(.QueryMultiple)来执行一次查询完成所有操作,而不是进行多次查询吗? - Marc Gravell
1
@MarcGravell 当我提出这个问题时,我实际上忘记了Dapper是建立在ADO.NET之上的。在我正确比较了所有测试结果(而不仅仅是浏览它们)之后,我必须承认我夸大了其作用,一些结果显示Dapper更快的最高时间差为9毫秒,几乎所有结果都表明它是可比较的,而其中一些结果则表明ADO.NET更快。非常抱歉浪费了您的时间。 - witoong623
13个回答

40

如果对于任何db或reflection方面有疑问,我会问自己:“Marc Gravell会怎么做?”

在这种情况下,他会使用FastMember!而你也应该这样做。它是Dapper中数据转换的基础,并且可以轻松地将您自己的DataReader映射到一个对象上(如果您不想使用Dapper)。

以下是将SqlDataReader转换为类型T的扩展方法:

请注意:此代码意味着依赖于FastMember,并且是为.NET Core编写的(但可以轻松转换为符合.NET Framework / Standard的代码)。

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

与反射相比,在我的测试中它要慢得多。如果您缓存成员,第二次运行时可能会获得更快的结果。 - manit
当然应该缓存访问器,否则每次都会编译和发出访问器。 - vivainio
Fastmember在内部执行此操作。但是,当然,您也可以这样做。这个答案并不意味着要作为最终解决方案,而是整体设计中的一个组成部分。 - pim

23

2022更新

现在我们有.NET 5和.NET 6可用,它们包括Source Generators——基于Roslyn的惊人功能,可以在编译时生成更多代码。这基本上是“AOT Reflection”(静态反射),允许您生成零开销、极速映射代码,这将彻底改变ORM世界。

现在回到问题上来——最快的映射IDataReader的方法是使用Source Generators。我们已经开始尝试这个功能,并且非常喜欢它。

这是一个我们正在开发的库,它正是这样做的(将DataReader映射到对象),随意“借鉴”一些代码示例:https://github.com/jitbit/MapDataReader

仍然100%有效的先前答案

最受欢迎的答案提到了@MarkGravel及其FastMember。但是,如果您已经使用Dapper,它也是他的组件之一,您可以像这样使用Dapper的GetRowParser

var parser = reader.GetRowParser<MyObject>(typeof(MyObject));

while (reader.Read())
{
    var myObject = parser(reader);
}

更加简便,适用于>1行:conn.Query<salesOrderHeader>(sql) - IngoB
1
当您不知道类型(泛型/反射)时,reader.GetRowParser(Type)非常适合读取到多个对象中。 - Matt R
在一个类中是否有可能只解析一次某个内容,然后将内部实现缓存起来? - Maulik Modi
@MaulikModi 这就是 Dapper 内部工作的方式,它会缓存预编译表达式。 - Alex from Jitbit
我非常想使用这个解决方案,但是当我在Postgres中尝试使用INSERT INTO RETURNING *;查询时,解析器似乎会忽略除第一个字段以外的所有字段。 - Kyle L.

15

以下是使ADO.NET代码更快的方法。

在进行查询时,请列出您选择的字段,而不是使用select *。这将让您确保即使数据库中的顺序改变,也可以按顺序获取字段。然后,在从Reader中获取这些字段时,通过索引而不是名称获取它们。使用索引更快。

此外,我建议除非有充分的业务理由,否则不要将字符串数据库字段设置为可为空。然后,如果没有值,请在数据库中存储一个空字符串。最后,我建议使用DataReader上的Get方法以按其类型获取字段,这样您的代码中就不需要进行强制转换。因此,例如,不要将DataReader[index++]值强制转换为int类型,而是使用DataReader.GetInt(index++)

因此,例如,这段代码:

 salesOrderHeader = new SalesOrderHeaderSQLserver();
 salesOrderHeader.SalesOrderId = (int)reader["SalesOrderId"];
 salesOrderHeader.SalesOrderNumber =       reader["SalesOrderNumber"] as string;
 salesOrderHeader.AccountNumber = reader["AccountNumber"] as string;

变成

 int index = 0;
 salesOrderHeader = new SalesOrderHeaderSQLserver();
 salesOrderHeader.SalesOrderId = reader.GetInt(index++);
 salesOrderHeader.SalesOrderNumber = reader.GetString(index++);
 salesOrderHeader.AccountNumber = reader.GetString(index++);

试一下,看看对你有什么帮助。


谢谢,虽然我懒得列出所有列名并使用 GetXXX 方法来获取位置 XD 的值,但当我将其映射到属性时容易出错。不过,如果更快的话,我会尝试。你能解释一下为什么字符串字段不应该是可空的吗? - witoong623
也许只有在需要高性能代码的情况下才值得这样做。创建一个可以为您编写这些代码块的代码生成器是解决懒惰问题的一种方法,但遗憾的是,这也需要工作 :-) - RonC
1
只是报告测试结果,通过列的位置获取值比通过列名获取值更快,在我的情况下,快了1毫秒。然而,这证明ADO.NET并不比Dapper慢,而Dapper在性能方面与ADO.NET相当。 - witoong623
基于序数的方法的缺点是,现在查询中的列有一个紧密耦合的顺序,并且它们的使用方式也很紧密。很容易让一个列表与另一个列表不同步,并且难以直观地映射这两个列表。我很好奇是否有一种清晰的方法可以在遍历之前将索引映射到列名,以便您可以获得索引查找的性能和列名查找的可读性。 - bingles
@bingles - 你与你在select语句中指定的列的顺序紧密耦合,而不是它们在数据库中的顺序。但是,索引访问与字段名访问之间的性能差异并不大。当然,索引访问是访问字段的最快方式。如果您通过名称访问字段,则需要花费额外的时间来定位该字段。我不会太担心按名称访问的成本,但是对于少量字段,通过索引很容易访问。 - RonC
@bingles 为了提高可读性,你可以使用常量。继续以上面的例子:
const int SalesOrderId = 0; salesOrderHeader.SalesOrderId = reader.GetInt(SalesOrderId); ...
- SteveB

9

修改@HouseCat的解决方案使其不区分大小写:

    /// <summary>
    /// Maps a SqlDataReader record to an object. Ignoring case.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="dataReader"></param>
    /// <param name="newObject"></param>
    /// <remarks>https://dev59.com/hlgR5IYBdhLWcg3wqOxW#52918088</remarks>
    public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
    {
        if (newObject == null) throw new ArgumentNullException(nameof(newObject));

        // Fast Member Usage
        var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
        var propertiesHashSet =
                objectMemberAccessor
                .GetMembers()
                .Select(mp => mp.Name)
                .ToHashSet(StringComparer.InvariantCultureIgnoreCase);

        for (int i = 0; i < dataReader.FieldCount; i++)
        {
            var name = propertiesHashSet.FirstOrDefault(a => a.Equals(dataReader.GetName(i), StringComparison.InvariantCultureIgnoreCase));
            if (!String.IsNullOrEmpty(name))
            {
                objectMemberAccessor[newObject, name]
                    = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
            }
        }
    }

编辑:这对于List<T>或结果中的多个表格不起作用。

编辑2:将调用函数更改为此可处理列表。无论如何,我将返回对象列表,并获取第一个索引(如果我期望单个对象)。我还没有研究过多个表格,但我会尝试。

    public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
    {
        if (newObject == null) throw new ArgumentNullException(nameof(newObject));

        // Fast Member Usage
        var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
        var propertiesHashSet =
                objectMemberAccessor
                .GetMembers()
                .Select(mp => mp.Name)
                .ToHashSet(StringComparer.InvariantCultureIgnoreCase);

        for (int i = 0; i < dataReader.FieldCount; i++)
        {
            var name = propertiesHashSet.FirstOrDefault(a => a.Equals(dataReader.GetName(i), StringComparison.InvariantCultureIgnoreCase));
            if (!String.IsNullOrEmpty(name))
            {
                //Attention! if you are getting errors here, then double check that your model and sql have matching types for the field name.
                //Check api.log for error message!
                objectMemberAccessor[newObject, name]
                    = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
            }
        }
    }

编辑3:更新以显示示例调用函数。

    public async Task<List<T>> ExecuteReaderAsync<T>(string storedProcedureName, SqlParameter[] sqlParameters = null) where T : class, new()
    {
        var newListObject = new List<T>();

        using (var conn = new SqlConnection(_connectionString))
        {
            using (SqlCommand sqlCommand = GetSqlCommand(conn, storedProcedureName, sqlParameters))
            {
                await conn.OpenAsync();
                using (var dataReader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.Default))
                {
                    if (dataReader.HasRows)
                    {
                        while (await dataReader.ReadAsync())
                        {
                            var newObject = new T();
                            dataReader.MapDataToObject(newObject);
                            newListObject.Add(newObject);
                        }
                    }
                }
            }
        }

        return newListObject;
    }

1
@HouseCat 谢谢,我已经在多个API上使用这个函数将近2年了。唯一的问题是当添加新对象时,数据库和模型之间偶尔会出现类型不匹配的情况,但我通过一个花哨的注释来解决了这个问题。 - Soenhay

8
pimbrouwers' answer中采用了方法并进行了轻微优化。减少了LINQ调用。
仅映射在对象和数据字段名称中都存在的属性。处理DBNull。其他假设是您的领域模型属性绝对等于表列/字段名称。
/// <summary>
/// Maps a SqlDataReader record to an object.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dataReader"></param>
/// <param name="newObject"></param>
public static void MapDataToObject<T>(this SqlDataReader dataReader, T newObject)
{
    if (newObject == null) throw new ArgumentNullException(nameof(newObject));

    // Fast Member Usage
    var objectMemberAccessor = TypeAccessor.Create(newObject.GetType());
    var propertiesHashSet =
            objectMemberAccessor
            .GetMembers()
            .Select(mp => mp.Name)
            .ToHashSet();

    for (int i = 0; i < dataReader.FieldCount; i++)
    {
        if (propertiesHashSet.Contains(dataReader.GetName(i)))
        {
            objectMemberAccessor[newObject, dataReader.GetName(i)]
                = dataReader.IsDBNull(i) ? null : dataReader.GetValue(i);
        }
    }
}

示例用法:

public async Task<T> GetAsync<T>(string storedProcedureName, SqlParameter[] sqlParameters = null) where T : class, new()
{
    using (var conn = new SqlConnection(_connString))
    {
        var sqlCommand = await GetSqlCommandAsync(storedProcedureName, conn, sqlParameters);
        var dataReader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.CloseConnection);

        if (dataReader.HasRows)
        {
            var newObject = new T();

            if (await dataReader.ReadAsync())
            { dataReader.MapDataToObject(newObject); }

            return newObject;
        }
        else
        { return null; }
    }
}

此版本区分大小写,Oracle始终返回列名大写。 - Carlos ABS
2
这是正确的。属性/列必须匹配并且区分大小写。但是,您可以将objectMemberAccessor键修改为字符串函数ToUpper() / ToLower()。如果我以后做Attributes的版本,我会修改这个解决方案。 - HouseCat

7
也许我将要介绍的方法并不是最有效的,但只需非常少的编程工作即可完成任务。我认为这里的主要优点在于,您不必处理除构建兼容(可映射)对象之外的数据结构。
如果您将SqlDataReader转换为DataTable,然后使用JsonConvert.SerializeObject进行序列化,您可以使用JsonConvert.DeserializeObject将其反序列化为已知对象类型。
以下是实现示例:
        SqlDataReader reader = null;
        SqlConnection myConnection = new SqlConnection();
        myConnection.ConnectionString = ConfigurationManager.ConnectionStrings["DatabaseConnection"].ConnectionString;
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.CommandType = CommandType.Text;
        sqlCmd.CommandText = "SELECT * FROM MyTable";
        sqlCmd.Connection = myConnection;
        myConnection.Open();
        reader = sqlCmd.ExecuteReader();

        var dataTable = new DataTable();
        dataTable.Load(reader);

        List<MyObject> myObjects = new List<MyObject>();

        if (dataTable.Rows.Count > 0)
        {
            var serializedMyObjects = JsonConvert.SerializeObject(dataTable);
            // Here you get the object
            myObjects = (List<MyObject>)JsonConvert.DeserializeObject(serializedMyObjects, typeof(List<MyObject>));
        }

        myConnection.Close();

棘手!我喜欢它。 - infocyde
DataTable与DataReader相比较慢。如果您在应用程序中使用DataTable作为数据模型,则它是更快的方法。 - manit
有同样的想法,但我来到这里是因为我认为一定有更好的方法。然后我看到有人实现了我的第一个想法。 :-) 虽然它不是最有效的,而且如果你期望大的结果集可能是个坏主意,但对于小结果集来说,它提供了最大的灵活性和最小的工作量。太好了。 - Steve In CO

6

您可以使用命令Install-Package DbDataReaderMapper或者使用IDE的软件包管理器来安装DbDataReaderMapper包。

然后,您可以创建您的数据访问对象(这里提供一个比您提供的更短的示例):

class EmployeeDao
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? Age { get; set; }
}

为了进行自动映射,您可以调用扩展方法MapToObject<T>()
var reader = await sqlCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
    var employeeObj = reader.MapToObject<EmployeeDao>();
}

这样做可以摆脱数十行难以阅读且难以维护的代码。

以下是一步步的示例: https://github.com/LucaMozzo/DbDataReaderMapper


5
List<T> result = new List<T>();
SqlDataReader reader = com.ExecuteReader();

while(reader.Read())
{                        
    Type type = typeof(T);
    T obj = (T)Activator.CreateInstance(type);
    PropertyInfo[] properties = type.GetProperties();

    foreach (PropertyInfo property in properties)
    {
        try
        {
            var value = reader[property.Name];
            if (value != null)
                property.SetValue(obj, Convert.ChangeType(value.ToString(), property.PropertyType));
        }
        catch{}                            
    }
    result.Add(obj);
}

建议:添加 "if (reader.HasRows) {}" 块,并将 "while(reader.Read())" 块放在其中,将语句 "Type type" 和 "PropertyInfo[] properties" 放在 "while(reader.Read())" 块上方。 - Bill Roberts

1
我们使用以下类来执行SQL查询并自动将行映射到对象。您可以轻松调整该类以适应您的需求。请注意,我们的方法依赖于FastMember,但您可以轻松修改代码以使用反射。
/// <summary>
/// Mapping configuration for a specific sql table to a specific class.
/// </summary>
/// <param name="Accessor">Used to access the target class properties.</param>
/// <param name="PropToRowIdxDict">Target class property name -> database reader row idx dictionary.</param>
internal record RowMapper(TypeAccessor Accessor, IDictionary<string, int> PropToRowIdxDict);

public class RawSqlHelperService
{
  /// <summary>
  /// Create a new mapper for the conversion of a <see cref="DbDataReader"/> row -> <typeparamref name="T"/>.
  /// </summary>
  /// <typeparam name="T">Target class to use.</typeparam>
  /// <param name="reader">Data reader to obtain column information from.</param>
  /// <returns>Row mapper object for <see cref="DbDataReader"/> row -> <typeparamref name="T"/>.</returns>
  private RowMapper GetRowMapper<T>(DbDataReader reader) where T : class, new()
  {
    var accessor = TypeAccessor.Create(typeof(T));
    var members = accessor.GetMembers();

    // Column name -> column idx dict
    var columnIdxDict = Enumerable.Range(0, reader.FieldCount).ToDictionary(idx => reader.GetName(idx), idx => idx);
    var propToRowIdxDict = members
      .Where(m => m.GetAttribute(typeof(NotMappedAttribute), false) == null)
      .Select(m =>
      {
        var columnAttr = m.GetAttribute(typeof(ColumnAttribute), false) as ColumnAttribute;
        var columnName = columnAttr == null
          ? m.Name
          : columnAttr.Name;

        return (PropertyName: m.Name, ColumnName: columnName);
      })
      .ToDictionary(x => x.PropertyName, x => columnIdxDict[x.ColumnName]);

    return new RowMapper(accessor, propToRowIdxDict);
  }

  /// <summary>
  /// Read <see cref="DbDataReader"/> current row as object <typeparamref name="T"/>.
  /// </summary>
  /// <typeparam name="T">The class to map to.</typeparam>
  /// <param name="reader">Data reader to read the current row from.</param>
  /// <param name="mapper">Mapping configuration to use to perform the mapping operation.</param>
  /// <returns>Resulting object of the mapping operation.</returns>
  private T ReadRowAsObject<T>(DbDataReader reader, RowMapper mapper) where T : class, new()
  {
    var (accessor, propToRowIdxDict) = mapper;
    var t = new T();

    foreach (var (propertyName, columnIdx) in propToRowIdxDict)
      accessor[t, propertyName] = reader.GetValue(columnIdx);

    return t;
  }

  /// <summary>
  /// Execute the specified <paramref name="sql"/> query and automatically map the resulting rows to <typeparamref name="T"/>.
  /// </summary>
  /// <typeparam name="T">Target class to map to.</typeparam>
  /// <param name="dbContext">Database context to perform the operation on.</param>
  /// <param name="sql">SQL query to execute.</param>
  /// <param name="parameters">Additional list of parameters to use for the query.</param>
  /// <returns>Result of the SQL query mapped to a list of <typeparamref name="T"/>.</returns>
  public async Task<IEnumerable<T>> ExecuteSql<T>(DbContext dbContext, string sql, IEnumerable<DbParameter> parameters = null) where T : class, new()
  {
    var con = dbContext.Database.GetDbConnection();
    await con.OpenAsync();

    var cmd = con.CreateCommand() as OracleCommand;
    cmd.BindByName = true;
    cmd.CommandText = sql;
    cmd.Parameters.AddRange(parameters?.ToArray() ?? new DbParameter[0]);

    var reader = await cmd.ExecuteReaderAsync();
    var records = new List<T>();
    var mapper = GetRowMapper<T>(reader);

    while (await reader.ReadAsync())
    {
      records.Add(ReadRowAsObject<T>(reader, mapper));
    }

    await con.CloseAsync();

    return records;
  }
}

支持的映射属性

我实现了对实体框架使用的NotMappedColumn属性的支持。

NotMapped 属性

使用此属性装饰的属性将被映射器忽略。

Column 属性

使用此属性可以自定义列名。如果没有使用此属性,则假定属性名为列名。

示例类

private class Test
{
  [Column("SDAT")]
  public DateTime StartDate { get; set; } // Column name = "SDAT"
  public DateTime EDAT { get; set; } // Column name = "EDAT"
  [NotMapped]
  public int IWillBeIgnored { get; set; }
}

与反射的比较

我还将使用FastMember方法与使用普通反射进行了比较。

为了进行比较,我从一个包含1000000行的表中查询了两个日期列,以下是结果:

方法 持续时间(秒)
FastMember 约1.6秒
反射 约2秒

感谢用户pim的启发。


1

我采用了pimbrouwers和HouseCat的答案,并做了修改。在我的情况下,数据库中的列名采用蛇形命名法。

public static T ConvertToObject<T>(string query) where T : class, new()
    {
        using (var conn = new SqlConnection(AutoConfig.ConnectionString))
        {
            conn.Open();
            var cmd = new SqlCommand(query) {Connection = conn};
            var rd = cmd.ExecuteReader();
            var mappedObject = new T();

            if (!rd.HasRows) return mappedObject;
            var accessor = TypeAccessor.Create(typeof(T));
            var members = accessor.GetMembers();
            if (!rd.Read()) return mappedObject;
            for (var i = 0; i < rd.FieldCount; i++)
            {
                var columnNameFromDataTable = rd.GetName(i);
                var columnValueFromDataTable = rd.GetValue(i);

                var splits = columnNameFromDataTable.Split('_');
                var columnName = new StringBuilder("");
                foreach (var split in splits)
                {
                    columnName.Append(CultureInfo.InvariantCulture.TextInfo.ToTitleCase(split.ToLower()));
                }

                var mappedColumnName = members.FirstOrDefault(x =>
                    string.Equals(x.Name, columnName.ToString(), StringComparison.OrdinalIgnoreCase));

                if(mappedColumnName == null) continue;
                var columnType = mappedColumnName.Type;

                if (columnValueFromDataTable != DBNull.Value)
                {
                    accessor[mappedObject, columnName.ToString()] = Convert.ChangeType(columnValueFromDataTable, columnType);
                }
            }

            return mappedObject;
        }
    }

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