Até a versão 3 do C#, não era possível fazer o que vou mostrar aqui. Havia como contornar, mas nada que soasse bem, ou ficasse muito bonito ou fácil de ler.

O objetivo é pegar um arquivo XML, qualquer arquivo XML, com qualquer esquema, lê-lo, e então ser capaz de acessar seus elementos como se fossem referências a outros objetos, e seus atributos como se fossem propriedades primitivas (strings).

Assim, um xml como esse:

<?xml version="1.0" encoding="utf-8" ?>
<raiz propriedade1="p1">
  <no1 propriedade2="p2">
    <no2 propriedade3="p3">
      <no3 propriedade4="p4" propriedade5="p5" >
        <no4 propriedade6="p6" propriedade7="p7" />
      </no3>
    </no2>
  </no1>
</raiz>

Deve ser possível de ser lido de alguma forma, e, no final, me apresentar um objeto que me permita fazer isso:

Assert.AreEqual("p7", leitor.Raiz.No1.No2.No3.No4.Propriedade7);

A questão é que o xml pode mudar, então como definir que os nós devem originar propriedades dinamicamente? Essa era uma limitação do C# (e do VB) até a versão 3 (9 no VB). E, para atender esse tipo de requisito ou utilizávamos um dicionário ou algo parecído, ou tínhamos que cair em linguagens dinâmicas para fazer esse trabalho. Em Ruby isso é feito com algumas magias de metaprogramação bem interessantes. Tudo isso mudou e agora é possível no C# e no VB.

Foi introduzido o conceito de dinamismo no C# na sua versão 4, sobre o qual eu bloguei a mais de um ano (vejam o post principal aqui, e algumas implicações aqui, aqui e aqui) e escrevi um artigo na .Net Magazine, também ano passado. Algumas coisas foram melhoradas de lá pra cá, e uma destas é a que vou mostrar agora.

Antes disso, vamos deixar clara a especificação do que eu quero. Todo mundo sabe que eu trabalho com TDD, então especifiquei o que quero com um teste. Mas esse não é um post sobre TDD, então vou colocar aqui um teste que resume tudo o que eu espero que aconteça:

[TestMethod]
public void Quando_Passo_Um_XML_Simples_Ele_Cria_Um_Objeto_Com_Propriedades_E_Filhos_E_Netos_Complexos()
{
    var xml = @"<?xml version=""1.0"" encoding=""utf-8"" ?>
                <raiz propriedade1=""p1"">
                  <no1 propriedade2=""p2"">
                    <no2 propriedade3=""p3"">
                      <no3 propriedade4=""p4"" propriedade5=""p5"" >
                        <no4 propriedade6=""p6"" propriedade7=""p7"" />
                      </no3>
                    </no2>
                  </no1>
                </raiz>";
    var leitor = LeitorXML.Ler(xml);
    Assert.AreEqual("p1", leitor.Raiz.Propriedade1);
    Assert.AreEqual("p2", leitor.Raiz.No1.Propriedade2);
    Assert.AreEqual("p3", leitor.Raiz.No1.No2.Propriedade3);
    Assert.AreEqual("p4", leitor.Raiz.No1.No2.No3.Propriedade4);
    Assert.AreEqual("p5", leitor.Raiz.No1.No2.No3.Propriedade5);
    Assert.AreEqual("p6", leitor.Raiz.No1.No2.No3.No4.Propriedade6);
    Assert.AreEqual("p7", leitor.Raiz.No1.No2.No3.No4.Propriedade7);
}

Notem que chamo o método estático “Ler” que retorna um objeto e acesso as propriedades deste objeto, que se baseiam no XML. Para ser capaz de fazer isso preciso trabalhar com um objeto dinâmico. O objeto dinâmico que estou usando foi introduzido pela Microsoft recentemente, e permite incluir membros dinamicamente, tanto propriedades quanto métodos. É o ExpandoObject. Na prática, você pode fazer isso:

dynamic o = new ExpandoObject();
o.Nome = "Giovanni";
o.ObterNomeMaiusculo = (Func<string>) ( () => o.Nome.ToUpper());

Assert.AreEqual("Giovanni", o.Nome);
Assert.AreEqual("GIOVANNI", o.ObterNomeMaiusculo());

Na linha 1 eu crio o objeto, e seto ele para dynamic. É importante lembrar de dizer que ele é dinâmico, porque se você declará-lo com var ou tipá-lo como ExpandoObject não vai poder trabalhá-lo dinamicamente. Na linha 2 eu crio uma propriedade, e na 3 eu crio um método. Nas linhas 5 e 6 eu chamo esta propriedade e este método.

O ExpandoObject também implementa um dicionário de string/objeto, onde ele guarda todos os membros (propriedades, métodos, etc) o que permite a você listar estes itens, e incluir e excluir itens de maneira ainda mais dinâmica.

Bem simples, certo?

Agora que vocês já sabem como funciona o ExpandoObject, já até devem saber como fazer o tal do leitor de XML. Aqui está o código:

public class LeitorXML
{
    public static dynamic Ler(string xml)
    {
        dynamic leitor = new ExpandoObject();
        var elementoRaiz = System.Xml.Linq.XElement.Parse(xml);                
        var raiz = CriarObjetoDinamicoAPartirDeElementoXML(elementoRaiz);
        ((IDictionary<string, object>) leitor).Add(elementoRaiz.Name.LocalName.DeixaPrimeiraLetraMaiuscula(), raiz);
        return leitor;
    }
    private static IDictionary<string, object> CriarObjetoDinamicoAPartirDeElementoXML(XElement elemento)
    {
        IDictionary<string, object> filho = new ExpandoObject();
        foreach (var atributo in elemento.Attributes())
            filho.Add(atributo.Name.LocalName.DeixaPrimeiraLetraMaiuscula(), atributo.Value);
        foreach (var elementoFilho in elemento.Elements())
            filho.Add(elementoFilho.Name.LocalName.DeixaPrimeiraLetraMaiuscula(), CriarObjetoDinamicoAPartirDeElementoXML(elementoFilho));                    
        return filho;
    }
}

O ExpandoObject que representa o leitor é criado logo na primeira linha do método Ler. A partir daí, leio os elementos do XML e os transformo em propriedades que apontam para outros ExpandoObjects. Os atributos são transformados em propriedades que retornam strings. Os nomes das propriedades são definidos a partir dos nomes dos elementos e dos atributos. Em 20 linhas tudo foi definido, esse código é suficiente para passar na especificação anterior (o único código que não está aí é um código auxiliar, um método de extensão, usado para tornar a primeira letra maiuscula em cada propriedades, ou seja, é cosmético).

Esse código vai se adaptar a qualquer xml que você gerar, desde que você respeite a convenção: elementos viram referências a outros objetos, atributos viram propriedades primitivas.

Para os que diziam que dynamic não servia pra nada, o que acharam?

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.