Utilisation de StringWriter pour la sérialisation XML

99

Je recherche actuellement un moyen simple de sérialiser des objets (en C # 3).

J'ai googlé quelques exemples et suis venu avec quelque chose comme:

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

Après avoir lu cette question que je me suis posée, pourquoi ne pas utiliser StringWriter? Cela semble beaucoup plus facile.

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

Un autre problème était que le premier exemple générait du XML que je ne pouvais pas simplement écrire dans une colonne XML de SQL Server 2005 DB.

La première question est: y a-t-il une raison pour laquelle je ne devrais pas utiliser StringWriter pour sérialiser un objet lorsque j'en ai besoin sous forme de chaîne par la suite? Je n'ai jamais trouvé de résultat en utilisant StringWriter lors de la recherche sur Google.

La seconde est, bien sûr: si vous ne devriez pas le faire avec StringWriter (pour quelque raison que ce soit), quelle serait une bonne et correcte manière?


Une addition:

Comme cela a déjà été mentionné par les deux réponses, je vais approfondir le problème XML to DB.

Lors de l'écriture dans la base de données, j'ai eu l'exception suivante:

System.Data.SqlClient.SqlException: analyse XML: ligne 1, caractère 38, impossible de changer le codage

Pour chaîne

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

J'ai pris la chaîne créée à partir de XmlTextWriter et je l'ai juste mise au format xml. Celui-ci ne fonctionnait pas (ni avec insertion manuelle dans la base de données).

Ensuite, j'ai essayé l'insertion manuelle (juste en écrivant INSERT INTO ...) avec encoding = "utf-16" qui a également échoué. La suppression de l'encodage a alors totalement fonctionné. Après ce résultat, je suis revenu au code StringWriter et le tour est joué - cela a fonctionné.

Problème: je ne comprends pas vraiment pourquoi.

chez Christian Hayter: Avec ces tests, je ne suis pas sûr de devoir utiliser utf-16 pour écrire dans la base de données. La définition de l'encodage sur UTF-16 (dans la balise xml) ne fonctionnerait-elle pas alors?

StampedeXV
la source
1
Je pars en expérience personnelle. SQL Server accepte uniquement UTF-16, et si vous lui transmettez autre chose, vous êtes à la merci de l'analyseur XML SQL Server et de ses tentatives de conversion des données. Plutôt que d'essayer de trouver un moyen de le tromper, je lui passe juste UTF-16 directement, ce qui fonctionnera toujours.
Christian Hayter
Comment écrivez-vous cela dans la base de données? Passez-vous une chaîne ou un tableau d'octets ou écrivez-vous dans un flux? S'il s'agit de l'une des deux dernières formes, vous devez vous assurer que votre codage déclaré correspond au codage réel de vos données binaires.
Jon Skeet
phew. L'essai manuel que j'ai fait en tant que requête dans MS SQL Management Studio. Les essais "codés" ont été écrits dans une chaîne qui a ensuite été transmise à un mappeur O / R qui écrit sous forme de chaîne (pour autant que je puisse suivre). En fait, je lui transmets la chaîne créée dans les deux exemples donnés dans ma question.
StampedeXV
Pour info aux lecteurs - quasi-doublons: stackoverflow.com/questions/384974/… et stackoverflow.com/questions/3760788/…
ziesemer
1
Je change ma réponse acceptée car je pense qu'elle répond réellement à ma question. Même si les autres réponses m'ont aidé à continuer mon travail, aux fins de Stackoverflow, je pense que la réponse de Solomon aidera les autres à mieux comprendre ce qui s'est passé. [Avertissement]: Je n'ai pas trouvé le temps de vraiment vérifier la réponse.
StampedeXV

Réponses:

1

<TL; DR> Le problème est plutôt simple, en fait: vous ne faites pas correspondre le codage déclaré (dans la déclaration XML) avec le type de données du paramètre d'entrée. Si vous avez ajouté manuellement <?xml version="1.0" encoding="utf-8"?><test/>à la chaîne, alors déclarer le SqlParametercomme étant de type SqlDbType.Xmlou SqlDbType.NVarCharvous donnerait l'erreur «impossible de changer de codage». Ensuite, lors de l'insertion manuelle via T-SQL, puisque vous avez changé le codage déclaré pour être utf-16, vous insérez clairement une VARCHARchaîne (non préfixée par un «N» majuscule, d'où un codage 8 bits, tel que UTF-8) et non une NVARCHARchaîne (préfixée par un "N" majuscule, d'où le codage UTF-16 LE 16 bits).

Le correctif aurait dû être aussi simple que:

  1. Dans le premier cas, lors de l'ajout de la déclaration déclarant encoding="utf-8": n'ajoutez simplement pas la déclaration XML.
  2. Dans le second cas, lors de l'ajout de la déclaration indiquant encoding="utf-16": soit
    1. n'ajoutez simplement pas la déclaration XML, OU
    2. ajoutez simplement un "N" au type de paramètre d'entrée: SqlDbType.NVarCharau lieu de SqlDbType.VarChar:-) (ou éventuellement passez à using SqlDbType.Xml)

(La réponse détaillée est ci-dessous)


Toutes les réponses ici sont trop compliquées et inutiles (indépendamment des 121 et 184 votes positifs pour les réponses de Christian et Jon, respectivement). Ils peuvent fournir un code fonctionnel, mais aucun d'entre eux ne répond réellement à la question. Le problème est que personne n'a vraiment compris la question, qui porte finalement sur le fonctionnement du type de données XML dans SQL Server. Rien contre ces deux personnes clairement intelligentes, mais cette question n'a rien à voir avec la sérialisation vers XML. L'enregistrement des données XML dans SQL Server est beaucoup plus facile que ce qui est sous-entendu ici.

La façon dont le XML est produit n'a pas vraiment d'importance tant que vous suivez les règles de création de données XML dans SQL Server. J'ai une explication plus approfondie (y compris un exemple de code de travail pour illustrer les points décrits ci-dessous) dans une réponse à cette question: Comment résoudre l'erreur «impossible de changer de codage» lors de l'insertion de XML dans SQL Server , mais les bases sont:

  1. La déclaration XML est facultative
  2. Le type de données XML stocke toujours les chaînes sous la forme UCS-2 / UTF-16 LE
  3. Si votre XML est UCS-2 / UTF-16 LE, alors vous:
    1. transmettez les données sous la forme NVARCHAR(MAX)ou XML/ SqlDbType.NVarChar(maxsize = -1) ou SqlDbType.Xml, ou si vous utilisez une chaîne littérale, elle doit être précédée d'un "N" majuscule.
    2. si vous spécifiez la déclaration XML, elle doit être "UCS-2" ou "UTF-16" (pas de vraie différence ici)
  4. Si votre XML est codé 8 bits (par exemple, "UTF-8" / "iso-8859-1" / "Windows-1252"), vous:
    1. besoin de spécifier la déclaration XML SI l'encodage est différent de la page de codes spécifiée par le classement par défaut de la base de données
    2. vous devez transmettre les données sous la forme VARCHAR(MAX)/ SqlDbType.VarChar(maxsize = -1), ou si vous utilisez une chaîne littérale, elle ne doit pas être précédée d'un «N» majuscule.
    3. Quel que soit le codage 8 bits utilisé, le "codage" noté dans la déclaration XML doit correspondre au codage réel des octets.
    4. Le codage 8 bits sera converti en UTF-16 LE par le type de données XML

Avec les points décrits ci-dessus à l'esprit, et étant donné que les chaînes dans .NET sont toujours UTF-16 LE / UCS-2 LE (il n'y a aucune différence entre celles-ci en termes d'encodage), nous pouvons répondre à vos questions:

Y a-t-il une raison pour laquelle je ne devrais pas utiliser StringWriter pour sérialiser un objet lorsque j'en ai besoin sous forme de chaîne par la suite?

Non, votre StringWriter code semble très bien (au moins je ne vois aucun problème dans mes tests limités en utilisant le 2ème bloc de code de la question).

La définition de l'encodage sur UTF-16 (dans la balise xml) ne fonctionnerait-elle pas alors?

Il n'est pas nécessaire de fournir la déclaration XML. Lorsqu'il est manquant, le codage est supposé être UTF-16 LE si vous passez la chaîne dans SQL Server en tant que NVARCHAR(ie SqlDbType.NVarChar) ou XML(ie SqlDbType.Xml). Le codage est supposé être la page de codes 8 bits par défaut s'il est transmis en tant que VARCHAR(c.-à-d.SqlDbType.VarChar ). Si vous avez des caractères ASCII non standard (c'est-à-dire des valeurs de 128 et plus) et que VARCHARvous passez en tant que , vous verrez probablement "?" pour les caractères BMP et "??" pour les caractères supplémentaires, car SQL Server convertira la chaîne UTF-16 de .NET en une chaîne 8 bits de la page de codes de la base de données actuelle avant de la reconvertir en UTF-16 / UCS-2. Mais vous ne devriez pas avoir d'erreur.

