Entrando en materia
Situémonos en el siguiente escenario: Nuestra aplicación tiene que imprimir la información de una figura determinada que se encuentra almacenada en una base de datos. Supongamos que existen varios tipos de figuras, y nuestro procedimiento almacenado retornará las columnas adecuadas en dependencia del tipo de figura que se quiera devolver.
Veamos la tabla que almacena las figuras en nuestra base de datos:
CREATE TABLE Shape
(
Type int not null,
Name varchar (50) not null,
Side1 int,
Side2 int,
Side3 int,
Radius float
);
Digamos que los tipos de figura soportados serán cuadrados, rectángulos, triángulos y círculos. La correspondencia de los valores en los campos con el tipo de figura será de la siguiente forma: los cuadrados tendrán solamente valor en la columna "Side1", los rectángulos en "Side1" y "Side2", los triángulos en "Side1", "Side2", y "Side3", mientras que los círculos solo tendrán valor en la columna "Radius". Todas las figuras tendrán un nombre el cuál se almacenará en la columna "Name". El siguiente código insertará algunos valores en nuestra tabla con propósito de probar nuestro ejemplo:insert into Shape (Type, Name, Side1)
values (1, "SquareA", 10)
insert into Shape (Type, Name, Side1)
values (1, "SquareB", 20)
insert into Shape (Type, Name, Side1, Side2)
values (2, "Rectangle", 20, 30)
insert into Shape (Type, Name, Side1, Side2, Side3)
values (3, "Triangle", 10, 20, 10)
insert into Shape (Type, Name, Radius)
values (4, "Circle", 15)
Ahora veamos nuestro procedimiento almacenado que, como mencioné anteriormente, retornará solamente las columnas relevantes en dependencia del tipo de figura que se quiera recuperar:create procedure dbo.GetShape
(
@Type int
)
as
begin
if @Type = 1
begin
select Name, Side1 from Shape where Type = @Type
end
else if @Type = 2
begin
select Name, Side1, Side2 from Shape where Type = @Type
end
else if @Type = 3
begin
select Name, Side1, Side2, Side3 from Shape where Type = @Type
end
else if @Type = 4
begin
select Name, Radius from Shape where Type = @Type
end
end
Hasta aquí nuestra base de datos ha quedado lista para hacer nuestras pruebas desde C#. La idea es crear un método que reciba un tipo de figura determinado (según los datos insertados en la tabla, un valor del 1 al 4) y retorne un listado de figuras con la información correspondiente, la cuál imprimiremos en la consola de nuestro ejemplo.Poniendo la cosa bien interesante
Hay unas cuantas formas de resolver nuestro problema sin muchas complicaciones, ya sea utilizando DataSets, Linq to SQL, EntityFramework, o cualquier otro método con el que podamos recuperar la información de nuestro procedimiento almacenado. El detalle es que resultaría bastante engorroso tener que crear clases específicas para representar cada tipo de figura en una aplicación donde el uso de las mismas será esfímero. Por otro lado, el hecho de tener una sola clase con varios campos en NULL no va con mi idea de programación orientada a objetos, así que tampoco quisiera optar por esa vía. Obviamente, hacer que mi método retorne objetos específicamente ligados con la estructura relacional de una base de datos como un DataSet o un DataReader tampoco está en mis planes.
Haciendo un paréntesis para coger aire, habrán notado que con toda intención estoy complicando las cosas para dar cabida a la solución que voy presentar. Cada aplicación puede o no tener determinadas restricciones en cuanto a lo que queremos y/o podemos hacer, y el hecho de que una solución resuelva un problema determinado no significa que sea la mejor variante posible. En el caso de Jorge en su aplicación, esta fue sin dudas la mejor solución de todas, así que permítanme la licencia de apretar todos los tornillos del ejemplo anterior para vernos obligado a resolver el problema con el código que estoy a punto de presentar.
Sigamos con el hilo del artículo: decía que tenemos que crear un método para retornar un listado de figuras pero no me apetece la idea de ponerme a definir clases para cada figura, ni quiero valores NULL en mis objetos, y mucho menos estructuras de bases de datos viajando hasta mis clases de interfaz de usuario. Dada la volatilidad de los objetos que vamos a recuperar de la base de datos (estas figuras serán solamente utilizadas en modo de solo lectura para ser impresas por pantalla), la mejor variante de toda sería retornar un listado de objetos anónimos. Así de simple.
Un momento, ¿objetos anónimos?
Exacto. ¿Para qué complicarnos definiendo cada tipo de figura como una clase? Simplemente retornaremos un objeto anónimo por cada resultado, y la interfaz de usuario se encargará de imprimir sus propiedades. Pero incluso esto no es tan simple como parece. Imagínense si tenemos que implementar un código como el que sigue (donde obviamente los valores específicos de cada campo vendrán de la base de datos y no estarán directamente en el código como en el fragmento siguiente):
switch (shapeType)
{
case 1: return new { Name = "Square", Side1 = 10 };
case 2: return new { Name = "Rectangle", Side1 = 20, Side2 = 30 };
...
}
Un "switch" no es la mejor variante de todas, ya que la flexibilidad de nuestro código será nula. Si necesitamos un nuevo tipo de figura, tendremos que añadir otro "case" con la definición de la misma, y más importante aún, el código no podremos reutilizarlo en más ninguna parte de la aplicación pues será completamente dependiente de la estructura de nuestra tabla "Shape". Lo ideal sería construir un método lo suficientemente genérico como para que sea capaz de construir un objeto anónimo a partir de cualquier resultado que devuelva nuestro procedimiento almacenado.El código mágico
Construir a partir de un resultado cualquiera un objeto anónimo no es una tarea que precisamente podamos llamar "sencilla". Lo primero que tenemos que tener en cuenta es que no hay otra variante que la de utilizar las herramientas de .NET para generar código de forma dinámica, y cualquiera que haya navegado en estas aguas podrá atestiguar que dicha actividad puede resultar bastante compleja. Comencemos con el código de nuestra clase "AnonymousObjectBuilder" para luego explicarla de forma general:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;
using System.Data.SqlClient;
using System.Data;
namespace Sample
{
public class AnonymousObjectBuilder
{
public static object Create(string[] fields, object[] values)
{
var assemblyName = new AssemblyName("Sample");
var assemblyBuilder = AppDomain.CurrentDomain
.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(
assemblyName.Name);
var typeBuilder = moduleBuilder.DefineType(
"AnonymousType",
TypeAttributes.Public);
var constructorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
values.Select(v => v.GetType()).ToArray());
var generator = constructorBuilder.GetILGenerator();
for (var i = 0; i < fields.Length; i++)
{
var field = typeBuilder.DefineField(
fields[i],
values[i].GetType(),
FieldAttributes.Public);
constructorBuilder.DefineParameter(
i + 1,
ParameterAttributes.None,
fields[i]);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_S, i + 1);
generator.Emit(OpCodes.Stfld, field);
}
generator.Emit(OpCodes.Ret);
return Activator.CreateInstance(
typeBuilder.CreateType(),
values);
}
}
}
Nuestra clase tendrá un método "Create" que recibirá dos parámetros: un listado de campos y un listado de valores. El objetivo del método será crear un objeto anónimo con esta información. Para ello lo primero que haremos será definir el ensamblado al cual pertenecerá nuestro objeto anónimo, y luego crear el tipo de dato que lo representará, que en este caso llamaremos "AnonymousType". A partir de allí nuestro método define un contructor para nuestro objeto anónimo, crea cada uno de los campos, y genera el código para el contructor, donde se asignarán estos campos con los valores suministrados como argumentos. Veamos las líneas más interesantes una por una: var constructorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
values.Select(v => v.GetType()).ToArray());
La sentencia anterior define el contructor de nuestra clase, donde el tipo de dato de cada uno de sus argumentos viene del listado de valores que le suministramos a nuestro método "Create". A partir de allí, tendremos que generar el código correspondiente de nuestro constructor, para lo cual obtenemos el generador IL correspondiente: var generator = constructorBuilder.GetILGenerator();Entonces, para cada uno de los campos especificados como argumentos en nuestro método "Create", crearemos un campo público en nuestra clase:
var field = typeBuilder.DefineField(
fields[i],
values[i].GetType(),
FieldAttributes.Public);
Definiremos un parámetro para nuestro constructor con el mismo nombre del campo: constructorBuilder.DefineParameter(
i + 1,
ParameterAttributes.None,
fields[i]);
Y por último generamos el código que asignará el argumento pasado al constructor al campo de nuestra clase: generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_S, i + 1); generator.Emit(OpCodes.Stfld, field);Las tres líneas anteriores pueden traducirse al español de la siguiente manera:
- Almacena en el stack el contexto actual (o "this"). El contexto siempre se encuentra en el argumento cero (de allí el Ldarg_0).
- Almacena en el stack el valor del argumento "i" del constructor, el cual representa cada uno de los parámetros que definimos anteriormente.
- Utilizando los valores que se encuentran el el stack, asigna el argumento "i" al campo "field" en el contexto actual ("this" en este caso). Stfld puede traducirse como "Store Field".
generator.Emit(OpCodes.Ret);Por último, crearemos y retornaremos una instancia del tipo de dato que acabamos de generar, donde especificaremos el listado de valores con el cual el objeto será inicializado:
return Activator.CreateInstance(typeBuilder.CreateType(), values);Y eso es todo para que nuestro "Create" esté preparado para construir cualquier objeto anónimo a partir de una lista de campos y valores. Probemos nuestra teoría con un pequeño ejemplo antes de volver a nuestra aplicación con las figuras:
static void Main()
{
dynamic obj1 = AnonymousObjectBuilder.Create(
new string[] { "Name", "Age" },
new object[] { "Santiago", 30 });
dynamic obj2 = AnonymousObjectBuilder.Create(
new string[] { "Name", "IsAwesome" },
new object[] { "Jorge", false});
Console.WriteLine(obj1.Name
+ " is awesome and he's " + obj1.Age + " years old.");
Console.WriteLine(obj2.Name
+ (obj2.IsAwesome ? " is " : " is not ") + "awesome.");
}
Si ejecutamos el ejemplo anterior, veremos que se imprimirá en pantalla las siguientes líneas: Santiago is awesome and he's 30 years old. Jorge is not awesome.Noten como estamos usando nuestra clase "AnonymousObjectBuilder" para construir nuestros objetos anónimos, y los asignamos a variables dinámicas para acceder a sus propiedades sin necesidad de usar reflection.
Regresando al ejemplo principal
Regresemos a nuestro ejemplo original con las figuras. Todo lo que quedaría aquí por hacer sería un método que conecte nuestro procedimiento almacenado con la clase "AnonymousObjectBuilder" para crear los objetos correspondientes. Este código es bien simple, así que no requiere de muchas explicaciones:
public static List<object> RetrieveShapes(int shapeType)
{
var objects = new List<object>();
using (var connection = new SqlConnection(
@"Data Source=(local)\sqlexpress;"
+ "Initial Catalog=sample;Integrated Security=True"))
{
connection.Open();
var command = new SqlCommand("dbo.GetShape", connection)
{ CommandType = CommandType.StoredProcedure };
command.Parameters.Add(new SqlParameter("@Type", shapeType));
var reader = command.ExecuteReader();
var fields = new List<string>();
for (var i = 0; i < reader.FieldCount; i++)
{
fields.Add(reader.GetName(i));
}
foreach (IDataRecord record in reader)
{
var values = new List<object>();
for (var i = 0; i < reader.FieldCount; i++)
{
values.Add(record[i]);
}
objects.Add(AnonymousObjectBuilder.Create(
fields.ToArray(),
values.ToArray()));
}
reader.Close();
}
return objects;
}
Lo único que vale la pena resaltar es la línea dónde el objeto anónimo es creado y añadido al listado que será retornado por el método. Noten como los campos y valores son recuperados del objeto "SqlDataReader" retornado por el método "ExecuteReader": objects.Add(AnonymousObjectBuilder.Create(
fields.ToArray(),
values.ToArray()));
Después de esto, solamente quedaría utilizar el método "RetrieveShapes" para imprimir, por ejemplo, el listado de todos los cuadrados en nuestra base de datos: static void Main()
{
var objects = RetrieveShapes(1);
foreach (dynamic obj in objects)
{
Console.WriteLine(obj.Name + " - " + obj.Side1);
}
}
De la misma forma, cambiando el valor del parámetro "shapeType" que recibe como argumento el método "RetrieveShapes", podremos retornar cada uno de los tipos de figura de la base de datos sin mayores contratiempos.Concluyendo
Nuestra clase "AnonymousObjectBuilder" puede resultar bastante útil cada vez que querramos resolver problemas similares al ejemplo de las figuras. En la gran mayoría de los casos EntityFramework o Linq to SQL (por citar dos ejemplos) resolverán nuestro problema, pero no siempre tienen por qué ser lo más indicado. "AnonymousObjectBuilder" servirá como una fábrica de objetos bien sencilla, que podremos utilizar para evitar todo el código que tendríamos que generar en otros casos.
Si alguna persona tiene otra idea al respecto, o ha utilizado algo similar, me encantaría que lo compartiera en los comentarios de este artículo. No lo creo, pero a lo mejor EntityFramework incluye algo como esto a lo que le he pasado por alto. En cualquier caso, cualquier opinión es bienvenida.
El uso de AnonymousObjects es algo casi inevitable en mi data access layer. Resulta que el jefe, que segun el es DBA, siempre le ha gustado cambiar el resultado de los stored procedures en caliente. Es decir, el queria la posibilidad de hacer un cambio(añadir/remover una columna) al result set de un stored procedure y que este se viera reflejado en el sistema, sin tener que recompilar o hacer ningun cambio en la pagina.
ReplyDeleteYo diria todo lo contrario, lo mas indicado hasta ahora seria usar Linq o EntityFramework como nuestro DAL, para algunos raros casos(1:1000000) podieramos usar AnonymousObjects.
Excelente post y más por mis 5 minutos de fama jeje.
ReplyDeleteEn serio, me parece que quedo bien expresada la idea de la necesidad de esta solución para los efímeros casos en que los "clientes" de nuestros productos definan estrategias de acción (como fue mi caso) y hay que implementar algo lo suficientemente flexible y sin romper los patrones de arquitectura (detesto exponer un objeto de DAL para el UI)
También concuerdo 100% con Sergio y si de mí dependiera evitaría al máximo este comportamiento, lo más saludable siempre es mantener una consistente estructura de objetos que mapean con el modelo de datos en DB y construir nuestra lógica de negocios a partir de las herramientas que nos brinde el ORM (Linq to Sql, Entity Framework); pero por desgracia no siempre tenemos la autoridad de definir estos elementos de arquitectura.
El ejemplo está muy bien aunque ... que peligro !!! :D
ReplyDeleteSalu2