it-swarm.dev

Usando StringWriter para serialização de XML

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

Eu 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 isto 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 foi, que o primeiro exemplo gerado XML eu não poderia simplesmente escrever em uma coluna XML do banco de dados SQL Server 2005.

A primeira pergunta é: Existe uma razão pela qual eu não deveria usar StringWriter para serializar um objeto quando eu precisar dele como uma string depois? Eu nunca encontrei um resultado usando StringWriter quando googling.

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


Adição:

Como já foi mencionado por ambas as respostas, vou mais adiante no problema XML para DB.

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

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

Para string

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

Eu peguei a string criada a partir do XmlTextWriter e coloquei como xml lá. Este não funcionou (nem com inserção manual no DB).

Depois tentei inserção manual (apenas escrevendo INSERT INTO ...) com codificação = "utf-16" que também falhou. Removendo a codificação totalmente funcionou então. Depois desse resultado eu mudei de volta para o código StringWriter e voila - funcionou.

Problema: Eu não entendo o porque.

em Christian Hayter: Com esses testes eu não tenho certeza se tenho que usar o utf-16 para escrever no DB. Não definir a codificação para o trabalho UTF-16 (no tag xml)?

91
StampedeXV

<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 da entrada parâmetro. Se você adicionou manualmente <?xml version="1.0" encoding="utf-8"?><test/> à string, declarar o SqlParameter como do tipo SqlDbType.Xml ou SqlDbType.NVarChar daria a você o erro "Cannot to the the encoding". Então, ao inserir manualmente via T-SQL, desde que você trocou a codificação declarada para utf-16, você estava claramente inserindo uma string VARCHAR (não prefixada com um maiúscula "N", portanto uma codificação de 8 bits, como UTF- 8) e não uma string NVARCHAR (prefixada com um maiúscula "N", daí a codificação LE de 16 bits UTF-16).

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

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

(Resposta detalhada é abaixo)


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

Não importa realmente 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 sobre esta questão: Como resolver o erro “incapaz de 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 as strings sempre como UCS-2/UTF-16 LE
  3. Se o seu XML é 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, então ele deve ser prefixado com um "N" maiúsculo.
    2. se especificar a declaração XML, ela deve ser "UCS-2" ou "UTF-16" (não há diferença real aqui)
  4. Se seu XML é 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ódigos especificada pelo padrão Collation do banco de dados
    2. você deve passar os dados como VARCHAR(MAX)/SqlDbType.VarChar (maxsize = -1), ou se estiver usando um literal de string, então ele deve not ​​ser prefixado com um maiúscula "N".
    3. Seja qual for a codificação de 8 bits usada, a "codificação" anotada 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 delineados acima em mente, e dado que strings no .NET são sempre UTF-16 LE/UCS-2 LE (não há diferença entre aqueles em termos de codificação), podemos responder às suas perguntas:

Existe uma razão pela qual eu não deveria usar StringWriter para serializar um objeto quando eu preciso dele como uma seqüência depois?

Não, o seu código StringWriter parece estar bem (pelo menos não vejo problemas no meu teste limitado usando o segundo bloco de código da questão).

Não definir a codificação para o trabalho UTF-16 (no tag xml)?

Não é necessário fornecer a declaração XML. Quando está faltando, a codificação é assumida como sendo UTF-16 LE if você passa a string para o SQL Server como NVARCHAR (ou seja, SqlDbType.NVarChar) ou XML (ou seja, SqlDbType.Xml). Presume-se que a codificação seja a página de código de 8 bits padrão, se for transmitida como VARCHAR (ou seja, SqlDbType.VarChar). Se você tem algum caractere ASCII não padrão (ou seja, valores 128 e acima) e está passando como VARCHAR, provavelmente verá "?" para BMP caracteres e "??" Para caracteres suplementares, 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, você deve passar para o SQL Server usando o tipo de dados correspondente de 8 ou 16 bits. Portanto, se você tiver uma declaração declarando que a codificação é UCS-2 ou UTF-16, você deve passar como SqlDbType.NVarChar ou SqlDbType.Xml. Ou, se você tiver uma declaração declarando que a codificação é uma das opções de 8 bits (ou seja, UTF-8, Windows-1252, iso-8859-1, etc), você [deve passar como SqlDbType.VarChar. A não correspondência da codificação declarada com o tipo de dados apropriado do SQL Server de 8 ou 16 bits resultará no erro "não foi possível alternar a codificação" que você estava obtendo.

Por exemplo, usando seu código de serialização baseado em StringWriter, 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 StringWriter não tem uma opção para OmitXmlDeclaration como XmlWriter faz), 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, já que é BMP O ponto de código U + 1234 e ???? é o Código de Caracteres Suplementares 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

Ergo, toda essa explicação de lado, a solução completa para a sua pergunta original é:

Você estava claramente passando a string como SqlDbType.VarChar. Alterne para SqlDbType.NVarChar e ele funcionará sem a necessidade de passar pela etapa extra de remover a declaração XML. Isso é preferível em relação à manutenção de SqlDbType.VarChar e à remoção da declaração XML, pois essa solução impedirá 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 ????.

1
Solomon Rutzky

Um problema com StringWriter é que por padrão ele não permite que você defina a codificação que ele 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ê o gravar em um arquivo. Eu tenho uma pequena turma para ajudar com isso:

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

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

Ou se você só precisa de UTF-8 (que é tudo o que eu preciso):

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ê precisará nos fornecer mais detalhes sobre o que aconteceu quando tentou, se quiser que possamos diagnosticá-lo/consertá-lo.

202
Jon Skeet

Ao serializar um documento XML em uma cadeia .NET, a codificação deve ser definida como UTF-16. As strings são armazenadas como UTF-16 internamente, portanto, essa é 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 para uma coluna xml deve ser codificada como UTF-16. O SQL Server rejeitará qualquer sequência em que a declaração XML não especifique UTF-16. Se a declaração XML não estiver presente, o padrão XML exigirá que o padrão seja UTF-8, portanto, o SQL Server também a rejeitará.

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);
        }
    }
}
126
Christian Hayter

Antes de tudo, cuidado para encontrar exemplos antigos. Você encontrou um que usa XmlTextWriter, que foi descontinuado a partir do .NET 2.0. XmlWriter.Create deve 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();
                }
            }
        }
    }
}
19
John Saunders
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
Mashudu Nemukuka