web-dev-qa-db-fra.com

Meilleures pratiques pour la sérialisation d'objets dans un format de chaîne personnalisé à utiliser dans un fichier de sortie

J'étais sur le point d'implémenter une substitution de ToString () sur une classe métier particulière afin de produire un format compatible avec Excel pour l'écriture dans un fichier de sortie, qui sera repris plus tard et traité. Voici à quoi ressemblent les données:

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

Ce n'est pas grave pour moi de créer une chaîne de format et de remplacer ToString(), mais cela modifiera le comportement de ToString() pour tous les objets que je décide de sérialiser de cette façon, ce qui rend l'implémentation de ToString() complètement désordonnée.

Maintenant, je lisais sur IFormatProvider , et une classe qui implémente cela semble être une bonne idée, mais je suis encore un peu confus quant à l’emplacement de toute cette logique et à la manière de construire la classe de formatage.

Que faites-vous lorsque vous devez créer un fichier CSV, délimité par des tabulations ou une autre chaîne arbitraire non XML à partir d'un objet?

26
Chris McCall

Voici un mode générique pour créer un fichier CSV à partir d'une liste d'objets, en utilisant la réflexion:

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

De nombreuses variantes peuvent être apportées, telles que l'écriture directe dans un fichier dans ToCsv () ou le remplacement de StringBuilder par un IEnumerable et des instructions de rendement.

60
Per Hejndorf

Voici une version simplifiée de l'idée de CSV de Per Hejndorf (sans la surcharge de mémoire car elle génère chaque ligne à tour de rôle). En raison de la demande générale, il prend également en charge les champs et les propriétés simples en utilisant Concat.

Mise à jour 18 mai 2017

Cet exemple n'a jamais été conçu pour être une solution complète, mais simplement pour faire avancer l'idée originale publiée par Per Hejndorf. Pour générer un fichier CSV valide, vous devez remplacer les caractères de délimitation de texte contenus dans le texte par une séquence de 2 caractères de délimitation. par exemple. une simple .Replace("\"", "\"\"").

Mise à jour 12 février 2016

Après avoir utilisé à nouveau mon propre code dans un projet aujourd'hui, j'ai réalisé que je n'aurais rien dû prendre pour acquis lorsque j'ai repris l'exemple de @Per Hejndorf. Il est plus logique de supposer un délimiteur par défaut de "," (virgule) et de définir le délimiteur comme second paramètre, facultatif. Ma propre version de bibliothèque fournit également un 3ème paramètre header qui contrôle si une ligne d'en-tête doit être renvoyée car vous ne souhaitez parfois que les données.

par exemple.

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

vous l'utilisez alors comme ceci pour les virgules:

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

ou comme ceci pour un autre délimiteur (par exemple, TAB):

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

Exemples pratiques

écrit la liste dans un fichier CSV délimité par des virgules

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

ou écrivez-le délimité par des tabulations

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

_ {Si vous avez des champs/propriétés complexes, vous devrez les filtrer hors des clauses de sélection.} _


Versions précédentes et détails ci-dessous:

Voici une version simplifiée de l'idée CSV de Per Hejndorf (sans la surcharge de mémoire car elle génère chaque ligne à tour de rôle) et ne comporte que 4 lignes de code :)

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

Vous pouvez le itérer comme ceci:

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

objects est une liste d'objets fortement typée.

Cette variante inclut à la fois des champs publics et des propriétés publiques 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());
    }
}
30
Gone Coding

En règle générale, je ne préconise que le remplacement de toString en tant qu'outil de débogage. Si c'est pour la logique métier, ce doit être une méthode explicite sur la classe/interface.

Pour une sérialisation simple comme celle-ci, je vous conseillerais de créer une classe distincte connaissant votre bibliothèque de sortie CSV et vos objets métier effectuant la sérialisation, au lieu de l'insérer dans les objets métier eux-mêmes.

De cette façon, vous vous retrouvez avec une classe par format de sortie qui produit une vue de votre modèle.

Pour une sérialisation plus complexe dans laquelle vous essayez d'écrire un graphe d'objets pour la persistance, envisagez de le placer dans les classes métier, mais uniquement si cela permet un code plus propre.

8
Tom

Le problème avec les solutions que j'ai trouvées jusqu'à présent est qu'elles ne vous permettent pas d'exporter un sous-ensemble de propriétés, mais uniquement l'objet entier. La plupart du temps, lorsque nous devons exporter des données au format CSV, nous devons "adapter" son format de manière précise. J'ai donc créé cette méthode d'extension simple qui me permet de le faire en transmettant un tableau de paramètres de type Func<T, string> à spécifiez le mappage.

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

Utilisation:

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

Génère:

Hayek,Friedrich
Rothbard,Murray
Brent,David
1
tocqueville

J'ai eu un problème avec la variation de HiTech Magic: deux propriétés avec la même valeur, une seule serait peuplée. Cela semble l'avoir corrigé:

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

La réponse de Gone Coding a été très utile. J'ai apporté quelques modifications à celui-ci afin de gérer les gremlins de texte qui ralentiraient la sortie.

 /******************************************************/
    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;
       }
    }
0
Chris Barnes