web-dev-qa-db-fra.com

Utilisation de StringWriter pour la sérialisation XML

Je suis actuellement à la recherche d'un moyen facile de sérialiser des objets (en C # 3).

J'ai googlé quelques exemples et j'ai trouvé 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 ceci question Je me suis demandé 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éré par XML ne pouvait pas simplement être écrit dans une colonne XML de la base de données SQL Server 2005.

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

Le second est, bien sûr: si vous ne le faites pas avec StringWriter (quelle que soit la raison), quelle serait la bonne et correcte méthode?


Une addition:

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

En écrivant 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 basculer le codage

Pour ficelle

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

J'ai pris la chaîne créée à partir de XmlTextWriter et je l'ai simplement mise en xml. Celui-ci n'a pas fonctionné (ni avec l'insertion manuelle dans la base de données).

Ensuite, j'ai essayé l'insertion manuelle (en écrivant simplement INSERT INTO ...) avec encoding = "utf-16", qui a également échoué. Supprimer l'encodage a totalement fonctionné alors. 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.

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 du codage sur UTF-16 (dans la balise xml) ne fonctionnerait-elle pas alors?

92
StampedeXV

<TL; DR> Le problème est assez simple. En réalité, vous ne faites pas correspondre le codage déclaré (dans la déclaration XML) avec le type de données de l'entrée. paramètre. Si vous avez ajouté manuellement _<?xml version="1.0" encoding="utf-8"?><test/>_ à la chaîne, le fait de déclarer SqlParameter de type _SqlDbType.Xml_ ou _SqlDbType.NVarChar_ vous donnerait alors l'erreur "Impossible de changer l'encodage". Ensuite, lors de l’insertion manuelle via T-SQL, puisque vous aviez commuté le codage déclaré sur _utf-16_, vous insériez clairement une chaîne VARCHAR (non précédée d’un "N" majuscule, d’où un Encodage sur 8 bits, tel que UTF-8) et non pas sur une chaîne NVARCHAR (préfixée par un "N" majuscule, d’où le codage LE à 16 bits UTF-16).

Le correctif aurait dû être aussi simple que:

  1. Dans le premier cas, lors de l'ajout de la déclaration indiquant _encoding="utf-8"_: simplement, n'ajoutez pas la déclaration XML.
  2. Dans le second cas, lors de l’ajout de la déclaration indiquant _encoding="utf-16"_: soit
    1. il suffit simplement de ne pas ajouter la déclaration XML, OU
    2. ajoutez simplement un "N" au type de paramètre d'entrée: _SqlDbType.NVarChar_ au lieu de _SqlDbType.VarChar_ :-) (ou éventuellement, passez à l'aide de _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 de Jon, respectivement). Ils peuvent fournir du code de travail, 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 concerne en définitive 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 au format XML. L'enregistrement de données XML dans SQL Server est beaucoup plus simple que ce qui est suggéré ici.

La façon dont le code XML est généré importe peu tant que vous suivez les règles de création de données XML dans SQL Server. J'ai une explication plus complète (y compris l'exemple de code de travail pour illustrer les points décrits ci-dessous) dans une réponse à cette question: Comment résoudre l'erreur "incapable de changer l'encodage" lors de l'insertion de XML dans SQL Server , mais les bases sont:

  1. La déclaration XML est optionnelle
  2. Le type de données XML stocke toujours les chaînes en tant que UCS-2/UTF-16 LE
  3. Si votre code XML est UCS-2/UTF-16 LE, vous:
    1. transmettez les données sous la forme NVARCHAR(MAX) ou XML/_SqlDbType.NVarChar_ (taille maximale = -1) ou _SqlDbType.Xml_, ou si vous utilisez un littéral de chaîne, vous devez le préfixer avec un "N" majuscule.
    2. si vous spécifiez la déclaration XML, il doit s'agir de "UCS-2" ou de "UTF-16" (aucune différence réelle ici)
  4. Si votre code XML est codé sur 8 bits (par exemple, "UTF-8"/"iso-8859-1"/"Windows-1252"), vous:
    1. nécessité de spécifier la déclaration XML SI le codage est différent de la page de code 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 un littéral de chaîne, vous devez pas être préfixé par un "N" majuscule.
    3. Quel que soit le codage à 8 bits utilisé, le "codage" indiqué dans la déclaration XML doit correspondre au codage réel des octets.
    4. Le codage sur 8 bits sera converti en UTF-16 LE par le type de données XML.

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

