Usando StringWriter para la serialización XML

99

Actualmente estoy buscando una forma fácil de serializar objetos (en C # 3).

Busqué en Google algunos ejemplos y se me ocurrió algo como:

MemoryStream memoryStream = new MemoryStream ( );
XmlSerializer xs = new XmlSerializer ( typeof ( MyObject) );
XmlTextWriter xmlTextWriter = new XmlTextWriter ( memoryStream, Encoding.UTF8 );
xs.Serialize ( xmlTextWriter, myObject);
string result = Encoding.UTF8.GetString(memoryStream .ToArray());

Después de leer esta pregunta, me pregunté, ¿por qué no usar StringWriter? Parece mucho más fácil.

XmlSerializer ser = new XmlSerializer(typeof(MyObject));
StringWriter writer = new StringWriter();
ser.Serialize(writer, myObject);
serializedValue = writer.ToString();

Otro problema fue que el primer ejemplo generó XML que no podía simplemente escribir en una columna XML de SQL Server 2005 DB.

La primera pregunta es: ¿Hay alguna razón por la que no debería usar StringWriter para serializar un Objeto cuando lo necesito como cadena después? Nunca encontré un resultado usando StringWriter al buscar en Google.

La segunda es, por supuesto: si no debería hacerlo con StringWriter (por las razones que sean), ¿cuál sería una forma buena y correcta?


Adición:

Como ya se mencionó en ambas respuestas, profundizaré en el problema de XML a DB.

Al escribir en la base de datos, obtuve la siguiente excepción:

System.Data.SqlClient.SqlException: análisis XML: línea 1, carácter 38, no se puede cambiar la codificación

Para cuerda

<?xml version="1.0" encoding="utf-8"?><test/>

Tomé la cadena creada a partir de XmlTextWriter y simplemente la puse como xml allí. Éste no funcionó (ni con inserción manual en la base de datos).

Luego probé la inserción manual (solo escribiendo INSERT INTO ...) con encoding = "utf-16" que también falló. Eliminar la codificación funcionó totalmente entonces. Después de ese resultado, volví al código StringWriter y listo, funcionó.

Problema: realmente no entiendo por qué.

en Christian Hayter: Con esas pruebas, no estoy seguro de tener que usar utf-16 para escribir en la base de datos. Entonces, ¿no funcionaría configurar la codificación en UTF-16 (en la etiqueta xml)?

StampedeXV
fuente
1
Voy por experiencia personal. SQL Server solo acepta UTF-16, y si le pasa algo más, está a merced del analizador XML de SQL Server y sus intentos de convertir los datos. En lugar de intentar encontrar una forma de engañarlo, simplemente le paso UTF-16 directamente, lo que siempre funcionará.
Christian Hayter
¿Cómo está escribiendo esto en la base de datos? ¿Le está pasando una cadena o una matriz de bytes o escribiendo en una secuencia? Si se trata de cualquiera de las dos últimas formas, debe asegurarse de que la codificación declarada coincida con la codificación real de sus datos binarios.
Jon Skeet
Uf. El intento manual que hice como Consulta en MS SQL Management Studio. Los intentos "codificados" se escribieron en una cadena que luego se pasó a un asignador O / R que escribe como una cadena (hasta donde pude seguir). De hecho, le estoy pasando la cadena que se creó en los dos ejemplos dados en mi pregunta.
StampedeXV
Para su información a los lectores, casi duplicados: stackoverflow.com/questions/384974/… y stackoverflow.com/questions/3760788/…
ziesemer
1
Estoy cambiando mi respuesta aceptada porque creo que realmente responde a mi pregunta. A pesar de que las otras respuestas me ayudaron a continuar mi trabajo, para el propósito de Stackoverflow, creo que la respuesta de Solomon ayudará a otros a comprender mejor lo que sucedió. [Descargo de responsabilidad]: No encontré tiempo para verificar realmente la respuesta.
StampedeXV

Respuestas:

1

<TL; DR> El problema es bastante simple, en realidad: no está haciendo coincidir la codificación declarada (en la declaración XML) con el tipo de datos del parámetro de entrada. Si agregó manualmente <?xml version="1.0" encoding="utf-8"?><test/>a la cadena, entonces declarar SqlParameterque es de tipo SqlDbType.Xmlo SqlDbType.NVarCharle daría el error "No se puede cambiar la codificación". Luego, al insertar manualmente a través de T-SQL, ya que cambió la codificación declarada para serutf-16 , claramente estaba insertando una VARCHARcadena (no con el prefijo "N" en mayúscula, por lo tanto, una codificación de 8 bits, como UTF-8) y no una NVARCHARcadena (con el prefijo "N" en mayúscula, de ahí la codificación UTF-16 LE de 16 bits).

La solución debería haber sido tan simple como:

  1. En el primer caso, al agregar la declaración que dice encoding="utf-8": simplemente no agregue la declaración XML.
  2. En el segundo caso, al agregar la declaración que indica encoding="utf-16" : o bien
    1. simplemente no agregue la declaración XML, O
    2. simplemente agregue una "N" al tipo de parámetro de entrada: en SqlDbType.NVarCharlugar de SqlDbType.VarChar:-) (o posiblemente incluso cambie a usar SqlDbType.Xml)

