Utilizzo di StringWriter per la serializzazione XML


99

Attualmente sto cercando un modo semplice per serializzare gli oggetti (in C # 3).

Ho cercato su Google alcuni esempi e ho trovato qualcosa come:

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

Dopo aver letto questa domanda mi sono chiesto, perché non usare StringWriter? Sembra molto più facile.

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

Un altro problema era che il primo esempio di XML generato non potevo semplicemente scrivere in una colonna XML di SQL Server 2005 DB.

La prima domanda è: c'è un motivo per cui non dovrei usare StringWriter per serializzare un oggetto quando ne ho bisogno come stringa in seguito? Non ho mai trovato un risultato utilizzando StringWriter quando cercavo su Google.

Il secondo è, ovviamente: se non dovessi farlo con StringWriter (per qualsiasi motivo), quale sarebbe un modo buono e corretto?


Aggiunta:

Come già menzionato da entrambe le risposte, approfondirò il problema da XML a DB.

Durante la scrittura nel database ho ottenuto la seguente eccezione:

System.Data.SqlClient.SqlException: analisi XML: riga 1, carattere 38, impossibile cambiare la codifica

Per stringa

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

Ho preso la stringa creata da XmlTextWriter e l'ho inserita come xml. Questo non ha funzionato (nemmeno con l'inserimento manuale nel DB).

In seguito ho provato l'inserimento manuale (semplicemente scrivendo INSERT INTO ...) con encoding = "utf-16" che non è riuscito. La rimozione della codifica ha funzionato completamente quindi. Dopo quel risultato sono tornato al codice StringWriter e voilà - ha funzionato.

Problema: non capisco davvero perché.

a Christian Hayter: Con quei test non sono sicuro di dover usare utf-16 per scrivere sul DB. L'impostazione della codifica su UTF-16 (nel tag xml) non funzionerebbe allora?


1
Vado per esperienza personale. SQL Server accetta solo UTF-16 e, se gli passi qualcos'altro, sei in balia del parser XML di SQL Server e dei suoi tentativi di convertire i dati. Piuttosto che cercare di trovare un modo per ingannarlo, lo passo direttamente a UTF-16, che funzionerà sempre.
Christian Hayter

Come stai scrivendo questo nel database? Stai passando una stringa o un array di byte o stai scrivendo in un flusso? Se si tratta di una delle ultime due forme, è necessario assicurarsi che la codifica dichiarata corrisponda alla codifica effettiva dei dati binari.
Jon Skeet

uff. Il tentativo manuale che ho eseguito come Query in MS SQL Management Studio. I tentativi "codificati" sono stati scritti su una stringa che è stata poi passata a un O / R Mapper che scrive come una stringa (per quanto ho potuto seguire). Infatti gli sto passando la stringa che è stata creata nei due esempi dati nella mia domanda.
StampedeXV


1
Sto cambiando la mia risposta accettata perché credo che risponda effettivamente alla mia domanda. Anche se le altre risposte mi stavano aiutando a continuare il mio lavoro, ai fini di Stackoverflow penso che la risposta di Solomon aiuterà gli altri a capire meglio cosa è successo. [Disclaimer]: non ho trovato il tempo per verificare veramente la risposta.
StampedeXV

Risposte:


1

<TL; DR> Il problema è piuttosto semplice, in realtà: non stai facendo corrispondere la codifica dichiarata (nella dichiarazione XML) con il tipo di dati del parametro di input. Se hai aggiunto manualmente <?xml version="1.0" encoding="utf-8"?><test/>alla stringa, dichiarando SqlParameterdi essere di tipo SqlDbType.Xmlo SqlDbType.NVarCharti darebbe l'errore "impossibile cambiare la codifica". Quindi, durante l'inserimento manuale tramite T-SQL, poiché hai cambiato la codifica dichiarata in utf-16, stai chiaramente inserendo una VARCHARstringa (non preceduta da una "N" maiuscola, quindi una codifica a 8 bit, come UTF-8) e non una NVARCHARstringa (preceduta da una "N" maiuscola, da cui la codifica UTF-16 LE a 16 bit).

