c# - executereader - Convertir filas de un lector de datos en resultados escritos
sqldatareader c# ejemplos (7)
Estoy usando una biblioteca de terceros que devuelve un lector de datos. Me gustaría una forma simple y lo más genérica posible para convertirla en una Lista de objetos.
Por ejemplo, digamos que tengo una clase ''Empleado'' con 2 propiedades EmployeeId y Nombre, me gustaría que el lector de datos (que contiene una lista de empleados) se convierta en Lista <Empleado>.
Supongo que no tengo más remedio que recorrer las filas del lector de datos y, para cada una de ellas, convertirlas en un objeto de Empleado que agregaré a la Lista. ¿Alguna solución mejor? Estoy utilizando C # 3.5 y, idealmente, me gustaría que fuera lo más genérico posible para que funcione con cualquier clase (los nombres de los campos en el DataReader coinciden con los nombres de las propiedades de los diversos objetos).
NOTA: Este es el código .NET Core
Una opción con un rendimiento estúpido, si no le importa una dependencia externa (el increíble paquete de nuget de Fast Member
):
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;
}
Usar:
public IEnumerable<T> GetResults<T>(SqlDataReader dr) where T : class, new()
{
while (dr.Read())
{
yield return dr.ConvertToObject<T>());
}
}
¿Realmente necesita una lista, o IEnumerable sería lo suficientemente bueno?
Sé que desea que sea genérico, pero un patrón mucho más común es tener un método de Fábrica estático en el tipo de objeto de destino que acepte un datarow (o IDataRecord). Eso se vería algo así:
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);
}
}
}
Entonces, si realmente necesita una lista en lugar de un IEnumerable, puede llamar a .ToList()
en los resultados. Supongo que también podría usar genéricos + un delegado para hacer que el código para este patrón también sea más reutilizable.
Actualización: vi esto de nuevo hoy y sentí como si escribiera el código genérico:
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);
Aunque no lo recomendaría para el código de producción, puede hacerlo de forma automática utilizando reflejo y genéricos:
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;
}
}
}
Luego puede usar CreateRecord<T>()
para crear una instancia de cualquier clase de los campos en un lector de datos.
<asp:GridView ID="GvEmps" runat="server" AutoGenerateColumns="true"></asp:GridView>
GvEmps.DataSource = Employee.GetEmployees();
GvEmps.DataBind();
Hemos implementado la siguiente solución y sentimos que funciona bastante bien. Es bastante simple y requiere un poco más de cableado que lo que haría un mapeador. Sin embargo, a veces es bueno tener el control manual y, honestamente, te conectas una vez y listo.
En pocas palabras: nuestros modelos de dominio implementan una interfaz que tiene un método que toma un IDataReader
y rellena las propiedades del modelo. Luego usamos Genéricos y Reflexión para crear una instancia del modelo y llamar al método Parse
en él.
Consideramos utilizar un constructor y pasarle IDataReader
, pero las verificaciones básicas de rendimiento que hicimos parecían sugerir que la interfaz era siempre más rápida (aunque solo un poco). Además, la ruta de la interfaz proporciona información instantánea a través de errores de compilación.
Una de las cosas que me gustan, es que puede utilizar private set
para propiedades como Age
en el siguiente ejemplo y establecerlas directamente desde la base de datos.
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;
}
}
}
}
}
}
Para llamarlo, simplemente proporcione el parámetro de tipo
IEnumerable<Foo> foos = DataLoader.GetRecords<Foo>(/* params */)
Para .NET Core 2.0:
Aquí hay un método de extensión que funciona con .NET CORE 2.0 para ejecutar RAW SQL y asignar resultados a LIST de tipos arbitrarios:
USO:
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;
}
}
}
}
Podrías construir un método de extensión como:
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;
}
y utilízalo como:
var employeeList = reader.ReadList(x => new Employee {
Name = x.GetString(0),
Age = x.GetInt32(1)
});
La sugerencia de Joel es buena. Puedes elegir devolver IEnumerable<T>
. Es fácil transformar el código anterior:
public static IEnumerable<T> GetEnumerator<T>(this IDataReader reader,
Func<IDataRecord, T> generator) {
while (reader.Read())
yield return generator(reader);
}
Si desea asignar automáticamente las columnas a las propiedades, la idea del código es la misma. Simplemente puede reemplazar la función del generator
en el código anterior con una función que interroga a typeof(T)
y establezca las propiedades en el objeto utilizando la reflexión leyendo la columna coincidente. Sin embargo, personalmente prefiero definir un método de fábrica (como el que se menciona en la respuesta de Joel) y pasar un delegado de él a esta función:
var list = dataReader.GetEnumerator(Employee.Create).ToList();
La solución más simple:
var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();
Luego selecciónalos para mapearlos a cualquier tipo.