使用StringWriter进行XML序列化

Sta*_*eXV 94 c# xml sql-server utf-8 xml-serialization

我正在寻找一种简单的方法来序列化对象(在C#3中).

我用Google搜索了一些例子,并得出了类似的结果:

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());
Run Code Online (Sandbox Code Playgroud)

读完这个问题之后我问自己,为什么不使用StringWriter?看起来容易得多.

XmlSerializer ser = new XmlSerializer(typeof(MyObject));
StringWriter writer = new StringWriter();
ser.Serialize(writer, myObject);
serializedValue = writer.ToString();
Run Code Online (Sandbox Code Playgroud)

另一个问题是,第一个示例生成的XML我不能只写入SQL Server 2005 DB的XML列.

第一个问题是:在我之后需要它作为字符串时,是否有理由不使用StringWriter序列化Object?在google搜索时,我从未找到使用StringWriter的结果.

第二个当然是:如果你不应该使用StringWriter(无论出于何种原因),这将是一个好的和正确的方法?


加成:

正如两个答案已经提到的那样,我将进一步讨论XML到DB的问题.

写入数据库时​​,我得到以下异常:

System.Data.SqlClient.SqlException:XML解析:第1行,字符38,无法切换编码

对于字符串

<?xml version="1.0" encoding="utf-8"?><test/>
Run Code Online (Sandbox Code Playgroud)

我从XmlTextWriter中获取了字符串,然后将xml放在那里.这个没有用(手动插入DB).

之后我尝试使用encoding ="utf-16"手动插入(只是编写INSERT INTO ...),这也失败了.删除编码完全有效.在那个结果之后我切换回StringWriter代码并且瞧 - 它工作了.

问题:我真的不明白为什么.

在Christian Hayter:通过这些测试,我不确定我是否必须使用utf-16来写入数据库.不会将编码设置为UTF-16(在xml标签中)吗?

Jon*_*eet 207

一个问题StringWriter是,默认情况下,它不允许您设置它所宣传的编码 - 因此您最终可以使用XML文档将其编码通告为UTF-16,这意味着如果您需要将其编码为UTF-16把它写到文件中.我有一个小班来帮忙解决这个问题:

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

    public StringWriterWithEncoding (Encoding encoding)
    {
        Encoding = encoding;
    }    
}
Run Code Online (Sandbox Code Playgroud)

或者如果你只需要UTF-8(这是我经常需要的):

public sealed class Utf8StringWriter : StringWriter
{
    public override Encoding Encoding => Encoding.UTF8;
}
Run Code Online (Sandbox Code Playgroud)

至于为何无法将XML保存到数据库中 - 如果您希望我们能够诊断/修复它,您必须向我们提供有关您尝试时发生的事情的更多详细信息.

  • 遗憾的是`StringWriter`并没有考虑到编码,但从来没有考虑过,感谢一个漂亮的小方法:) (4认同)
  • 并且"XML解析:第1行,第38行,无法切换编码"可以通过"settings.Indent = false; settings.OmitXmlDeclaration = false;"来解决. (2认同)
  • @Nyerguds:所以用这种东西创建一个Nuget包,然后总是很容易理解.我宁愿这样做,也不要牺牲代码的可读性,这基本上是关于其他一些要求. (2认同)

Chr*_*ter 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 你*不必编码为UTF-16 - 但你必须确保你使用的编码符合`StringWriter`所期望的编码.看我的回答.内部存储格式与此无关. (8认同)

Joh*_*ers 19

首先,要小心找到旧的例子.您找到了一个使用的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();
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我只能投票一次,但这应该是最重要的答案.最后,只要`XmlReader`可以解析它,声明或使用什么编码无关紧要.它将被预先解析到数据库,然后DB不需要知道有关字符编码的任何信息 - UTF-16或其他.特别要注意的是,XML声明甚至不会与数据库中的数据一起保留,无论使用哪种方法插入它.请不要通过额外的转换运行XML来浪费,如此处和其他地方的其他答案所示. (2认同)

