使用 StringWriter 进行 XML 序列化

Using StringWriter for XML Serialization

提问人:StampedeXV 提问时间:10/14/2009 最后编辑:Solomon RutzkyStampedeXV 更新时间:11/29/2022 访问量:171567

问:

我目前正在寻找一种简单的方法来序列化对象(在 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 我不能只写入 SQL Server 2005 DB 的 XML 列。

第一个问题是:当我以后需要将 Object 作为字符串时,我是否应该使用 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 标签中)不起作用吗?

C# SQL Server UTF-8 XML 序列化

评论

1赞 Christian Hayter 10/14/2009
我要谈谈个人经验。SQL Server 只接受 UTF-16,如果向它传递任何其他内容,则会受到 SQL Server XML 分析器及其转换数据的尝试的摆布。我没有试图找到一种愚弄它的方法,我只是直接传递它 UTF-16,这总是有效的。
0赞 Jon Skeet 10/14/2009
你是怎么把它写到数据库的?你是向它传递一个字符串,还是一个字节数组,或者写入一个流?如果是后两种形式中的任何一种,则需要确保声明的编码与二进制数据的实际编码匹配。
0赞 StampedeXV 10/14/2009
唷。我在 MS SQL Management Studio 中作为查询进行的手动尝试。“编码”尝试被写入一个字符串,然后将其传递给 O/R 映射器,该映射器以字符串形式写入(据我所知)。事实上,我正在向它传递在我的问题中给出的两个示例中创建的字符串。
0赞 ziesemer 1/25/2012
仅供读者参考 - 近乎重复:stackoverflow.com/questions/384974/... 和 stackoverflow.com/questions/3760788/...
1赞 StampedeXV 4/2/2019
我正在改变我接受的答案,因为我相信它实际上回答了我的问题。尽管其他答案帮助我继续我的工作,但就 Stackoverflow 而言,我认为所罗门的答案将帮助其他人更好地理解发生了什么。[免责声明]:我没有时间真正验证答案。

答:

244赞 Jon Skeet 10/14/2009 #1

一个问题是,默认情况下,它不允许你设置它所公布的编码 - 所以你最终会得到一个XML文档,将其编码宣传为UTF-16,这意味着如果你把它写到一个文件中,你需要把它编码为UTF-16。不过,我有一个小班级可以帮助解决这个问题:StringWriter

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保存到数据库中 - 如果你希望我们能够诊断/修复它,你必须向我们提供有关你尝试时发生的情况的更多细节。

评论

0赞 StampedeXV 10/14/2009
我现在更详细地介绍了数据库问题。请参阅问题。
4赞 Chau 4/12/2011
可悲的是没有考虑到编码,但绝不会少,谢谢你的一个漂亮的小方法:)StringWriter
2赞 M.G.E 12/16/2014
而“XML解析:第1行,字符38,无法切换编码”可以通过“设置”来解决。缩进 = false;设置。OmitXmlDeclaration = 假;"
0赞 Nyerguds 1/14/2015
我通常通过简单地使用编码正确的 a 和 a 来解决这个问题。 毕竟,具有可自定义编码的(期望的类型)。MemoryStreamStreamWriterStreamWriterTextWriterXmlWriter.Create
3赞 Jon Skeet 1/14/2015
@Nyerguds:所以用这种东西创建一个 Nuget 包,那么它总是很容易获得的。我宁愿这样做,也不愿损害代码的可读性,这从根本上讲是关于其他一些要求的。
126赞 Christian Hayter 10/14/2009 #2

将 XML 文档序列化为 .NET 字符串时,编码必须设置为 UTF-16。字符串在内部以 UTF-16 格式存储,因此这是唯一有意义的编码。如果要以不同的编码存储数据,请改用字节数组。

SQL Server 的工作原理与此类似;传递到列中的任何字符串都必须编码为 UTF-16。SQL Server 将拒绝 XML 声明未指定 UTF-16 的任何字符串。如果 XML 声明不存在,则 XML 标准要求它默认为 UTF-8,因此 SQL Server 也会拒绝该声明。xml

考虑到这一点,这里有一些用于进行转换的实用方法。

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

评论

0赞 StampedeXV 10/14/2009
请参阅问题补充。我不明白我的测试结果,这似乎与你关于数据库总是想要/接受/需要 UTF-16 的说法相矛盾。
9赞 Jon Skeet 10/14/2009
不必编码为 UTF-16,但您必须确保您使用的编码与预期匹配。看我的答案。内部存储格式在这里无关紧要。StringWriter
0赞 StampedeXV 10/14/2009
好的,我明白了。在我的新示例中:完全不使用编码使数据库自行决定使用哪种编码 - 这就是它工作的原因。我现在理解正确吗?
1赞 Christian Hayter 2/9/2011
@SteveC:对不起,我的错误。我从VB手动转换了代码,其中可以隐式转换为任何类型。我已经更正了代码。警告必须是 Resharper 独有的,编译器本身不会反对,并且是合法的。NothingDeserializeSerialize
1赞 ziesemer 8/5/2013
扩展 Jon Skeet 的评论,不,不需要 UTF-16。请参阅 stackoverflow.com/a/8998183/751158,了解证明这一点的具体示例。
21赞 John Saunders 10/14/2009 #3

