Використання StringWriter для серіалізації XML


99

Зараз я шукаю простий спосіб серіалізації об’єктів (у C # 3).

Я погуглив кілька прикладів і придумав щось на зразок:

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

Прочитавши це запитання, я запитав себе, чому б не використовувати StringWriter? Це здається набагато простіше.

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

Ще одна проблема полягала в тому, що перший приклад, що генерував XML, я не міг просто записати у стовпець XML бази даних SQL Server 2005.

Перше питання: Чи є причина, чому я не повинен використовувати StringWriter для серіалізації об'єкта, коли мені він потрібен як рядок після цього? Я ніколи не знайшов результату за допомогою StringWriter під час гуглювання.

Друге, звичайно: Якщо ви не хочете робити це з StringWriter (з яких-небудь причин), що було б хорошим і правильним способом?


Доповнення:

Як уже було зазначено в обох відповідях, я продовжую переходити до проблеми XML до БД.

Під час написання до бази даних я отримав такий виняток:

System.Data.SqlClient.SqlException: Аналіз XML: рядок 1, символ 38, не в змозі переключити кодування

Для рядка

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

Я взяв рядок, створений з XmlTextWriter, і просто поставив туди як xml. Це не спрацювало (ні з ручним вставленням у БД).

Після цього я спробував вставити вручну (просто писав INSERT INTO ...) з encoding = "utf-16", який також не вдався. Видалення кодування тоді повністю спрацювало. Після цього результату я перейшов до коду StringWriter і voila - він спрацював.

Проблема: я не дуже розумію, чому.

Крістіан Хейтер: За допомогою цих тестів я не впевнений, що мені потрібно використовувати utf-16 для запису в БД. Не вдалося б тоді встановити кодування на UTF-16 (у тезі xml)?


1
Я продовжую особистий досвід. SQL Server приймає лише UTF-16, і якщо ви передасте щось інше, ви перебуваєте на милість аналізатора XML SQL Server та його спроб перетворити дані. Замість того, щоб намагатися знайти спосіб обдурити це, я просто передаю його безпосередньо UTF-16, який завжди буде працювати.
Крістіан Хейтер

Як ви це записуєте до бази даних? Ви передаєте йому рядок або масив байтів чи записуєте в потік? Якщо це будь-яка з двох останніх форм, вам потрібно переконатися, що заявлене кодування відповідає дійсному кодуванню ваших двійкових даних.
Джон Скіт,

феу. Посібник, який я зробив як запит у MS SQL Management Studio. "Зашифровані" спроби були записані в рядок, який потім був переданий O / R Mapper, який пише як рядок (наскільки я міг слідувати). Насправді я передаю їй рядок, який був створений у двох прикладах, наведених у моєму запитанні.
StampedeXV


1
Я змінюю прийняту відповідь, оскільки вважаю, що вона насправді відповідає на моє запитання. Навіть незважаючи на те, що інші відповіді допомагали мені продовжувати свою роботу, я вважаю, що відповідь Соломона допоможе іншим краще зрозуміти, що сталося. [Застереження]: Я не знайшов часу, щоб справді перевірити відповідь.
StampedeXV,

Відповіді:


1

<TL; DR> Насправді проблема досить проста: ви не узгоджуєте заявлене кодування (у декларації XML) з типом даних вхідного параметра. Якщо ви вручну додали <?xml version="1.0" encoding="utf-8"?><test/>до рядка, то оголосивши SqlParameterтип типу SqlDbType.Xmlабо SqlDbType.NVarCharпризведе до помилки "не в змозі переключити кодування". Потім, вставляючи вручну через T-SQL, оскільки ви переключили заявлене кодування на таке utf-16, ви чітко вставляли VARCHARрядок (не префіксовану великим регістром "N", отже, 8-бітове кодування, наприклад UTF-8) а не NVARCHARрядок (з префіксом верхнього регістру "N", отже, 16-бітове кодування UTF-16 LE).

Виправлення повинно бути таким же простим, як:

  1. У першому випадку, додаючи декларацію із зазначенням encoding="utf-8": просто не додайте декларацію XML.
  2. У другому випадку при додаванні декларації із зазначенням encoding="utf-16": або
    1. просто не додайте декларацію XML, АБО
    2. просто додайте "N" до типу вхідного параметра: SqlDbType.NVarCharзамість SqlDbType.VarChar:-) (або, можливо, навіть переключитесь на використання SqlDbType.Xml)

(Детальна відповідь нижче)


