Las mejores prácticas para la serialización de objetos en un formato de cadena personalizada para su uso en un archivo de salida

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

Pregunta

Yo estaba a punto de poner en práctica una anulación de ToString () en una clase de negocio en particular con el fin de producir un formato de Excel de usar para escribir en un archivo de salida, que se recogió después y procesada. Esto es lo que se supone que los datos para parecerse a:

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

No es un gran problema para mí de hacer sólo un formato de cadena y anular ToString(), pero eso va a cambiar el comportamiento de ToString() por cualquier objeto decido serializar esta manera, por lo que la implementación de ToString() toda desigual a través de la biblioteca.

Ahora, he estado leyendo sobre IFormatProvider y una clase que implementa suena como una buena idea, pero todavía estoy un poco confundido acerca de dónde toda esta lógica debe residir y cómo construir la clase de formateador.

¿Qué hacen ustedes cuando se necesita para hacer un CSV, delimitado por tabuladores o alguna otra cadena arbitraria no XML de un objeto?

¿Fue útil?

Solución

Aquí hay una manera genérica para la creación de CSV a partir de una lista de objetos, utilizando la reflexión:

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

Muchas variaciones se pueden hacer, como escribir directamente a un archivo en ToCsv (), o la sustitución de la StringBuilder con una declaraciones IEnumerable y rendimiento.

Otros consejos

Esta es una versión simplificada de la idea de CSV por Hejndorf (sin la sobrecarga de la memoria, ya que da lugar a cada línea a su vez). Debido a la demanda popular, también es compatible con los campos y las propiedades simples mediante el uso de Concat.

Actualización 18 de mayo de 2017

Este ejemplo no fue diseñado para ser una solución completa, simplemente hacer avanzar la idea original publicado por Per Hejndorf. Para generar CSV válido tiene que sustituir ningún carácter delimitador de texto, dentro del texto, con una secuencia de 2 caracteres delimitadores. p.ej. un .Replace("\"", "\"\"") sencilla.

Actualización 12 de Feb el año 2016

Después de usar mi propio código de nuevo en un proyecto hoy en día, me di cuenta de que no debería haber tomado nada por sentado cuando empecé en el ejemplo de @Per Hejndorf. Tiene más sentido para asumir un delimitador predeterminado "" (coma) y hacer que el delimitador de la segunda, opcional , el parámetro. Mi propia versión de la biblioteca también proporciona un parámetro header tercero que controla si una fila de cabecera debe ser devuelto como a veces solo deseas los datos.

por ejemplo.

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

por lo que a continuación, utilizar de esta manera para delimitado por comas:

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

o así por otro delimitador (TAB por ejemplo):

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

Ejemplos prácticos

lista de escritura a un archivo CSV delimitado por comas

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

o escribirlo delimitado por tabuladores

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

Si tiene campos / propiedades complejas que necesitará para filtrar fuera de las cláusulas SELECT.


Las versiones anteriores y detalles a continuación:

Esta es una versión simplificada de la idea de CSV por Hejndorf (sin la sobrecarga de la memoria, ya que da lugar a cada línea a su vez) y tiene sólo 4 líneas 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());
    }
}

Se puede recorrer de esta manera:

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

donde objects es una lista estricta de tipos de objetos.

Esta variación incluye ambos campos públicos y propiedades 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 regla general Abogo única anulando toString como una herramienta para la depuración, si es por la lógica de negocio que debe ser un método explícito de la clase / interfaz.

Para la serialización simple como esto te sugiero tener una clase separada que sabe acerca de su biblioteca de salida CSV y sus objetos de negocio que hace la serialización en lugar de empujar la serialización en el negocio de los objetos mismos.

De esta manera se termina con una clase por cada formato de salida que produce una vista del modelo.

Para la serialización más complejo en el que está tratando de escribir un gráfico de objetos para la persistencia me gustaría considerar ponerlo en las clases de negocios -. Pero sólo si se hace por un código más limpio

El problema con las soluciones que he encontrado hasta ahora es que no dejan que se exporta un subconjunto de propiedades, pero sólo el objeto entero. La mayoría de las veces, cuando tenemos que exportar datos en formato CSV, tenemos que "adaptar" su formato de una manera precisa, así que creé este método simple extensión que me permite hacer que al hacer pasar una serie de parámetros de tipo Func<T, string> a especificar la asignación.

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)

Genera:

Hayek,Friedrich
Rothbard,Murray
Brent,David

tuve un problema variación del HiTech magia eran dos propiedades con el mismo valor, sólo uno obtendría poblada. Esto parece haber arreglado:

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

Gone de Codificación de respuesta fue muy útil. Hice algunos cambios en él con el fin de manejar los gremlins de texto que la manguera de la salida.

 /******************************************************/
    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 bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top