Search code examples
c#attributescsvhelper

Use attributes to make headers more human readable with CSVHelper


I am trying to use CSVHelper to serialize a database that is constructed out of multiple classes like shown below. I would like to make the csv a bit more human readable by adding information on units (when appropriate) and by ordering the data so that the "Name" always appears first. The rest can come in whatever order.

I have a class like shown below.

[DataContract(IsReference = true)]
public class OpaqueMaterial : LibraryComponent
{
    [DataMember]
    [Units("W/m.K")]
    public double Conductivity { get; set; } = 2.4;

    [DataMember]
    public string Roughness { get; set; } = "Rough";
}
[DataContract]
public abstract class LibraryComponent
{
     [DataMember, DefaultValue("No name")]
     public string Name { get; set; } = "No name";
}

To avoid writing seprarate read write functions for each class I am reading and writing with templated functions like given below:

public void writeLibCSV<T>(string fp, List<T> records)
{
    using (var sw = new StreamWriter(fp))
    {
        var csv = new CsvWriter(sw);
        csv.WriteRecords(records);
    }
}
public List<T> readLibCSV<T>(string fp)
{
    var records = new List<T>();
    using (var sr = new StreamReader(fp))
    {
        var csv = new CsvReader(sr);
        records = csv.GetRecords<T>().ToList();
    }
    return records;
}

That I then use in the code to read and write as such:

writeLibCSV<OpaqueMaterial>(folderPath + @"\OpaqueMaterial.csv", lib.OpaqueMaterial.ToList());
List<OpaqueMaterial> inOpaqueMaterial = readLibCSV<OpaqueMaterial>(folderPath + @"\OpaqueMaterial.csv");

The CSV output then looks like:

Conductivity, Roughnes, Name
2.4, Rough, No Name

I would like to come out as:

Name, Conductivity [W/m.K], Roughness
No Name, 2.4, Rough

I know that the reordering is possible using maps like:

public class MyClassMap : ClassMap<OpaqueMaterial>
{
    public MyClassMap()
    {
        Map(m => m.Name).Index(0);
        AutoMap();
    }
}

I would like to make this abstract so that I dont have to apply a different mapping to every class. I was not able to find an example that could help with adding the custom headers. Any suggestions or help would be greatly appreciated.


Solution

  • You could create a generic version of ClassMap<T> that will automatically inspect the type T using reflection and then construct the mapping dynamically based on the properties it finds and based on the attributes that may or may not be attached to it.

    Without knowing the CsvHelper library too well, something like this should work:

    public class AutoMap<T> : ClassMap<T>
    {
        public AutoMap()
        {
            var properties = typeof(T).GetProperties();
    
            // map the name property first
            var nameProperty = properties.FirstOrDefault(p => p.Name == "Name");
            if (nameProperty != null)
                MapProperty(nameProperty).Index(0);
    
            foreach (var prop in properties.Where(p => p != nameProperty))
                MapProperty(prop);
        }
    
        private MemberMap MapProperty(PropertyInfo pi)
        {
            var map = Map(typeof(T), pi);
    
            // set name
            string name = pi.Name;
            var unitsAttribute = pi.GetCustomAttribute<UnitsAttribute>();
            if (unitsAttribute != null)
                name = $"{name} {unitsAttribute.Unit}";
            map.Name(name);
    
            // set default
            var defaultValueAttribute = pi.GetCustomAttribute<DefaultValueAttribute>();
            if (defaultValueAttribute != null)
                map.Default(defaultValueAttribute.Value);
    
            return map;
        }
    }
    

    Now, you just need to create a AutoMap<T> for every type T that you want to support.

    I’ve added examples for a UnitsAttribute and the DefaultValueAttribute, that should give you an idea on how to proceed with more attributes if you need more.