Serialização XML e LINQ to XML
Objective
XML ainda é o que você encontra em serviços SOAP, formatos do Office, arquivos de
configuração, feeds e muitas integrações legadas. O .NET dá duas portas de entrada:
serializadores que mapeiam objetos para XML (XmlSerializer,
DataContractSerializer) e o LINQ to XML (XDocument, XElement), uma API de
consulta e edição para XML que você não quer mapear. Este conceito cobre o que cada
serializador inclui e pula em silêncio, as surpresas de namespace e encoding, como
consultar e modificar com LINQ to XML (incluindo um laço que pula elementos sem
avisar), quando fazer streaming com XmlReader e as configurações do parser que
decidem se um documento hostil consegue ler um arquivo local ou esgotar a memória.
Use Cases
- Ler ou produzir um contrato XML (um feed, um formato de nota, um endpoint da era SOAP).
- Extrair alguns valores de um arquivo XML grande sem escrever classes para ele.
- Editar um arquivo de configuração preservando tudo que você não tocou.
- Aceitar um upload de XML vindo de fora da sua fronteira de confiança.
Deep Dive
XmlSerializer: forma pública, atributos, padrões
O XmlSerializer mapeia propriedades públicas de leitura/escrita e campos públicos
para elementos. Toda a saída abaixo é do .NET 10.0.0 (SDK 10.0.100). Com
[XmlRoot], [XmlAttribute], [XmlElement], [XmlIgnore] e
[XmlArray]/[XmlArrayItem] num Order:
plaintext<?xml version="1.0" encoding="utf-16"?> <order xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" id="7"> <customer>Ana & Co</customer> <Created>2024-01-02T03:04:05Z</Created> <Status>Shipped</Status> <lines><line sku="A"><Qty>2</Qty></line><line sku="B"><Qty>1</Qty></line></lines> </order>
(Indentação reduzida.) Coisas fáceis de perder:
- Duas declarações de namespace (
xsi,xsd) são adicionadas por padrão. Passar umXmlSerializerNamespacescontendo("", "")as remove. Escrevendo com umXmlWritereOmitXmlDeclaration = true, saiu<order id="7"><customer>...</order>. encoding="utf-16"aparece porque serializei para umStringWriter, cujo encoding é UTF-16. A declaração segue o writer, não a sua intenção;XDocument.Save(StringWriter)fez o mesmo. Escreva num stream comXmlWriterSettings.Encodingquando o arquivo precisa ser UTF-8.- Uma string
null(Note) foi simplesmente omitida, a propriedade com[XmlIgnore]foi omitida e voltou como'', e um enum foi escrito pelo nome (Shipped). - Só membros públicos: para uma classe com uma propriedade pública, uma interna, uma
privada e um campo público, a saída trouxe apenas
FieldePub. - Um tipo sem construtor sem parâmetros falha quando você constrói o serializador:
InvalidOperationException: NoCtor cannot be serialized because it does not have a parameterless constructor.É a falha rápida; não fica adiada para o primeiroSerialize. - Polimorfismo precisa ser declarado: uma
List<Animal>com itensCateDogfuncionou depois que listei[XmlElement("Cat", typeof(Cat)), XmlElement("Dog", typeof(Dog))].
A leitura é permissiva e barulhenta em lugares diferentes. Para <Line sku='Z' extra='1'> com um elemento <Bogus> desconhecido, a desserialização teve sucesso e
descartou os dois em silêncio; o evento UnknownElement (e UnknownAttribute) é o
gancho se você quiser saber. Um valor ruim, <Qty>abc</Qty>, lançou
InvalidOperationException: There is an error in XML document (1, 31) com uma
FormatException dentro, e uma raiz errada deu <Other xmlns=''> was not expected.
Leia sempre a exceção interna e a linha e posição.
DataContractSerializer: membros por adesão
O DataContractSerializer é a alternativa da era WCF com o padrão oposto: só os
membros marcados com [DataMember] são escritos, e uma propriedade sem o atributo é
pulada. Para [DataContract] class Dc { [DataMember] A; [DataMember(Order = 2)] B; int NotMember } a saída foi
<Dc xmlns="http://schemas.datacontract.org/2004/07/" xmlns:i="..."><A>1</A><B>x</B></Dc>.
Repare no namespace derivado do namespace CLR. O guia de segurança avisa sobre outra
classe, NetDataContractSerializer, que faz desserialização polimórfica irrestrita:
não confunda as duas.
LINQ to XML: consulte sem classes
XDocument e XElement são uma árvore em memória. A documentação o descreve como a
opção para código novo. O único conceito a aprender são os namespaces: um nome é
XNamespace + nome local, e uma consulta sem o namespace não acha nada.
csharpXNamespace s = "urn:shop";
var q = doc.Root!.Elements(s + "order").Select(o => new {
Id = (int)o.Attribute("id")!,
Customer = (string)o.Element(s + "customer")!,
Total = o.Elements(s + "line").Sum(x => (int)x.Attribute("qty")! * (decimal)x.Attribute("price")!) });
// order 1 Ana total 22.0 order 2 Bo total 47.5No mesmo documento doc.Root.Elements("order") contou 0 porque os elementos estão
em urn:shop. Os casts explícitos são a API de leitura: (string?) num elemento
ausente é null, mas (int) num atributo ausente lançou ArgumentNullException e
(int) no texto Ana lançou FormatException. Faça cast para int? quando o valor
puder faltar. XPath funciona por System.Xml.XPath: XPathSelectElements com um
XmlNamespaceManager ligando um prefixo a urn:shop achou o pedido em aberto.
A edição é direta: SetAttributeValue("status", "shipped"),
Add(new XElement(...)), Remove(). A armadilha é remover enquanto enumera. Em
<r><a/><a/><a/><a/></r>, foreach (var a in root.Elements("a")) a.Remove();
deixou 3 de 4 elementos, porque remover um nó perturba a enumeração preguiçosa.
Materializar antes, foreach (var a in root.Elements("a").ToList()) a.Remove();,
deixou 0, e a extensão root.Elements("a").Remove() também, já que ela faz a cópia
por você.
Streaming de arquivos grandes com XmlReader
XDocument carrega tudo. Para um arquivo grande, XmlReader mais XNode.ReadFrom
entrega um XElement pequeno por vez:
csharpwhile (!rd.EOF)
if (rd.NodeType == XmlNodeType.Element && rd.Name == "item") { var el = (XElement)XNode.ReadFrom(rd); /* use el */ }
else rd.Read();
// [0:0] [1:1] [2:4] [3:9] [4:16]ReadFrom deixa o reader depois do elemento, então o ramo else Read() não deve
pular o próximo. É o padrão comum, e mantém a memória proporcional a um único item.
As configurações do parser são uma fronteira de segurança
Um documento XML pode declarar entidades, e uma entidade pode apontar para um
arquivo ou uma URL (XXE), ou expandir de forma exponencial. O que acontece depende
do ponto de entrada. Testei um documento cujo DTD define &x; como
file:///tmp/csf-secret.txt:
| Ponto de entrada | Resultado |
|---|---|
XmlReader.Create com configurações padrão |
XmlException: For security reasons DTD is prohibited (o padrão é DtdProcessing.Prohibit, segundo a documentação) |
XDocument.Parse(string) e XmlDocument.LoadXml |
interpretou, entidade ficou vazia (sem resolver, arquivo não lido) |
XmlSerializer.Deserialize |
InvalidOperationException |
só DtdProcessing.Parse |
interpretou, entidade vazia |
DtdProcessing.Parse mais XmlResolver = new XmlUrlResolver() |
leu o arquivo: TOP-SECRET-CONTENT |
Ou seja, a divulgação de arquivo exigiu duas adesões deliberadas no .NET 10. A
expansão de entidades é o outro eixo. Um pequeno documento "billion laughs" (quatro
níveis aninhados de 10 expansões, 10^4 cópias de lol) expandiu para 30000
caracteres via XDocument.Parse sem reclamação, e a mesma forma com 8 níveis (715
caracteres de entrada, 300 milhões de saída se expandisse) lançou XmlException: The input document has exceeded a limit set by MaxCharactersFromEntities tanto no
XDocument.Parse quanto no XmlDocument.LoadXml. Num XmlReaderSettings novo com
Parse a propriedade valia 10000000, embora a documentação dê o padrão como 0,
então defina você mesmo em vez de confiar em qualquer um dos dois. Definir 1000 fez o
documento de 30000 caracteres falhar. O padrão seguro para entrada externa:
csharpvar s = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null,
MaxCharactersInDocument = 10_000_000 };
var doc = XDocument.Load(XmlReader.Create(stream, s));Não medi o efeito de um limite de tamanho de documento. Seja qual for a versão,
mantenha DTDs proibidos a menos que você controle o documento, e nunca passe um
XmlUrlResolver para entrada não confiável.
Trade-offs
XmlSerializeré convenção sobre contrato. Tudo que é público entra, o que é prático e vaza propriedades novas para o formato na rede.csharp[XmlIgnore] public string Secret { get; set; } = ""; // exclua explicitamenteDataContractSerializeré explícito e deriva um namespace. Padrões mais seguros para membros, mais cerimônia, um namespace que você talvez não queira.- LINQ to XML é dinâmico e não verificado. Sem schema em tempo de compilação;
nós ausentes viram
nullou exceção conforme o cast. - Consultas preguiçosas com mutação geram bugs. Materialize antes de remover.
XDocumenté limitado pela memória. Faça streaming de entradas grandes comXmlReadere processe um elemento por vez.- Padrão não é política. Defina
DtdProcessing,XmlResolvere o limite de entidades de forma explícita onde a entrada cruza uma fronteira de confiança.