CSVHelper: dynamically write to CSV

74 Views Asked by At

Let's say I have the following interface:

public interface IStateInformation
{
    public int TaskId { get; set; }
    public string Name { get; set; }
}

and the following class implementing this interface:

public class ExternalStateInformation : IStateInformation
{
    public int TaskId { get; set; }
    public string Name { get; set; }
    public string External { get; set; }
}

Now I want to write the following class to a CSV:

public class SwitchingData
{
    public string DateTime { get; set; }
    public int EpisodeId { get; set; }
    public IStateInformation SourceState { get; set; }
    public IStateInformation TargetState { get; set; }
}

As you can see, this class includes IStateInformation objects. When I try to write a record to a CSV file, only the properties of IStateInformation are considered (TaskId and Name) even when e.g. SourceState is of type ExternalStateInformation (therefore External is missing). My question is, how can I write the properties of a dynamic type instead of the static one? I tried to use ClassMap but without success.

Details

The writing script looks like this:

...         
using (var stream = File.Open(path, FileMode.Append))
using (var writer = new StreamWriter(stream))
using (var csv = new CsvWriter(writer, config))
{
     if(type != null)
     {
         csv.Context.RegisterClassMap(type);
     }
                
     csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
     csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);
     csv.WriteRecords(data);
}
...

where type was the ClassMap I played with.

2

There are 2 best solutions below

0
Ling On BEST ANSWER

In the end I have written my own functions writing the header and the records. My given example would lead to the following header:

DateTime,EpisodeId,SourceState_TaskId,SourceState_Name,SourceState_External,TargetState_TaskId,TargetState_Name,TargetState_External

If TargetState would implement the IStateInformation interface with another additional property, let's say Internal, the header would look like:

DateTime,EpisodeId,SourceState_TaskId,SourceState_Name,SourceState_External,TargetState_TaskId,TargetState_Name,TargetState_External,TargetState_Internal

General Writing Function

private static void WriteToCSV<T>(string path, TypeConverterOptions options, List<T> data, FileMode mode)
{
    using (var stream = File.Open(path, mode))
    using (var writer = new StreamWriter(stream))
    using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
    {
        csv.Context.TypeConverterOptionsCache.AddOptions<DateTime>(options);
        csv.Context.TypeConverterOptionsCache.AddOptions<DateTime?>(options);

        List<Type> structure = GetStructureOfCSV(csv, data);

        if (mode == FileMode.Create)
        {
            WriteHeaderToCSV(csv, data);
            csv.NextRecord();
        }

        foreach (var record in data)
        {
            WriteRecordToCSV(csv, record, structure);
            csv.NextRecord();
        }
    }

    if (mode == FileMode.Create)
    {
        Debug.Log(String.Format("Write data to new file {0}", path));
    }
    else if (mode == FileMode.Append)
    {
        Debug.Log(String.Format("Write data to existing file {0}", path));
    }
}

Function to get the Structure of the Object

This is needed to write default values instead of null values which would lead to too less CSV fields.

    private static List<Type> GetStructureOfCSV<T>(CsvWriter csv, List<T> data)
    {
        BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
        MemberInfo[] members = data[0].GetType().GetMemberInfos(bindingFlags);
        List<Type> structure = new List<Type>
        {
            data[0].GetType()
        };

        foreach (var thisVar in members)
        {
            if (!HasCSVHelperBuiltInConverter(thisVar.GetUnderlyingType()))
            {
                try
                {
                    List<T> records = data.Where(x => thisVar.GetValue(x) != null).ToList();
                    List<object> values = records.Select(x => thisVar.GetValue(x)).ToList();
                    structure = structure.Concat(GetStructureOfCSV(csv, values)).ToList();
                }
                catch (InvalidOperationException)
                {
                }
            }
        }

        return structure;
    }

Check for ReferenceConverter

    private static bool HasCSVHelperBuiltInConverter(Type type)
    {
        return TypeDescriptor.GetConverter(type).GetType() != typeof(ReferenceConverter);
    }

Header

    private static void WriteHeaderToCSV<T>(CsvWriter csv, List<T> data, string prefix = "")
    {
        BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
        MemberInfo[] members = data[0].GetType().GetMemberInfos(bindingFlags);

        foreach (var thisVar in members)
        {
            if (!HasCSVHelperBuiltInConverter(thisVar.GetUnderlyingType()))
            {
                try
                {
                    List<T> records = data.Where(x => thisVar.GetValue(x) != null).ToList();
                    List<object> values = records.Select(x => thisVar.GetValue(x)).ToList();
                    WriteHeaderToCSV(csv, values, thisVar.Name);
                }
                catch (InvalidOperationException)
                {
                    csv.WriteField(thisVar.Name);
                }
            }
            else
            {
                if (prefix != "" && prefix != null)
                {
                    csv.WriteField(string.Format("{0}_{1}", prefix, thisVar.Name));
                }
                else
                {
                    csv.WriteField(thisVar.Name);
                }
            }
        }
    }

Records

    private static void WriteRecordToCSV(CsvWriter csv, object record, List<Type> structure)
    {
        BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
        MemberInfo[] members = record.GetType().GetMemberInfos(bindingFlags);
        List<Type> local_structure = new List<Type>(structure);

        local_structure.RemoveAt(0);

        foreach (var thisVar in members)
        {
            if (!HasCSVHelperBuiltInConverter(thisVar.GetUnderlyingType()))
            {
                if(thisVar.GetValue(record) != null)
                {
                    WriteRecordToCSV(csv, thisVar.GetValue(record), local_structure);
                }
                else
                {
                    object instance = Activator.CreateInstance(local_structure.First());
                    csv.WriteRecord(instance);
                }
            }
            else
            {
                csv.WriteField(thisVar.GetValue(record));
            }
        }
    }
2
David Specht On

It's not pretty, but it appears that this ClassMap would work.

public sealed class SwitchingDataClassMap : ClassMap<SwitchingData>
{
    public SwitchingDataClassMap() 
    {
        Map(m => m.DateTime).Index(0);
        Map(m => m.EpisodeId).Index(1);
        Map(m => m.SourceState.TaskId).Name("SourceStateTaskId").Index(2);
        Map(m => m.SourceState.Name).Name("SourceStateName").Index(3);
        Map(m => ((ExternalStateInformation)m.SourceState).External)
            .Convert(args => args.Value.SourceState is ExternalStateInformation ?
                    ((ExternalStateInformation)args.Value.SourceState).External :
                    null)
            .Name("SourceStateExternal").Index(4);
        Map(m => m.TargetState.TaskId).Name("TargetStateTaskId").Index(5);
        Map(m => m.TargetState.Name).Name("TargetStateName").Index(6);
        Map(m => ((ExternalStateInformation)m.TargetState).External)
            .Convert(args => args.Value.TargetState is ExternalStateInformation ?
                    ((ExternalStateInformation)args.Value.TargetState).External :
                    null)
            .Name("TargetStateExternal").Index(7);
    }
}