La correzione avrebbe dovuto essere semplice come:

  1. Nel primo caso, quando si aggiunge la dichiarazione che indica encoding="utf-8": semplicemente non aggiungere la dichiarazione XML.
  2. Nel secondo caso, quando si aggiunge la dichiarazione che dichiara encoding="utf-16": o
    1. semplicemente non aggiungere la dichiarazione XML, OR
    2. aggiungi semplicemente una "N" al tipo di parametro di input: SqlDbType.NVarCharinvece di SqlDbType.VarChar:-) (o eventualmente passa anche all'uso SqlDbType.Xml)

(La risposta dettagliata è sotto)


Tutte le risposte qui sono troppo complicate e non necessarie (indipendentemente dai 121 e 184 voti positivi per le risposte di Christian e Jon, rispettivamente). Potrebbero fornire codice funzionante, ma nessuno di loro risponde effettivamente alla domanda. Il problema è che nessuno ha capito veramente la domanda, che in ultima analisi riguarda il funzionamento del tipo di dati XML in SQL Server. Niente contro quelle due persone chiaramente intelligenti, ma questa domanda ha poco a che fare con la serializzazione in XML. Il salvataggio dei dati XML in SQL Server è molto più semplice di quanto implicito qui.

Non importa come viene prodotto l'XML purché si seguano le regole su come creare dati XML in SQL Server. Ho una spiegazione più approfondita (incluso un codice di esempio funzionante per illustrare i punti delineati di seguito) in una risposta a questa domanda: come risolvere l'errore "impossibile cambiare la codifica" quando si inserisce XML in SQL Server , ma le basi sono:

  1. La dichiarazione XML è facoltativa
  2. Il tipo di dati XML memorizza le stringhe sempre come UCS-2 / UTF-16 LE
  3. Se il tuo XML è UCS-2 / UTF-16 LE, allora:
    1. passare i dati come NVARCHAR(MAX)o XML/ SqlDbType.NVarChar(maxsize = -1) o SqlDbType.Xml, o se si utilizza una stringa letterale, deve essere preceduta da una "N" maiuscola.
    2. se si specifica la dichiarazione XML, deve essere "UCS-2" o "UTF-16" (nessuna differenza reale qui)
  4. Se il tuo XML è codificato a 8 bit (ad es. "UTF-8" / "iso-8859-1" / "Windows-1252"), allora:
    1. è necessario specificare la dichiarazione XML SE la codifica è diversa dalla tabella codici specificata dalle regole di confronto predefinite del database
    2. è necessario passare i dati come VARCHAR(MAX)/ SqlDbType.VarChar(maxsize = -1), oppure se si utilizza una stringa letterale allora deve non essere preceduto da un maiuscola "N".
    3. Qualunque sia la codifica a 8 bit utilizzata, la "codifica" annotata nella dichiarazione XML deve corrispondere alla codifica effettiva dei byte.
    4. La codifica a 8 bit verrà convertita in UTF-16 LE dal tipo di dati XML

Tenendo a mente i punti sopra delineati e dato che le stringhe in .NET sono sempre UTF-16 LE / UCS-2 LE (non c'è differenza tra quelle in termini di codifica), possiamo rispondere alle tue domande:

C'è un motivo per cui non dovrei usare StringWriter per serializzare un oggetto quando ne ho bisogno come stringa in seguito?

No, il tuo StringWritercodice sembra andare bene (almeno non vedo problemi nei miei test limitati usando il 2 ° blocco di codice dalla domanda).

L'impostazione della codifica su UTF-16 (nel tag xml) non funzionerebbe allora?

Non è necessario fornire la dichiarazione XML. Quando manca, si presume che la codifica sia UTF-16 LE se si passa la stringa in SQL Server come NVARCHAR(ie SqlDbType.NVarChar) o XML(ie SqlDbType.Xml). Si presume che la codifica sia la Code Page predefinita a 8 bit se passata come VARCHAR(cioè SqlDbType.VarChar). Se hai caratteri ASCII non standard (cioè valori 128 e superiori) e stai passando come VARCHAR, allora probabilmente vedrai "?" per i caratteri BMP e "??" per i caratteri supplementari come SQL Server convertirà la stringa UTF-16 da .NET in una stringa a 8 bit della pagina codici del database corrente prima di riconvertirla in UTF-16 / UCS-2. Ma non dovresti ricevere alcun errore.

D'altra parte, se si specifica la dichiarazione XML, è necessario passare a SQL Server utilizzando il tipo di dati corrispondente a 8 bit o 16 bit. Quindi, se hai una dichiarazione che afferma che la codifica è UCS-2 o UTF-16, devi passare come SqlDbType.NVarCharo SqlDbType.Xml. Oppure, se si dispone di una dichiarazione attestante che la codifica è una delle opzioni a 8 bit (cioè UTF-8, Windows-1252, iso-8859-1, ecc), allora si deve passare come SqlDbType.VarChar. La mancata corrispondenza della codifica dichiarata con il tipo di dati SQL Server corretto a 8 o 16 bit comporterà l'errore "impossibile cambiare la codifica" che stavi ricevendo.

Ad esempio, utilizzando il StringWritercodice di serializzazione basato sul tuo , ho semplicemente stampato la stringa risultante dell'XML e l'ho usata in SSMS. Come puoi vedere di seguito, la dichiarazione XML è inclusa (perché StringWriternon ha un'opzione per OmitXmlDeclarationapprezzare XmlWriter), il che non pone problemi fintanto che passi la stringa come il tipo di dati corretto di SQL Server:

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

Come puoi vedere, gestisce anche i caratteri oltre lo standard ASCII, dato che è BMP Code Point U + 1234, ed 😸è Supplementary Character Code Point U + 1F638. Tuttavia, quanto segue:

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

si traduce nel seguente errore:

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

Ergo, a parte tutte queste spiegazioni, la soluzione completa alla tua domanda originale è:

Stavi chiaramente passando la stringa come SqlDbType.VarChar. Passa a SqlDbType.NVarChare funzionerà senza dover eseguire il passaggio aggiuntivo di rimozione della dichiarazione XML. Ciò è preferibile rispetto a mantenere SqlDbType.VarChare rimuovere la dichiarazione XML perché questa soluzione impedirà la perdita di dati quando l'XML include caratteri ASCII non standard. Per esempio:

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

Come puoi vedere, questa volta non ci sono errori, ma ora c'è una perdita di dati 🙀.


Penso di essere stato il motivo di queste risposte troppo complicate, poiché fondamentalmente avevo due domande in una. Mi piace molto la tua risposta concisa e la proverò la prossima volta che dovrò memorizzare XML in DB. Quindi, se vedo bene questo: hai spiegato le sfide con l'archiviazione di XML su DB. Jon Skeet ha riassunto i problemi con l'utilizzo di StringWriter quando si lavora con XML (eccetto UTF-16) e Christian Hayter fornisce un bel modo per lavorarci.
StampedeXV

@StampedeXV ho aggiornato la mia risposta (alcune modifiche per chiarezza + nuove cose per illustrare meglio i punti). Si spera che ora sia più chiaro che, sebbene entrambe queste risposte siano buone da sole, non sono necessarie in alcun modo per rispondere alla tua domanda. Si occupano della serializzazione XML in C # / .NET, ma questa domanda riguarda davvero il salvataggio di XML in SQL Server. Forniscono informazioni utili e potrebbero essere un codice migliore di quello fornito originariamente, ma nessuno di loro (né nessuno degli altri qui) sono veramente in tema. Ma questa non è roba ben documentata, da qui la confusione.
Solomon Rutzky

@StampedeXV Le mie revisioni avevano senso? Ho appena aggiunto una sezione di riepilogo all'inizio che potrebbe essere più chiara. Per farla breve: a meno che non ci fosse qualcos'altro di cui non hai incluso i dettagli nella domanda, allora sembra che il tuo codice fosse corretto al 99% e probabilmente avrebbe potuto essere risolto con l'aggiunta di una singola maiuscola " N ". Non è necessaria alcuna codifica speciale e il codice di Christian è carino, ma i miei test mostrano che restituisce la serializzazione identica al tuo 2 ° blocco di codice, tranne che il tuo inserisce un CRLF dopo la dichiarazione XML. Scommetto che sei cambiato in SqlDbType.NVarCharo Xml.
Solomon Rutzky

sto ancora cercando di trovare il tempo per controllarlo da solo. Sicuramente suona bene e logico, ma non sono sicuro che sarebbe sufficiente per cambiare una risposta accettata.
StampedeXV

216

Un problema StringWriterè che per impostazione predefinita non ti consente di impostare la codifica che pubblicizza , quindi puoi finire con un documento XML che pubblicizza la sua codifica come UTF-16, il che significa che devi codificarlo come UTF-16 se lo desideri scriverlo su un file. Ho una piccola classe per aiutare con questo però:

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

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

O se hai solo bisogno di UTF-8 (che è tutto ciò di cui ho spesso bisogno):

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

Per quanto riguarda il motivo per cui non è stato possibile salvare il tuo XML nel database, dovrai fornirci maggiori dettagli su cosa è successo quando hai provato, se vuoi che siamo in grado di diagnosticare / aggiustarlo.


Sono entrato più in dettaglio per il problema del database ora. Vedi domanda.
StampedeXV

4
Purtroppo StringWriternon tiene conto della codifica, ma non di meno, grazie per un piccolo metodo ingegnoso :)
Chau

2
E "Analisi XML: riga 1, carattere 38, impossibile cambiare la codifica" può essere risolto da "settings.Indent = false; settings.OmitXmlDeclaration = false;"
MGE

Di solito aggiro questo problema semplicemente usando a MemoryStreame a StreamWritercon la codifica corretta. StreamWriter è un TextWriter(il tipo che si XmlWriter.Createaspetta) con codifica personalizzabile, dopotutto.
Nyerguds

2
@Nyerguds: Quindi crea un pacchetto Nuget con questo genere di cose, quindi è sempre facile arrivarci. Preferisco farlo piuttosto che compromettere la leggibilità del codice che è fondamentalmente su qualche altro requisito.
Jon Skeet

126

Quando si serializza un documento XML in una stringa .NET, la codifica deve essere impostata su UTF-16. Le stringhe sono archiviate internamente come UTF-16, quindi questa è l'unica codifica che ha senso. Se desideri memorizzare i dati in una codifica diversa, utilizza invece un array di byte.

SQL Server funziona secondo un principio simile; qualsiasi stringa passata in una xmlcolonna deve essere codificata come UTF-16. SQL Server rifiuterà qualsiasi stringa in cui la dichiarazione XML non specifica UTF-16. Se la dichiarazione XML non è presente, lo standard XML richiede che sia predefinita su UTF-8, quindi SQL Server rifiuterà anche quella.

Tenendo presente questo, ecco alcuni metodi di utilità per eseguire la conversione.

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

Vedi aggiunta domanda. Non capisco i risultati del mio test, sembra contraddire la tua affermazione che il DB vuole / prende / necessita sempre UTF-16.
StampedeXV

9
Non devi codificare come UTF-16, ma devi assicurarti che la codifica che usi corrisponda a ciò che si StringWriteraspetta. Vedi la mia risposta. Il formato di archiviazione interna è irrilevante qui.
Jon Skeet

ok ho capito. Nel mio nuovo esempio: lasciare completamente fuori la codifica ha fatto sì che il DB decidesse da solo quale codifica era stata utilizzata - ecco perché ha funzionato. Capisco che sia corretto adesso?
StampedeXV

1
@ SteveC: Scusa, errore mio. Ho convertito a mano il codice da VB, in cui Nothingè implicitamente convertibile in qualsiasi tipo. Ho corretto il Deserializecodice. L' Serializeavvertimento deve essere solo Resharper, il compilatore da solo non si oppone ed è legale farlo.
Christian Hayter,

1
Estendendo il commento di Jon Skeet, no, UTF-16 non è richiesto. Fare riferimento a stackoverflow.com/a/8998183/751158 per un esempio concreto che lo dimostri.
ziesemer

20

Prima di tutto, fai attenzione a trovare vecchi esempi. Ne hai trovato uno che utilizza XmlTextWriter, che è deprecato a partire da .NET 2.0. XmlWriter.Createdovrebbe essere usato invece.

Ecco un esempio di serializzazione di un oggetto in una colonna 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();
                }
            }
        }
    }
}

