SQL Server 使用StringWriter进行XML序列化

tkclm6bt  于 2022-12-03  发布在  其他
关注(0)|答案(7)|浏览(235)

我目前正在寻找一种简单的方法来序列化对象(在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放在那里。这个不起作用(手工插入到DB中也不起作用)。
之后我尝试手动插入(只写INSERT INTO...),编码为“utf-16”,同样失败。删除编码完全成功了。之后我切换回StringWriter代码,瞧-它成功了。
问题:我真的不明白为什么。
克里斯蒂安·海特:通过这些测试,我不确定是否必须使用utf-16来写入DB。那么将编码设置为UTF-16(在xml标记中)不就可以了吗?

sg3maiej

sg3maiej1#

StringWriter的一个问题是默认情况下it doesn't let you set the encoding which it advertises-所以你最终会得到一个XML文档,它的编码是UTF-16,这意味着如果你把它写进一个文件,你需要把它编码成UTF-16。我有一个小类来帮助你:

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

    public StringWriterWithEncoding (Encoding encoding)
    {
        Encoding = encoding;
    }    
}

或者,如果您只需要UTF-8(我通常只需要UTF-8):

public sealed class Utf8StringWriter : StringWriter
{
    public override Encoding Encoding => Encoding.UTF8;
}

至于为什么无法将XML保存到数据库-如果您希望我们能够诊断/修复它,您必须给予更多有关尝试时发生了什么的详细信息。

ymzxtsji

ymzxtsji2#

将XML文档序列化为.NET字符串时,必须将编码设置为UTF-16。字符串在内部存储为UTF-16,因此这是唯一有意义的编码。如果要以不同的编码存储数据,请改用字节数组。
SQLServer的工作原理与此类似;传入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);
        }
    }
}
jvlzgdj9

jvlzgdj93#

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

ghhkc1vu4#

**〈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声明即可。
1.在第二种情况下,当添加声明encoding="utf-16"时:或者
1.只需不添加XML声明,或者
1.只需在输入参数类型中添加“N”:SqlDbType.NVarChar而不是SqlDbType.VarChar:-)(或者甚至可能切换到使用SqlDbType.Xml
(详细回复如下)
这里所有的答案都过于复杂和不必要(尽管Christian和Jon的答案分别获得了121和184张赞成票)。他们可能提供了工作代码,但没有一个人真正回答了这个问题。问题是没有人真正理解这个问题,这个问题最终是关于SQL Server中的XML数据类型如何工作的。这两位聪明的人没有什么不好的,但是这个问题与序列化为XML几乎没有关系,将XML数据保存到SQLServer中要比这里所暗示的简单得多。
只要遵循在SQL Server中创建XML数据的规则,XML是如何生成的并不重要。在回答这个问题时,我有一个更全面的解释(包括工作示例代码来说明下面列出的要点):How to solve “unable to switch the encoding” error when inserting XML into SQL Server,但基本的是:

  1. XML声明是可选的
  2. XML数据类型始终将字符串存储为UCS-2 / UTF-16 LE
    1.如果您的XML是UCS-2 / UTF-16 LE,则您:
    1.以NVARCHAR(MAX)XML/SqlDbType.NVarChar(maxsize = -1)或SqlDbType.Xml的形式传入数据,或者如果使用字符串文字,则必须以大写字母“N”作为前缀。
    1.如果指定XML声明,则必须是“UCS-2”或“UTF-16”(此处没有真实的区别)
    1.如果您的XML是8位编码(例如“UTF-8”/“iso-8859-1”/“Windows-1252”),则您:
    1.如果编码与数据库的缺省排序规则指定的代码页不同,则需要指定XML声明
    1.您必须以VARCHAR(MAX)/SqlDbType.VarChar(maxsize = -1)的形式传入数据,或者如果使用字符串文字,则 * 不能 * 使用大写字母“N”作为前缀。
    1.无论使用什么8位编码,XML声明中注明的“编码”必须与字节的实际编码相匹配。
  3. 8位编码将由XML数据类型转换为UTF-16 LE
    记住上面列出的要点,* 和 * 假定.NET中的字符串 * 总是 * UTF-16 LE / UCS-2 LE(在编码方面它们之间没有区别),我们可以回答您的问题:
    当我以后需要一个Object作为字符串时,有什么理由不应该使用StringWriter来序列化它呢?
    不,您的StringWriter代码看起来很好(至少我在使用问题中的第二个代码块进行的有限测试中没有看到任何问题)。
    那么将编码设置为UTF-16(在xml标记中)不就可以了吗?
    没有必要提供XML声明,* 如果 * 将字符串作为NVARCHAR传递到SQL Server,则假定编码为UTF-16 LE(即SqlDbType.NVarChar)或XML(即SqlDbType.Xml)。如果以VARCHAR形式传入,则假定编码为默认的8位代码页(例如SqlDbType.VarChar)。如果您有任何非标准ASCII字符(即值128及以上)并且作为VARCHAR传入,则您可能会看到“?”表示BMP字符,“??”表示补充字符,因为SQL Server会将UTF-16字符串从.NET转换为当前数据库的8位字符串。的代码页,然后再将其转换回UTF-16 / UCS-2。但您应该不会收到任何错误。
    另一方面,如果您指定了XML声明,则 * 必须 * 使用匹配的8位或16位数据类型传入SQL Server。因此,如果您有一个声明,声明编码是UCS-2或UTF-16,则 * 必须 * 作为SqlDbType.NVarCharSqlDbType.Xml传入。或者,如果您有一个声明,说明编码是8位选项之一(即UTF-8Windows-1252iso-8859-1等),则 * 必须 * 作为SqlDbType.VarChar传入。如果声明的编码与正确的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>

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

eimct9ow

eimct9ow5#

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

a0zr77ik6#

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

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

q5lcpyga7#

它可能在其他地方已经介绍过,但只需将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>

使用“Answered”条目中描述的序列化方法是一种在目标字段中包含原始标题的方法,但结果是剩余的XML文本被包含在XML <string></string>标记中。
代码中的表适配器是使用Visual Studio 2013“添加新数据源:Insert方法的五个参数Map到SQL Server表中的字段。

相关问题