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?