使用 StringWriter 进行 XML 序列化

Posted

技术标签:

【中文标题】使用 StringWriter 进行 XML 序列化【英文标题】:Using StringWriter for XML Serialization 【发布时间】:2010-12-06 14:07:50 【问题描述】:

我目前正在寻找一种简单的方法来序列化对象(在 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());

读完question之后,我问自己,为什么不用StringWriter呢?好像容易多了。

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

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

第一个问题是:当我需要它作为一个字符串时,我是否有理由不使用 StringWriter 序列化一个对象?谷歌搜索时,我从未使用 StringWriter 找到结果。

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


加法:

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

写入数据库时​​出现以下异常:

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

对于字符串

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

我把从 XmlTextWriter 创建的字符串作为 xml 放在那里。这一个不起作用(手动插入数据库也不行)。

之后,我尝试使用 encoding="utf-16" 手动插入(只是编写 INSERT INTO ... ),但也失败了。 然后完全删除编码。得到那个结果后,我切换回 StringWriter 代码,瞧——它起作用了。

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

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

【问题讨论】:

我将进行个人体验。 SQL Server 仅接受 UTF-16,如果您传递其他任何内容,您将受制于 SQL Server XML 解析器及其转换数据的尝试。我没有试图找到一种欺骗它的方法,而是直接将其传递为 UTF-16,这将始终有效。 你是如何将它写入数据库的?你是传递一个字符串,还是一个字节数组,还是写入一个流?如果是后两种形式之一,则需要确保声明的编码与二进制数据的实际编码匹配。 唷。我在 MS SQL Management Studio 中作为查询进行的手动尝试。 “编码”尝试被写入一个字符串,然后传递给一个 O/R 映射器,该映射器作为一个字符串写入(据我所知)。事实上,我将在我的问题中给出的两个示例中创建的字符串传递给它。 仅供读者参考 - 几乎重复:***.com/questions/384974/… 和 ***.com/questions/3760788/… 我正在更改我接受的答案,因为我相信它实际上回答了我的问题。尽管其他答案帮助我继续我的工作,但出于 *** 的目的,我认为所罗门的答案将帮助其他人更好地理解发生了什么。 [免责声明]:我没有时间真正验证答案。 【参考方案1】:

StringWriter 的一个问题是默认情况下it doesn't let you set the encoding which it advertises - 因此您最终会得到一个将其编码为 UTF-16 的 XML 文档,这意味着您需要将其编码为 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 保存到数据库 - 如果您希望我们能够诊断/修复它,您必须向我们提供有关您尝试时发生的情况的更多详细信息。

【讨论】:

我现在详细介绍了数据库问题。见问题。 很遗憾StringWriter 没有考虑到编码,但永远不会少,感谢一个漂亮的小方法:) 和“XML解析:第1行,第38个字符,无法切换编码”可以通过“settings.Indent = false; settings.OmitXmlDeclaration = false;”解决 我通常通过使用正确编码的MemoryStreamStreamWriter 来解决这个问题。毕竟StreamWriter 一个TextWriterXmlWriter.Create 期望的类型),具有可自定义的编码。 @Nyerguds:所以用这种东西创建一个 Nuget 包,那么它总是很容易上手。我宁愿这样做,也不愿牺牲代码的可读性,这基本上是关于其他一些要求的。【参考方案2】:

将 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 的陈述相矛盾。 不必必须编码为 UTF-16 - 但您必须确保您使用的编码符合 StringWriter 的预期。看我的回答。内部存储格式在这里无关紧要。 好的,我明白了。在我的新示例中:完全保留编码使数据库自己决定使用哪种编码 - 这就是它起作用的原因。我现在理解正确了吗? @SteveC:对不起,我的错。我从 VB 中手动转换了代码,其中Nothing 可以隐式转换为任何类型。我已经更正了Deserialize 代码。 Serialize 警告必须是 Resharper-only 的事情,编译器本身不会反对,这样做是合法的。 扩展 Jon Skeet 的评论,不,不需要 UTF-16。请参阅***.com/a/8998183/751158 了解具体示例。【参考方案3】:

首先,小心寻找旧的例子。您已经找到了一个使用 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();
                
            
        
    

【讨论】:

我只能投票一次,但这应该是这里的最佳答案。最后,声明或使用什么编码并不重要,只要XmlReader 可以解析它。它将预先解析后发送到数据库,然后数据库不需要知道任何关于字符编码的信息——UTF-16 或其他。特别要注意,XML 声明甚至不会与数据库中的数据一起保存,无论使用哪种方法插入它。请不要浪费通过额外的转换运行 XML,如此处和其他地方的其他答案所示。【参考方案4】:
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;