首先,谨防寻找旧例子。您已经找到一个使用 ,该版本自 .NET 2.0 起已弃用。 应该改用。XmlTextWriterXmlWriter.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赞 ziesemer 1/25/2012
我只能投一次票,但这值得成为这里的最高答案。最后,声明或使用什么编码并不重要,只要可以解析它。它将被预先解析发送到数据库,然后数据库不需要知道任何关于字符编码的信息 - UTF-16 或其他。特别要注意的是,无论使用哪种方法插入 XML 声明,XML 声明都不会与数据库中的数据一起保留。请不要通过额外的转换来运行 XML 来浪费,如此处和其他地方的其他答案所示。XmlReader
1赞 Mashudu Nemukuka 4/16/2013 #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;
}
-2赞 DLG 8/19/2014 #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”数据类型字段中,但“header”行被删除。您在结果记录中看到的只是

<test></test>

使用“已回答”条目中描述的序列化方法是在目标字段中包含原始标头的一种方式,但结果是剩余的 XML 文本包含在 XML 标记中。<string></string>

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

评论

3赞 Tim Abell 7/19/2016
说真的 - 不要这样做。曾。如果我想在我的 xml 中包含一些提到“UTF-8”的散文怎么办 - 您刚刚将我的数据更改为我没有说的内容!
3赞 DLG 4/27/2018
感谢您指出代码中的错误。而不是bodyXML.Replace(“UTF-8”, “UTF-16”),应该有专注于将UTF-8更改为UTF-16的XML标头的代码。我真正想指出的是,通过在源XML的标头中进行此更改,然后可以使用XML数据类型字段将XML的主体插入到SQL表记录中,并且标头被剥离。由于我现在不记得的原因(四年前!),结果在当时是有用的。是的,使用“替换”的愚蠢错误。它发生了。
1赞 Solomon Rutzky 12/5/2018 #6

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

修复应该像以下简单一样简单:

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

(详细回复如下)


这里的所有答案都过于复杂和没有必要(无论克里斯蒂安和乔恩的答案分别有 121 票和 184 票)。他们可能会提供工作代码,但实际上没有一个回答了这个问题。问题在于没有人真正理解这个问题,这个问题最终是关于 SQL Server 中的 XML 数据类型如何工作的。这并不反对这两个明显聪明的人,但这个问题与序列化为 XML 几乎没有关系。将 XML 数据保存到 SQL Server 中比此处所暗示的要容易得多。

只要遵循如何在 SQL Server 中创建 XML 数据的规则,如何生成 XML 并不重要。在对这个问题的回答中,我有一个更彻底的解释(包括工作示例代码来说明下面概述的要点):如何解决将 XML 插入 SQL Server 时的“无法切换编码”错误,但基础知识是:

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

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

当我以后需要 String Writer 作为字符串时,我不应该使用 StringWriter 来序列化 Object 有什么原因吗?

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

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

无需提供 XML 声明。如果缺少它,如果将字符串作为 (i.e. ) 或 (i.e. ) 传递到 SQL Server,则假定编码为 UTF-16 LE。如果传入为 (即 )。如果您有任何非标准 ASCII 字符(即值 128 及以上)并且以 的形式传入,则您可能会看到 BMP 字符的“?”和补充字符的“??”,因为 SQL Server 会将 .NET 中的 UTF-16 字符串转换为当前数据库代码页的 8 位字符串,然后再将其转换回 UTF-16/UCS-2。但你不应该得到任何错误。NVARCHARSqlDbType.NVarCharXMLSqlDbType.XmlVARCHARSqlDbType.VarCharVARCHAR

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

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

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

因此,撇开所有这些解释不谈,您原始问题的完整解决方案是:

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

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

如您所见,这次没有错误,但现在有 数据丢失 🙀 .

评论

0赞 StampedeXV 12/7/2018
我想我是这个过于复杂的答案的原因,因为我基本上有两个问题合二为一。我真的很喜欢你简洁的答案,下次我必须将XML存储在DB中时会尝试一下。因此,如果我没看错的话:您解释了将 XML 存储到 DB 的挑战。Jon Skeet 总结了在处理 XML 时使用 StringWriter 的问题(UTF-16 除外),Christian Hayter 提供了一种很好的处理方法。
0赞 Solomon Rutzky 12/8/2018
@StampedeXV 我更新了我的答案(为了清楚起见,做了一些更改+新的东西,以更好地说明要点)。希望现在更清楚,虽然这两个答案本身都很好,但它们对于回答你的问题来说并不是必需的。他们处理 C# / .NET 中的 XML 序列化,但这个问题实际上是关于在 SQL Server 中保存 XML。它们提供了很好了解的信息,并且可能比您最初提供的代码更好,但它们(或此处的任何其他人)都不是真正切合主题的。但这不是有据可查的东西,因此令人困惑。
0赞 Solomon Rutzky 12/14/2018
@StampedeXV 我的修改有意义吗?我刚刚在顶部添加了一个可能更清晰的摘要部分。长话短说:除非还有其他事情发生,而你没有在问题中包括细节,否则看起来你的代码是 99% 正确的,并且可能可以通过添加一个大写字母“N”来修复。不需要特殊的编码内容,Christian 的代码很好,但我的测试表明它返回的序列化与您的第二个代码块相同,除了您在 XML 声明后放置了一个 CRLF。我敢打赌你改成了 or .SqlDbType.NVarCharXml
0赞 StampedeXV 12/14/2018
还在努力找时间自己检查一下。这当然听起来不错且合乎逻辑,但不确定这是否足以改变一个公认的答案。
1赞 Johannes Mols 11/29/2022 #7

对于需要已批准答案的 F# 版本的任何人:

type private Utf8StringWriter() =
    inherit StringWriter()
    override _.Encoding = System.Text.Encoding.UTF8