En revanche, si vous spécifiez la déclaration XML, vous devez passer à SQL Server en utilisant le type de données 8 bits ou 16 bits correspondant. Donc, si vous avez une déclaration indiquant que le codage est soit UCS-2, soit UTF-16, vous devez passer en tant que SqlDbType.NVarCharou SqlDbType.Xml. Ou, si vous avez une déclaration indiquant que le codage est ( à savoir l' une des options 8 bits UTF-8, Windows-1252, iso-8859-1, etc.), alors vous devez passer comme SqlDbType.VarChar. Le fait de ne pas faire correspondre le codage déclaré avec le type de données SQL Server 8 ou 16 bits approprié entraînera l'erreur «Impossible de changer le codage» que vous obteniez.

Par exemple, en utilisant votre StringWritercode de sérialisation basé sur votre code, j'ai simplement imprimé la chaîne résultante du XML et l'ai utilisée dans SSMS. Comme vous pouvez le voir ci-dessous, la déclaration XML est incluse (car elle StringWritern'a pas d'option pour OmitXmlDeclarationaimer XmlWriter), ce qui ne pose aucun problème tant que vous passez la chaîne en tant que type de données SQL Server correct:

-- 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>

Comme vous pouvez le voir, il gère même les caractères au-delà de l'ASCII standard, étant donné qu'il s'agit du point de code BMP U + 1234 et du 😸point de code de caractère supplémentaire U + 1F638. Cependant, ce qui suit:

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

entraîne l'erreur suivante:

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

Ergo, toutes ces explications mises à part, la solution complète à votre question initiale est:

Vous passiez clairement la chaîne en tant que SqlDbType.VarChar. Basculez vers SqlDbType.NVarCharet cela fonctionnera sans avoir à passer par l'étape supplémentaire de suppression de la déclaration XML. Cela est préférable à la conservation SqlDbType.VarCharet à la suppression de la déclaration XML car cette solution empêchera la perte de données lorsque le XML comprend des caractères ASCII non standard. Par exemple:

-- 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>

Comme vous pouvez le voir, il n'y a pas d'erreur cette fois, mais maintenant il y a une perte de données 🙀.

Solomon Rutzky
la source
Je pense que j'étais la raison de ces réponses trop compliquées, car j'avais essentiellement deux questions en une. J'aime vraiment votre réponse concise et je vais l'essayer la prochaine fois que je dois stocker XML dans DB. Donc si je vois bien: vous avez expliqué les défis liés au stockage de XML dans DB. Jon Skeet a résumé les problèmes liés à l'utilisation de StringWriter lorsque vous travaillez avec XML (à l'exception de UTF-16) et Christian Hayter fournit une bonne façon de travailler simplement avec.
StampedeXV
@StampedeXV J'ai mis à jour ma réponse (quelques changements pour plus de clarté + des nouveautés pour mieux illustrer les points). J'espère qu'il est maintenant plus clair que si ces deux réponses sont bonnes en elles-mêmes, elles ne sont en aucun cas nécessaires pour répondre à votre question. Ils traitent de la sérialisation XML en C # / .NET, mais cette question concerne en réalité l'enregistrement de XML dans SQL Server. Ils fournissent des informations qu'il est bon de savoir et qui pourraient être un meilleur code que celui que vous avez fourni à l'origine, mais ni l'un ni l'autre (ni aucun des autres ici) ne sont vraiment sur le sujet. Mais ce n'est pas bien documenté, d'où la confusion.
Solomon Rutzky
@StampedeXV Mes révisions avaient-elles un sens? Je viens d'ajouter une section de résumé en haut qui pourrait être plus claire. Pour faire court: à moins qu'il y ait autre chose dont vous n'avez pas inclus de détails dans la question, il semble que votre code était correct à 99% et aurait probablement pu être corrigé en ajoutant une seule majuscule. " N ". Aucun encodage spécial n'est nécessaire, et le code de Christian est sympa, mais mes tests montrent qu'il renvoie une sérialisation identique à votre 2ème bloc de code, sauf que le vôtre met un CRLF après la déclaration XML. Je parie que vous avez changé pour SqlDbType.NVarCharou Xml.
Solomon Rutzky
essayant toujours de trouver le temps de le vérifier moi-même. Cela semble certainement bon et logique, mais pas sûr que ce soit suffisant pour modifier une réponse acceptée.
StampedeXV
216