Усі відповіді тут є надмірно складними та непотрібними (незалежно від 121 та 184 голосів за відповіді Крістіана та Йона відповідно). Вони можуть надати робочий код, але жоден з них насправді не відповідає на питання. Проблема полягає в тому, що ніхто по-справжньому не зрозумів питання, що в кінцевому підсумку полягає в тому, як працює тип даних XML на SQL Server. Нічого проти цих двох явно розумних людей, але це питання майже не має нічого спільного з серіалізацією до XML. Збереження XML-даних у SQL Server набагато простіше, ніж те, що тут мається на увазі.

Насправді не важливо, як виробляється XML, якщо ви дотримуєтесь правил створення XML-даних на SQL Server. У мене є більш ретельне пояснення (включаючи код робочого прикладу, щоб проілюструвати наведені нижче пункти) у відповіді на це запитання: Як вирішити помилку "не в змозі переключити кодування" під час вставки XML у SQL Server , але основними є:

  1. Декларація XML не є обов'язковою
  2. Тип XML зберігає рядки завжди як UCS-2 / UTF-16 LE
  3. Якщо ваш XML - UCS-2 / UTF-16 LE, то ви:
    1. передавати дані як NVARCHAR(MAX)або XML/ / SqlDbType.NVarCharmaxsize = -1), або SqlDbType.Xml, якщо використовується літеральний рядок, то він повинен бути встановлений з великого регістру "N".
    2. якщо вказується декларація XML, вона повинна бути або "UCS-2", або "UTF-16" (тут немає ніякої реальної різниці)
  4. Якщо ваш XML кодований 8-бітовим (наприклад, "UTF-8" / "iso-8859-1" / "Windows-1252"), ви:
    1. необхідно вказати декларацію XML, якщо кодування відрізняється від сторінки коду, визначеної зіставленням бази даних за замовчуванням
    2. ви повинні передавати дані як VARCHAR(MAX)/ SqlDbType.VarChar(maxsize = -1), або якщо ви використовуєте рядковий літерал, то він не повинен бути префіксом з великого регістру "N".
    3. Що б не використовувалося 8-бітове кодування, "кодування", зазначене в декларації XML, повинно відповідати фактичному кодуванню байтів.
    4. 8-бітове кодування буде перетворено в UTF-16 LE за типом даних XML

Зважаючи на окреслені вище пункти та враховуючи, що рядки в .NET завжди є UTF-16 LE / UCS-2 LE (різниці між кодуванням немає), ми можемо відповісти на ваші запитання:

Чи є причина, чому я не повинен використовувати StringWriter для серіалізації об'єкта, коли мені він потрібен як рядок після цього?

Ні, ваш StringWriterкод здається чудовим (принаймні, я не бачу проблем у своєму обмеженому тестуванні з використанням другого блоку коду з питання).

Не вдалося б тоді встановити кодування на UTF-16 (у тезі xml)?

Не потрібно надавати декларацію XML. Якщо він відсутній, кодування вважається UTF-16 LE, якщо ви передаєте рядок у SQL Server як NVARCHAR(тобто SqlDbType.NVarChar) або XML(тобто SqlDbType.Xml). Кодування вважається 8-бітовою кодовою сторінкою за замовчуванням, якщо вона передається як VARCHAR(тобто SqlDbType.VarChar). Якщо у вас є будь-які нестандартні символи ASCII (тобто значення 128 і вище) і передаються як " VARCHAR," ви, ймовірно, побачите "?" для символів BMP та "??" для додаткових символів як SQL Server перетворить рядок UTF-16 з .NET в 8-бітну рядок кодової сторінки поточної бази даних, перш ніж перетворити її назад в UTF-16 / UCS-2. Але ви не повинні отримувати жодних помилок.

З іншого боку, якщо ви вказуєте декларацію XML, вам потрібно перейти в SQL Server, використовуючи відповідний 8-бітний або 16-бітний тип даних. Отже, якщо у вас є декларація, що вказує, що кодування є або UCS-2, або UTF-16, ви повинні ввести як SqlDbType.NVarCharабо SqlDbType.Xml. Або, якщо у вас є заява про те , що кодування є одним з 8-бітних варіантів (тобто UTF-8, Windows-1252, iso-8859-1і т.д.), то ви повинні пройти як SqlDbType.VarChar. Невідповідність заявленого кодування правильному типу даних 8 або 16 біт SQL Server призведе до помилки "не в змозі переключити кодування", яку ви отримували.

Наприклад, використовуючи ваш StringWriterкод серіалізації на основі, я просто надрукував отриманий рядок XML і використав його в SSMS. Як ви бачите нижче, декларація XML включена (оскільки StringWriterне має можливості OmitXmlDeclarationподобатися XmlWriter), що не створює проблем, якщо ви передаєте рядок у правильний тип даних 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>

