Le migliori pratiche per la serializzazione di oggetti in un formato stringa personalizzata per l'uso in un file di output

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

Domanda

Stavo per implementare un override di ToString () su una particolare classe business al fine di produrre un formato Excel-friendly per scrivere in un file di output, che sarà preso in seguito ed elaborato. Ecco ciò che i dati si suppone a guardare come:

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

E 'un grosso problema per me fare solo una stringa di formato e sovrascrivere ToString(), ma che cambierà il comportamento di ToString() per tutti gli oggetti decido di serializzare in questo modo, rendendo l'implementazione di ToString() tutto cencioso di fronte alla biblioteca.

Ora, ho letto su IFormatProvider e una classe che implementa suona come una buona idea, ma sono ancora un po 'confuso su dove tutto questa logica dovrebbe risiedere e come costruire la classe formattatore.

Che cosa voi ragazzi fare quando hai bisogno di fare un CSV, delimitato da tabulazioni o qualche altra stringa arbitraria non XML di un oggetto?

È stato utile?

Soluzione

Ecco un modo generico per la creazione di file CSV da un elenco di oggetti, utilizzando la riflessione:

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

Molteplici varianti possono essere apportate, come scrivere direttamente a un file in ToCsv () o sostituendo la StringBuilder con un bilancio IEnumerable e resa.

Altri suggerimenti

Ecco una versione semplificata dell'idea CSV di Per Hejndorf (senza il sovraccarico di memoria in quanto produce ogni linea in giro). A causa della grande richiesta supporta anche entrambi i campi e le proprietà semplici con l'uso di Concat.

Aggiornamento 18 maggio 2017

Questo esempio non è mai stato destinato ad essere una soluzione completa, solo avanzando l'idea originale pubblicato da Per Hejndorf. Per generare CSV valido è necessario sostituire ogni carattere di delimitazione del testo, all'interno del testo, con una sequenza di 2 caratteri di delimitazione. per esempio. un semplice .Replace("\"", "\"\"").

Aggiornamento 12 Feb 2016

Dopo aver usato ancora una volta il mio codice in un progetto di oggi, ho capito che non avrei dovuto prendere nulla per scontato quando ho iniziato dall'esempio di @Per Hejndorf. Ha più senso assumere un delimitatore di default "" (virgola) e rendere il delimitatore il secondo, opzionale , il parametro. La mia versione della libreria fornisce anche un parametro header terzo che controlla se una riga di intestazione deve essere restituito come a volte desideri solo i dati.

per es.

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

in modo da quindi utilizzarlo come questo per delimitato da virgole:

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

o simili per un altro delimitatore (ad esempio TAB):

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

Esempi pratici

Elenco di scrittura in un file CSV delimitato da virgole

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

o scrivere tabulazioni

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

Se si dispone di campi / proprietà complesse è necessario filtrare fuori delle clausole di selezione.


Le versioni precedenti e dettagli di seguito:

Ecco una versione semplificata dell'idea CSV di Per Hejndorf (senza il sovraccarico memoria, come cede ogni linea in giro) e ha solo 4 linee di codice:)

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

È possibile iterare in questo modo:

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

dove objects è una lista fortemente tipizzato di oggetti.

Questa variazione comprende entrambi i campi pubblici e semplici proprietà pubbliche:

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

Come regola generale sono favorevole solo l'override toString come uno strumento per il debug, se è per la logica di business che dovrebbe essere un metodo esplicito sulla classe / interfaccia.

Per la semplice serializzazione come questo mi piacerebbe suggeriamo di avere una classe separata che conosce la vostra libreria di output in formato CSV e gli oggetti di business che fa la serializzazione piuttosto che spingere la serializzazione nel business stessi oggetti.

In questo modo si finisce con una classe per ogni formato di output che produce una visione del modello.

Per la serializzazione più complesso in cui si sta cercando di scrivere un oggetto grafico per la persistenza che avevo in considerazione mettendolo nelle classi di business -. Ma solo se rende il codice più pulito

Il problema con le soluzioni che ho trovato finora è che essi non consentono di esportare un sottoinsieme di proprietà, ma solo l'intero oggetto. La maggior parte del tempo, quando abbiamo bisogno di esportare i dati in formato CSV, abbiamo bisogno di "personalizzare" il suo formato in modo preciso, così ho creato questo semplice metodo di estensione che mi permette di fare che facendo passare un array di parametri di tipo Func<T, string> a specificare la mappatura.

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

Utilizzo:

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

Genera:

Hayek,Friedrich
Rothbard,Murray
Brent,David

Ho avuto un problema di variazione del HiTech magia erano due oggetti con lo stesso valore, solo si otterrebbe popolato. Questo sembra essere risolto:

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

di codifica Andato è stato molto utile. Ho fatto alcune modifiche ad esso, al fine di gestire i gremlins di testo che il tubo di uscita.

 /******************************************************/
    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;
       }
    }
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top