Serializacja obiektu jako XML UTF-8 w .NET

112

Prawidłowe usuwanie obiektów zostało usunięte dla zwięzłości, ale jestem zszokowany, jeśli jest to najprostszy sposób zakodowania obiektu jako UTF-8 w pamięci. Musi być łatwiejszy sposób, prawda?

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
źródło
1
Jestem zdezorientowany ... czy nie jest domyślne kodowanie UTF-8?
flq
@flq, tak, wartość domyślna to UTF-8, chociaż nie ma to większego znaczenia, ponieważ wczytuje go z powrotem do łańcucha, podobnie jak utf8EncodedXmlUTF-16.
Jon Hanna
1
@Garry, czy możesz wyjaśnić, skoro Jon Skeet i ja odpowiadamy na różne pytania. Czy chcesz, aby obiekt był serializowany jako UTF-8, czy chcesz ciąg XML, który deklaruje się jako UTF-8, a zatem będzie miał poprawną deklarację po późniejszym zakodowaniu w UTF-8? (w takim przypadku najprostszym sposobem jest brak deklaracji, ponieważ jest to ważne zarówno dla UTF-8, jak i UTF-16).
Jon Hanna,
@Jon Czytając wstecz, moje pytanie jest niejednoznaczne. Miałem to wyprowadzanie do ciągu głównie do celów debugowania. W praktyce prawdopodobnie przesyłałbym strumieniowo bajty na dysk lub przez HTTP, co sprawia, że ​​twoja odpowiedź jest bardziej bezpośrednio związana z moim problemem. Głównym problemem, jaki miałem, była deklaracja UTF-8 w XML, ale aby być dokładniejszym, powinienem unikać pośrednika ciągu, aby faktycznie wysyłać / utrwalać bajty UTF-8, a nie zależne od platformy (myślę) kodowanie.
Garry Shutler

Odpowiedzi:

55

Twój kod nie umieszcza UTF-8 w pamięci, gdy odczytujesz go z powrotem w łańcuchu, więc nie jest już w UTF-8, ale z powrotem w UTF-16 (chociaż najlepiej byłoby rozważyć łańcuchy na wyższym poziomie niż jakiekolwiek kodowanie, z wyjątkiem sytuacji, gdy jest to do tego zmuszone).

Aby uzyskać rzeczywiste oktety UTF-8, możesz użyć:

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

Pominąłem tę samą dyspozycję, którą zostawiłeś. Nieznacznie preferuję następujące (z pozostawieniem normalnej utylizacji):

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

Jest to mniej więcej taka sama złożoność, ale pokazuje, że na każdym etapie istnieje rozsądny wybór zrobienia czegoś innego, z czego najpilniejszą jest serializacja do innego miejsca niż do pamięci, na przykład do pliku TCP / IP strumień, baza danych itp. Podsumowując, nie jest to aż tak szczegółowe.

Jon Hanna
źródło
4
Również. Jeśli chcesz pominąć zestawienie komponentów, możesz użyć XmlWriter.Create(memoryStream, new XmlWriterSettings { Encoding = new UTF8Encoding(false) }).
ony
Jeśli ktoś (taki jak ja) musi odczytać XML utworzony tak, jak pokazuje Jon, pamiętaj, aby zmienić położenie strumienia pamięci na 0, w przeciwnym razie pojawi się wyjątek „Brak elementu głównego”. Zrób więc to: memStm.Position = 0; XmlReader xmlReader = XmlReader.Create (memStm)
Sudhanshu Mishra
277

Nie, możesz użyć a, StringWriteraby pozbyć się półproduktu MemoryStream. Jednak, aby wymusić na XML, musisz użyć a, StringWriterktóry zastępuje Encodingwłaściwość:

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

Lub jeśli jeszcze nie używasz C # 6:

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

Następnie:

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

