使用StringWriter进行XML序列化


99

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

阅读了这个问题后,我问自己,为什么不使用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 to 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标记中)就行了吗?


1
我正在经历个人经历。SQL Server仅接受UTF-16,并且如果您传递其他任何内容,则将受到SQL Server XML解析器及其尝试转换数据的支配。与其尝试寻找一种欺骗方法,不如直接将其传递给UTF-16,它将始终有效。
克里斯蒂安·海特

您如何将其写入数据库?您是将其传递为字符串还是字节数组,还是写入流中?如果是后两种形式之一,则需要确保声明的编码与二进制数据的实际编码匹配。
乔恩·斯基特

唷。我在MS SQL Management Studio中作为查询进行的手动尝试。将“编码”尝试写入一个字符串,然后将其传递给O / R映射器,该O / R映射器以字符串形式写入(据我所知)。实际上,我将在问题中给出的两个示例中创建的字符串传递给了它。
StampedeXV


1
我正在更改接受的答案,因为我相信它实际上可以回答我的问题。尽管其他答案正在帮助我继续工作,但出于Stackoverflow的目的,我认为所罗门的答案将帮助其他人更好地了解发生了什么。[免责声明]:我没有时间真正验证答案。
StampedeXV

Answers:


1

<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编码)。

该修复程序应该很简单:

  1. 在第一种情况下,添加声明时 encoding="utf-8":只需不添加XML声明。
  2. 在第二种情况下,添加声明时encoding="utf-16"
    1. 根本不添加XML声明,或者
    2. 只需在输入参数类型中添加“ N”:SqlDbType.NVarChar而不是: -) SqlDbType.VarChar(甚至可能切换到using SqlDbType.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. 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位编码将通过XML数据类型转换为UTF-16 LE

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

为什么在以后需要字符串时不使用StringWriter序列化对象的原因呢?

不,您的StringWriter代码看起来还不错(至少在我使用问题的第二个代码块进行的有限测试中,我没有发现任何问题)。

这样,将编码设置为UTF-16(在xml标记中)就行了吗?

无需提供XML声明。如果缺少该字符串,则如果将字符串作为NVARCHAR(即SqlDbType.NVarChar)或XML(即SqlDbType.Xml)传递给SQL Server,则假定编码为UTF-16 LE 。如果以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.NVarChar或形式传递SqlDbType.Xml。或者,如果你有一个声明,表示编码是8位的选项之一(即UTF-8Windows-1252iso-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>

如您所见,鉴于BMP代码点U + 1234和😸补充字符代码点U + 1F638 ,它甚至可以处理超出标准ASCII的字符。但是,以下内容:

-- 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存储在DB中时将尝试一下。因此,如果我看对了:您已经解释了将XML存储到DB时遇到的挑战。Jon Skeet在处理XML(UTF-16除外)时使用StringWriter总结了问题,Christian Hayter提供了一种很好的方式来使用它。
StampedeXV

@StampedeXV我更新了答案(为清楚起见进行了一些更改,另加了新内容以更好地说明要点)。希望现在可以更清楚地了解到,虽然这两个答案本身都是好的,但并不需要以任何方式回答您的问题。他们处理C#/ .NET中的XML序列化,但是这个问题实际上是关于在SQL Server中保存XML。它们提供的信息很容易理解,并且可能比您最初提供的代码更好,但是它们(或此处的其他任何一个)都不是真正的主题。但这不是有据可查的东西,因此很混乱。
所罗门·鲁兹基

@StampedeXV我的修订有意义吗?我只是在顶部添加了一个摘要部分,可能会更清楚。长话短说:除非发生其他事情,而您没有在问题中包含细节,那么您的代码看起来是99%正确的,并且可能可以通过添加一个大写“ N”。不需要特殊的编码内容,Christian的代码很好,但是我的测试表明,它返回的序列化与您的第二个代码块相同,只是您的XML声明后面加上了CRLF。我敢打赌,您更改为SqlDbType.NVarCharXml
所罗门·鲁兹基

仍在尝试寻找时间自己检查。这听起来固然合理,但并不确定是否足以改变一个可接受的答案。
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它没有考虑编码,但更不用说了,感谢一个漂亮的小方法:)
Chau

2
而“ XML解析:第1行,字符38,无法切换编码”可以通过“ settings.Indent = false; settings.OmitXmlDeclaration = false;”解决
MGE 2014年

我通常通过使用带有正确编码的a MemoryStream和a 来解决此问题StreamWriter。毕竟StreamWriter 一种具有可自定义编码的TextWriterXmlWriter.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期望的匹配。看我的答案。内部存储格式与此处无关。
乔恩·斯基特

好的,我了解。在我的新示例中:完全不使用编码,DB自行决定使用哪种编码-这就是为什么它起作用。我现在知道它正确吗?
StampedeXV

1
@SteveC:对不起,我的错。我手动转换了VB中的代码,其中的代码Nothing可以隐式转换为任何类型。我已经更正了Deserialize代码。该Serialize警告必须是ReSharper的,唯一的事情,对自己不反对的编译器,它是合法的事情。
Christian Hayter

1
根据Jon Skeet的评论,不,不需要UTF-16。请参阅stackoverflow.com/a/8998183/751158上的具体示例进行演示。
ziesemer

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可以解析它即可。它将被预先解析地发送到数据库,然后DB不需要了解任何有关字符编码的信息-UTF-16或其他。特别要注意的是,无论使用哪种方法插入XML声明,数据库中的数据都不会持久保存XML声明。如此处和其他地方的其他答案所示,请不要通过额外的转换来运行XML,以免造成浪费。
ziesemer,2012年

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

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

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


2
更换?这真可笑。
mgilberties

2
严重-不要这样做。曾经 如果我想在XML中包含一些提到“ UTF-8”的散文,该怎么办-您刚刚将我的数据更改为我没有说过的内容!
蒂姆·阿贝尔

2
感谢您指出代码中的错误。而不是bodyXML.Replace(“ UTF-8”,“ UTF-16”),应该有将重点放在将UTF-8更改为UTF-16的XML标头上的代码。我真正要指出的是,通过对源XML的标头进行此更改,然后可以使用XML数据类型字段将XML主体插入到SQL表记录中,并剥离标头。由于种种原因,我现在(四年前!)不记得了,当时的结果很有用。是的,使用“替换”愚蠢的错误。它发生了。
DLG
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.