(La respuesta detallada está a continuación)


Todas las respuestas aquí son demasiado complicadas e innecesarias (independientemente de los 121 y 184 votos a favor de las respuestas de Christian y Jon, respectivamente). Es posible que proporcionen un código de trabajo, pero ninguno de ellos realmente responde a la pregunta. El problema es que nadie entendió realmente la pregunta, que en última instancia se trata de cómo funciona el tipo de datos XML en SQL Server. Nada en contra de esas dos personas claramente inteligentes, pero esta pregunta tiene poco o nada que ver con la serialización en XML. Guardar datos XML en SQL Server es mucho más fácil de lo que se implica aquí.

Realmente no importa cómo se produce el XML siempre que siga las reglas de cómo crear datos XML en SQL Server. Tengo una explicación más completa (incluido un código de ejemplo de trabajo para ilustrar los puntos que se describen a continuación) en una respuesta a esta pregunta: Cómo resolver el error "no se puede cambiar la codificación" al insertar XML en SQL Server , pero los conceptos básicos son:

  1. La declaración XML es opcional
  2. El tipo de datos XML almacena cadenas siempre como UCS-2 / UTF-16 LE
  3. Si su XML es UCS-2 / UTF-16 LE, entonces:
    1. pasar los datos como NVARCHAR(MAX)o XML/ SqlDbType.NVarChar(maxsize = -1) o SqlDbType.Xml, o si usa una cadena literal, debe tener el prefijo "N" en mayúscula.
    2. si especifica la declaración XML, debe ser "UCS-2" o "UTF-16" (no hay diferencia real aquí)
  4. Si su XML está codificado en 8 bits (por ejemplo, "UTF-8" / "iso-8859-1" / "Windows-1252"), entonces:
    1. es necesario especificar la declaración XML SI la codificación es diferente a la página de códigos especificada por la intercalación predeterminada de la base de datos
    2. debe pasar en los datos mientras que VARCHAR(MAX)/ SqlDbType.VarChar(maxsize = -1), o si se utiliza una cadena literal entonces debe no ser prefijado con una mayúscula "N".
    3. Cualquiera que sea la codificación de 8 bits que se utilice, la "codificación" indicada en la declaración XML debe coincidir con la codificación real de los bytes.
    4. La codificación de 8 bits se convertirá en UTF-16 LE por el tipo de datos XML

Teniendo en cuenta los puntos descritos anteriormente, y dado que las cadenas en .NET siempre son UTF-16 LE / UCS-2 LE (no hay diferencia entre ellas en términos de codificación), podemos responder a sus preguntas:

¿Hay alguna razón por la que no debería usar StringWriter para serializar un objeto cuando lo necesito como una cadena después?

No, su StringWritercódigo parece estar bien (al menos no veo problemas en mis pruebas limitadas usando el segundo bloque de código de la pregunta).

Entonces, ¿no funcionaría configurar la codificación en UTF-16 (en la etiqueta xml)?

No es necesario proporcionar la declaración XML. Cuando falta, se asume que la codificación es UTF-16 LE si pasa la cadena a SQL Server como NVARCHAR(ie SqlDbType.NVarChar) o XML(ie SqlDbType.Xml). Se supone que la codificación es la página de códigos de 8 bits predeterminada si se pasa como VARCHAR(es decir SqlDbType.VarChar). Si tiene caracteres ASCII no estándar (es decir, valores 128 y superiores) y los está pasando comoVARCHAR , es probable que vea "?" para caracteres BMP y "??" para caracteres suplementarios, ya que SQL Server convertirá la cadena UTF-16 de .NET en una cadena de 8 bits de la página de códigos de la base de datos actual antes de volver a convertirla en UTF-16 / UCS-2. Pero no debería recibir ningún error.

Por otro lado, si especifica la declaración XML, debe pasar a SQL Server utilizando el tipo de datos de 8 o 16 bits correspondiente. Entonces, si tiene una declaración que indica que la codificación es UCS-2 o UTF-16, debe pasar como SqlDbType.NVarCharo SqlDbType.Xml. O, si usted tiene una declaración de que la codificación es una de las opciones de 8 bits (es decir UTF-8, Windows-1252, iso-8859-1, etc), entonces usted debe pasar en tan SqlDbType.VarChar. Si no coincide la codificación declarada con el tipo de datos de SQL Server de 8 o 16 bits adecuado, se producirá el error "No se puede cambiar la codificación" que estaba recibiendo.

Por ejemplo, usando su StringWritercódigo de serialización basado en, simplemente imprimí la cadena resultante del XML y la usé en SSMS. Como puede ver a continuación, se incluye la declaración XML (porque StringWriterno tiene una opción para Me OmitXmlDeclarationgusta XmlWriter), lo que no plantea ningún problema siempre que pase la cadena como el tipo de datos SQL Server correcto:

-- Upper-case "N" prefix == NVARCHAR, hence no error:
DECLARE @Xml XML = N'<?xml version="1.0" encoding="utf-16"?>
<string>Test ሴ😸</string>';
SELECT @Xml;
-- <string>Test ሴ😸</string>

Como puede ver, incluso maneja caracteres más allá de ASCII estándar, dado que es el punto de código BMP U + 1234, y 😸es el punto de código de carácter suplementario U + 1F638. Sin embargo, lo siguiente:

-- No upper-case "N" prefix on the string literal, hence VARCHAR:
DECLARE @Xml XML = '<?xml version="1.0" encoding="utf-16"?>
<string>Test ሴ😸</string>';

da como resultado el siguiente error:

Msg 9402, Level 16, State 1, Line XXXXX
XML parsing: line 1, character 39, unable to switch the encoding

Ergo, dejando de lado toda esa explicación, la solución completa a su pregunta original es:

Claramente estabas pasando la cuerda como SqlDbType.VarChar. Cambie a SqlDbType.NVarChary funcionará sin necesidad de pasar por el paso adicional de eliminar la declaración XML. Se prefiere a mantener SqlDbType.VarChary eliminar la declaración XML porque esta solución evitará la pérdida de datos cuando el XML incluye caracteres ASCII no estándar. Por ejemplo:

-- No upper-case "N" prefix on the string literal == VARCHAR, and no XML declaration:
DECLARE @Xml2 XML = '<string>Test ሴ😸</string>';
SELECT @Xml2;
-- <string>Test ???</string>

Como puede ver, esta vez no hay ningún error, pero ahora hay pérdida de datos 🙀.

Salomón Rutzky
fuente
Creo que yo fui el motivo de estas respuestas demasiado complicadas, ya que básicamente tenía dos preguntas en una. Realmente me gusta su respuesta concisa y la probaré la próxima vez que tenga que almacenar XML en DB. Entonces, si veo esto bien: explicó los desafíos de almacenar XML en DB. Jon Skeet resumió los problemas con el uso de StringWriter cuando se trabaja con XML (excepto para UTF-16) y Christian Hayter proporciona una buena manera de trabajar con él.
StampedeXV
@StampedeXV Actualicé mi respuesta (algunos cambios para mayor claridad + cosas nuevas para ilustrar mejor los puntos). Con suerte, ahora está más claro que, si bien ambas respuestas son buenas por sí mismas, no son necesarias de ninguna manera para responder a su pregunta. Se ocupan de la serialización XML en C # / .NET, pero esta pregunta realmente se trata de guardar XML en SQL Server. Proporcionan información que es bueno saber y podría ser un código mejor que el que proporcionó originalmente, pero ninguno de ellos (ni ninguno de los demás aquí) está realmente en el tema. Pero esto no está bien documentado, de ahí la confusión.
Solomon Rutzky
@StampedeXV ¿Mis revisiones tenían sentido? Acabo de agregar una sección de resumen en la parte superior que podría ser más clara. En pocas palabras: a menos que haya algo más que no haya incluido en la pregunta, entonces parece que su código era correcto en un 99% y probablemente podría haberse solucionado con la adición de una sola mayúscula " NORTE". No se necesitan cosas especiales de codificación, y el código de Christian es bueno, pero mis pruebas muestran que devuelve una serialización idéntica a su segundo bloque de código, excepto que el suyo pone un CRLF después de la declaración XML. Apuesto a que cambiaste a SqlDbType.NVarCharo Xml.
Solomon Rutzky
todavía tratando de encontrar el tiempo para comprobarlo yo mismo. Ciertamente suena bien y lógico, pero no estoy seguro de que sea suficiente para cambiar una respuesta aceptada.
StampedeXV
216

Un problema StringWriteres que, de forma predeterminada , no le permite configurar la codificación que anuncia , por lo que puede terminar con un documento XML que anuncia su codificación como UTF-16, lo que significa que debe codificarlo como UTF-16 si escríbalo en un archivo. Sin embargo, tengo una clase pequeña para ayudar con eso:

public sealed class StringWriterWithEncoding : StringWriter
{
    public override Encoding Encoding { get; }

    public StringWriterWithEncoding (Encoding encoding)
    {
        Encoding = encoding;
    }    
}

O si solo necesita UTF-8 (que es todo lo que a menudo necesito):

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

En cuanto a por qué no pudo guardar su XML en la base de datos, tendrá que darnos más detalles sobre lo que sucedió cuando lo intentó, si desea que podamos diagnosticarlo / solucionarlo.

Jon Skeet
fuente
Entré en más detalles sobre el problema de la base de datos ahora. Ver pregunta.
StampedeXV
4
Triste StringWriter, no tiene en cuenta la codificación, pero no obstante, gracias por un pequeño método ingenioso :)
Chau
2
Y "Análisis XML: línea 1, carácter 38, no se puede cambiar la codificación" se puede resolver con "settings.Indent = false; settings.OmitXmlDeclaration = false;"
MGE
Por lo general, soluciono esto simplemente usando ay MemoryStreama StreamWritercon la codificación correcta. StreamWriter es un TextWriter(el tipo que XmlWriter.Createespera) con codificación personalizable, después de todo.
Nyerguds
2
@Nyerguds: Cree un paquete Nuget con este tipo de cosas, entonces siempre será fácil de conseguir. Prefiero hacer eso que comprometer la legibilidad del código, que se trata fundamentalmente de algún otro requisito.
Jon Skeet
126

Al serializar un documento XML en una cadena .NET, la codificación debe establecerse en UTF-16. Las cadenas se almacenan como UTF-16 internamente, por lo que esta es la única codificación que tiene sentido. Si desea almacenar datos en una codificación diferente, use una matriz de bytes en su lugar.

SQL Server funciona con un principio similar; cualquier cadena que se pase a una xmlcolumna debe codificarse como UTF-16. SQL Server rechazará cualquier cadena en la que la declaración XML no especifique UTF-16. Si la declaración XML no está presente, entonces el estándar XML requiere que esté predeterminado en UTF-8, por lo que SQL Server también lo rechazará.

Teniendo esto en cuenta, aquí hay algunos métodos de utilidad para realizar la conversión.

public static string Serialize<T>(T value) {

    if(value == null) {
        return null;
    }

    XmlSerializer serializer = new XmlSerializer(typeof(T));

    XmlWriterSettings settings = new XmlWriterSettings()
    {
        Encoding = new UnicodeEncoding(false, false), // no BOM in a .NET string
        Indent = false,
        OmitXmlDeclaration = false
    };

    using(StringWriter textWriter = new StringWriter()) {
        using(XmlWriter xmlWriter = XmlWriter.Create(textWriter, settings)) {
            serializer.Serialize(xmlWriter, value);
        }
        return textWriter.ToString();
    }
}

public static T Deserialize<T>(string xml) {

    if(string.IsNullOrEmpty(xml)) {
        return default(T);
    }

    XmlSerializer serializer = new XmlSerializer(typeof(T));

    XmlReaderSettings settings = new XmlReaderSettings();
    // No settings need modifying here

    using(StringReader textReader = new StringReader(xml)) {
        using(XmlReader xmlReader = XmlReader.Create(textReader, settings)) {
            return (T) serializer.Deserialize(xmlReader);
        }
    }
}
Christian Hayter
fuente
Consulte la adición de preguntas. No entiendo los resultados de mi prueba, parece contradecir su afirmación de que la base de datos siempre quiere / toma / necesita UTF-16.
StampedeXV
9
Usted no tiene que codificar como UTF-16 - pero usted tiene que asegurarse de que la codificación que utiliza coincide con lo que las StringWriterEspera. Mira mi respuesta. El formato de almacenamiento interno es irrelevante aquí.
Jon Skeet
ok, eso lo entiendo. En mi nuevo ejemplo: dejar la codificación completamente fuera hizo que la base de datos decidiera por sí misma qué codificación se usaba, por eso funcionó. ¿Lo entiendo bien ahora?
StampedeXV
1
@SteveC: Lo siento, mi error. Convertí manualmente el código de VB, en el que Nothingimplícitamente se puede convertir a cualquier tipo. He corregido el Deserializecódigo. La Serializeadvertencia debe ser solo para Resharper, el compilador por sí solo no se opone y es legal hacerlo.
Christian Hayter
1
Ampliando el comentario de Jon Skeet, no, no se requiere UTF-16. Consulte stackoverflow.com/a/8998183/751158 para obtener un ejemplo concreto que demuestre esto.
ziesemer
20

En primer lugar, tenga cuidado de no encontrar ejemplos antiguos. Ha encontrado uno que usa XmlTextWriter, que está obsoleto a partir de .NET 2.0. XmlWriter.Createdebería utilizarse en su lugar.

Aquí hay un ejemplo de serialización de un objeto en una columna XML:

public void SerializeToXmlColumn(object obj)
{
    using (var outputStream = new MemoryStream())
    {
        using (var writer = XmlWriter.Create(outputStream))
        {
            var serializer = new XmlSerializer(obj.GetType());
            serializer.Serialize(writer, obj);
        }

        outputStream.Position = 0;
        using (var conn = new SqlConnection(Settings.Default.ConnectionString))
        {
            conn.Open();

            const string INSERT_COMMAND = @"INSERT INTO XmlStore (Data) VALUES (@Data)";
            using (var cmd = new SqlCommand(INSERT_COMMAND, conn))
            {
                using (var reader = XmlReader.Create(outputStream))
                {
                    var xml = new SqlXml(reader);

                    cmd.Parameters.Clear();
                    cmd.Parameters.AddWithValue("@Data", xml);
                    cmd.ExecuteNonQuery();
                }
            }
        }
    }
}
John Saunders
fuente
2
Solo puedo votar esto una vez, pero esta merece ser la mejor respuesta aquí. Al final, no importa qué codificación se declare o se use, siempre y cuando se XmlReaderpueda analizar. Se enviará previamente analizado a la base de datos, y luego la base de datos no necesita saber nada sobre codificaciones de caracteres, UTF-16 o de otro tipo. En particular, tenga en cuenta que las declaraciones XML ni siquiera se conservan con los datos de la base de datos, independientemente del método que se utilice para insertarlos. No desperdicie ejecutando XML a través de conversiones adicionales, como se muestra en otras respuestas aquí y en otros lugares.
ziesemer
1
public static T DeserializeFromXml<T>(string xml)
{
    T result;
    XmlSerializerFactory serializerFactory = new XmlSerializerFactory();
    XmlSerializer serializer =serializerFactory.CreateSerializer(typeof(T));

    using (StringReader sr3 = new StringReader(xml))
    {
        XmlReaderSettings settings = new XmlReaderSettings()
        {
            CheckCharacters = false // default value is true;
        };

        using (XmlReader xr3 = XmlTextReader.Create(sr3, settings))
        {
            result = (T)serializer.Deserialize(xr3);
        }
    }

    return result;
}
Mashudu Nemukuka
fuente
-1

Es posible que se haya cubierto en otro lugar, pero simplemente cambiar la línea de codificación de la fuente XML a 'utf-16' permite que el XML se inserte en un tipo de datos 'xml' de SQL Server.

using (DataSetTableAdapters.SQSTableAdapter tbl_SQS = new DataSetTableAdapters.SQSTableAdapter())
{
    try
    {
        bodyXML = @"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><test></test>";
        bodyXMLutf16 = bodyXML.Replace("UTF-8", "UTF-16");
        tbl_SQS.Insert(messageID, receiptHandle, md5OfBody, bodyXMLutf16, sourceType);
    }
    catch (System.Data.SqlClient.SqlException ex)
    {
        Console.WriteLine(ex.Message);
        Console.ReadLine();
    }
}

El resultado es que todo el texto XML se inserta en el campo de tipo de datos 'xml', pero se elimina la línea de 'encabezado'. Lo que ves en el registro resultante es solo

<test></test>

El uso del método de serialización descrito en la entrada "Respondido" es una forma de incluir el encabezado original en el campo de destino, pero el resultado es que el texto XML restante se incluye en una <string></string>etiqueta XML .

El adaptador de tabla en el código es una clase creada automáticamente con el asistente "Agregar nueva fuente de datos: de Visual Studio 2013". Los cinco parámetros del método Insertar se asignan a los campos de una tabla de SQL Server.

DLG
fuente
2
¿Reemplazar? Esto es muy gracioso.
mgilberties
2
En serio, no hagas esto. Nunca. ¿Y si quisiera incluir algo de prosa en mi xml que mencione "UTF-8"? ¡Acabas de cambiar mis datos por algo que no dije!
Tim Abell
2
Gracias por señalar un error en el código. En lugar de bodyXML.Replace ("UTF-8", "UTF-16"), debería haber un código que se centre en el encabezado XML que cambia UTF-8 a UTF-16. Lo que realmente estaba tratando de señalar es al hacer este cambio en el encabezado del XML de origen, luego el cuerpo del XML se puede insertar en un registro de tabla SQL utilizando un campo de tipo de datos XML y el encabezado se elimina. Por razones que no recuerdo ahora (¡hace cuatro años!), El resultado fue algo útil en ese momento. Y sí, error tonto al usar 'Reemplazar'. Sucede.
DLG