Oczywiście możesz uczynić Utf8StringWriterbardziej ogólną klasę, która akceptuje dowolne kodowanie w swoim konstruktorze - ale z mojego doświadczenia wynika, że ​​UTF-8 jest zdecydowanie najczęściej wymaganym "niestandardowym" kodowaniem dla StringWriter:)

Teraz, jak mówi Jon Hanna, nadal będzie to wewnętrznie UTF-16, ale prawdopodobnie w pewnym momencie przekażesz go do czegoś innego, aby przekonwertować go na dane binarne ... w tym momencie możesz użyć powyższego ciągu, przekonwertuj go na bajty UTF-8 i wszystko będzie dobrze - ponieważ deklaracja XML określi "utf-8" jako kodowanie.

EDYCJA: Krótki, ale kompletny przykład pokazujący, jak działa:

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

Wynik:

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

Zwróć uwagę na deklarowane kodowanie „utf-8”, które, jak sądzę, było tym, czego chcieliśmy.

Jon Skeet
źródło
2
Nawet po zastąpieniu parametru Encoding w StringWriter nadal wysyła zapisane dane do StringBuilder, więc nadal jest to UTF-16. A ciąg może być zawsze tylko UTF-16.
Jon Hanna
4
@Jon: Czy próbowałeś tego? Mam i to działa. Tutaj ważne jest zadeklarowane kodowanie; oczywiście wewnętrznie łańcuch jest nadal UTF-16, ale to nie robi żadnej różnicy, dopóki nie zostanie przekonwertowany na binarny (który może używać dowolnego kodowania, w tym UTF-8). TextWriter.EncodingWłaściwość jest używana przez serializatora XML w celu określenia, które kodowanie nazwa określić w samym dokumencie.
Jon Skeet
2
@Jon: A jakie było deklarowane kodowanie? Z mojego doświadczenia wynika, że to, jakie pytania takie jak to są naprawdę starają się zrobić - stworzyć dokument XML, który deklaruje się być w UTF-8. Jak mówisz, to najlepiej, aby nie uwzględniać tekst być w dowolnym kodowaniem aż trzeba ... ale jako dokument XML deklaruje kodowania, to coś trzeba rozważyć.
Jon Skeet
2
@Garry, najprostszym, o jakim mogę teraz pomyśleć, jest wzięcie drugiego przykładu w mojej odpowiedzi, ale kiedy tworzysz XmlWriterto z metodą fabryczną, która przyjmuje XmlWriterSettingsobiekt i ma OmitXmlDeclarationwłaściwość ustawioną na true.
Jon Hanna
4
+1 Twoje Utf8StringWriterrozwiązanie jest niezwykle ładne i czyste
Adriano Carneiro
17

Bardzo dobra odpowiedź wykorzystująca dziedziczenie, pamiętaj tylko o nadpisaniu inicjalizatora

public class Utf8StringWriter : StringWriter
{
    public Utf8StringWriter(StringBuilder sb) : base (sb)
    {
    }
    public override Encoding Encoding { get { return Encoding.UTF8; } }
}
Sebastian Castaldi
źródło
dzięki, uważam, że jest to najbardziej elegancka opcja
Prokurors
5

Znalazłem ten wpis na blogu, który bardzo dobrze wyjaśnia problem i definiuje kilka różnych rozwiązań:

(usunięto martwy link)

Zdecydowałem się na pomysł, że najlepszym sposobem na to jest całkowite pominięcie deklaracji XML w pamięci. W rzeczywistości i tak jest to UTF-16, ale deklaracja XML nie wydaje się sensowna, dopóki nie zostanie zapisana w pliku z określonym kodowaniem; i nawet wtedy deklaracja nie jest wymagana. Przynajmniej nie wydaje się przerywać deserializacji.

Jak wspomina @Jon Hanna, można to zrobić za pomocą XmlWriter utworzonego w następujący sposób:

XmlWriter writer = XmlWriter.Create (output, new XmlWriterSettings() { OmitXmlDeclaration = true });
Dave Andersen
źródło