Existe-t-il une raison pour laquelle je ne devrais pas utiliser StringWriter pour sérialiser un objet lorsque j'en ai besoin par la suite?

Non, votre code StringWriter semble être parfait (du moins, je ne vois aucun problème dans mes tests limités utilisant le deuxième bloc de code de la question).

La définition du codage 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 if vous transmettez la chaîne à SQL Server sous la forme NVARCHAR (c'est-à-dire _SqlDbType.NVarChar_) ou XML (ie _SqlDbType.Xml_). Le codage est supposé être la page de code sur 8 bits par défaut si elle est passée sous la forme VARCHAR (c'est-à-dire _SqlDbType.VarChar_). Si vous avez des caractères non standard ASCII (c'est-à-dire les valeurs 128 et supérieures) et que vous passez en tant que VARCHAR, alors vous verrez probablement "?" for BMP caractères et "??" pour Caractères supplémentaires comme SQL Server convertira la chaîne UTF-16 de .NET en chaîne de 8 bits de la page de code de la base de données actuelle avant de la reconvertir en UTF-16/UCS-2, mais vous ne devriez pas avoir d'erreur.

D'autre part, si vous spécifiez la déclaration XML, vous passez must dans SQL Server en utilisant le type de données 8 ou 16 bits correspondant. Donc, si vous avez une déclaration indiquant que le codage est UCS-2 ou UTF-16, alors vous devez vous passez en tant que _SqlDbType.NVarChar_ ou _SqlDbType.Xml_. Ou bien, si vous avez une déclaration indiquant que le codage est l'une des options à 8 bits (c.-à-d. _UTF-8_, _Windows-1252_, _iso-8859-1_, etc.), vous devez passer en tant que _SqlDbType.VarChar_. Si vous ne faites pas correspondre le codage déclaré au type de données SQL Server approprié 8 ou 16 bits, vous obtiendrez l'erreur "Impossible de changer le codage".

Par exemple, en utilisant votre code de sérialisation basé sur StringWriter, j'ai simplement imprimé la chaîne résultante du code XML et je l'ai utilisée dans SSMS. Comme vous pouvez le voir ci-dessous, la déclaration XML est incluse (parce que StringWriter n'a pas d'option pour OmitXmlDeclaration comme XmlWriter a), 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 constater, il gère même les caractères au-delà du code ASCII standard, étant donné que __ est BMP Point de code U + 1234 et _????_ est un caractère supplémentaire. Point de code U + 1F638 Cependant, les éléments suivants:

_-- 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, toute cette explication mise à part, la solution complète à votre question initiale est la suivante:

Vous avez clairement passé la chaîne sous la forme _SqlDbType.VarChar_. Passez à _SqlDbType.NVarChar_ et cela fonctionnera sans avoir à passer par l'étape supplémentaire consistant à supprimer la déclaration XML. Ceci est préférable à conserver _SqlDbType.VarChar_ et à supprimer 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 constater, il n'y a pas d'erreur cette fois, mais maintenant il y a une perte de données.

1
Solomon Rutzky

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

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

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

Ou si vous n’avez besoin que d’UTF-8 (ce dont j’ai souvent besoin):

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

Pour ce qui est de savoir pourquoi vous ne pouvez pas enregistrer votre code 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 diagnostiquer/résoudre le problème.

203
Jon Skeet

Lors de la sérialisation d'un document XML en chaîne .NET, le codage doit être défini sur UTF-16. Les chaînes sont stockées en tant que 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 une colonne xml doit être codée au format UTF-16. SQL Server rejettera toute chaîne pour 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 définie par défaut sur UTF-8. SQL Server la rejettera également.

Gardant cela à l’esprit, voici quelques méthodes d’utilité 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);
        }
    }
}
126
Christian Hayter

Tout d’abord, méfiez-vous des anciens exemples. Vous en avez trouvé un qui utilise XmlTextWriter, qui est obsolète à partir de .NET 2.0. XmlWriter.Create devrait ê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();
                }
            }
        }
    }
}
19
John Saunders
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;
}
1
Mashudu Nemukuka