Рекомендации по сериализации объектов в пользовательский строковый формат для использования в выходном файле

Я как раз собирался реализовать переопределение ToString() в конкретном бизнес-классе, чтобы создать удобный для Excel формат для записи в выходной файл, который будет получен позже и обработан. Вот как должны выглядеть данные:

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

Мне не нужно просто форматировать строку и переопределять ToString(), но это изменит поведение ToString() для любых объектов, которые я решил сериализовать таким образом, делая реализацию ToString() всех оборванных через библиотеку.

Теперь я читал IFormatProvider, а класс, реализующий его, звучит как хорошая идея, но я все еще немного запутался в том, где должна находиться вся эта логика и как построить класс форматирования.

Что вы делаете, когда вам нужно сделать CSV, разделитель табуляции или какую-либо другую не-XML произвольную строку из объекта?

Ответ 1

Вот общий способ создания CSV из списка объектов, используя отражение:

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

Могут быть сделаны многие изменения, например, прямое изъятие файла в ToCsv() или замена StringBuilder на инструкции IEnumerable и yield.

Ответ 2

Вот упрощенная версия идеи CSV Per Hejndorf (без накладных расходов памяти, поскольку она дает каждую строку в свою очередь). Благодаря популярному спросу он также поддерживает как поля, так и простые свойства с помощью Concat.

Обновление 18 мая 2017 года

Этот пример никогда не был предназначен для полного решения, просто продвигая оригинальную идею, опубликованную Пер Хейндорфом. Для генерации правильного CSV вам нужно заменить любые текстовые разделители, в тексте, последовательностью из двух символов разделителя. например простой .Replace("\"", "\"\"").

Обновление 12 февраля 2016 г.

После использования моего собственного кода снова в проекте сегодня я понял, что не должен был принимать что-либо само собой разумеющееся, когда я начал с примера @Per Hejndorf. Имеет смысл предположить разделитель по умолчанию "," (запятая) и сделать разделитель вторым необязательным параметром. Моя собственная версия библиотеки также предоставляет третий параметр header, который определяет, следует ли возвращать строку заголовка, поскольку иногда вам нужны только данные.

например.

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

, чтобы затем использовать его для разделителя с запятой:

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

или как это для другого разделителя (например, TAB):

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

Практические примеры

записать список в CSV файл с разделителями-запятыми

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

или напишите его с разделителем табуляции

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

Если у вас есть сложные поля/свойства, вам необходимо отфильтровать их из предложений select.


Предыдущие версии и данные ниже:

Вот упрощенная версия идеи CSV Пер Хейндорфа (без накладных расходов памяти, поскольку она дает каждую строку по очереди) и имеет только 4 строки кода:)

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

Вы можете выполнить итерацию следующим образом:

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

где objects - строго типизированный список объектов.

Этот вариант включает как общедоступные поля, так и простые общедоступные свойства:

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

Ответ 3

Как эмпирическое правило, я рекомендую только переопределить toString как инструмент для отладки, если для бизнес-логики это должен быть явный метод для класса/интерфейса.

Для простой такой сериализации я бы предложил иметь отдельный класс, который знает о вашей выходной библиотеке CSV и ваших бизнес-объектах, которые выполняют сериализацию, а не толкают сериализацию в сами бизнес-объекты.

Таким образом, вы получаете класс для каждого формата вывода, который создает представление вашей модели.

Для более сложной сериализации, когда вы пытаетесь записать граф объектов для сохранения, я бы подумал о том, чтобы положить его в бизнес-классы, но только в том случае, если он делает более чистый код.

Ответ 4

У меня возникла проблема, когда вариация HiTech Magic была двумя свойствами с одинаковым значением, только один был бы заполнен. Кажется, это исправлено:

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

Ответ 5

Ответ Gone Coding был очень полезен. Я внес некоторые изменения в него, чтобы обрабатывать текстовые гремлины, которые будут выдавать результат.

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

Ответ 6

Проблема с решениями, которые я нашел до сих пор, заключается в том, что они не позволяют экспортировать подмножество свойств, а только весь объект. В большинстве случаев, когда нам нужно экспортировать данные в CSV, нам нужно "точно настроить" его формат, поэтому я создал этот простой метод расширения, который позволяет мне это сделать, передав массив параметров типа Func<T, string> чтобы указать отображение.

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

Применение:

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

Формирует:

Hayek,Friedrich
Rothbard,Murray
Brent,David