Usando StringWriter para serialização XML

99

Atualmente, estou procurando uma maneira fácil de serializar objetos (em C # 3).

Pesquisei alguns exemplos e encontrei algo como:

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

Depois de ler esta pergunta eu me perguntei, por que não usar StringWriter? Parece muito mais fácil.

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

Outro problema era que o primeiro exemplo gerou XML que não pude simplesmente escrever em uma coluna XML do SQL Server 2005 DB.

A primeira pergunta é: Existe uma razão pela qual eu não devo usar StringWriter para serializar um Object quando eu preciso dele como uma string posteriormente? Nunca encontrei um resultado usando StringWriter ao pesquisar no Google.

A segunda é, claro: se você não deveria fazer isso com StringWriter (por qualquer motivo), qual seria uma maneira correta e correta?


Adição:

Como já foi mencionado por ambas as respostas, irei mais adiante no problema de XML para banco de dados.

Ao gravar no banco de dados, obtive a seguinte exceção:

System.Data.SqlClient.SqlException: análise XML: linha 1, caractere 38, incapaz de mudar a codificação

Para corda

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

Peguei a string criada no XmlTextWriter e coloquei como xml lá. Este não funcionou (nem com inserção manual no BD).

Depois, tentei a inserção manual (apenas escrevendo INSERT INTO ...) com encoding = "utf-16" que também falhou. Remover a codificação funcionou totalmente então. Depois desse resultado, voltei para o código StringWriter e pronto - funcionou.

Problema: eu realmente não entendo por quê.

at Christian Hayter: Com esses testes, não tenho certeza se devo usar utf-16 para escrever no DB. Definir a codificação para UTF-16 (na tag xml) não funcionaria então?

StampedeXV
fonte
1
Estou passando por experiência pessoal. O SQL Server aceita apenas UTF-16 e, se você passar qualquer outra coisa, estará à mercê do analisador XML do SQL Server e de suas tentativas de converter os dados. Em vez de tentar encontrar uma maneira de enganá-lo, apenas passo o UTF-16 diretamente, o que sempre funcionará.
Christian Hayter,
Como você está escrevendo isso no banco de dados? Você está passando uma string ou uma matriz de bytes ou escrevendo em um fluxo? Se for uma das duas últimas formas, você precisa se certificar de que sua codificação declarada corresponde à codificação real de seus dados binários.
Jon Skeet,
ufa. A tentativa manual que fiz como Consulta no MS SQL Management Studio. As tentativas "codificadas" foram escritas em uma string que foi então passada para um mapeador O / R que escreveu como uma string (pelo que eu pude acompanhar). Na verdade, estou passando a string que foi criada nos dois exemplos dados em minha pergunta.
StampedeXV
Para sua informação, para leitores - quase duplicatas: stackoverflow.com/questions/384974/… e stackoverflow.com/questions/3760788/…
ziesemer
1
Estou mudando minha resposta aceita porque acredito que realmente responde à minha pergunta. Embora as outras respostas tenham me ajudado a continuar meu trabalho, para o propósito do Stackoverflow, acho que a resposta de Solomon ajudará outros a entender melhor o que aconteceu. [Isenção de responsabilidade]: Não encontrei tempo para realmente verificar a resposta.
StampedeXV

Respostas:

1

<TL; DR> O problema é bastante simples, na verdade: você não está combinando a codificação declarada (na declaração XML) com o tipo de dados do parâmetro de entrada. Se você adicionou manualmente <?xml version="1.0" encoding="utf-8"?><test/>à string, declarar SqlParameterque é do tipo SqlDbType.Xmlou SqlDbType.NVarChargeraria o erro "não foi possível mudar a codificação". Então, ao inserir manualmente via T-SQL, uma vez que você mudou a codificação declarada para ser utf-16, você estava claramente inserindo uma VARCHARstring (não prefixada com um "N" maiúsculo, portanto, uma codificação de 8 bits, como UTF-8) e não uma NVARCHARstring (prefixada com um "N" maiúsculo, daí a codificação UTF-16 LE de 16 bits).

A correção deveria ter sido tão simples quanto:

  1. No primeiro caso, ao adicionar a declaração informando encoding="utf-8": simplesmente não adicione a declaração XML.
  2. No segundo caso, ao adicionar a declaração informando encoding="utf-16": ou
    1. simplesmente não adicione a declaração XML, OU
    2. simplesmente adicione um "N" ao tipo de parâmetro de entrada: em SqlDbType.NVarCharvez de SqlDbType.VarChar:-) (ou, possivelmente, mude para usar SqlDbType.Xml)

(A resposta detalhada está abaixo)


