Melhores práticas para a serialização de objetos para um formato seqüência personalizada para uso em um arquivo de saída

StackOverflow https://stackoverflow.com/questions/1179816

Pergunta

Eu estava prestes a implementar uma substituição de ToString () em uma classe de negócio em particular, a fim de produzir um formato Excel-friendly para escrever para um arquivo de saída, que vai ser mais tarde e processados. Aqui está o que os dados são suposto a aparência:

5555555 "LASTN SR, FIRSTN"  5555555555  13956 STREET RD     TOWNSVILLE  MI  48890   25.88   01-003-06-0934

Não é nenhum grande negócio para me para fazer apenas uma seqüência de formato e override ToString(), mas isso vai mudar o comportamento de ToString() por quaisquer objetos que decidem serializar dessa forma, tornando a implementação de ToString() todos irregular do outro lado da biblioteca.

Agora, eu estive lendo sobre IFormatProvider e uma classe implementando isso soa como uma boa idéia, mas eu ainda estou um pouco confuso sobre onde toda esta lógica deve residir e como construir a classe formatador.

O que vocês fazem quando você precisa fazer um CSV, delimitados por tabulações ou alguma outra cadeia arbitrária não-XML para fora de um objeto?

Foi útil?

Solução

Aqui está uma maneira genérica para a criação de CSV a partir de uma lista de objetos, usando a reflexão:

    public static string ToCsv<T>(string separator, IEnumerable<T> objectlist)
    {
        Type t = typeof(T);
        FieldInfo[] fields = t.GetFields();

        string header = String.Join(separator, fields.Select(f => f.Name).ToArray());

        StringBuilder csvdata = new StringBuilder();
        csvdata.AppendLine(header);

        foreach (var o in objectlist) 
            csvdata.AppendLine(ToCsvFields(separator, fields, o));

        return csvdata.ToString();
    }

    public static string ToCsvFields(string separator, FieldInfo[] fields, object o)
    {
        StringBuilder linie = new StringBuilder();

        foreach (var f in fields)
        {
            if (linie.Length > 0)
                linie.Append(separator);

            var x = f.GetValue(o);

            if (x != null)
                linie.Append(x.ToString());
        }

        return linie.ToString();
    }

Muitas variações podem ser feitas, como escrever diretamente para um arquivo no ToCsv (), ou substituir o StringBuilder com um IEnumerable e rendimento declarações.

Outras dicas

Aqui está uma versão simplificada da idéia CSV de Per Hejndorf (sem a sobrecarga de memória, uma vez que produz cada linha, por sua vez). Devido à demanda popular também suporta ambos os campos e propriedades simples pelo uso de Concat.

Atualização de 18 de maio de 2017

Este exemplo nunca foi destinado a ser uma solução completa, apenas avançar a ideia original publicado por Per Hejndorf. Para gerar CSV válido você precisa substituir quaisquer caracteres delimitador de texto, dentro do texto, com uma seqüência de 2 caracteres delimitadores. por exemplo. um .Replace("\"", "\"\"") simples.

Atualização de 12 de fevereiro de 2016

Depois de usar meu próprio código novamente em um projeto hoje, eu percebi que não deveria ter tomado nada como garantido quando eu comecei a partir do exemplo de @Per Hejndorf. Faz mais sentido para assumir um delimitador padrão "" (vírgula) e fazer o delimitador o segundo, opcional , parâmetro. Minha própria versão da biblioteca também fornece um 3º parâmetro header que controla se uma linha de cabeçalho deve ser retornado como às vezes você só quer o de dados.

por exemplo.

public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    if (header)
    {
        yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
    }
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}

para que, em seguida, usá-lo como este para delimitado por vírgula:

foreach (var line in ToCsv(objects))
{
    Console.WriteLine(line);
}

ou como esta para outra delimitador (por exemplo, TAB):

foreach (var line in ToCsv(objects, "\t"))
{
    Console.WriteLine(line);
}

Exemplos práticos

Lista de gravação para um arquivo CSV delimitado por vírgula

using (TextWriter tw = File.CreateText("C:\testoutput.csv"))
{
    foreach (var line in ToCsv(objects))
    {
        tw.WriteLine(line);
    }
}

ou escrevê-lo guia-delimitado

