Serializando um objeto como XML UTF-8 em .NET

112

O descarte adequado de objetos foi removido por questão de brevidade, mas estou chocado se esta é a maneira mais simples de codificar um objeto como UTF-8 na memória. Tem que haver uma maneira mais fácil, não é?

var serializer = new XmlSerializer(typeof(SomeSerializableObject));

var memoryStream = new MemoryStream();
var streamWriter = new StreamWriter(memoryStream, System.Text.Encoding.UTF8);

serializer.Serialize(streamWriter, entry);

memoryStream.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(memoryStream, System.Text.Encoding.UTF8);
var utf8EncodedXml = streamReader.ReadToEnd();
Garry Shutler
fonte
1
Estou confuso ... não é a codificação UTF-8 padrão?
flq
@flq, sim, o padrão é UTF-8, embora não importe muito, já que ele está lendo novamente em uma string, então utf8EncodedXmlé UTF-16.
Jon Hanna,
1
@Garry, você pode esclarecer, já que Jon Skeet e eu estamos respondendo a perguntas diferentes. Você deseja que o objeto seja serializado como UTF-8 ou uma string XML que se declare como UTF-8 e, portanto, terá a declaração correta quando codificada posteriormente em UTF-8? (nesse caso, a maneira mais simples é não ter declaração, já que isso é válido para UTF-8 e UTF-16).
Jon Hanna,
@Jon Lendo de volta, há ambigüidade na minha pergunta. Tive a saída para uma string principalmente para fins de depuração. Na prática, eu provavelmente estaria transmitindo bytes, seja para o disco ou sobre HTTP, o que torna sua resposta mais diretamente relevante para o meu problema. O principal problema que tive foi a declaração de UTF-8 no XML, mas para ser mais preciso, devo evitar o intermediário de uma string para enviar / persistir bytes UTF-8 reais em vez de um dependente de plataforma (eu acho) codificação.
Garry Shutler

Respostas:

55

Seu código não coloca o UTF-8 na memória conforme você o lê em uma string novamente, então não está mais em UTF-8, mas em UTF-16 (embora idealmente seja melhor considerar strings em um nível superior qualquer codificação, exceto quando forçado a fazê-lo).

Para obter os octetos UTF-8 reais que você pode usar:

var serializer = new XmlSerializer(typeof(SomeSerializableObject));

var memoryStream = new MemoryStream();
var streamWriter = new StreamWriter(memoryStream, System.Text.Encoding.UTF8);

serializer.Serialize(streamWriter, entry);

byte[] utf8EncodedXml = memoryStream.ToArray();

Eu deixei de fora a mesma disposição que você deixou. Sou ligeiramente favorável ao seguinte (com o descarte normal deixado):

var serializer = new XmlSerializer(typeof(SomeSerializableObject));
using(var memStm = new MemoryStream())
using(var  xw = XmlWriter.Create(memStm))
{
  serializer.Serialize(xw, entry);
  var utf8 = memStm.ToArray();
}

O que é quase a mesma quantidade de complexidade, mas mostra que em cada estágio há uma escolha razoável para fazer outra coisa, a mais urgente delas é serializar para algum lugar diferente da memória, como um arquivo, TCP / IP stream, banco de dados, etc. Resumindo, não é tão prolixo.

Jon Hanna
fonte
4
Além disso. Se você deseja suprimir BOM, você pode usar XmlWriter.Create(memoryStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false) }).
ony
Se alguém (como eu) precisar ler o XML criado como Jon mostra, lembre-se de reposicionar o fluxo de memória para 0, caso contrário, você receberá uma exceção dizendo "Elemento raiz está faltando". Então faça o seguinte: memStm.Position = 0; XmlReader xmlReader = XmlReader.Create (memStm)
Sudhanshu Mishra
276

Não, você pode usar um StringWriterpara se livrar do intermediário MemoryStream. No entanto, para forçá-lo em XML, você precisa usar um StringWriterque substitui a Encodingpropriedade:

public class Utf8StringWriter : StringWriter
{
    public override Encoding Encoding => Encoding.UTF8;
}

Ou se ainda não estiver usando o C # 6:

public class Utf8StringWriter : StringWriter
{
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}

Então:

var serializer = new XmlSerializer(typeof(SomeSerializableObject));
string utf8;
using (StringWriter writer = new Utf8StringWriter())
{
    serializer.Serialize(writer, entry);
    utf8 = writer.ToString();
}

Obviamente, você pode fazer Utf8StringWriterem uma classe mais geral que aceita qualquer codificação em seu construtor - mas na minha experiência UTF-8 é de longe a codificação "personalizada" mais comumente necessária para um StringWriter:)

Agora, como Jon Hanna disse, isso ainda será UTF-16 internamente, mas provavelmente você vai passá-lo para outra pessoa em algum ponto, para convertê-lo em dados binários ... nesse ponto, você pode usar a string acima, converta-o em bytes UTF-8 e tudo ficará bem - porque a declaração XML especificará "utf-8" como a codificação.

EDIT: Um exemplo curto, mas completo para mostrar isso funcionando:

using System;
using System.Text;
using System.IO;
using System.Xml.Serialization;

public class Test
{    
    public int X { get; set; }

    static void Main()
    {
        Test t = new Test();
        var serializer = new XmlSerializer(typeof(Test));
        string utf8;
        using (StringWriter writer = new Utf8StringWriter())
        {
            serializer.Serialize(writer, t);
            utf8 = writer.ToString();
        }
        Console.WriteLine(utf8);
    }


    public class Utf8StringWriter : StringWriter
    {
        public override Encoding Encoding => Encoding.UTF8;
    }
}

Resultado:

<?xml version="1.0" encoding="utf-8"?>
<Test xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <X>0</X>
</Test>

Observe a codificação declarada de "utf-8" que é o que queríamos, eu acredito.

Jon Skeet
fonte
2
Mesmo quando você substitui o parâmetro Encoding em StringWriter, ele ainda envia os dados gravados para um StringBuilder, portanto, ainda é UTF-16. E a string só pode ser UTF-16.
Jon Hanna,
4
@Jon: Você já experimentou? Eu tenho, e funciona É a codificação declarada que é importante aqui; obviamente, internamente a string ainda é UTF-16, mas isso não faz nenhuma diferença até que seja convertida para binário (que pode usar qualquer codificação, incluindo UTF-8). A TextWriter.Encodingpropriedade é usada pelo serializador XML para determinar qual nome de codificação especificar no próprio documento.
Jon Skeet,
2
@Jon: E qual foi a codificação declarada? Na minha experiência, é isso que questões como essa realmente tentam fazer - criar um documento XML que se declare em UTF-8. Como você disse, é melhor não considerar o texto em qualquer codificação até que você precise ... mas como o documento XML declara uma codificação, isso é algo que você precisa considerar.
Jon Skeet,
2
@Garry, o mais simples que consigo pensar agora é pegar o segundo exemplo em minha resposta, mas quando você criar o XmlWriterfaça isso com o método de fábrica que pega um XmlWriterSettingsobjeto e tem a OmitXmlDeclarationpropriedade definida como true.
Jon Hanna,
4
+1 Sua Utf8StringWritersolução é extremamente agradável e limpa
Adriano Carneiro
17

Muito boa resposta usando herança, apenas lembre-se de substituir o inicializador

public class Utf8StringWriter : StringWriter
{
    public Utf8StringWriter(StringBuilder sb) : base (sb)
    {
    }
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}
Sebastian castaldi
fonte
obrigado, acho que esta é a mais elegante das opções
Prokurors,
5

Achei esta postagem do blog que explica o problema muito bem e define algumas soluções diferentes:

(link morto removido)

Eu me conformei com a ideia de que a melhor maneira de fazer isso é omitir completamente a declaração XML quando estiver na memória. Na verdade, é UTF-16 nesse ponto de qualquer maneira, mas a declaração XML não parece significativa até que tenha sido gravada em um arquivo com uma codificação específica; e mesmo assim a declaração não é necessária. Não parece interromper a desserialização, pelo menos.

Como @Jon Hanna menciona, isso pode ser feito com um XmlWriter criado assim:

XmlWriter writer = XmlWriter.Create (output, new XmlWriterSettings() { OmitXmlDeclaration = true });
Dave Andersen
fonte