Todas as respostas aqui são muito complicadas e desnecessárias (independentemente dos 121 e 184 votos positivos para as respostas de Christian e Jon, respectivamente). Eles podem fornecer código funcional, mas nenhum deles realmente responde à pergunta. O problema é que ninguém realmente entendeu a questão, que em última análise é sobre como funciona o tipo de dados XML no SQL Server. Nada contra essas duas pessoas claramente inteligentes, mas essa questão tem pouco ou nada a ver com a serialização para XML. Salvar dados XML no SQL Server é muito mais fácil do que o que está implícito aqui.

Realmente não importa como o XML é produzido, desde que você siga as regras de como criar dados XML no SQL Server. Eu tenho uma explicação mais completa (incluindo código de exemplo de trabalho para ilustrar os pontos descritos abaixo) em uma resposta a esta pergunta: Como resolver o erro “não foi possível mudar a codificação” ao inserir XML no SQL Server , mas o básico é:

  1. A declaração XML é opcional
  2. O tipo de dados XML armazena strings sempre como UCS-2 / UTF-16 LE
  3. Se o seu XML for UCS-2 / UTF-16 LE, então você:
    1. passe os dados como NVARCHAR(MAX)ou XML/ SqlDbType.NVarChar(maxsize = -1) ou SqlDbType.Xml, ou se estiver usando um literal de string, ele deve ser prefixado com um "N" maiúsculo.
    2. se especificar a declaração XML, deve ser "UCS-2" ou "UTF-16" (nenhuma diferença real aqui)
  4. Se o seu XML for codificado em 8 bits (por exemplo, "UTF-8" / "iso-8859-1" / "Windows-1252"), você:
    1. precisa especificar a declaração XML SE a codificação for diferente da página de código especificada pelo agrupamento padrão do banco de dados
    2. tem de passar na dados como VARCHAR(MAX)/ SqlDbType.VarChar(maxsize = -1), ou se utilizando uma cadeia de caracteres, então ele deve não ser prefixado com uma letra maiúscula "N".
    3. Qualquer que seja a codificação de 8 bits usada, a "codificação" observada na declaração XML deve corresponder à codificação real dos bytes.
    4. A codificação de 8 bits será convertida em UTF-16 LE pelo tipo de dados XML

Com os pontos descritos acima em mente, e considerando que as strings em .NET são sempre UTF-16 LE / UCS-2 LE (não há diferença entre elas em termos de codificação), podemos responder às suas perguntas:

Existe uma razão pela qual eu não devo usar StringWriter para serializar um objeto quando eu precisar dele como uma string posteriormente?

Não, seu StringWritercódigo parece estar bom (pelo menos não vejo problemas em meu teste limitado usando o segundo bloco de código da pergunta).

Definir a codificação para UTF-16 (na tag xml) não funcionaria então?

Não é necessário fornecer a declaração XML. Quando está ausente, a codificação é considerada UTF-16 LE se você passar a string para o SQL Server como NVARCHAR(ou seja SqlDbType.NVarChar) ou XML( ou seja SqlDbType.Xml). A codificação é considerada a página de código padrão de 8 bits se for passada como VARCHAR(ou seja SqlDbType.VarChar). Se você tiver caracteres ASCII não padrão (ou seja, valores 128 e acima) e estiver transmitindo como VARCHAR, provavelmente verá "?" para caracteres BMP e "??" para caracteres suplementares, pois o SQL Server converterá a string UTF-16 do .NET em uma string de 8 bits da página de código do banco de dados atual antes de convertê-la novamente em UTF-16 / UCS-2. Mas você não deve receber nenhum erro.

Por outro lado, se você especificar a declaração XML, deverá passar para o SQL Server usando o tipo de dados correspondente de 8 ou 16 bits. Portanto, se você tiver uma declaração afirmando que a codificação é UCS-2 ou UTF-16, deverá passar como SqlDbType.NVarCharou SqlDbType.Xml. Ou, se você tem uma declaração de que a codificação é uma das opções de 8 bits (ou seja UTF-8, Windows-1252, iso-8859-1, etc), então você deve passar em como SqlDbType.VarChar. A falha em combinar a codificação declarada com o tipo de dados SQL Server de 8 ou 16 bits adequado resultará no erro "não foi possível alternar a codificação" que você estava recebendo.

Por exemplo, usando seu StringWritercódigo de serialização baseado em seu , eu simplesmente imprimi a string resultante do XML e a usei no SSMS. Como você pode ver abaixo, a declaração XML está incluída (porque StringWriternão tem a opção de OmitXmlDeclarationlike XmlWriterfaz), o que não representa nenhum problema, desde que você passe a string como o tipo de dados correto do 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>