using (TextWriter tw = File.CreateText("C:\testoutput.txt"))
{
    foreach (var line in ToCsv(objects, "\t"))
    {
        tw.WriteLine(line);
    }
}

Se você tem campos complexos / propriedades que você precisa para filtrá-los das cláusulas SELECT.


As versões anteriores e os detalhes abaixo:

Aqui está uma versão simplificada da idéia CSV de Per Hejndorf (sem a sobrecarga de memória, uma vez que produz cada linha, por sua vez) e tem apenas 4 linhas de código:)

public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    FieldInfo[] fields = typeof(T).GetFields();
    yield return String.Join(separator, fields.Select(f => f.Name).ToArray());
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString()).ToArray());
    }
}

Você pode iterar-lo assim:

foreach (var line in ToCsv(",", objects))
{
    Console.WriteLine(line);
}

onde objects é uma lista com rigidez de tipos de objetos.

Esta variação inclui ambos os campos públicos e propriedades públicas simples:

public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}

Como regra geral eu só defendo substituir toString como ferramenta de depuração, se é para a lógica de negócios deve ser um método explícito na classe / interface.

Para a serialização simples como este eu sugiro ter uma classe separada que sabe sobre a sua biblioteca de saída CSV e seus objetos de negócios que faz a serialização em vez de empurrar a serialização no negócio objetos em si.

Desta forma você acaba com uma classe por formato de saída que produz uma visão do seu modelo.

Para a serialização mais complexo onde você está tentando escrever um gráfico de objeto para a persistência eu consideraria colocá-la nas classes de negócio -. Mas só se faz para um código mais limpo

O problema com as soluções que eu encontrei até agora é que eles não permitem que você exportar um subconjunto de propriedades, mas apenas o objeto inteiro. Na maioria das vezes, quando precisamos de dados de exportação em CSV, precisamos "tailor" seu formato de uma forma precisa, por isso criei este método simples extensão que me permite fazer isso por meio de um conjunto de parâmetros do tipo Func<T, string> para especificar o mapeamento.

public static string ToCsv<T>(this IEnumerable<T> list, params Func<T, string>[] properties)
{
    var columns = properties.Select(func => list.Select(func).ToList()).ToList();

    var stringBuilder = new StringBuilder();

    var rowsCount = columns.First().Count;

    for (var i = 0; i < rowsCount; i++)
    {
        var rowCells = columns.Select(column => column[i]);

        stringBuilder.AppendLine(string.Join(",", rowCells));
    }

    return stringBuilder.ToString();
}

Uso:

philosophers.ToCsv(x => x.LastName, x => x.FirstName)

Gera:

Hayek,Friedrich
Rothbard,Murray
Brent,David

Tive um problema variação da Magia HiTech eram duas propriedades com o mesmo valor, apenas um iria ficar povoada. Este parece ter corrigido isso:

        public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
    {
        FieldInfo[] fields = typeof(T).GetFields();
        PropertyInfo[] properties = typeof(T).GetProperties();
        yield return String.Join(separator, fields.Select(f => f.Name).Union(properties.Select(p => p.Name)).ToArray());
        foreach (var o in objectlist)
        {
            yield return string.Join(separator, (properties.Select(p => (p.GetValue(o, null) ?? "").ToString())).ToArray());
        }
    }

A resposta de Codificação ido foi muito útil. Fiz algumas alterações a ele, a fim de gremlins texto alça que iria mangueira de saída.

 /******************************************************/
    public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
    {
       FieldInfo[] fields = typeof(T).GetFields();
       PropertyInfo[] properties = typeof(T).GetProperties();
       string str1;
       string str2;

       if(header)
       {
          str1 = String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p => p.Name)).ToArray());
          str1 = str1 + Environment.NewLine;
          yield return str1;
       }
       foreach(var o in objectlist)
       {
          //regex is to remove any misplaced returns or tabs that would
          //really mess up a csv conversion.
          str2 = string.Join(separator, fields.Select(f => (Regex.Replace(Convert.ToString(f.GetValue(o)), @"\t|\n|\r", "") ?? "").Trim())
             .Concat(properties.Select(p => (Regex.Replace(Convert.ToString(p.GetValue(o, null)), @"\t|\n|\r", "") ?? "").Trim())).ToArray());

          str2 = str2 + Environment.NewLine;
          yield return str2;
       }
    }
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top