using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;
using CsvHelper.Configuration;
using CsvHelper.Configuration.Attributes;
using CsvHelper.Expressions;
using CsvHelper.TypeConversion;
public class ValidatingCsvReader : CsvReader
public ValidatingCsvReader(TextReader reader, CultureInfo culture, bool leaveOpen = false) : this(new CsvParser(reader, culture, leaveOpen)) { }
public ValidatingCsvReader(TextReader reader, CsvConfiguration configuration) : this(new CsvParser(reader, configuration)) { }
public ValidatingCsvReader(IParser parser) : base(parser) { }
public Action<CsvContext, List<string>> OnUnmappedCsvHeaders { get; set; }
public override void ValidateHeader(Type type)
base.ValidateHeader(type);
var headerRecord = HeaderRecord;
var mapped = new BitArray(headerRecord.Length);
var map = Context.Maps[type];
FlagMappedHeaders(map, mapped);
var unmappedHeaders = Enumerable.Range(0, headerRecord.Length).Where(i => !mapped[i]).Select(i => headerRecord[i]).ToList();
if (unmappedHeaders.Count > 0)
OnUnmappedCsvHeaders?.Invoke(Context, unmappedHeaders);
protected virtual void FlagMappedHeaders(ClassMap map, BitArray mapped)
foreach (var parameter in map.ParameterMaps)
if (parameter.Data.Ignore)
if (parameter.Data.IsConstantSet)
if (parameter.Data.IsIndexSet && !parameter.Data.IsNameSet)
if (parameter.ConstructorTypeMap != null)
FlagMappedHeaders(parameter.ConstructorTypeMap, mapped);
else if (parameter.ReferenceMap != null)
FlagMappedHeaders(parameter.ReferenceMap.Data.Mapping, mapped);
var index = GetFieldIndex(parameter.Data.Names.ToArray(), parameter.Data.NameIndex, true);
foreach (var memberMap in map.MemberMaps)
if (memberMap.Data.Ignore || !CanRead(memberMap))
if (memberMap.Data.ReadingConvertExpression != null || memberMap.Data.IsConstantSet)
if (memberMap.Data.IsIndexSet && !memberMap.Data.IsNameSet)
var index = GetFieldIndex(memberMap.Data.Names.ToArray(), memberMap.Data.NameIndex, true);
foreach (var referenceMap in map.ReferenceMaps)
if (!CanRead(referenceMap))
FlagMappedHeaders(referenceMap.Data.Mapping, mapped);
public int? Count { get; set; }
public string Name1 { get; set; }
public string Name2 { get; set; }
public class VisitMap : ClassMap<VisitExport>
Map(m => m.Count).Name("Count");
Map(m => m.Name1).Name("Name").NameIndex(0);
Map(m => m.Name2).Name("Name").NameIndex(1);
public static void Test()
var inputList = new List<VisitExport>
new VisitExport { Count = 101, Name1 = "name 101", Name2 = "customer address 101" },
new VisitExport { Count = null, Name1 = "name 1", Name2 = "customer address 1" },
new VisitExport { Count = 202, Name1 = "name 202", Name2 = "customer address 202" },
var json0 = JsonConvert.SerializeObject(inputList);
var csv1 = TestOutput(inputList, new VisitMap());
Console.WriteLine("CSV export for model:");
var csv1Lower = csv1.ToLowerInvariant();
var newList1 = TestInput(csv1, new VisitMap());
var newList11 = TestInput(csv1Lower, new VisitMap(), (args) => args.Header.ToLowerInvariant());
Console.WriteLine("Caught expected exception reading CSV with unknown column: {0}",
Assert.Throws<CsvHelperException>(() =>
TestInput(GetUnknownColumnCsv(), new VisitMap());
Console.WriteLine("Caught expected exception reading lowercase CSV with unknown column: {0}",
Assert.Throws<CsvHelperException>(() =>
TestInput(GetUnknownColumnCsv().ToLowerInvariant(), new VisitMap(), (args) => args.Header.ToLowerInvariant());
Console.WriteLine("Caught expected exception: {0}",
Assert.Throws<HeaderValidationException>(() =>
TestInput(GetMissingColumnCsv(), new VisitMap(), (args) => args.Header.ToLowerInvariant());
var json1 = JsonConvert.SerializeObject(newList1);
var json11 = JsonConvert.SerializeObject(newList11);
Assert.AreEqual(json0, json1);
Assert.AreEqual(json0, json11);
public static List<VisitExport> TestInput(string csv, ClassMap<VisitExport> map, PrepareHeaderForMatch prepareHeaderForMatch = default)
var culture = CultureInfo.GetCultureInfo("en-GB");
var config = new CsvConfiguration (culture);
if (prepareHeaderForMatch != null)
config.PrepareHeaderForMatch = prepareHeaderForMatch;
using (var reader = new StringReader(csv))
using (var readCsv = new ValidatingCsvReader(reader, config)
OnUnmappedCsvHeaders = (context, headers) => throw new CsvHelperException(context, string.Format("Unmapped CSV headers: \"{0}\"", string.Join(",", headers))),
readCsv.Context.RegisterClassMap(map);
var fileContent = readCsv.GetRecords<VisitExport>();
return fileContent.ToList();
public static string TestOutput(List<VisitExport> inputList, ClassMap<VisitExport> map)
var culture = CultureInfo.GetCultureInfo("en-GB");
var config = new CsvConfiguration (culture);
var sb = new StringBuilder();
using (var textWriter = new StringWriter(sb))
using (var csvWriter = new CsvWriter(textWriter, config))
csvWriter.Context.RegisterClassMap(map);
csvWriter.WriteRecords(inputList);
static string GetUnknownColumnCsv() => @"Name,Name,Count,Name
CustomerAddress 1,name 1,,
CustomerAddress 101,name 101,101,
CustomerAddress 202,name 202,202,
static string GetMissingColumnCsv() => @"Name,Count
public static void Main()
Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , GetNetCoreVersion());
Console.WriteLine("CsvHelper: " + typeof(CsvConfiguration).Assembly.FullName);
Console.WriteLine("Failed with unhandled exception: ");
public static string GetNetCoreVersion()
var assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;
var assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App");
if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2)
return assemblyPath[netCoreAppIndex + 1];