Рекомендации по сериализации объектов в пользовательский строковый формат для использования в выходном файле
-
19-09-2019 - |
Вопрос
Я как раз собирался реализовать переопределение 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;
}
}