Un problème avec StringWriterest que par défaut, il ne vous permet pas de définir l'encodage qu'il annonce - vous pouvez donc vous retrouver avec un document XML annonçant son encodage en UTF-16, ce qui signifie que vous devez l'encoder en UTF-16 si vous l'écrire dans un fichier. J'ai une petite classe pour aider avec ça:

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

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

Ou si vous n'avez besoin que de l'UTF-8 (ce dont j'ai souvent besoin):

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

Quant à savoir pourquoi vous n'avez pas pu enregistrer votre XML dans la base de données, vous devrez nous donner plus de détails sur ce qui s'est passé lorsque vous avez essayé, si vous voulez que nous puissions le diagnostiquer / le réparer.

Jon Skeet
la source
Je suis entré plus en détail pour le problème de la base de données maintenant. Voir la question.
StampedeXV
4
Triste le StringWriterne prend pas en compte l'encodage, mais jamais moins, merci pour une petite méthode astucieuse :)
Chau
2
Et "Analyse XML: ligne 1, caractère 38, impossible de changer le codage" peut être résolu par "settings.Indent = false; settings.OmitXmlDeclaration = false;"
MGE
Je contourne généralement cela en utilisant simplement a MemoryStreamet a StreamWriteravec le bon encodage. StreamWriter est un TextWriter(le type qui XmlWriter.Createattend) avec un encodage personnalisable, après tout.
Nyerguds
2
@Nyerguds: Alors créez un package Nuget avec ce genre de chose, alors c'est toujours facile d'y accéder. Je préfère faire cela plutôt que de compromettre la lisibilité du code qui est fondamentalement une autre exigence.
Jon Skeet
126

Lors de la sérialisation d'un document XML dans une chaîne .NET, le codage doit être défini sur UTF-16. Les chaînes sont stockées au format UTF-16 en interne, c'est donc le seul encodage qui ait du sens. Si vous souhaitez stocker des données dans un codage différent, utilisez plutôt un tableau d'octets.

SQL Server fonctionne sur un principe similaire; toute chaîne passée dans unxml colonne doit être codée en UTF-16. SQL Server rejettera toute chaîne dans laquelle la déclaration XML ne spécifie pas UTF-16. Si la déclaration XML n'est pas présente, la norme XML exige qu'elle soit par défaut UTF-8, donc SQL Server la rejettera également.

En gardant cela à l'esprit, voici quelques méthodes utilitaires pour effectuer la conversion.

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
la source
Voir l'ajout de question. Je ne comprends pas les résultats de mes tests, cela semble contredire votre déclaration selon laquelle la base de données veut toujours / prend / a besoin de l'UTF-16.
StampedeXV
9
Vous n'avez pas besoin d'encoder en UTF-16 - mais vous devez vous assurer que l'encodage que vous utilisez correspond à ce que vous attendez StringWriter. Voyez ma réponse. Le format de stockage interne n'est pas pertinent ici.
Jon Skeet
ok que je comprends. Dans mon nouvel exemple: laisser le codage complètement hors de la base de données a fait décider par lui-même quel codage était utilisé - c'est pourquoi cela fonctionnait. Est-ce que je comprends bien maintenant?
StampedeXV
1
@SteveC: Désolé, mon erreur. J'ai converti à la main le code de VB, dans lequel Nothingest implicitement convertible en n'importe quel type. J'ai corrigé le Deserializecode. L' Serializeavertissement doit être une chose réservée à Resharper, le compilateur seul ne fait pas d'objection et il est légal de le faire.
Christian Hayter
1
S'étendant sur le commentaire de Jon Skeet, non, UTF-16 n'est pas nécessaire. Veuillez consulter stackoverflow.com/a/8998183/751158 pour un exemple concret illustrant cela.
ziesemer
20

Tout d'abord, méfiez-vous de trouver d'anciens exemples. Vous en avez trouvé un qui utilise XmlTextWriter, qui est obsolète à partir de .NET 2.0.XmlWriter.Createdevrait être utilisé à la place.

Voici un exemple de sérialisation d'un objet dans une colonne 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
la source
2
Je ne peux voter qu'une seule fois, mais cela mérite d'être la meilleure réponse ici. En fin de compte, peu importe le codage déclaré ou utilisé, du moment que le XmlReaderpeut l'analyser. Il sera envoyé pré-analysé à la base de données, puis la base de données n'a pas besoin de savoir quoi que ce soit sur les encodages de caractères - UTF-16 ou autre. En particulier, notez que les déclarations XML ne sont même pas persistantes avec les données de la base de données, quelle que soit la méthode utilisée pour l'insérer. Veuillez ne pas gaspiller en exécutant XML via des conversions supplémentaires, comme indiqué dans d'autres réponses ici et ailleurs.
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
la source
-1

Cela a peut-être été traité ailleurs, mais le simple fait de changer la ligne de codage de la source XML en «utf-16» permet au XML d'être inséré dans le type xml'data d'un 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();
    }
}

