Использование 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 при поиске в Google.

Во-вторых, конечно: если вы не должны делать это с помощью 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 и вуаля - все заработало.

Проблема: я действительно не понимаю, почему.

Кристиан Хейтер: С этими тестами я не уверен, что мне нужно использовать utf-16 для записи в БД. Тогда не будет ли работать кодировка UTF-16 (в теге xml)?

Паническое бегствоXV
источник
1
Собираюсь на личном опыте. SQL Server принимает только UTF-16, и если вы передадите ему что-нибудь еще, вы окажетесь во власти синтаксического анализатора SQL Server XML и его попыток преобразовать данные. Вместо того, чтобы пытаться найти способ обмануть его, я просто передаю UTF-16 напрямую, что всегда будет работать.
Кристиан Хейтер,
Как вы это записываете в базу данных? Вы передаете ему строку, массив байтов или записываете в поток? Если это одна из двух последних форм, вам необходимо убедиться, что заявленная вами кодировка соответствует фактической кодировке ваших двоичных данных.
Джон Скит,
уф. Ручную попытку я сделал как запрос в MS SQL Management Studio. «Закодированные» попытки записывались в строку, которая затем передавалась в O / R Mapper, который записывал как строку (насколько я мог понять). Фактически я передаю ему строку, которая была создана в двух примерах, приведенных в моем вопросе.
StampedeXV,
К вашему сведению читателям - почти дубликаты: stackoverflow.com/questions/384974/… и stackoverflow.com/questions/3760788/…
ziesemer
1
Я меняю свой принятый ответ, поскольку считаю, что он действительно отвечает на мой вопрос. Несмотря на то, что другие ответы помогли мне продолжить мою работу, я думаю, что для Stackoverflow ответ Соломона поможет другим лучше понять, что произошло. [Отказ от ответственности]: У меня не было времени, чтобы действительно проверить ответ.
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.NVarChar(maxsize = -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 ". Никакого специального кодирования не требуется, и код Кристиана хорош, но мое тестирование показывает, что он возвращает сериализацию, идентичную вашему 2-му блоку кода, за исключением того, что ваш помещает 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 05
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», но строка «заголовок» удаляется. То, что вы видите в результирующей записи, просто

<test></test>

Использование метода сериализации, описанного в записи «Ответил», - это способ включения исходного заголовка в целевое поле, но в результате оставшийся текст XML заключен в <string></string>тег XML .

Адаптер таблицы в коде - это класс, автоматически созданный с помощью мастера Visual Studio 2013 «Добавить новый источник данных:». Пять параметров для вставки метода сопоставляют поля в таблице SQL Server.

DLG
источник
2
Заменить? Это весело.
mgilberties
2
Серьезно - не делай этого. Когда-либо. Что, если бы я хотел включить в свой xml какую-нибудь прозу, в которой упоминается «UTF-8» - вы только что изменили мои данные на то, чего я не сказал!
Tim Abell
2
Спасибо, что указали на ошибку в коде. Вместо bodyXML.Replace («UTF-8», «UTF-16») должен быть код, который фокусируется на изменении заголовка XML с UTF-8 на UTF-16. На что я действительно пытался указать, так это путем внесения этого изменения в заголовок исходного XML, после чего тело XML можно затем вставить в запись таблицы SQL с использованием поля типа данных XML, а заголовок будет удален. По причинам, которые я сейчас не помню (четыре года назад!), В то время результат был чем-то полезным. И да, глупая ошибка при использовании «Заменить». Такое случается.
DLG