问题
We want a generic approach for using CsvHelper. Depending the situation, the format of the CSV file is different. When the external system has all the required info more columns are present. I need to determine if the name of column is present in the CSV file (atm this is done in ResponseMap).
So we have an interface
List<T> ConvertTo<T, TMap>(string file, Dictionary<string, string> headers) where T : class
where TMap : ClassMap<T>;
I'm calling it like this.
_csvRepository.ConvertTo<ResponseNgi, ResponseMap>(file, headers);
The dictionary contains the name of the columns depending the situation we are dealing with (e.g. all info = 13 columns, not all info = 7 columns)
The implementation of the ConvertTo method:
public List<T> ConvertTo<T, TMap>(string file, Dictionary<string,string> headers) where T : class
where TMap : ClassMap<T>
{
using (var reader = new StreamReader(file, Encoding.GetEncoding("iso-8859-1")))
using (var csvReader = new CsvReader(reader))
{
var good = new List<T>();
// Set up CSV Helper
csvReader.Configuration.Delimiter = ";";
csvReader.Configuration.IgnoreQuotes = true;
csvReader.Configuration.HasHeaderRecord = true;
csvReader.Configuration.HeaderValidated = null;
csvReader.Configuration.MissingFieldFound = null;
csvReader.Configuration.TrimOptions = TrimOptions.Trim;
csvReader.Configuration.RegisterClassMap(new ResponseMap(headers));
// csvReader.Configuration.RegisterClassMap<TMap>(); Can't pass dictionary like this
// rest of the processing
return good.ToList();
}
}
As you can see in the code snippet, I have explicitly used ResponseMap in order to be able to pass the headers. it doesn't feel right.
And the ResponseMap class, which is a subclass of ClassMap.
public class ResponseMap : ClassMap<ResponseNgi>
{
public ResponseMap(Dictionary<string, string> headers)
{
Map(m => m.Street).Name(headers["street"]);
Map(m => m.HouseNumber).Name(headers["house number"]);
Map(m => m.PostalCode).Name(headers["postal code"]);
Map(m => m.MunicipalitySection).Name(headers["municipality section"]);
Map(m => m.Municipality).Name(headers["municipality"]);
Map(m => m.StreetCode).Name(headers["street code"]);
if (headers.ContainsKey("x"))
Map(m => m.Xlambert).Name(headers["x"]).TypeConverter<CustomDoubleConverter>();
if (headers.ContainsKey("y"))
Map(m => m.YLambert).Name(headers["y"]).TypeConverter<CustomDoubleConverter>();
if (headers.ContainsKey("z"))
Map(m => m.ZLambert).Name(headers["z"]).TypeConverter<CustomDoubleConverter>();
if (headers.ContainsKey("nrn"))
Map(m => m.Nrn).Name(headers["nrn"]);
if (headers.ContainsKey("source"))
Map(m => m.Source).Name(headers["source"]);
if (headers.ContainsKey("method"))
Map(m => m.Method).Name(headers["method"]);
Map(m => m.GeoadresId).Name(headers["geoadresId"]);
}
}
How can I call RegisterClassMap with a generic type (TMap) and be able to map the CSV properly depending the situation?
回答1:
I have an extension method I use for mapping headers to a generic type. I have a pull request to allow the same extension method to pass in an Optional variable. Unfortunately, the optional part doesn't currently work, but you could set the 6 columns that aren't always used to Optional in your ResponseMap. You could also use the extension method with Dictionary<string, string> instead of IEnumerable<CsvMapping>.
public static List<T> ConvertTo<T, TMap>(string file, IEnumerable<CsvMapping> headers) where T : class
where TMap : ClassMap<T>
{
using (var reader = new StreamReader(file, Encoding.GetEncoding("iso-8859-1")))
using (var csvReader = new CsvReader(reader))
{
var good = new List<T>();
// Set up CSV Helper
csvReader.Configuration.Delimiter = ";";
csvReader.Configuration.IgnoreQuotes = true;
csvReader.Configuration.HasHeaderRecord = true;
csvReader.Configuration.HeaderValidated = null;
csvReader.Configuration.MissingFieldFound = null;
csvReader.Configuration.TrimOptions = TrimOptions.Trim;
var classMap = csvReader.Configuration.RegisterClassMap<TMap>();
classMap.Map(headers);
good = csvReader.GetRecords<T>().ToList();
return good.ToList();
}
}
public class CsvMapping
{
public string CsvHeaderName { get; set; }
public string PropertyName { get; set; }
public bool Optional { get; set; }
}
public static class CsvHelperExtensions
{
public static void Map<T>(this ClassMap<T> classMap, IEnumerable<CsvMapping> csvMappings)
{
foreach (var mapping in csvMappings)
{
var property = typeof(T).GetProperty(mapping.PropertyName);
if (property == null)
{
throw new ArgumentException($"Class {typeof(T).Name} does not have a property named {mapping.PropertyName}");
}
if (mapping.CsvHeaderName != null)
{
classMap.Map(typeof(T), property).Name(mapping.CsvHeaderName);
}
//// Pull request to CsvHelper to fix the Optional() method. 3/14/2019
//if (mapping.Optional)
//{
// classMap.Map(typeof(T), property).Optional();
//}
}
}
}
public class ResponseMap : ClassMap<ResponseNgi>
{
public ResponseMap()
{
Map(m => m.Street);
Map(m => m.HouseNumber);
Map(m => m.PostalCode);
Map(m => m.MunicipalitySection);
Map(m => m.Municipality);
Map(m => m.StreetCode);
Map(m => m.Xlambert).TypeConverter<CustomDoubleConverter>().Optional();
Map(m => m.YLambert).TypeConverter<CustomDoubleConverter>().Optional();
Map(m => m.ZLambert).TypeConverter<CustomDoubleConverter>().Optional();
Map(m => m.Nrn).Optional();
Map(m => m.Source).Optional();
Map(m => m.Method).Optional();
Map(m => m.GeoadresId);
}
}
来源:https://stackoverflow.com/questions/59177675/generic-types-in-registerclassmap-with-parameters