Reputation: 33
My problem is as follows. I want to write the class properties to a CSV file with the help of CsvHelper under WPF c#, then later read them back. One of the referenced classes has a non-default, parameterized constructor. How can I make CsvHelper invoke its constructor correctly?
Therefore I use the following map file.
public class TradeLogRecMap : ClassMap<TradeLogRec>
{
public TradeLogRecMap()
{
AutoMap(CSVConfig);
}
}
With these classes and structs
public class Liquidity
{
static readonly Dictionary<int, string> Values = new Dictionary<int, string>
{
{0, "None"},
{1, "Added Liquidity"},
{2, "Removed Liquidity"},
{3, "Liquidity Routed Out" }
};
public Liquidity(int p)
{
Value = Values.ContainsKey(p) ? p : 0;
}
public int Value { get; set; }
public override string ToString()
{
return Values[Value];
}
}
public class Execution
{
public Liquidity LastLiquidity { get; set; }
public Execution()
{
LastLiquidity = new Liquidity(0);
}
}
public struct TradeLogRec
{
public Execution execution { set; get; }
}
When I write the CSV file is looks OK. But reading it back it says that it misses the header "p". Adding a "p" header to the CSV file solves that but doing so is not an option. Also adding a default constructor
public Liquidity()
{
}
solves the problem. But that's not an option ether.
Why does it expect the header "p" anyhow?
Is it possible to solve it with some constructor settings in the map file or CvsHelper configuration?
Or is it a bug?
Upvotes: 3
Views: 2453
Reputation: 116980
As of CsvHelper 27.1.1, when CsvHelper attempts to construct a class during reading, it will use the parameterless constructor if present. If none is present, it will use the parameterized constructor with the most parameters [1]. When a parameterized constructor is invoked, CSV columns are matched to constructor arguments by exact match on the argument name. In your Liquidity
class the argument you would like to map to the "Value" column is named p
, so the match cannot be made.
So, what are your options?
Firstly, if you can modify your Liquidity
class, you could rename the p
constructor argument to Value
:
public Liquidity(int Value)
{
// Make the constructor parameter name match the property name exactly for CsvHelper
this.Value = Values.ContainsKey(Value) ? Value : 0;
}
Having done so, everything will just work. Demo fiddle #1 here.
Secondly, you could modify Liquidity
to add [CsvHelper.Configuration.Attributes.Name("Value")]
to p
:
public Liquidity([CsvHelper.Configuration.Attributes.Name("Value")] int p)
{
Value = Values.ContainsKey(p) ? p : 0;
}
Demo fiddle #2 here.
Thirdly, if you cannot modify your classes in any way, you could override the default reference mapping generated by AutoMap
and supply your own mapping for Liquidity
.
Create the following ClassMap<Liquidity>
:
public class LiquidityMap : ClassMap<Liquidity>
{
public LiquidityMap()
{
Map(m => m.Value);
Parameter("p").Name(nameof(Liquidity.Value));
}
}
And modify TradeLogRecMap
to use it as follows:
public class TradeLogRecMap : ClassMap<TradeLogRec>
{
public TradeLogRecMap()
{
// Automap TradeLogRecMap
AutoMap(CSVConfig); // CSVConfig was not shown in your question, I used new CsvConfiguration(CultureInfo.InvariantCulture) { }
// Get the reference map for Execution
var executionRefMap = ReferenceMaps.Find<TradeLogRec>(m => m.execution);
// Get its reference map for LastLiquidity
var liquidityRefMap = executionRefMap.Data.Mapping.ReferenceMaps.Find<Execution>(m => m.LastLiquidity);
// Remove the auto-generated reference map for LastLiquidity
executionRefMap.Data.Mapping.ReferenceMaps.Remove(liquidityRefMap);
// And add a reference map for LastLiquidity using LiquidityMap
executionRefMap.Data.Mapping.ReferenceMaps.Add(new MemberReferenceMap(liquidityRefMap.Data.Member, new LiquidityMap()));
}
}
Demo fiddle #3 here.
And finally you could modify CsvConfiguration.PrepareHeaderForMatch
to automatically remap any column named "p" to "Value":
public static CsvConfiguration CSVConfig =>
new CsvConfiguration(CultureInfo.InvariantCulture)
{
PrepareHeaderForMatch = a => a.Header == "p" ? nameof(Liquidity.Value) : a.Header,
};
Then use it when constructing your CsvReader
:
public static List<TradeLogRec> DeserializeTradeLogRecMapFromCsv(TextReader reader)
{
using (var csv = new CsvReader(reader, CSVConfig))
{
csv.Context.RegisterClassMap<TradeLogRecMap>();
return csv.GetRecords<TradeLogRec>().ToList();
}
}
This seems a little kludgy because it maps all columns named "Value" to "p", not just columns of Liquidity
- but it does work. Demo fiddle #4 here.
[1] This behavior is configured via CsvConfiguration.ShouldUseConstructorParameter
and CsvConfiguration.GetConstructor
.
Upvotes: 7