Criando um leitor de xml com as novas capacidades dinâmicas de C#4

Email

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?

Email
Esse post foi publicado em Sem categoria e marcado por Giovanni Bassi. Marcar link permanente.

Sobre Giovanni Bassi

Arquiteto e desenvolvedor, agilista, pai, filho, namorado, escalador, provocador.
Programa porque gosta, e começou a trabalhar com isso porque acha que trabalhar como administrador é meio chato. Por esse motivo sempre diz que nunca mais vai virar gerente de ninguém. E também porque acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é melhor que hierarquia. Mas isso é outro assunto.
Foi reconhecido  Microsoft MVP depois que alguém notou que ele não dormia a noite pra ficar escrevendo artigos, cuidando e participando do .Net Architects, gravando o podcast Tecnoretórica, escrevendo posts no blog e falando o que bem entende no twitter @giovannibassi. E por falar nisso é no twitter que conta pra todos que gerencia de projetos deve ser feita pelo time e não por um gerentes, que greves em TI são coisas sem sentido e que stored procedure com regras de negócio são malígnas.
Você já deve ter percebido (até porque está lá na primeira frase) que Giovanni é agilista. De tanto gostar disso ele trouxe os programas de certificação e treinamento  PSD e PSM da Scrum.org pro Brasil, e por causa deles, do MVP e de algum trabalho que aparece tem que ficar indo pros EUA de vez enquando, coisa que prefere não fazer. (É bem comum você ouvir ele perguntando porque a Scrum.org e a Microsoft não estão na Itália, por exemplo.)
Junto com alguns Jedis criou a Lambda3, que, apesar de ser pequena e de não ser muito comum no Brasil, insiste em fazer projetos e consultoria direito. Por causa da Lambda3 ele tem trabalhado mais do que quando era consultor independente, mas menos do que a maioria das pessoas. Quer dizer, isso se você considerar que os trabalhos junto à comunidade não são trabalho, caso contrário ele trabalha mais que a maioria das pessoas.
Recentemente ele resolveu que merecia viver melhor e ganhar uns anos de vida e desistiu de ser sedentário, fazendo algum barulho de vez em quando com os amigos no twitter com a hashtag #DotNetEmForma. Por causa do convite recente de amigos do lado Open Source (que ele respeita e admira), começou a escalar, e agora está sempre com as mãos machucadas. Mas ainda dá pra programar. Você encontra ele sempre em algum evento, como o TechEd, e o DNAD, mas também outros menos comuns para o pessoal do .NET, como a RubyConf. Nesses eventos, ou ele está vendo palestras, ou batendo papo com alguém, ou codando alguma aplicação que alguém achou que dava pra fazer durante o evento.
  • http://addwatch.wordpress.com/ José Filipe

    Sempre achei o esquema de dynamics questionável, não por "não servir de nada", como você citou, mas por achar que abre a porta pra gambiarras nervosas (tem toda a discussão que restringir isto é o papel de um bom arquiteto ou desenvolvedor, whatever)..

    Independente disto, o exemplo desse post é realmente fantástico. Conseguir criar propriedades dinâmicas com base em dados quaisquer abre um número de possibilidades muito grande pra modelos que podem ser customizados posteriormente, muito legal.

    E viva os metadados!

  • http://www.viniciusquaiato.com/ Vinicius Quaiato

    Giggio, parabéns pelo artigo.

    Duas perguntinhas:
    1 – Quais as implicações de performance para o abuso no uso de dynamics?
    2 – Diferentemente do var e dos anonymous types, é possível trabalhar com dynamic fora do escopo local. No entanto eu não tenho ajuda do intellisense e devo conhecer a estrutura do objeto em desgin time, certo?

    Abraços,
    Vinicius Quaiato.

  • http://unplugged.giggio.net/ Giovanni Bassi

    Vinicius,
    1) Há um impacto em performance, mas o CLR dá um jeito de cachear isso pra minimizar. Mas há, como em toda linguagem dinâmica.
    2) Certo. Também como toda linguagem dinâmica, você tem que saber a estrutura do objeto. Tem prós, e tem contras. Consegue abrir mão da checagem em compile time? :)