Search code examples
c#csvunity-game-enginecsvhelper

CSVHelper: dynamically write to CSV


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.


Solution

  • 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());
                        WriteRecordToCSV(csv, instance, local_structure);
                    }
                }
                else
                {
                    csv.WriteField(thisVar.GetValue(record));
                }
            }
        }