Como você pode ver, ele até lida com caracteres além do ASCII padrão, visto que é o ponto de código BMP U + 1234 e 😸é o ponto de código de caractere suplementar U + 1F638. No entanto, o seguinte:

-- No upper-case "N" prefix on the string literal, hence VARCHAR:
DECLARE @Xml XML = '<?xml version="1.0" encoding="utf-16"?>
<string>Test ሴ😸</string>';

resulta no seguinte erro:

Msg 9402, Level 16, State 1, Line XXXXX
XML parsing: line 1, character 39, unable to switch the encoding

Portanto, toda essa explicação à parte, a solução completa para sua pergunta original é:

Você estava claramente passando a corda como SqlDbType.VarChar. Alterne para SqlDbType.NVarChare ele funcionará sem a necessidade de passar pela etapa extra de remoção da declaração XML. Isso é preferível a manter SqlDbType.VarChare remover a declaração XML porque esta solução evitará a perda de dados quando o XML incluir caracteres ASCII não padrão. Por exemplo:

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

Como você pode ver, não há erro desta vez, mas agora há perda de dados 🙀.

Solomon Rutzky
fonte
Acho que fui a razão para essas respostas complicadas, já que basicamente tinha duas perguntas em uma. Gostei muito da sua resposta concisa e tentarei na próxima vez que precisar armazenar XML no banco de dados. Então, se entendi direito: você explicou os desafios de armazenar XML no banco de dados. Jon Skeet resumiu problemas com o uso de StringWriter ao trabalhar com XML (exceto para UTF-16) e Christian Hayter fornece uma boa maneira de trabalhar com ele.
StampedeXV
@StampedeXV Eu atualizei minha resposta (algumas mudanças para maior clareza + coisas novas para ilustrar melhor os pontos). Esperançosamente, está mais claro agora que, embora ambas as respostas sejam boas sozinhas, elas não são necessárias de forma alguma para responder à sua pergunta. Eles lidam com a serialização de XML em C # / .NET, mas esta questão é realmente sobre como salvar XML no SQL Server. Eles fornecem informações que é bom saber e podem ser um código melhor do que o fornecido originalmente, mas nenhum deles (nem qualquer um dos outros aqui) são verdadeiramente no tópico. Mas isso não é um material bem documentado, daí a confusão.
Solomon Rutzky
@StampedeXV Minhas revisões fizeram sentido? Acabei de adicionar uma seção de resumo no topo que pode ser mais clara. Resumindo: a menos que houvesse algo mais acontecendo que você não incluiu detalhes na pergunta, parece que seu código estava 99% correto e provavelmente poderia ter sido corrigido com a adição de uma única caixa alta " N ". Nenhuma codificação especial é necessária, e o código de Christian é bom, mas meu teste mostra que ele retorna a serialização idêntica ao seu segundo bloco de código, exceto que o seu coloca um CRLF após a declaração XML. Aposto que você mudou para SqlDbType.NVarCharou Xml.
Solomon Rutzky
ainda estou tentando encontrar tempo para verificar sozinho. Certamente parece bom e lógico, mas não tenho certeza de que seria suficiente para alterar uma resposta aceita.
StampedeXV
216

Um problema StringWriteré que, por padrão, ele não permite que você defina a codificação que anuncia - então você pode acabar com um documento XML anunciando sua codificação como UTF-16, o que significa que você precisa codificá-lo como UTF-16 se você escreva em um arquivo. Eu tenho uma pequena classe para ajudar com isso:

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

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

Ou se você só precisar de UTF-8 (que é tudo de que preciso frequentemente):

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

Quanto ao motivo pelo qual você não pôde salvar seu XML no banco de dados - você terá que nos dar mais detalhes sobre o que aconteceu quando você tentou, se quiser que possamos diagnosticar / consertar.

Jon Skeet
fonte
Entrei em mais detalhes sobre o problema do banco de dados agora. Veja a pergunta.
StampedeXV
4
Infelizmente, o StringWriternão leva em conta a codificação, mas nunca menos, obrigado por um método bacana :)
Chau
2
E "análise XML: linha 1, caractere 38, não é possível mudar a codificação" pode ser resolvido por "settings.Indent = false; settings.OmitXmlDeclaration = false;"
MGE
Normalmente, consigo contornar isso simplesmente usando ae MemoryStreama StreamWritercom a codificação correta. StreamWriter é um TextWriter(o tipo XmlWriter.Createesperado) com codificação personalizável, afinal.
Nyerguds
2
@Nyerguds: Então, crie um pacote Nuget com esse tipo de coisa, então é sempre fácil de obter. Prefiro fazer isso do que comprometer a legibilidade do código, que é fundamentalmente sobre algum outro requisito.
Jon Skeet
126