Як бачите, він навіть обробляє символи, що перевищують стандартний ASCII, враховуючи, що це BMP- 😸кодова точка U + 1234, і є додатковою кодовою точкою коду U + 1F638. Однак наступне:

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

призводить до наступної помилки:

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

Ерго, все це пояснення вбік, повне рішення вашого початкового питання:

Ви чітко передавали рядок як SqlDbType.VarChar. Перейдіть на, SqlDbType.NVarCharі він працюватиме без необхідності пройти додатковий крок видалення декларації XML. Це краще, ніж зберігання SqlDbType.VarCharта видалення декларації XML, оскільки це рішення запобігає втраті даних, коли XML включає нестандартні символи ASCII. Наприклад:

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

Як бачите, помилок цього разу немає, але зараз є втрата даних 🙀.


Я думаю, що я був причиною цих складних відповідей, оскільки в основному у мене було два питання в одному. Мені дуже подобається ваша коротка відповідь, і я спробую її наступного разу, коли мені доведеться зберігати XML у БД. Тож якщо я бачу це правильно: ви пояснили проблеми зі зберіганням XML у БД. Джон Скіт резюмує проблеми з використанням StringWriter під час роботи з XML (за винятком UTF-16), а Крістіан Хейтер пропонує хороший спосіб просто працювати з ним.
StampedeXV

@StampedeXV Я оновив свою відповідь (кілька змін для ясності + нові речі, щоб краще проілюструвати точки). Сподіваємось, зараз зрозуміліше, що хоча обидва ці відповіді хороші самі по собі, вони ні в якому разі не потрібні для того, щоб відповісти на ваше запитання. Вони мають справу з серіалізацією XML у C # / .NET, але це питання справді стосується збереження XML у SQL Server. Вони надають інформацію, яку корисно знати, і може бути кращим кодом, ніж ви надавали спочатку, але жоден з них (ані жоден з інших тут) не є справді тематичним. Але це не добре задокументовані речі, звідси плутанина.
Соломон Руцький

@StampedeXV Чи мали сенс мої зміни? Я щойно додав розділ резюме вгорі, що може бути зрозумілішим. Коротше кажучи: якщо не відбулося щось інше, про що ви не включили деталі у питанні, то, схоже, ваш код був правильним на 99% і, можливо, його можна було б виправити, додавши одну велику регістр " N ". Спеціальні матеріали для кодування не потрібні, і код Крістіана приємний, але моє тестування показує, що він повертає серіалізацію, ідентичну вашому другому блоку коду, за винятком того, що ваш ставить CRLF після декларації XML. Б'юсь об заклад, ви змінили на SqlDbType.NVarCharабо Xml.
Соломон Руцкі

все ще намагаючись знайти час, щоб перевірити це сам. Це, звичайно, звучить добре і логічно, але не впевнений, що цього було б достатньо, щоб змінити прийняту відповідь.
StampedeXV

216

Одна з проблем StringWriterполягає в тому, що за замовчуванням він не дозволяє встановити кодування, яке він рекламує - так що ви можете закінчити документ XML, що рекламує його кодування як UTF-16, а це означає, що вам потрібно кодувати його як UTF-16, якщо ви запишіть його у файл. У мене є невеликий клас, який допоможе з цим:

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

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

Або якщо вам потрібен лише UTF-8 (а це все, що мені часто потрібно):

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

Що стосується того, чому ви не змогли зберегти свій XML у базі даних, вам доведеться повідомити нам більше подробиць про те, що сталося під час спроби, якщо ви хочете, щоб ми змогли це діагностувати / виправити.


Зараз я детальніше розібрався з проблемою бази даних. Дивіться питання.
StampedeXV

4
Сумно StringWriter, але не враховує кодування, але тим не менше, дякую за чудовий маленький метод :)
Чау

2
І "Синтаксичний розбір XML: рядок 1, символ 38, неможливо переключити кодування" можна вирішити за допомогою "settings.Indent = false; settings.OmitXmlDeclaration = false;"
MGE

Зазвичай я обходжу це шляхом простого використання a MemoryStreamі a StreamWriterз правильним кодуванням. StreamWriter цеTextWriter (тип , який XmlWriter.Createочікує) з можливістю настройки кодування, в кінці кінців.
Nyerguds

2
@Nyerguds: Тож створіть пакет Nuget з подібними речами, тоді до них завжди легко дістатись. Я вважаю за краще робити це, ніж компрометувати читабельність коду, що принципово стосується деяких інших вимог.
Джон Скіт

