Serializar un objeto como UTF-8 XML en .NET

112

Se eliminó la eliminación adecuada de objetos por brevedad, pero me sorprende si esta es la forma más sencilla de codificar un objeto como UTF-8 en la memoria. Tiene que haber una manera más fácil, ¿no?

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
fuente
posible duplicado de una forma más fácil de serializar la clase C # como texto XML
Garry Shutler
1
Estoy confundido ... ¿no es la codificación predeterminada UTF-8?
flq
@flq, sí, el valor predeterminado es UTF-8, aunque no importa mucho ya que lo está leyendo nuevamente en una cadena, también lo utf8EncodedXmles UTF-16.
Jon Hanna
1
@Garry, puedes aclarar, ya que Jon Skeet y yo estamos respondiendo preguntas diferentes. ¿Desea que el objeto se serialice como UTF-8 o desea una cadena XML que se declare a sí misma como UTF-8 y, por lo tanto, tenga la declaración correcta cuando se codifique posteriormente en UTF-8? (en cuyo caso la forma más sencilla es no tener ninguna declaración, ya que eso es válido tanto para UTF-8 como para UTF-16).
Jon Hanna
@Jon Leyendo, hay ambigüedad en mi pregunta. Lo tenía saliendo a una cadena principalmente con fines de depuración. En la práctica, probablemente estaría transmitiendo bytes, ya sea al disco o a través de HTTP, lo que hace que su respuesta sea más directamente relevante para mi problema. El principal problema que tuve fue la declaración de UTF-8 en el XML, pero para ser más preciso, debería evitar el intermediario de una cadena para poder enviar / persistir bytes UTF-8 reales en lugar de una plataforma dependiente (creo) codificación.
Garry Shutler

Respuestas:

55

Su código no almacena el UTF-8 en la memoria cuando lo vuelve a leer en una cadena, por lo que ya no está en UTF-8, sino en UTF-16 (aunque idealmente es mejor considerar las cadenas en un nivel más alto que cualquier codificación, excepto cuando sea forzado a hacerlo).

Para obtener los octetos UTF-8 reales, puede 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();

Dejé fuera la misma disposición que usted dejó. Estoy un poco a favor de lo siguiente (dejando la eliminación normal):

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();
}

Lo que es casi la misma cantidad de complejidad, pero muestra que en cada etapa hay una opción razonable para hacer otra cosa, la más urgente de las cuales es serializar en otro lugar que no sea la memoria, como un archivo, TCP / IP. flujo, base de datos, etc. En general, no es tan detallado.

Jon Hanna
fuente
4
También. Si desea suprimir la lista de materiales, puede utilizarXmlWriter.Create(memoryStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false) }) .
ony
Si alguien (como yo) necesita leer el XML creado como muestra Jon, recuerde reposicionar el flujo de memoria a 0; de lo contrario, obtendrá una excepción que dice "Falta el elemento raíz". Entonces haz esto: memStm.Position = 0; XmlReader xmlReader = XmlReader.Create (memStm)
Sudhanshu Mishra
276

No, puede usar a StringWriterpara deshacerse del intermedio MemoryStream. Sin embargo, para forzarlo a XML, debe usar un StringWriterque anule la Encodingpropiedad:

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

O si aún no está usando C # 6:

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

Luego:

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

Obviamente, puede convertirlo Utf8StringWriteren una clase más general que acepte cualquier codificación en su constructor, pero en mi experiencia, UTF-8 es, con mucho, la codificación "personalizada" más comúnmente requerida para un StringWriter:)

Ahora, como dice Jon Hanna, esto seguirá siendo UTF-16 internamente, pero presumiblemente lo pasará a otra cosa en algún momento, para convertirlo en datos binarios ... en ese momento puede usar la cadena anterior, conviértalo en bytes UTF-8 y todo estará bien, porque la declaración XML especificará "utf-8" como codificación.

EDITAR: Un ejemplo breve pero completo para mostrar este funcionamiento:

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>

Tenga en cuenta la codificación declarada de "utf-8" que es lo que queríamos, creo.

Jon Skeet
fuente
2
Incluso cuando anula el parámetro Encoding en StringWriter, todavía envía los datos escritos a StringBuilder, por lo que sigue siendo UTF-16. Y la cadena solo puede ser UTF-16.
Jon Hanna
3
@Jon: ¿Lo has probado? Yo tengo, y funciona. Es la codificación declarada lo que es importante aquí; obviamente, internamente, la cadena sigue siendo UTF-16, pero eso no hace ninguna diferencia hasta que se convierte a binario (que podría usar cualquier codificación, incluido UTF-8). El TextWriter.Encodingserializador XML utiliza la propiedad para determinar qué nombre de codificación especificar dentro del documento.
Jon Skeet
2
@Jon: ¿Y cuál fue la codificación declarada? En mi experiencia, eso es lo que preguntas como esta realmente intentan hacer: crear un documento XML que se declare en UTF-8. Como usted dice, lo mejor es no tener en cuenta el texto que estar en cualquier codificación hasta que necesite ... pero a medida que el documento XML declara una codificación, eso es algo que hay que tener en cuenta.
Jon Skeet
2
@Garry, lo más simple que se me ocurre en este momento es tomar el segundo ejemplo en mi respuesta, pero cuando lo crea, XmlWriterhágalo con el método de fábrica que toma un XmlWriterSettingsobjeto y tiene la OmitXmlDeclarationpropiedad establecida en true.
Jon Hanna
4
+1 Su Utf8StringWritersolución es extremadamente agradable y limpia
Adriano Carneiro
17

Muy buena respuesta usando herencia, solo recuerde anular el inicializador

public class Utf8StringWriter : StringWriter
{
    public Utf8StringWriter(StringBuilder sb) : base (sb)
    {
    }
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}
Sebastián Castaldi
fuente
gracias, creo que esta es la opción más elegante
Prokurors
5

Encontré esta publicación de blog que explica muy bien el problema y define algunas soluciones diferentes:

(enlace muerto eliminado)

Me he conformado con la idea de que la mejor manera de hacerlo es omitir completamente la declaración XML cuando está en la memoria. Realmente es UTF-16 en ese punto de todos modos, pero la declaración XML no parece significativa hasta que se ha escrito en un archivo con una codificación particular; e incluso entonces no se requiere la declaración. No parece romper la deserialización, al menos.

Como menciona @Jon Hanna, esto se puede hacer con un XmlWriter creado así:

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