Ao serializar um documento XML em uma string .NET, a codificação deve ser definida como UTF-16. As strings são armazenadas como UTF-16 internamente, portanto, esta é a única codificação que faz sentido. Se você quiser armazenar dados em uma codificação diferente, use uma matriz de bytes.

O SQL Server funciona em um princípio semelhante; qualquer string passada em uma xmlcoluna deve ser codificada como UTF-16. O SQL Server rejeitará qualquer string em que a declaração XML não especifique UTF-16. Se a declaração XML não estiver presente, o padrão XML exige que o padrão seja UTF-8, portanto, o SQL Server também rejeitará isso.

Tendo isso em mente, aqui estão alguns métodos utilitários para fazer a conversão.

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);
        }
    }
}
Christian Hayter
fonte
Veja adição de perguntas. Não entendo o resultado do meu teste, parece contradizer sua afirmação de que o DB sempre quer / tira / precisa de UTF-16.
StampedeXV
9
Você não precisa codificar como UTF-16 - mas precisa ter certeza de que a codificação usada corresponde ao StringWriteresperado. Veja minha resposta. O formato de armazenamento interno é irrelevante aqui.
Jon Skeet,
ok isso eu entendo. No meu novo exemplo: deixar a codificação completamente de fora fez com que o banco de dados decidisse por si mesmo qual codificação seria usada - é por isso que funcionou. Eu entendi correto agora?
StampedeXV
1
@SteveC: Desculpe, meu erro. Converti manualmente o código do VB, que Nothingé implicitamente conversível para qualquer tipo. Eu corrigi o Deserializecódigo. O Serializeaviso deve ser apenas um Resharper, o compilador por si só não faz objeções e é legal fazer isso.
Christian Hayter
1
Seguindo o comentário de Jon Skeet, não, o UTF-16 não é necessário. Consulte stackoverflow.com/a/8998183/751158 para um exemplo concreto que demonstra isso.
ziesemer
20

Em primeiro lugar, tome cuidado para não encontrar exemplos antigos. Você encontrou um que usa XmlTextWriter, que está obsoleto a partir do .NET 2.0. XmlWriter.Createdeve ser usado em seu lugar.

Aqui está um exemplo de serialização de um objeto em uma coluna 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();
                }
            }
        }
    }
}
John Saunders
fonte
2
Só posso votar uma vez, mas essa merece ser a principal resposta aqui. No final, não importa qual codificação é declarada ou usada, desde que o XmlReaderpossa analisá-la. Ele será enviado pré-analisado para o banco de dados, e então o DB não precisa saber nada sobre codificação de caracteres - UTF-16 ou outro. Em particular, observe que as declarações XML nem mesmo são persistentes com os dados no banco de dados, independentemente do método usado para inseri-los. Não desperdice executando XML por meio de conversões extras, conforme mostrado em outras respostas aqui e em outros lugares.
ziesemer
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;
}
Mashudu Nemukuka
fonte
-1

Pode ter sido abordado em outro lugar, mas simplesmente alterar a linha de codificação da fonte XML para 'utf-16' permite que o XML seja inserido em um tipo de dados xml do SQL Server.

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

O resultado é que todo o texto XML é inserido no campo de tipo de dados 'xml', mas a linha 'cabeçalho' é removida. O que você vê no registro resultante é apenas

<test></test>

Usar o método de serialização descrito na entrada "Respondido" é uma maneira de incluir o cabeçalho original no campo de destino, mas o resultado é que o texto XML restante é colocado em um XML <string></string> tag .

O adaptador de tabela no código é uma classe construída automaticamente usando o Visual Studio 2013 "Add New Data Source: wizard. Os cinco parâmetros para o método Insert mapeiam para campos em uma tabela SQL Server.

DLG
fonte
2
Substituir? Isso é hilário.
mgilberties
2
Sério - não faça isso. Sempre. E se eu quisesse incluir em meu xml alguma prosa que mencionasse "UTF-8" - você acabou de alterar meus dados para algo que eu não disse!
Tim Abell
2
Obrigado por apontar um erro no código. Em vez de bodyXML.Replace ("UTF-8", "UTF-16"), deve haver código que se concentre no cabeçalho XML alterando UTF-8 para UTF-16. O que eu estava realmente tentando apontar é que, ao fazer essa alteração no cabeçalho do XML de origem, o corpo do XML pode ser inserido em um registro de tabela SQL usando um campo de tipo de dados XML e o cabeçalho é removido. Por motivos que não me lembro agora (quatro anos atrás!) O resultado foi algo útil na época. E sim, erro estúpido usando 'Substituir'. Acontece.
DLG de