Generación dinámica de objetos usando ExpandoObject
He estado en la necesidad de convertir archivos de Excel con sus propias columnas definidas a uno nuevo con columnas dinámicas, y estaba un poco confundido sobre cómo hacerlo correctamente sin algunos problemas graves de rendimiento.
Se me ocurrió un tutorial que puedes encontrar aquí que usa .NET ExpandoObject que prácticamente te permite crear un objeto y agregar miembros dinámicos.
Objeto Expando
La definición de Microsoft es:
Representa un objeto cuyos miembros se pueden agregar y eliminar dinámicamente en tiempo de ejecución.
Y tiene algunas observaciones:
La clase ExpandoObject le permite agregar y eliminar miembros de sus instancias en tiempo de ejecución y también establecer y obtener valores de estos miembros. Esta clase admite el enlace dinámico, lo que le permite utilizar una sintaxis estándar como sampleObject.sampleMember en lugar de una sintaxis más compleja como sampleObject.GetAttribute(“sampleMember”).
Comportamiento actual
Tenemos un ExcelExportService que al pasarle un List<T>, en este caso ExcelItem, usará Reflection para crear un archivo xlsx.
Hasta ahora, nuestro código se ve así:
class ExcelItem
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string Age { get; set; }
public string CreatedAt { get; set; }
public string UpdatedAt { get; set; }
}
ExcelItem es el objeto con todas las propiedades que se utilizan para generar el archivo de Excel.
public static byte[] GetExcelBytes() {
List<ExcelItem> excelItems = new List<ExcelItem>();
ExcelExportService exportService = new ExcelExportService();
foreach (var items in itemsToExcel)
{
excelItems.Add(new ExcelItem()
{
Id = item.Id,
Name = item.Name,
Surname = item.Surname,
Age = item.Age,
CreatedAt = item.CreatedAt
UpdatedAt = item.UpdatedAt
});
}
return exportService.ExportToExcel(excelItems);
}
Este enfoque funciona perfectamente, el archivo xlsx se genera sin problemas siendo cada columna cada propiedad, usarías este método GetExcelBytes así, por ejemplo, para guardarlo en un archivo:
File.WriteAllBytes("path-to-somewhere/my-file.xlsx", GetExcelBytes());
El problema
Dado que lo que estamos haciendo bien es usar todas las propiedades del objeto ExcelItem, lo único que quiero es no usarlas todas, solo usar quizás 2 o 3 de ellas.
También es un requisito que no quiero cambiar el código, todo lo debe hacer el administrator, quien decidirá qué columnas se deben mostrar y puede que conozca o no las propiedades en el código.
TLDR: todo debe ser dinámico, tenemos un objeto con propiedades y debemos asegurarnos de que el archivo generado tenga N propiedades de ese objeto, pero obviamente no codificadas.
Columnas dinámicas
El cambio sería usar un objeto dynamic, porque nos gustaría establecer qué propiedades del objeto se usarán para generar la lista de columnas.
Digamos que tenemos un objeto con muchas propiedades, como una tonelada de ellas, y realmente no queremos cambiar el objeto ExcelItem cada vez que hacemos un cambio, creamos una tabla ColumnExcelItem, que se usará para generar ese ExcelItem.
Estructura de la base de datos
Guardamos esta definición en nuestra base de datos con algo como esto:
Id PropertyName
---------------------
1 Name
2 Surname
3 Age
Tenga en cuenta que tenemos menos valores que las propiedades existentes del objeto ExcelItem, este tiene propiedades 6 y solo tenemos 3 listado en la base de datos.
También tenga en cuenta que PropertyName debe coincidir con el nombre de propiedad del objeto que usaré para la asignación dinámica.
Construyendo el objeto
Ahora que tenemos la tabla de la base de datos, necesitamos crear el repositorio para obtenerla y poder usarla en el código.No haré un tutorial de esta parte, me saltaré el comienzo de la nueva función GetExcelBytes().
public static byte[] GetExcelBytes(List<string> columns) {
List<ExcelItem> excelItems = new List<ExcelItem>();
List<dynamic> objectsToExcel = new List<dynamic>();
ExcelExportService exportService = new ExcelExportService();
foreach (var item in itemsToExcel)
{
dynamic newObject = new ExpandoObject();
foreach (var col in columns)
{
this.AddProperty(newObject, col, product.GetType().GetProperty(col).GetValue(item, null));
}
objectsToExcel.Add(newObject);
}
return exportService.ExportToExcel(objectsToExcel);
}
Ahora crearemos un objeto con algunas de las propiedades de ExcelItem, pero completamente dinámico. En lugar de usar las 6 propiedades, solo usamos 3 de ellas, la que solo queremos enviar.
Ahora supongamos que este objeto ExcelItem tiene 200 propiedades, una locura, pero podría suceder.
Lo único que tengo que hacer es insertar esas propiedades que quiero renderizar en el archivo de Excel a esa tabla ColumnExcelItem y listo.
Casos
En este caso solo usamos un caso único, pero digamos que desea tener informes diferentes para algunos usuarios. Digamos que el rol admin debe obtener todas las propiedades, entonces crearías algo como esta estructura en la base de datos.
ReportingTemplates
Id PropertyName
---------------------
1 Admin
2 Users
ReportingTemplateItems
Id TemplateId PropertyName
---------------------
1 1 Id
2 1 Name
3 1 Surname
4 1 Age
5 1 CreatedAt
6 1 UpdatedAt
7 2 Name
8 2 Surname
9 2 Age
Luego, al obtener la plantilla, podría tener diferentes columnas, todas dinámicamente y sin código relacionado.
Si tenemos un usuario con el rol Admins, el archivo contendrá las columnas Id, Name, Surname, Age, CreatedAt, UpdatedAt.
En el caso de que lo generes usando el rol Users, tendrá Name, Age. Surname.
Conclusión
Esta es una forma genial de aprender cómo funciona dynamic en .NET, y es una solución realmente buena cuando necesitas tener diferentes roles o plantillas para diferentes usuarios y no estás realmente interesado en invertir tiempo en codificar cosas.