126

Під час послідовного введення XML-документа в рядок .NET кодування повинно бути встановлено на UTF-16. Струни зберігаються як UTF-16 внутрішньо, тому це єдине кодування, яке має сенс. Якщо ви хочете зберігати дані в іншому кодуванні, замість цього ви використовуєте байтовий масив.

SQL Server працює за аналогічним принципом; будь-яка рядок, що передається у xmlстовпець, повинна кодуватися як UTF-16. SQL Server відхилить будь-який рядок, де в декларації XML не вказано UTF-16. Якщо декларація XML відсутня, тоді стандарт XML вимагає, щоб за замовчуванням він використовував UTF-8, тому SQL Server також відхилить це.

Маючи це на увазі, ось деякі корисні методи здійснення конверсії.

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

Дивіться доповнення до питань. Я не розумію своїх результатів тесту, здається , це суперечить вашому твердженню, що БД завжди хоче / бере / потребує UTF-16.
StampedeXV

9
Вам не потрібно кодувати як UTF-16 - але ви повинні переконатися, що кодування, яке ви використовуєте, відповідає StringWriterочікуваному. Дивіться мою відповідь. Формат внутрішнього зберігання тут не має значення.
Джон Скіт

добре, що я розумію. У моєму новому прикладі: залишення кодування повністю змусило БД самостійно вирішити, яке кодування було використано - ось чому воно спрацювало. Я зараз це правильно розумію?
StampedeXV

1
@SteveC: Вибачте, моя помилка. Я вручну перетворив код з VB, який Nothingнеявно конвертується в будь-який тип. Я виправив Deserializeкод. SerializeПопередження повинне бути Resharper тільки річ, компілятор сам по собі не заперечує , і це законно зробити.
Крістіан Хейтер,

1
Поширюючи коментар Джона Скіта, ні, UTF-16 не потрібен. Будь ласка, зверніться до stackoverflow.com/a/8998183/751158 для конкретного прикладу, що демонструє це.
ziesemer

20

Перш за все, остерігайтеся пошуку старих прикладів. Ви знайшли такий, що використовує XmlTextWriter, який не підтримується на .NET 2.0. XmlWriter.Createслід використовувати замість цього.

Ось приклад серіалізації об’єкта в стовпець 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
Я можу проголосувати за це лише один раз, але це заслуговує на найкращу відповідь. Зрештою, не має значення, яке кодування оголошується чи використовується, доки його XmlReaderможна розібрати. Він буде відправлений попередньо проаналізованим до бази даних, і тоді БД не потрібно знати нічого про кодування символів - UTF-16 чи інше. Зокрема, зверніть увагу, що декларації XML навіть не зберігаються з даними в базі даних, незалежно від того, який метод використовується для їх вставки. Не витрачайте сміття, використовуючи XML за допомогою додаткових перетворень, як показано в інших відповідях тут і деінде.
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

Можливо, це було охоплено в іншому місці, але просто зміна рядка кодування джерела XML на 'utf-16' дозволяє XML вставляти у тип xml'data типу 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();
    }
}

В результаті весь текст XML вставляється в поле типу "xml", але рядок "header" видаляється. Те, що ви бачите в отриманому записі, просто

<test></test>

Використання методу серіалізації, описаного у записі "Відповів", є способом включення оригінального заголовка в цільове поле, але результат полягає в тому, що текст, що залишився XML, укладений у <string></string>тег XML .

Адаптер таблиці в коді - це клас, автоматично створений за допомогою Visual Studio 2013 "Майстер додавання нового джерела даних: майстер. П'ять параметрів до карти методу" Вставити "до полів у таблиці SQL Server.


2
Замінити? Це смішно.
mgilberties

2
Серйозно - не робіть цього. Колись. Що робити, якщо я хотів включити якусь прозу в мій xml, який згадував "UTF-8" - ти просто змінив мої дані на те, що я не сказав!
Тім Абелл

2
Дякуємо, що вказали на помилку в коді. Замість bodyXML.Replace ("UTF-8", "UTF-16") повинен бути код, який фокусується на заголовку XML, змінюючи UTF-8 на UTF-16. Те, що я насправді намагався вказати, це внести цю зміну в заголовок вихідного XML, тоді тіло XML може бути вставлено в запис таблиці SQL за допомогою поля типу даних XML, і заголовок позбавлений. З причин, які я зараз не пам'ятаю (чотири роки тому!), Результат був на той час корисним. І так, німа помилка використання "Замінити". Це буває.
DLG
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.