【讨论】:

【参考方案5】:

它可能已在其他地方介绍过,但只需将 XML 源的编码行更改为“utf-16”,就可以将 XML 插入 SQL Server 的“xml”数据类型。

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>

使用“Answered”条目中描述的序列化方法是一种将原始标题包含在目标字段中的方法,但结果是剩余的 XML 文本包含在 XML &lt;string&gt;&lt;/string&gt; 标记中。

代码中的表适配器是使用 Visual Studio 2013“添加新数据源:向导”自动构建的类。Insert 方法的五个参数映射到 SQL Server 表中的字段。

【讨论】:

替换?这很有趣。 说真的 - 不要这样做。曾经。如果我想在我的 xml 中包含一些提到“UTF-8”的散文怎么办 - 你刚刚将我的数据更改为我没有说的内容! 感谢您指出代码中的错误。而不是 bodyXML.Replace("UTF-8", "UTF-16") 应该有专注于将 UTF-8 更改为 UTF-16 的 XML 标头的代码。我真正想指出的是,通过在源 XML 的标头中进行此更改,然后可以使用 XML 数据类型字段将 XML 的主体插入到 SQL 表记录中,并剥离标头。由于我现在不记得的原因(四年前!),结果在当时是有用的。是的,使用“替换”的愚蠢错误。它发生了。【参考方案6】:

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

修复应该很简单:

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

(详细回复如下)


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

XML 的生成方式并不重要,只要您遵循如何在 SQL Server 中创建 XML 数据的规则即可。我在这个问题的答案中有更详尽的解释(包括说明下面概述的要点的工作示例代码):How to solve “unable to switch the encoding” error when inserting XML into SQL Server,但基础是:

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

考虑到上述几点,鉴于 .NET 中的字符串总是 UTF-16 LE / UCS-2 LE(它们之间没有区别在编码方面),我们可以回答您的问题:

当我需要一个对象作为字符串之后,我不应该使用 StringWriter 来序列化它有什么原因吗?

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

然后将编码设置为 UTF-16(在 xml 标签中)不起作用吗?

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

另一方面,如果您确实指定了 XML 声明,那么您必须使用匹配的 8 位或 16 位数据类型传递给 SQL Server。因此,如果您有声明说明编码是 UCS-2 或 UTF-16,那么您必须SqlDbType.NVarCharSqlDbType.Xml 的形式传入。或者,如果您有声明说明编码是 8 位选项之一(即 UTF-8Windows-1252iso-8859-1 等),那么您必须以 @ 的形式传入987654353@。未能将声明的编码与正确的 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 存储到数据库的挑战。 Jon Skeet 总结了在使用 XML(UTF-16 除外)时使用 StringWriter 的问题,Christian Hayter 提供了一种很好的方式来使用它。 @StampedeXV 我更新了我的答案(为了清楚起见做了一些更改+新的东西以更好地说明要点)。希望现在更清楚了,虽然这两个答案本身都很好,但它们在任何情况下都不是回答您的问题所必需的。他们处理 C# / .NET 中的 XML 序列化,但这个问题实际上是关于在 SQL Server 中保存 XML。它们提供了值得了解的信息,并且可能比您最初提供的代码更好,但它们(或此处的任何其他)都不是真正的主题。但这不是有据可查的东西,因此造成了混乱。 @StampedeXV 我的修改有意义吗?我刚刚在顶部添加了一个可能更清晰的摘要部分。长话短说:除非还有其他事情你没有在问题中包含细节,那么看起来你的代码是 99% 正确的,并且可能已经通过添加一个大写字母“ N”。不需要特殊的编码内容,Christian 的代码很好,但我的测试表明它返回的序列化与您的第二个代码块相同,除了您在 XML 声明之后放置了一个 CRLF。我打赌你改成了SqlDbType.NVarCharXml 还在找时间自己检查。这当然听起来不错且合乎逻辑,但不确定是否足以改变已接受的答案。

以上是关于使用 StringWriter 进行 XML 序列化的主要内容,如果未能解决你的问题,请参考以下文章

XML文件解析!!!

StringWriter 的 toString 方法更改 TimeZone

StringWriter类的使用

使用 Servicestack.Text 进行 XML 反序列化

使用模型进行xml序列化/反序列化

在 C# 中使用德文小数分隔符对双精度值进行 XML 反序列化