sábado, 17 de diciembre de 2011

Utilizando Reflection para el Acceso a Datos

Hola a todos, pues hace tiempo programe una clase utilitaria para automatizar la transformación de objetos ADO.Net que contenian información recuperada de una fuente de datos a POCO (Plain Old CLR Objects) utilizando reflection. Pues sin nada que hacer este sábado por la noche, decidí escribir este post sobre este DataTransformer

Primero comentar que reflection es el proceso en el cual un programa puede modificar su propia estructura en tiempo de ejecución (gracias wikipedia). Bueno esto se lo puede entender de muchas formas, pero un ejemplo simple sería el de crear la instancia de una clase cualquiera en tiempo de ejecución, por ejemplo pasandole como parámetro el nombre de esta, tal vez es un poco abstracto comprender esto pero en wikipedia hay un ejemplo muy bueno, que prácticamente hace la creación de un objeto de la forma clásica y utilizando reflection:


//Without reflection
Foo foo = new Foo();
foo.Hello();

//With reflection
object foo = Activator.CreateInstance(null, "Foo");
foo.GetType().GetMethod("Hello").Invoke(foo, null);

Ahora, como podríamos aplicar esto para automatizar el acceso a datos?, primeramente, esto se aplica en caso que nuestra capa de acceso a datos no cuente con un ORM. Siempre se dará el caso que una empresa requiera que la capa de acceso a datos se realice utilizando Ado.net puro y no un ORM, de todos modos, si nuestra aplicación tiene una arquitectura por lo menos de tres capas, se realizará un mapeo manual de los objetos ado.net a nuestro POCO, por ejemplo:


 public List<Persona> GetPersonas() {
  SqlConnection conn = new SqlConnection(Config.GetDbString("mibase"));
  string sql = "SP_GetPersonas";
  SqlDataAdapter adapter = new SqlDataAdapter(sql, conn);
  
  DataTable personas = new DataTable();
  
  try {
   adapter.Fill(personas);
   List<Persona> result = new List<Persona>();
   foreach(DataRow row in personas.Rows) {
    Persona temp = new Persona();
    temp.Codigo = row["Codigo"].ToString();
    temp.Nombre = row["Nombre"].ToString();
    
    result.Add(temp);
   }
   
   return result;
  }
  catch(Exception ex) {
   throw ex;
  }
 }

Como se ve en el ejemplo, basicamente se esta haciendo un mapeo manual de una estructura DataTable a un objeto de tipo Persona, hasta aca todo simple, yo no me haria problema si tengo que hacer esto para 10 tablas, pero las estructuras empresariales pueden ser de decenas de decenas de tablas, sin incluir vistas. Entonces aca es donde entra reflection, para crear objetos dinámicamente.

Al siguiente clase como lo mencione arriba, realiza la transformación de una objeto Ado.net en un POCO, básicamente crea la instancia de un objeto que yo especifico, recorre sus propiedades y las compara con el nombre de las columnas de nuestro objeto Ado.net, si coinciden asigna el valor del objeto Ado.net a nuestro POCO.

Este es el código de DataTransformer:


public static class DataTransformer {
        /// <summary>
        /// Realiza la conversión de un DataTable a 
        /// una lista genérica de tipo TargetType
        /// </summary>
        /// <typeparam name="TargetType">Tipo destino de la lista</typeparam>
        /// <param name="table">Origen de datos</param>
        /// <returns>Lista genérica</returns>
        /// <exception cref="Exception: En caso que los tipos del origen de datos no coincidan 
        /// con los de TargetType"></exception>
        public static List<TargetType> DataTableToGenericList<TargetType>(DataTable table) {
            List<TargetType> resultList = new List<TargetType>();
            DataColumn temp;
            TargetType item;

            item = Activator.CreateInstance<TargetType>();
            PropertyInfo[] properties = item.GetType().GetProperties();

            foreach (DataRow row in table.Rows) {
                item = Activator.CreateInstance<TargetType>();
                foreach (PropertyInfo property in properties) {
                    temp = table.Columns[property.Name];
                    if (temp != null) {
                        if (temp.DataType.Name.Equals(property.PropertyType.Name)) {
                            property.SetValue(item, row[temp], null);
                        }
                        else {
                            throw new Exception("El tipo de la propiedad y columna son diferentes.");
                        }
                    }
                }
                resultList.Add(item);
            }
            return resultList;
        }
        /// <summary>
        /// Realiza la conversión de un IDataReader a 
        /// una lista genérica de tipo TargetType
        /// </summary>
        /// <typeparam name="TargetType">Tipo destino de la lista</typeparam>
        /// <param name="reader">Origen de datos</param>
        /// <returns>Lista genérica</returns>
        /// <exception cref="Exception: En caso que los tipos del origen de datos no coincidan 
        /// con los de TargetType"></exception>
        public static List<TargetType> DataReaderToGenericList<TargetType>(IDataReader reader) {
            List<TargetType> resultList = new List<TargetType>();
            object propertyValue;
            int columnNumber = reader.FieldCount;
            TargetType item;

            item = Activator.CreateInstance<TargetType>();
            PropertyInfo[] properties = item.GetType().GetProperties();

            while (reader.Read()) {
                item = Activator.CreateInstance<TargetType>();
                foreach (PropertyInfo property in properties) {
                    for (int i = 0; i < columnNumber; i++) {
                        if (reader.GetName(i).ToLower().Equals(property.Name.ToLower())) {
                            if (reader.GetFieldType(i).Name.Equals(property.PropertyType.Name)) {
                                propertyValue = reader.GetValue(i);
                                property.SetValue(item, propertyValue, null);
                                break;
                            }
                            else {
                                throw new Exception("El tipo de la propiedad y columna son diferentes.");
                            }
                        }
                    }
                }
                resultList.Add(item);
            }
            return resultList;
        }

        public static TargetType DataTableToObject<TargetType>(DataTable table) {
            List<TargetType> lista = DataTableToGenericList<TargetType>(table);
            if (lista.Count > 0)
                return lista[0];
            else
                return default(TargetType);
        }
    }

Analicemos un poco el método DataTableToGenericList. Primero notar que recibe como parámetro el objeto Ado.net del cual queremos transformar los datos, luego tenemos el tipo al cual queremos transformar, obviamente este tipo tiene que ser POCO. Luego se crea una lista de objetos del tipo que se nos da como parametro, luego obtiene en un arreglo de tipo PropertyInfo las propiedades de este objeto.

Finalmente recorre las filas del DataTable, y por cada fila, recorre el arreglo que contiene las propiedades y las compara con las columnas del DataTable y voila!!

Una vez que tenemos este transformador de datos, nuestro código para obtener una lista de personas, tendríamos lo siguiente:


 public List<Persona> GetPersonas() {
  SqlConnection conn = new SqlConnection(Config.GetDbString("mibase"));
  string sql = "SP_GetPersonas";
  SqlDataAdapter adapter = new SqlDataAdapter(sql, conn);
  
  DataTable personas = new DataTable();
  
  try {
   adapter.Fill(personas);
   return DataTransformer.DataTableToGenericList<Persona>(personas);
   
   return result;
  }
  catch(Exception ex) {
   throw ex;
  }
 }

Y como pueden observar, ya no es necesario mapear los objetos manualmente.

Bueno, eso es todo para esta entrada, ya me cayo el sueño, espero les sea de utilidad.

Saludos

3 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. Ahora esta mas claro para mi a que te referias con usar reflection para obtener objetos a partir de consultas, gracias!!

    ResponderEliminar
  3. Muy interesante!!, desarrolle algo similar solo que el match con POCO es con DataReder tambien. trate de hacer un método genérico para esta actividad una capa mas abajo, por lo que para crear una lista uso algo así:

    List lBanner = new spMiniBanner().Exe();

    ResponderEliminar