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

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

Вопрос

Я как раз собирался реализовать переопределение 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, из объекта?

Это было полезно?

Решение

Вот общий способ создания 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.

Другие советы

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

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

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

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

После повторного использования моего собственного кода в сегодняшнем проекте я понял, что мне не следовало принимать что-либо как должное, когда я начинал с примера @Per Hejndorf.Имеет больше смысла использовать разделитель по умолчанию "," (запятая) и сделать разделитель вторым, необязательно, параметр.Моя собственная версия библиотеки также предоставляет 3-ю 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);
}

или как это для другого разделителя (напримерВКЛАДКА):

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

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

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

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

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

Проблема с решениями, которые я нашел до сих пор, заключается в том, что они не позволяют экспортировать подмножество свойств, а только весь объект.В большинстве случаев, когда нам нужно экспортировать данные в 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

У меня была проблема, вариацией 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());
        }
    }

Ответ 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;
       }
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top