使用csvHelper动态创建列

23

我有一个工人,他的各个领域都是从服务器获取的。我正在使用CSVHelper软件包将此类转换为Excel表格。 工人拥有以下领域:

class Worker
{ 
    string name;
    string phone;
    string age;
    Dictionary<string,object> customerField;
}
我可以像这样映射姓名、电话号码和号码:
class WorkerMap : CsvClassMap<Worker>
{
    public WorkerMap()
    {
        Map(m => m.name);
        Map(m => m.phone);
        Map(m => m.age);
    }
}

我通过以下方式生成地图:

csv.Configuration.RegisterClassMap<WorkerMap>();

按照以下方式编写工人名单:

csv.WriteRecords(workerList);

我该如何将customerField字典映射到Excel表格中,使键(字符串)成为另一列的列名,值(对象)成为该列的值。

CSVHelper能够帮助我们在运行时完成吗?我查阅了文档,但没有找到适合我的东西。


1
请查看此线程,了解一些不错的解决方案: https://github.com/JoshClose/CsvHelper/issues/719。 - nawfal
3个回答

38

我认为目前不支持编写字典。 首先,CsvHelper很难知道要写入哪些标题。 幸运的是,手动使用CsvWriter一个字段一个字段地写入并不太复杂。 如果我们假设每个Worker在customerField中具有相同的键,则您的代码可能如下所示。

var firstWorker = workerList.First();
var keys = firstWorker.customerField.Keys.ToList();

var headers = new []{ "name", "phone", "age"}.Concat(keys).ToList();
var csv = new CsvWriter( textWriter );

// Write the headers
foreach( var header in headers )
{
    csv.WriteField(header);
}
csv.NextRecord();

// Write the rows
foreach( var item in workerList)
{
    csv.WriteField(item.name);
    csv.WriteField(item.phone);
    csv.WriteField(item.age);
    var dict = worker.customerField;
    foreach (var key in keys)
    {
        csv.WriteField(dict[key]);
    }
    csv.NextRecord();
}

这段代码未经过测试,但应该可以让你接近所需的行为。如果列表中的customerField字典键不一致,则代码会稍微复杂些,但仍然可解决。


1
谢谢Michael。这指引我找到了我需要的答案。 - hyoyin_Kyuoma

7

不支持字典,但支持ExpandoObject。

https://github.com/JoshClose/CsvHelper/blob/48e70742e06007dae3a635c418b7e3358f667c4f/src/CsvHelper.Tests/Writing/MultipleHeadersTest.cs

https://github.com/JoshClose/CsvHelper/blob/b74a2f95a101158f4cdedd25fae6e8392b58855b/src/CsvHelper.Tests/Writing/DynamicTests.cs

如果您点击上面的第一个链接,您会在第50行和第57行找到使用WriteDynamicHeader方法的代码。

借助扩展方法,我为每个记录创建了一个ExpandoObject,并使用CsvHelper将该对象写入。参数名为documentDictionary<string,object>是我希望从中创建CSV记录的内容。

public static class DictionaryCsvExtentions
{
    public static dynamic BuildCsvObject(this Dictionary<string, object> document)
    {
        dynamic csvObj = new ExpandoObject();

        foreach (var p in document)
        {
            AddProperty(csvObj, p.Key, p.Value);
        }

        return csvObj;
    }

    private static void AddProperty(ExpandoObject expando, string propertyName, object propertyValue)
    {
        var expandoDict = expando as IDictionary<string, object>;
        if (expandoDict.ContainsKey(propertyName))
        {
            expandoDict[propertyName] = propertyValue;
        }
        else
        {
            expandoDict.Add(propertyName, propertyValue);
        }
    }
}

现在,我可以像这样从我的字典创建一个 ExpandoObject:
var csvObj = myDictonary.BuildCsvObject();

有了上面链接中Josh的测试结果,我们现在可以很顺畅地使用字典来处理CsvHelper。我认为这并不是对Michael方案的改进,只是一种不同的方法。

在此要感谢从字典代码中得到启发的基本ExpandoObject,来源于https://www.oreilly.com/learning/building-c-objects-dynamically(那里有更多的解释!)


4

最近遇到了相同的问题,为了完整起见,再发布另一个答案。我们有一个相当复杂的类似于ClassMap<Worker>的类,不想失去它。此外,我们需要进行CSV写入和读取,因此需要来自robs答案的ExpandoObject。最终,这种方法是Michael Richardson和robs答案的结合,应该带来两者之间的最佳效果。

此外,在读取CSV文件期间区分字典字段的好方法是在它们的前缀中加上像"customerField."这样的东西。

首先,我们需要将字典转换为/from Worker.customerField:

public static class WorkerExtensions
{
    const string CustomerFieldPrefix = nameof(Worker.customerField) + ".";

    public static dynamic GetCustomerFieldExpando(this Worker worker)
    {
        var expando = new ExpandoObject() as IDictionary<string, object>;

        foreach (var fieldPair in worker.customerField)
        {
            expando[CustomerFieldPrefix + fieldPair.Key] = fieldPair.Value ?? "";
        }

        return expando;
    }

    public static void SetCustomerField(this Worker worker, ExpandoObject expando)
    {
        var columnsToValues = expando as IDictionary<string, object>;

        foreach (var columnValuePair in columnsToValues)
        {
            if (columnValuePair.Key.StartsWith(CustomerFieldPrefix)
                && columnValuePair.Key.Length > CustomerFieldPrefix.Length)
            {
                string key = columnValuePair.Key.Substring(CustomerFieldPrefix.Length);
                worker.customerField[key] = columnValuePair.Value;
            }
        }
    }
}

接下来,CSV写入可以使用ClassMapExpandoObject,代码如下:
        csv.Configuration.HasHeaderRecord = true;
        csv.Configuration.RegisterClassMap<WorkerMap>();

        csv.WriteHeader<Worker>();
        (workers.First().GetCustomerFieldExpando() as IDictionary<string, object>)
            .Keys.ToList().ForEach(key => csv.WriteField(key));
        csv.NextRecord();

        foreach (var worker in workers)
        {
            csv.WriteRecord(worker);
            csv.WriteRecord(worker.GetCustomerFieldExpando());
            csv.NextRecord();
        }

最后,CSV读取也可以同时结合ClassMapExpandoObject使用:

        List<Worker> workers = new List<Worker>();

        csv.Configuration.HasHeaderRecord = true;
        csv.Configuration.RegisterClassMap<WorkerMap>();

        csv.Read();
        csv.ReadHeader();
        var columns = csv.Context.HeaderRecord;

        while (csv.Read())
        {
            var worker = csv.GetRecord<Worker>();
            workers.Add(worker);

            ExpandoObject expando = csv.GetRecord<dynamic>();
            worker.SetCustomerField(expando);
        }

在读取CSV时,如果你想要将真实类型读入字典值中(而不仅仅是字符串),情况会变得更加复杂。为了能够从 ExpandoObject 转换为正确的类型,我们需要预定义字典键和数据类型之间的关联。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接