Sérialisation d'un objet au format XML UTF-8 dans .NET

112

L'élimination correcte des objets a été supprimée pour des raisons de brièveté, mais je suis choqué si c'est le moyen le plus simple d'encoder un objet en UTF-8 en mémoire. Il doit y avoir un moyen plus simple, n'est-ce pas?

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
la source
1
Je suis confus ... n'est-ce pas le codage par défaut UTF-8?
flq
@flq, oui, la valeur par défaut est UTF-8, même si cela n'a pas beaucoup d'importance puisqu'il la lit à nouveau dans une chaîne, tout utf8EncodedXmlcomme UTF-16.
Jon Hanna
1
@Garry, pouvez-vous clarifier, puisque Jon Skeet et moi répondons à des questions différentes. Voulez-vous que l'objet soit sérialisé en UTF-8, ou voulez-vous une chaîne XML qui se déclare comme UTF-8 et qui aura donc la déclaration correcte lorsqu'elle sera ultérieurement encodée en UTF-8? (dans ce cas, le moyen le plus simple est de ne pas avoir de déclaration, car c'est valable à la fois pour UTF-8 et UTF-16).
Jon Hanna
@Jon En relisant, il y a une ambiguïté dans ma question. Je l'ai fait sortir dans une chaîne principalement à des fins de débogage. En pratique, je serais probablement en train de diffuser des octets, soit sur disque, soit via HTTP, ce qui rend votre réponse plus directement pertinente par rapport à mon problème. Le principal problème que j'ai eu était la déclaration de UTF-8 dans le XML, mais pour être plus précis, je devrais éviter l'intermédiaire d'une chaîne afin que je fasse réellement envoyer / persister des octets UTF-8 plutôt qu'une plate-forme dépendante (je pense) codage.
Garry Shutler

Réponses:

55

Votre code n'obtient pas l'UTF-8 en mémoire lorsque vous le relisez à nouveau dans une chaîne, donc ce n'est plus en UTF-8, mais en UTF-16 (bien que, idéalement, il soit préférable de considérer les chaînes à un niveau supérieur à tout encodage, sauf lorsque cela est forcé).

Pour obtenir les octets UTF-8 réels, vous pouvez utiliser:

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

J'ai laissé de côté la même disposition que vous avez laissée. Je préfère légèrement ce qui suit (avec une élimination normale laissée):

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

Ce qui est à peu près le même niveau de complexité, mais montre qu'à chaque étape, il y a un choix raisonnable de faire autre chose, dont le plus urgent est de sérialiser vers un endroit autre que la mémoire, comme un fichier, TCP / IP flux, base de données, etc. Dans l'ensemble, ce n'est pas vraiment bavard.

Jon Hanna
la source
4
Aussi. Si vous souhaitez supprimer la nomenclature, vous pouvez utiliser XmlWriter.Create(memoryStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false) }).
ony
Si quelqu'un (comme moi) a besoin de lire le XML créé comme Jon le montre, n'oubliez pas de repositionner le flux de mémoire sur 0, sinon vous obtiendrez une exception disant "L'élément racine est manquant". Alors faites ceci: memStm.Position = 0; XmlReader xmlReader = XmlReader.Create (memStm)
Sudhanshu Mishra
276

Non, vous pouvez utiliser a StringWriterpour vous débarrasser de l'intermédiaire MemoryStream. Cependant, pour le forcer en XML, vous devez utiliser un StringWriterqui remplace la Encodingpropriété:

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

Ou si vous n'utilisez pas encore C # 6:

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

Ensuite:

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

De toute évidence, vous pouvez créer Utf8StringWriterune classe plus générale qui accepte n'importe quel encodage dans son constructeur - mais d'après mon expérience, UTF-8 est de loin l'encodage "personnalisé" le plus couramment requis pour un StringWriter:)

Maintenant, comme le dit Jon Hanna, ce sera toujours UTF-16 en interne, mais vous allez probablement le passer à autre chose à un moment donné, pour le convertir en données binaires ... à ce stade, vous pouvez utiliser la chaîne ci-dessus, convertissez-le en octets UTF-8, et tout ira bien - car la déclaration XML spécifiera "utf-8" comme encodage.

EDIT: Un exemple court mais complet pour montrer ce fonctionnement:

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

Résultat:

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

Notez l'encodage déclaré de "utf-8" qui est ce que nous voulions, je crois.

Jon Skeet
la source
2
Même lorsque vous remplacez le paramètre Encoding sur StringWriter, il envoie toujours les données écrites à un StringBuilder, donc c'est toujours UTF-16. Et la chaîne ne peut jamais être que UTF-16.
Jon Hanna
3
@Jon: Avez-vous essayé? J'ai, et ça marche. C'est l' encodage déclaré qui est important ici; évidemment en interne, la chaîne est toujours UTF-16, mais cela ne fait aucune différence jusqu'à ce qu'elle soit convertie en binaire (qui pourrait utiliser n'importe quel encodage, y compris UTF-8). La TextWriter.Encodingpropriété est utilisée par le sérialiseur XML pour déterminer le nom de codage à spécifier dans le document lui-même.
Jon Skeet
2
@Jon: Et quel était le codage déclaré? D'après mon expérience, c'est ce que des questions comme celle-ci essaient vraiment de faire - créer un document XML qui se déclare être en UTF-8. Comme vous le dites, il est préférable de ne pas considérer le texte comme étant dans un encodage jusqu'à ce que vous en ayez besoin ... mais comme le document XML déclare un encodage, c'est quelque chose que vous devez considérer.
Jon Skeet
2
@Garry, le plus simple auquel je puisse penser pour le moment est de prendre le deuxième exemple de ma réponse, mais lorsque vous créez le, XmlWriterfaites-le avec la méthode de fabrique qui prend un XmlWriterSettingsobjet et que la OmitXmlDeclarationpropriété est définie sur true.
Jon Hanna
4
+1 Votre Utf8StringWritersolution est extrêmement belle et propre
Adriano Carneiro
17

Très bonne réponse utilisant l'héritage, n'oubliez pas de remplacer l'initialiseur

public class Utf8StringWriter : StringWriter
{
    public Utf8StringWriter(StringBuilder sb) : base (sb)
    {
    }
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}
Sébastien Castaldi
la source
merci, je trouve que c'est la plus élégante des options
Prokurors
5

J'ai trouvé ce billet de blog qui explique très bien le problème, et définit quelques solutions différentes:

(lien mort supprimé)

J'ai opté pour l'idée que la meilleure façon de le faire est d'omettre complètement la déclaration XML lorsqu'elle est en mémoire. C'est en fait UTF-16 à ce stade de toute façon, mais la déclaration XML ne semble pas significative tant qu'elle n'a pas été écrite dans un fichier avec un codage particulier; et même alors, la déclaration n'est pas requise. Cela ne semble pas casser la désérialisation, au moins.

Comme le mentionne @Jon Hanna, cela peut être fait avec un XmlWriter créé comme ceci:

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