ChatGPT解决这个技术问题 Extra ChatGPT

使用 StringWriter 进行 XML 序列化

我目前正在寻找一种简单的方法来序列化对象(在 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 映射器,该映射器作为一个字符串写入(据我所知)。事实上,我将在我的问题中给出的两个示例中创建的字符串传递给它。
我正在更改我接受的答案,因为我相信它实际上回答了我的问题。尽管其他答案帮助我继续我的工作,但出于 Stackoverflow 的目的,我认为所罗门的答案将帮助其他人更好地理解发生了什么。 [免责声明]:我没有找到时间来真正验证答案。

J
Jon Skeet

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 is 是具有可自定义编码的 TextWriterXmlWriter.Create 期望的类型)。
@Nyerguds:所以用这种东西创建一个 Nuget 包,那么它总是很容易上手。我宁愿这样做,也不愿损害代码的可读性,这基本上是关于其他一些要求的。
l
lordnik22

将 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。请参阅 stackoverflow.com/a/8998183/751158 中的具体示例来说明这一点。
J
John Saunders

首先,小心寻找旧的例子。您找到了一个使用 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,如此处和其他地方的其他答案所示。
S
Solomon Rutzky

<TL;DR> 实际上,问题相当简单:您没有将声明的编码(在 XML 声明中)与输入参数的数据类型匹配。如果您手动将 <?xml version="1.0" encoding="utf-8"?><test/> 添加到字符串中,那么将 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)

(详细回复如下)

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

只要您遵循如何在 SQL Server 中创建 XML 数据的规则,如何生成 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 声明中注明的“编码”必须与字节的实际编码相匹配。 XML 数据类型将 8 位编码转换为 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 等),那么您必须以 { 6}。未能将声明的编码与正确的 8 位或 16 位 SQL Server 数据类型匹配将导致您遇到的“无法切换编码”错误。

例如,使用基于 StringWriter 的序列化代码,我只需打印 XML 的结果字符串并在 SSMS 中使用它。正如您在下面看到的,包含了 XML 声明(因为 StringWriter 没有像 XmlWriter 那样的 OmitXmlDeclaration 选项),只要您将字符串作为正确的 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
仍然想找时间自己检查一下。这当然听起来不错且合乎逻辑,但不确定这是否足以改变已接受的答案。
s
splash
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;
}

D
DLG

它可能已在其他地方介绍过,但只需将 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 <string></string> 标记中。

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


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