Inicio > C# > Automatizar lectura de un SqlDataReader en C#

Automatizar lectura de un SqlDataReader en C#

Buenos días a todos,

en esta ocasión, traigo algo que a más de uno le puede interesar.

Se trata de una clase c# que nos va a ayudar a ejecutar procedimientos almacenados Sql, sobre todo, procedimientos que nos devuelvan conjuntos de filas y mapearlas a objetos de clases definidas previamente.

Por ejemplo, tengo un procedimiento almacenado que tiene la siguiente instrucción


SELECT clave, descripcion, fechaCreacion, clienteID,
fechaEntrega, folioInicial, importe, estatus, esPermanente
FROM remision
WHERE fechaCreacion BETWEEN @fechaInicio AND @fechaFinal

El codigo puesto arriba es completamente ficticio, lo pongo sólo para ilustrar el ejemplo.

Continuamos, deseo tener una lista de objetos mapeado al resultado de la consulta anterior. Bien, la definición de mi clase sería más o menos así:


public class Remision
{
public string Clave { get; set; }
public string Descripcion { get; set; }
public DateTime FechaCreacion { get; set; }
public int ClienteID { get; set; }
public DateTime FechaEntrega { get; set; }
public int FolioInicial { get; set; }
public Decimal Importe { get; set; }
public string Estatus { get; set; }
public bool EsPermanente { get; set; }
}

Y el código para obtener mi lista de objetos sería más o menos así (No voy a poner bloques try-catch porque es sólo ilustrativo. Es media noche 😉 ):


SqlConnection cnn = new SqlConnection(cadenaConexion);
cnn.Open();
SqlCommand comando = cnn.CreateCommand();
comando.CommandType = CommandType.StoredProcedure;
comando.CommandText = "procedimiento_a_ejecutar";
comando.Parameters.AddWithValue("@fechaInicio", valorFechaInicio);
comando.Parameters.AddWithValue("@fechaFinal", valorFechaFinal);
SqlDataReader dr = comando.ExecuteReader();

// Esta es mi lista de objetos final
List<Remision> remisiones = new List<Remision>();

while (consulta.Read())
{
// Se hace el fetch de cada campo
Remision r = new Remision();

r.Clave = dr["clave"].ToString();
r.Descripcion = dr["descripcion"].ToString();
r.FechaCreacion = dr.GetDateTime("fechaCreacion");
r.ClienteID = dr.GetInt32("clienteID");
r.FechaEntrega = dr.GetDateTime("fechaEntrega");
r.FolioInicial = dr.GetInt32("folioInicial");
r.Importe = dr.GetDecimal("importe");
r.Estatus = dr.GetString("estatus");
r.EsPermanente = dr.GetBoolean("espermanente");

// Se agrega el objeto a la lista
remisiones.Add(r);
}

consulta.Close();

Uuuf, menos mal que sólo fueron esos campos. Al código anterior súmenle validaciones y demás cosas.

Lo anterior es muy simple, recorrer el SqlDataReader y llenar cada uno de los objetos y ponerlos en la lista.

Las cosas se complican cuando regresamos muchos más campos en la consulta del procedimiento almacenado.

Bueno, al objetivo de esta entrada.

El objetivo es reemplazar el código anterior, por un código mucho más simple. A continuación pongo la llamada final:


SqlConnection cnn = new SqlConnection(conexionString);
cnn.Open();
SqlHelper helper = new SqlHelper(cnn, null);

var lista = helper.ExecuteSqlProc<Remision>("procedimiento_a_ejecutar",
new SqlParameter("@fechaInicio", valorFechaInicio),
new SqlParameter("@fechaFinal", valorFechaFinal));

// En la variable "lista", tenemos la lista de objeto Remision.
return lista;

Hemos reducido muchísimo el código utilizado para recorrer un SqlDataReader y crear una lista de objetos a partir de este.

Ahora, como logramos esto, bien, pues en seguido pongo el código de nuestra clase SqlHelper.