2
Posso votare solo una volta, ma questa merita di essere la risposta migliore qui. Alla fine, non importa quale codifica viene dichiarata o utilizzata, purché sia ​​in XmlReadergrado di analizzarla. Verrà inviato pre-analizzato al database, quindi il DB non ha bisogno di sapere nulla sulle codifiche dei caratteri - UTF-16 o altro. In particolare, si noti che le dichiarazioni XML non vengono nemmeno mantenute con i dati nel database, indipendentemente dal metodo utilizzato per inserirle. Per favore non sprecare eseguendo XML attraverso conversioni extra, come mostrato in altre risposte qui e altrove.
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;
}

-1

Potrebbe essere stato trattato altrove, ma la semplice modifica della riga di codifica del sorgente XML in "utf-16" consente di inserire l'XML in un tipo di dati xml di 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();
    }
}

Il risultato è che tutto il testo XML viene inserito nel campo del tipo di dati "xml" ma la riga "intestazione" viene rimossa. Quello che vedi nel record risultante è giusto

<test></test>

L'uso del metodo di serializzazione descritto nella voce "Con risposta" è un modo per includere l'intestazione originale nel campo di destinazione, ma il risultato è che il testo XML rimanente è racchiuso in un <string></string>tag XML .

L'adattatore tabella nel codice è una classe creata automaticamente utilizzando la procedura guidata Aggiungi nuova origine dati: di Visual Studio 2013. I cinque parametri del metodo di inserimento si associano ai campi in una tabella di SQL Server.


2
Sostituire? Questo è divertente.
mgilberties

2
Seriamente, non farlo. Mai. E se volessi includere una prosa nel mio xml che menziona "UTF-8" - hai appena cambiato i miei dati in qualcosa che non ho detto!
Tim Abell

2
Grazie per aver segnalato un errore nel codice. Invece di bodyXML.Replace ("UTF-8", "UTF-16") dovrebbe esserci codice che si concentra sull'intestazione XML che cambia UTF-8 in UTF-16. Quello che stavo davvero cercando di sottolineare è apportando questa modifica all'intestazione dell'XML di origine, quindi il corpo dell'XML può essere inserito in un record di tabella SQL utilizzando un campo di tipo di dati XML e l'intestazione viene rimossa. Per ragioni che non ricordo ora (quattro anni fa!) Il risultato era qualcosa di utile all'epoca. E sì, stupido errore usando "Sostituisci". Succede.
DLG
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.