Le résultat est que tout le texte XML est inséré dans le champ de type de données «xml» mais que la ligne «en-tête» est supprimée. Ce que vous voyez dans l'enregistrement résultant est juste

<test></test>

L'utilisation de la méthode de sérialisation décrite dans l'entrée "Answered" est un moyen d'inclure l'en-tête d'origine dans le champ cible, mais le résultat est que le texte XML restant est inclus dans une <string></string>balise XML .

L'adaptateur de table dans le code est une classe créée automatiquement à l'aide de l'assistant Ajouter une nouvelle source de données de Visual Studio 2013. Les cinq paramètres de la méthode Insertion sont mappés aux champs d'une table SQL Server.

DLG
la source
2
Remplacer? C'est hilarant.
mgilberties
2
Sérieusement - ne faites pas ça. Déjà. Et si je voulais inclure une prose dans mon XML qui mentionnait "UTF-8" - vous venez de changer mes données en quelque chose que je n'ai pas dit!
Tim Abell
2
Merci d'avoir signalé une erreur dans le code. Plutôt que bodyXML.Replace ("UTF-8", "UTF-16"), il devrait y avoir du code qui se concentre sur l'en-tête XML changeant UTF-8 en UTF-16. Ce que j'essayais vraiment de souligner, c'est qu'en apportant ce changement dans l'en-tête du XML source, le corps du XML peut ensuite être inséré dans un enregistrement de table SQL à l'aide d'un champ de type de données XML et l'en-tête est supprimé. Pour des raisons dont je ne me souviens pas maintenant (il y a quatre ans!), Le résultat était quelque chose d'utile à l'époque. Et oui, erreur stupide en utilisant «Remplacer». Ça arrive.
DLG