public class SqlHelper
{
private SqlConnection _cn = null;
private SqlTransaction _tran = null;
public SqlHelper(SqlConnection cnn, SqlTransaction tran)
{
this._cn = cnn;
this._tran = tran;
}

public List<T> ExecuteList<T>(string procName, params SqlParameter[] parametros)
{
SqlDataReader dr = null;
try
{
SqlCommand comando = this._cn.CreateCommand();
if (this._tran != null)
comando.Transaction = this._tran;

comando.CommandType = CommandType.StoredProcedure;
comando.CommandText = procName;

if (parametros != null)
{
foreach (SqlParameter p in parametros)
comando.Parameters.Add(p);
}

dr = comando.ExecuteReader();

// Se invoca el método para obtener la lista de propiedades del tipo de objeto indicado
// enviándole como parámetro una instancia del tipo genérico
object objecto = Activator.CreateInstance<T>();

// Obtiene una lista de campos que han hecho match
List<string> camposMatch = GetPropertyMatch(objecto, dr);

// Se itera el darareader
// y se leen los campos que han hecho match
List<T> listaARegresar = new List<T>();
while (dr.Read())
{
// Se crea una nueva instanacia en cada iteración
objecto = Activator.CreateInstance<T>();
foreach (string s in camposMatch)
{
PropertyInfo p = objecto.GetType().GetProperty(s);
if (!dr.IsDBNull(dr.GetOrdinal(s)))
{
switch (p.PropertyType.Name.ToLower())
{
case "boolean":
p.SetValue(objecto, dr.GetBoolean(dr.GetOrdinal(s)), null);
break;
case "byte":
p.SetValue(objecto, dr.GetByte(dr.GetOrdinal(s)), null);
break;
case "char":
p.SetValue(objecto, dr.GetString(dr.GetOrdinal(s)), null);
break;
case "decimal":
p.SetValue(objecto, dr.GetDecimal(dr.GetOrdinal(s)), null);
break;
case "double":
p.SetValue(objecto, dr.GetDouble(dr.GetOrdinal(s)), null);
break;
case "single": // float
p.SetValue(objecto, dr.GetFloat(dr.GetOrdinal(s)), null);
break;
case "int32":
p.SetValue(objecto, dr.GetInt32(dr.GetOrdinal(s)), null);
break;
case "int64":
p.SetValue(objecto, dr.GetInt64(dr.GetOrdinal(s)), null);
break;
case "sbyte":
p.SetValue(objecto, dr.GetByte(dr.GetOrdinal(s)), null);
break;
case "int16":
p.SetValue(objecto, dr.GetInt16(dr.GetOrdinal(s)), null);
break;
case "uint32":
p.SetValue(objecto, dr.GetInt32(dr.GetOrdinal(s)), null);
break;
case "uint64":
p.SetValue(objecto, dr.GetInt64(dr.GetOrdinal(s)), null);
break;
case "uint16":
p.SetValue(objecto, dr.GetInt16(dr.GetOrdinal(s)), null);
break;
case "string":
p.SetValue(objecto, dr[s].ToString(), null);
break;
case "datetime":
p.SetValue(objecto, dr.GetDateTime(dr.GetOrdinal(s)), null);
break;
case "timespan":
p.SetValue(objecto, dr.GetDateTime(dr.GetOrdinal(s)), null);
break;
}
//if (p.PropertyType.Name.ToLower() == "string")
//    p.SetValue(objecto, dr.GetValue(dr.GetOrdinal(s)).ToString(), null);
//else
//    p.SetValue(objecto, dr.GetValue(dr.GetOrdinal(s)), null);
}
}
listaARegresar.Add((T)objecto);
}
dr.Close();

return listaARegresar;
}
catch (Exception ex)
{
throw;
}
finally
{
if (dr != null && !dr.IsClosed)
dr.Close();

dr = null;
}
}

public object ExecuteScalar(string procName, params SqlParameter[] parametros)
{
try
{
SqlCommand comando = this._cn.CreateCommand();
if (this._tran != null)
comando.Transaction = this._tran;
comando.CommandType = CommandType.StoredProcedure;
comando.CommandText = procName;
if (parametros != null)
{
foreach (SqlParameter p in parametros)
comando.Parameters.Add(p);
}

object resp = comando.ExecuteScalar();

return resp;
}
catch (Exception ex)
{
throw;
}
finally
{
}
}

/// <summary>
/// Regresa el número de filas afectadas en la ejecución
/// </summary>
/// <param name="procName"></param>
/// <param name="parametros"></param>
/// <returns></returns>
public int ExecuteNonQuery(string procName, params SqlParameter[] parametros)
{
try
{
SqlCommand comando = this._cn.CreateCommand();
if (this._tran != null)
comando.Transaction = this._tran;
comando.CommandType = CommandType.StoredProcedure;
comando.CommandText = procName;
if (parametros != null)
{
foreach (SqlParameter p in parametros)
comando.Parameters.Add(p);
}

int resp = comando.ExecuteNonQuery();

return resp;
}
catch (Exception ex)
{
throw;
}
finally
{
}
}

#region Metodos privados

private List<string> GetPropertyMatch(object objecto, SqlDataReader dr)
{
List<string> lProp = GetGenericPropertyList(objecto);

// Obtener una lista de nombres de campos del datareader
List<string> nombrescamposdr = new List<string>();
for (int i = 0; i < dr.FieldCount; i++)
{
nombrescamposdr.Add(dr.GetName(i));
}

// En este punto, ya tenemos las 2 listas, la lista de propiedades del objeto Genérico
// y la lista de campos que ha devuelto la ejecución de la consuta
// Ahora, hay que hacer un match, para saber cuáles serán leídas del datareader
List<string> camposMatch = new List<string>();
foreach (string s in lProp)
{
foreach (string s1 in nombrescamposdr)
{
if (s.ToLower() == s1.ToLower())
{
camposMatch.Add(s);
break;
}
}
}
return camposMatch;
}

private List<string> GetGenericPropertyList(object target)
{
List<string> result = new List<string>();

foreach (MemberInfo mi in target.GetType().GetMembers())
{
if (mi.MemberType == MemberTypes.Property)
{
PropertyInfo pi = mi as PropertyInfo;
if (pi != null)
{
result.Add(pi.Name);
}
}
}
return result;
}

#endregion
}