Sol*_*zky 1

<TL;DR>实际上,问题相当简单:您没有将声明的编码(在 XML 声明中)与输入参数的数据类型相匹配。如果您手动添加<?xml version="1.0" encoding="utf-8"?><test/>到字符串,则将其声明为orSqlParameter类型会出现“无法切换编码”错误。然后,当通过 T-SQL 手动插入时,由于您将声明的编码切换为,因此显然插入了一个字符串(没有以大写“N”为前缀,因此是 8 位编码,例如 UTF-8)而不是字符串(以大写“N”为前缀,因此是 16 位 UTF-16 LE 编码)。SqlDbType.XmlSqlDbType.NVarCharutf-16VARCHARNVARCHAR

\n\n

修复应该很简单:

\n\n
    \n
  1. 在第一种情况下,当添加声明时,说明encoding="utf-8":只需不要添加 XML 声明。
  2. \n
  3. 在第二种情况下,当添加声明时encoding="utf-16":要么\n\n
      \n
    1. 只需不添加 XML 声明,或者
    2. \n
    3. 只需在输入参数类型中添加“N”:SqlDbType.NVarChar而不是SqlDbType.VarChar:-) (或者甚至可能切换到 using SqlDbType.Xml
    4. \n
  4. \n
\n\n

(详细回复如下)

\n\n
\n\n

这里所有的答案都过于复杂和不必要(不管 Christian 和 Jon 的答案分别有 121 和 184 票赞成)。他们可能提供工作代码,但没有一个真正回答问题。问题是没有人真正理解这个问题,这个问题最终是关于 SQL Server 中的 XML 数据类型如何工作的。没有什么可以反对这两个显然很聪明的人,但是这个问题与序列化到 XML 几乎没有关系。将 XML 数据保存到 SQL Server 中比此处暗示的要容易得多。

\n\n

只要遵循如何在 SQL Server 中创建 XML 数据的规则,如何生成 XML 并不重要。我在这个问题的答案中有更彻底的解释(包括工作示例代码来说明下面概述的要点):How to Solution \xe2\x80\x9cunable to switch the encoding\xe2\x80\x9d error when inserting XML into SQL服务器,但基础知识是:

\n\n
    \n
  1. XML 声明是可选的
  2. \n
  3. XML 数据类型始终将字符串存储为 UCS-2 / UTF-16 LE
  4. \n
  5. 如果您的 XML 是 UCS-2 / UTF-16 LE,那么您:\n\n
      \n
    1. NVARCHAR(MAX)以或XML/ SqlDbType.NVarChar(maxsize = -1) 或 的形式传入数据SqlDbType.Xml,或者如果使用字符串文字,则必须以大写“N”作为前缀。
    2. \n
    3. 如果指定 XML 声明,则必须是“UCS-2”或“UTF-16”(这里没有真正的区别)
    4. \n
  6. \n
  7. 如果您的 XML 是 8 位编码的(例如“UTF-8”/“iso-8859-1”/“Windows-1252”),那么您:\n\n
      \n
    1. 如果编码与数据库默认排序规则指定的代码页不同,则需要指定 XML 声明
    2. \n
    3. VARCHAR(MAX)您必须以/ (maxsize = -1)的形式传入数据SqlDbType.VarChar,或者如果使用字符串文字,则不得以大写“N”作为前缀。
    4. \n
    5. 无论使用哪种 8 位编码,XML 声明中注明的“编码”都必须与字节的实际编码相匹配。
    6. \n
    7. 8 位编码将由 XML 数据类型转换为 UTF-16 LE
    8. \n
  8. \n
\n\n

考虑到上面概述的要点,考虑到 .NET 中的字符串始终是为UTF-16 LE / UCS-2 LE(编码方面没有区别),我们可以回答您的问题:

\n\n
\n

当我之后需要将对象作为字符串时,是否有理由不使用 StringWriter 来序列化对象?

\n
\n\n

不,你的StringWriter代码似乎很好(至少我在使用问题中的第二个代码块进行的有限测试中没有看到任何问题)。

\n\n
\n

那么将编码设置为 UTF-16(在 xml 标记中)行不通吗?

\n
\n\n

无需提供 XML 声明。当它丢失时,如果将字符串作为NVARCHAR(ie SqlDbType.NVarChar) 或XML(ie ) 传递到 SQL Server ,则假定编码为 UTF-16 LE SqlDbType.XmlVARCHAR如果传入 as (ie ) ,则假定编码为默认 8 位代码页SqlDbType.VarChar。如果您有任何非标准 ASCII 字符(即值 128 及以上)并以 传入VARCHAR,那么您可能会看到“?” 对于 BMP 字符和“??” 对于补充字符,因为 SQL Server 会将 .NET 中的 UTF-16 字符串转换为当前数据库代码页的 8 位字符串,然后再将其转换回 UTF-16 / UCS-2。但你不应该得到任何错误。

\n\n

另一方面,如果您指定了 XML 声明,则必须使用匹配的 8 位或 16 位数据类型传递到 SQL Server。因此,如果您有声明声明编码是 UCS-2 或 UTF-16,那么您必须传入 asSqlDbType.NVarCharSqlDbType.Xml。或者,如果您有一个声明,声明编码是 8 位选项之一(即UTF-8Windows-1252iso-8859-1等),那么您必须传入 as SqlDbType.VarChar。如果声明的编码与正确的 8 或 16 位 SQL Server 数据类型不匹配,将导致您收到“无法切换编码”错误。

\n\n

例如,使用StringWriter基于 - 的序列化代码,我只需打印 XML 的结果字符串并在 SSMS 中使用它。正如您在下面看到的,包含了 XML 声明(因为StringWriter没有OmitXmlDeclaration类似的选项XmlWriter),只要您将字符串作为正确的 SQL Server 数据类型传递,就不会造成任何问题:

\n\n
-- Upper-case "N" prefix == NVARCHAR, hence no error:\nDECLARE @Xml XML = N\'<?xml version="1.0" encoding="utf-16"?>\n<string>Test \xe1\x88\xb4</string>\';\nSELECT @Xml;\n-- <string>Test \xe1\x88\xb4</string>\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如您所看到的,它甚至可以处理超出标准 ASCII 的字符,前提\xe1\x88\xb4是 BMP 代码点 U+1234 和补充字符代码点 U+1F638。然而,以下情况:

\n\n
-- No upper-case "N" prefix on the string literal, hence VARCHAR:\nDECLARE @Xml XML = \'<?xml version="1.0" encoding="utf-16"?>\n<string>Test \xe1\x88\xb4</string>\';\n
Run Code Online (Sandbox Code Playgroud)\n\n

结果出现以下错误:

\n\n
Msg 9402, Level 16, State 1, Line XXXXX\nXML parsing: line 1, character 39, unable to switch the encoding\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

因此,除了所有这些解释之外,您原来问题的完整解决方案是:

\n\n

您显然是在 as 中传递字符串SqlDbType.VarChar。切换到SqlDbType.NVarChar,它将可以工作,无需执行删除 XML 声明的额外步骤。这比保留SqlDbType.VarChar和删除 XML 声明更可取,因为当 XML 包含非标准 ASCII 字符时,此解决方案将防止数据丢失。例如:

\n\n
-- No upper-case "N" prefix on the string literal == VARCHAR, and no XML declaration:\nDECLARE @Xml2 XML = \'<string>Test \xe1\x88\xb4</string>\';\nSELECT @Xml2;\n-- <string>Test ???</string>\n
Run Code Online (Sandbox Code Playgroud)\n\n

正如您所看到的,这次没有错误,但现在出现了数据丢失