Reputation: 11187
I'm using CSVHelper (thanks Josh Close) to read a CSV file which works great. I'm now trying to use it to map that file to some internal classes; however, the CSV's I'm mapping vary by customer but all need to map to my internal classes. I need to allow the customer to define how the CSV maps to my POCO objects.
I'm storing the customer defined mappings as Dictionary<string,int>
-- ["Firstname", 20],["Lastname",21],["Address.Line1",30], ["Address.Line2",31], etc.
I have a dynamic map class that works for dynamically mapping based on a given mapping at runtime. My problem lies in dealing with reference type properties. Here is what I have so far.
POCO Classes
public class Client
{
public int Id {get; set;}
public string Firstname {get; set;}
public string Lastname {get; set;}
public Address Address {get; set;}
...
}
public class Address
{
public string Line1 {get; set;}
public string Line2 {get; set;}
public string City {get; set;}
...
}
Based on a few posts that I've run across here and here, I came up with the following that uses a defined mapping to map a CSV dynamically.
Dynamic Map
public class BaseCSVMap<T> : ClassMap<T> where T : class
{
public void CreateMap(Dictionary<string,int> mappings)
{
foreach(var mapping in mappings)
{
var propname = mapping.Key;
var csvIndex = mapping.Value;
var member = typeof(T).GetProperty(propname);
Map(typeof(T), member).Index(csvIndex);
}
}
}
Using Dynamic Map
var id = 2; //Customer 2
var mappings = dataContext.Mappings.Where(m => m.id = id); //Get customer 2's map
using(var reader = File.OpenText(@"c:\temp\testfile.csv"))
{
var csv = new CsvReader(reader);
csv.Configuration.HasHeaderRecord = true; //hardcoded for now
var map = new BaseCSVMap<Client>();
map.CreateMap(mappings);
csv.Configuration.RegisterClassMap(map);
var records = csv.GetRecords<Client>();
}
I added the following in my BaseCSVMap<T>
class, which works great if all my reference properties are strings, but doesn't work so well when a property is something else.
var member = typeof(T).GetProperty(propname);
//New code
//Mapping would look like ["Address.Line1",78]
if(member.GetType().IsClass)
{
string exp = $"c.{propname}";
var p = Expression.Parameter(typeof(T), "c");
var e = System.Linq.Dynamic.Core.DynamicExpressionParser.ParseLambda(new [] {p}, null, exp);
Map((Expression<Func<T,string>>)e).Index(csvIndex);
}
I also looked for a way to take advantage of the Reference mapping that CSVHelper makes available, but was unable to figure out how to do that in a dynamic fashion.
Looking for some guidance in how to accomplish defining a dynamic map for a reference type with CSVHelper.
Upvotes: 7
Views: 3980
Reputation: 9074
I was able to do it by checking the property type and create the dynamic lambda expression according to the property type. You would need to make certain you check for all the property types in your class, including nullable
types.
public class BaseCSVMap<T> : ClassMap<T> where T : class
{
public void CreateMap(Dictionary<string, int> mappings)
{
foreach (var mapping in mappings)
{
var propname = mapping.Key;
var csvIndex = mapping.Value;
var param = Expression.Parameter(typeof(T), "x");
var property = (MemberExpression)propname.Split('.').Aggregate<string, Expression>(param, Expression.PropertyOrField);
if (property.Type == typeof(string))
{
var expression = Expression.Lambda<Func<T, string>>(property, param);
Map(expression).Index(csvIndex);
}
else if (property.Type == typeof(int))
{
var expression = Expression.Lambda<Func<T,int>>(property, param);
Map(expression).Index(csvIndex);
}
else if (property.Type == typeof(int?))
{
var expression = Expression.Lambda<Func<T, int?>>(property, param);
Map(expression).Index(csvIndex);
}
else
{
throw new Exception("No mapping for type " + property.Type);
}
}
}
}
FYI for those using this, there are a few changes in the current version of CsvHelper 30.0.1
void Main()
{
var mappings = new Dictionary<string, int>
{
{"Id", 0},
{"FirstName", 1},
{"LastName", 2},
{"Address.Line1", 5},
{"Address.Line2", 3},
{"Address.City", 4}
};
using (var reader = new StringReader("Id,FirstName,LastName,Line2,City,Line1\n1,My,Name,Address2,Cincinnati,Address1"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
var map = new BaseCSVMap<Client>();
map.CreateMap(mappings);
csv.Context.RegisterClassMap(map);
var records = csv.GetRecords<Client>().Dump();
}
}
Upvotes: 0