No voy a explicar el código porque las partes más importantes están comentadas.

La clase tienes 2 métodos auxiliares más, uno para ejecutar procedimientos escalares y uno más para ejecutar procedimientos que no devuelvan filas.

Bueno, ya para terminar, les dejo un bloque de código de un proyecto real, donde se ve claramente como ejecuto un procedimiento almacenado que me devuelve una lista de clientes por una zona, y me crea una lista de objetos utilizando la clase descrita anteriormente.


    SqlConnection cnn = new SqlConnection(cadenaConexion);
    cnn.Open();

    SqlHelper sqlHelper = new SqlHelper(cnn, null);
    
    List<Cliente> clientesList = sqlHelper.ExecuteList<Cliente>("spr_clientes_select_list",
                                                             new SqlParameter("@zonaId", 10));

    cnn.Close();

    // Mi variable clientesList contiene una lista de objetos de tipo Cliente que corresponden 
    // a la zonaId = 10

A esta clase ya le he hecho varias mejoras, como por ejemplo, la capacidad de poder leer los campos de tipo Byte[], teniendo sólo una propiedad de tipo Byte en la clase C#.

Si tienen alguna duda del funcionamiento, sólo escriban y trataré de responderla.

Espero que este código sea de mucha ayuda, ya que a mí me ha ayudado bastante.

Bueno, que estén bien y hasta la próxima.

Anuncios
  1. Angelbyte
    julio 21, 2013 en 10:01 am

    y el metodo ExecuteSqlProc???

    • julio 27, 2013 en 8:48 am

      Tienes razón.
      Modifiqué el código de la clase pero no modifiqué la entrada del artículo.
      El método ExecuteSqlProc, lo renombré a ExecuteList.

      Disculpa el descuido.

      Saludos.

  2. AngelByte
    julio 28, 2013 en 6:14 pm

    no hay problerma amigo, mas bien agradecerte por la info; podrias escribir tambien un articulo describiendo que tan recomendable es usar estos metodos en cuanto a performance. Gracias

    • julio 29, 2013 en 7:33 pm

      Qué tal Angel.
      En realidad no he hecho pruebas para medir el performance de la aplicación. A mi me ha funcionado bastante bien y nunca he tenido problemas de performance porque siempre tratamos de mostrar registros de manera paginada. Funciona muy bien.

      Te dejo un enlace donde se toca el tema del performance cuando se utiliza reflection(este es mi caso). Hay una entrada que cuenta que hicieron pruebas, y empezaron a notar problemas de performance cuando se iteraban más de 100,000 registros. Esto para mi caso es excelente, ya que no tengo el problema de mostrar tantos registros en una sóla consulta, al menos por ahora.

      El enlace es http://stackoverflow.com/questions/25458/how-costly-is-net-reflection . Revísalo y saca tus conclusiones.

      Saludos.

  1. No trackbacks yet.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

w

Conectando a %s

A %d blogueros les gusta esto: