Geração dinâmica de objetos usando ExpandoObject

· 4 min de leitura

Eu estava precisando converter arquivos do Excel com suas próprias colunas definidas para um novo com colunas dinâmicas e fiquei meio confuso sobre como fazer isso corretamente, sem sérios problemas de desempenho.

Cheguei a um tutorial que você pode encontrar aqui que usa .NET ExpandoObject que praticamente permite criar um objeto e adicionar membros dinâmicos.

ExpandoObject

A definição da Microsoft é:

Representa um objeto cujos membros podem ser adicionados e removidos dinamicamente em tempo de execução.

E tem algumas observações:

A classe ExpandoObject permite adicionar e excluir membros de suas instâncias em tempo de execução e também definir e obter valores desses membros. Esta classe suporta ligação dinâmica, o que permite usar sintaxe padrão como sampleObject.sampleMember em vez de sintaxe mais complexa como sampleObject.GetAttribute(“sampleMember”).

Comportamento atual

Temos um ExcelExportService que, ao passar um List<T>, neste caso ExcelItem, usará Reflection para criar um arquivo xlsx.

Até agora, nosso código está assim:

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 é o objeto com todas as propriedades que estão sendo utilizadas para gerar o arquivo 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);
}

Esta abordagem funciona perfeitamente, o arquivo xlsx é gerado sem problemas com cada coluna sendo cada propriedade, você usaria este método GetExcelBytes assim, por exemplo, para salvá-lo em um arquivo:

File.WriteAllBytes("path-to-somewhere/my-file.xlsx", GetExcelBytes());

O problema

Já que o que estamos fazendo certo é usar todas as propriedades do objeto ExcelItem, tudo que eu quero é não usar todas elas, apenas usando talvez 2 ou 3 delas.

Também é um requisito que eu não queira alterar o código, tudo deve ser feito pelo administrator, que decidirá quais colunas devem ser mostradas, podendo ou não conhecer as propriedades do código.

TLDR: tudo deve ser dinâmico, temos um objeto com propriedades e devemos ter certeza de que o arquivo gerado possui N propriedades desse objeto, mas obviamente não codificado.

Colunas dinâmicas

A mudança seria utilizar um objeto dynamic, pois gostaríamos de definir quais propriedades do objeto serão utilizadas para gerar a lista de colunas.

Digamos que temos um objeto com muitas propriedades, como uma tonelada delas, e não queremos realmente alterar o objeto ExcelItem toda vez que fizermos uma alteração, criamos uma tabela ColumnExcelItem, que será usada para gerar esse ExcelItem.

Estrutura do banco de dados

Salvamos esta definição em nosso banco de dados com algo assim:

Id    PropertyName
---------------------
1     Name
2     Surname
3     Age

Observe que temos menos valores que as propriedades existentes do objeto ExcelItem, sendo que ele possui propriedades 6 e só temos 3 listado no banco de dados.

Observe também que PropertyName deve corresponder ao nome da propriedade do objeto que usarei para a atribuição dinâmica.

Construindo o objeto

Agora que temos a tabela do banco de dados, precisamos construir o repositório para obtê-la e poder usá-la no código.Não farei um tutorial desta parte, vou pular logo no início da nova função 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);
}

Agora teremos a criação de um objeto com algumas das propriedades de ExcelItem, mas completamente dinâmico. Em vez de usar todas as 6 propriedades, estamos usando apenas 3 delas, aquela que queremos apenas enviar.

Agora vamos fingir que este objeto ExcelItem tem 200 propriedades, louca, mas isso pode acontecer.

A única coisa que preciso fazer é inserir as propriedades que desejo renderizar no arquivo Excel naquela tabela ColumnExcelItem e pronto.

Casos

Neste caso usamos apenas um caso, mas digamos que você deseja ter relatórios diferentes para alguns usuários. Digamos que o papel admin deva obter todas as propriedades, então você criaria algo como esta estrutura no banco de dados

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

Então, ao obter o modelo, você poderá ter colunas diferentes, todas dinamicamente e sem código relacionado.

Se tivermos um usuário com a função Admins, o arquivo conterá as colunas Id, Name, Surname, Age, CreatedAt, UpdatedAt.

Caso você gere-o usando a função Users, ele terá Name, Age. Surname.

Conclusão

Esta é uma maneira legal de aprender como o dynamic funciona no .NET, e é uma solução muito boa quando você precisa ter funções ou modelos diferentes para usuários diferentes e não está realmente interessado em investir tempo codificando coisas.