将对象序列化为自定义字符串格式以在输出文件中使用的最佳做法

时间:2009-07-24 19:49:06

标签: c# serialization formatprovider

我正准备在特定的业务类上实现ToString()的覆盖,以便生成一个Excel友好的格式来写入输出文件,稍后将对其进行拾取和处理。这是数据应该是什么样的:

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

创建格式字符串并覆盖ToString()对我来说没什么大不了的,但这会改变ToString()对于我决定以这种方式序列化的任何对象的行为,从而实现{ {1}}所有人都穿过图书馆。

现在,我一直在阅读IFormatProvider,并且实现它的类听起来是个好主意,但我仍然对所有这些逻辑应该驻留的位置以及如何构建格式化程序感到困惑类。

当你需要从对象中制作CSV,制表符分隔符或其他非XML任意字符串时,你们会做什么?

8 个答案:

答案 0 :(得分:62)

以下是使用反射从对象列表创建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()中的文件,或者用IEnumerable和yield语句替换StringBuilder。

答案 1 :(得分:32)

以下是Per Hejndorf的CSV概念的简化版本(没有内存开销,因为它依次产生每一行)。由于受欢迎的需求,它还使用Concat支持字段和简单属性。

2017年5月18日更新

这个例子从来没有打算成为一个完整的解决方案,只是推进Per Hejndorf发布的原始想法。要生成有效的CSV,您需要使用2个分隔符字符序列替换文本中的任何文本分隔符。例如一个简单的.Replace("\"", "\"\"")

2016年2月12日更新

今天在项目中再次使用我自己的代码后,我意识到当我从@Per Hejndorf的例子开始时,我不应该把任何事情视为理所当然。假设默认分隔符“,”(逗号)并使分隔符成为第二个可选参数更有意义。我自己的库版本还提供了第3个header参数,该参数控制是否应该返回标题行,因为有时您只需要数据。

e.g。

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子句中过滤掉。


以前的版本和详情如下:

以下是Per Hejndorf的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());
    }
}

答案 2 :(得分:8)

根据经验,我主张只重写toString作为调试工具,如果是业务逻辑,它应该是类/接口上的显式方法。

对于像这样的简单序列化,我建议有一个单独的类,它知道你的CSV输出库和执行序列化的业务对象,而不是将序列化推送到业务对象本身。

这样,您最终会得到一个每个输出格式的类,它会生成模型视图。

对于更复杂的序列化,你试图写出一个持久性的对象图,我会考虑把它放在业务类中 - 但只有它能使代码更清晰。

答案 3 :(得分:1)

我到目前为止找到的解决方案的问题是,它们不允许您导出属性的子集,而只导出整个对象。大多数情况下,当我们需要以CSV格式导出数据时,我们需要定制&#34;定制&#34;它的格式是精确的,所以我创建了这个简单的扩展方法,允许我通过传递类型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

答案 4 :(得分:0)

我有一个问题,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 :(得分:0)

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 :(得分:0)

ServiceStack.Text是流行的NuGet软件包,它支持CSV serialization。然后,这就是您需要的所有代码:

CsvSerializer.SerializeToCsv(foo)

如果不想使用标题,请首先使用以下代码:

CsvConfig<Foo>.OmitHeaders = true;

答案 7 :(得分:0)

去编码的答案很棒!我对Gone Coding的答案做了一些更改,以使用双引号限定字段,并使属性迭代器忽略没有索引参数的任何属性,即启用属性名称获取器和设置器的类中的属性:

FieldInfo[] fields = typeof(T).GetFields();
PropertyInfo[] properties = typeof(T).GetProperties().Where(x => x.GetIndexParameters().Length == 0).ToArray();
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());
}