// Copyright 2009-2022 Josh Close
// This file is a part of CsvHelper and is dual licensed under MS-PL and Apache 2.0.
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
// https://github.com/JoshClose/CsvHelper
using System;
using System.Collections.Generic;
using System.IO;
using CsvHelper.Configuration;
using CsvHelper.TypeConversion;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using CsvHelper.Expressions;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Configuration;
namespace CsvHelper
{
///
/// Reads data that was parsed from .
///
public class CsvReader : IReader
{
private readonly Lazy recordManager;
private readonly bool detectColumnCountChanges;
private readonly Dictionary> namedIndexes = new Dictionary>();
private readonly Dictionary namedIndexCache = new Dictionary();
private readonly Dictionary typeConverterOptionsCache = new Dictionary();
private readonly MemberMapData reusableMemberMapData = new MemberMapData(null);
private readonly bool hasHeaderRecord;
private readonly HeaderValidated headerValidated;
private readonly ShouldSkipRecord? shouldSkipRecord;
private readonly ReadingExceptionOccurred readingExceptionOccurred;
private readonly CultureInfo cultureInfo;
private readonly bool ignoreBlankLines;
private readonly MissingFieldFound missingFieldFound;
private readonly bool includePrivateMembers;
private readonly PrepareHeaderForMatch prepareHeaderForMatch;
private CsvContext context;
private bool disposed;
private IParser parser;
private int columnCount;
private int currentIndex = -1;
private bool hasBeenRead;
private string[]? headerRecord;
///
public virtual int ColumnCount => columnCount;
///
public virtual int CurrentIndex => currentIndex;
///
public virtual string[]? HeaderRecord => headerRecord;
///
public virtual CsvContext Context => context;
///
public virtual IReaderConfiguration Configuration { get; private set; }
///
public virtual IParser Parser => parser;
///
/// Creates a new CSV reader using the given .
///
/// The reader.
/// The culture.
/// true to leave the open after the object is disposed, otherwise false.
public CsvReader(TextReader reader, CultureInfo culture, bool leaveOpen = false) : this(new CsvParser(reader, culture, leaveOpen)) { }
///
/// Creates a new CSV reader using the given and
/// and as the default parser.
///
/// The reader.
/// The configuration.
/// true to leave the open after the object is disposed, otherwise false.
public CsvReader(TextReader reader, IReaderConfiguration configuration, bool leaveOpen = false) : this(new CsvParser(reader, configuration, leaveOpen)) { }
///
/// Creates a new CSV reader using the given .
///
/// The used to parse the CSV file.
public CsvReader(IParser parser)
{
Configuration = parser.Configuration as IReaderConfiguration ?? throw new ConfigurationException($"The {nameof(IParser)} configuration must implement {nameof(IReaderConfiguration)} to be used in {nameof(CsvReader)}.");
this.parser = parser ?? throw new ArgumentNullException(nameof(parser));
context = parser.Context ?? throw new InvalidOperationException($"For {nameof(IParser)} to be used in {nameof(CsvReader)}, {nameof(IParser.Context)} must also implement {nameof(CsvContext)}.");
context.Reader = this;
recordManager = new Lazy(() => ObjectResolver.Current.Resolve(this));
cultureInfo = Configuration.CultureInfo;
detectColumnCountChanges = Configuration.DetectColumnCountChanges;
hasHeaderRecord = Configuration.HasHeaderRecord;
headerValidated = Configuration.HeaderValidated;
ignoreBlankLines = Configuration.IgnoreBlankLines;
includePrivateMembers = Configuration.IncludePrivateMembers;
missingFieldFound = Configuration.MissingFieldFound;
prepareHeaderForMatch = Configuration.PrepareHeaderForMatch;
readingExceptionOccurred = Configuration.ReadingExceptionOccurred;
shouldSkipRecord = Configuration.ShouldSkipRecord;
}
///
public virtual bool ReadHeader()
{
if (!hasHeaderRecord)
{
throw new ReaderException(context, "Configuration.HasHeaderRecord is false.");
}
headerRecord = parser.Record;
ParseNamedIndexes();
return headerRecord != null;
}
///
/// Validates the header to be of the given type.
///
/// The expected type of the header
public virtual void ValidateHeader()
{
ValidateHeader(typeof(T));
}
///
/// Validates the header to be of the given type.
///
/// The expected type of the header.
public virtual void ValidateHeader(Type type)
{
if (hasHeaderRecord == false)
{
throw new InvalidOperationException($"Validation can't be performed on a the header if no header exists. {nameof(Configuration.HasHeaderRecord)} can't be false.");
}
CheckHasBeenRead();
if (headerRecord == null)
{
throw new InvalidOperationException($"The header must be read before it can be validated.");
}
if (context.Maps[type] == null)
{
context.Maps.Add(context.AutoMap(type));
}
var map = context.Maps[type];
var invalidHeaders = new List();
ValidateHeader(map, invalidHeaders);
var args = new HeaderValidatedArgs(invalidHeaders.ToArray(), context);
headerValidated?.Invoke(args);
}
///
/// Validates the header to be of the given type.
///
/// The mapped classes.
/// The invalid headers.
protected virtual void ValidateHeader(ClassMap map, List invalidHeaders)
{
foreach (var parameter in map.ParameterMaps)
{
if (parameter.Data.Ignore)
{
continue;
}
if (parameter.Data.IsConstantSet)
{
// If ConvertUsing and Constant don't require a header.
continue;
}
if (parameter.Data.IsIndexSet && !parameter.Data.IsNameSet)
{
// If there is only an index set, we don't want to validate the header name.
continue;
}
if (parameter.ConstructorTypeMap != null)
{
ValidateHeader(parameter.ConstructorTypeMap, invalidHeaders);
}
else if (parameter.ReferenceMap != null)
{
ValidateHeader(parameter.ReferenceMap.Data.Mapping, invalidHeaders);
}
else
{
var index = GetFieldIndex(parameter.Data.Names, parameter.Data.NameIndex, true);
var isValid = index != -1 || parameter.Data.IsOptional;
if (!isValid)
{
invalidHeaders.Add(new InvalidHeader { Index = parameter.Data.NameIndex, Names = parameter.Data.Names.ToList() });
}
}
}
foreach (var memberMap in map.MemberMaps)
{
if (memberMap.Data.Ignore || !CanRead(memberMap))
{
continue;
}
if (memberMap.Data.ReadingConvertExpression != null || memberMap.Data.IsConstantSet)
{
// If ConvertUsing and Constant don't require a header.
continue;
}
if (memberMap.Data.IsIndexSet && !memberMap.Data.IsNameSet)
{
// If there is only an index set, we don't want to validate the header name.
continue;
}
var index = GetFieldIndex(memberMap.Data.Names, memberMap.Data.NameIndex, true);
var isValid = index != -1 || memberMap.Data.IsOptional;
if (!isValid)
{
invalidHeaders.Add(new InvalidHeader { Index = memberMap.Data.NameIndex, Names = memberMap.Data.Names.ToList() });
}
}
foreach (var referenceMap in map.ReferenceMaps)
{
if (!CanRead(referenceMap))
{
continue;
}
ValidateHeader(referenceMap.Data.Mapping, invalidHeaders);
}
}
///
public virtual bool Read()
{
// Don't forget about the async method below!
bool hasMoreRecords;
do
{
hasMoreRecords = parser.Read();
hasBeenRead = true;
}
while (hasMoreRecords && (shouldSkipRecord?.Invoke(new ShouldSkipRecordArgs(this)) ?? false));
currentIndex = -1;
if (detectColumnCountChanges && hasMoreRecords)
{
if (columnCount > 0 && columnCount != parser.Count)
{
var csvException = new BadDataException(string.Empty, parser.RawRecord, context, "An inconsistent number of columns has been detected.");
var args = new ReadingExceptionOccurredArgs(csvException);
if (readingExceptionOccurred?.Invoke(args) ?? true)
{
throw csvException;
}
}
columnCount = parser.Count;
}
return hasMoreRecords;
}
///
public virtual async Task ReadAsync()
{
bool hasMoreRecords;
do
{
hasMoreRecords = await parser.ReadAsync().ConfigureAwait(false);
hasBeenRead = true;
}
while (hasMoreRecords && (shouldSkipRecord?.Invoke(new ShouldSkipRecordArgs(this)) ?? false));
currentIndex = -1;
if (detectColumnCountChanges && hasMoreRecords)
{
if (columnCount > 0 && columnCount != parser.Count)
{
var csvException = new BadDataException(string.Empty, parser.RawRecord, context, "An inconsistent number of columns has been detected.");
var args = new ReadingExceptionOccurredArgs(csvException);
if (readingExceptionOccurred?.Invoke(args) ?? true)
{
throw csvException;
}
}
columnCount = parser.Count;
}
return hasMoreRecords;
}
///
public virtual string? this[int index]
{
get
{
CheckHasBeenRead();
return GetField(index);
}
}
///
public virtual string? this[string name]
{
get
{
CheckHasBeenRead();
return GetField(name);
}
}
///
public virtual string? this[string name, int index]
{
get
{
CheckHasBeenRead();
return GetField(name, index);
}
}
///
public virtual string? GetField(int index)
{
CheckHasBeenRead();
// Set the current index being used so we
// have more information if an error occurs
// when reading records.
currentIndex = index;
if (index >= parser.Count || index < 0)
{
var args = new MissingFieldFoundArgs(null, index, context);
missingFieldFound?.Invoke(args);
return default;
}
var field = parser[index];
return field;
}
///
public virtual string? GetField(string name)
{
CheckHasBeenRead();
var index = GetFieldIndex(name);
if (index < 0)
{
return null;
}
return GetField(index);
}
///
public virtual string? GetField(string name, int index)
{
CheckHasBeenRead();
var fieldIndex = GetFieldIndex(name, index);
if (fieldIndex < 0)
{
return null;
}
return GetField(fieldIndex);
}
///
public virtual object? GetField(Type type, int index)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter(type);
return GetField(type, index, converter);
}
///
public virtual object? GetField(Type type, string name)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter(type);
return GetField(type, name, converter);
}
///
public virtual object? GetField(Type type, string name, int index)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter(type);
return GetField(type, name, index, converter);
}
///
public virtual object? GetField(Type type, int index, ITypeConverter converter)
{
CheckHasBeenRead();
reusableMemberMapData.Index = index;
reusableMemberMapData.TypeConverter = converter;
if (!typeConverterOptionsCache.TryGetValue(type, out TypeConverterOptions typeConverterOptions))
{
typeConverterOptions = TypeConverterOptions.Merge(new TypeConverterOptions { CultureInfo = cultureInfo }, context.TypeConverterOptionsCache.GetOptions(type));
typeConverterOptionsCache.Add(type, typeConverterOptions);
}
reusableMemberMapData.TypeConverterOptions = typeConverterOptions;
var field = GetField(index);
return converter.ConvertFromString(field, this, reusableMemberMapData);
}
///
public virtual object? GetField(Type type, string name, ITypeConverter converter)
{
CheckHasBeenRead();
var index = GetFieldIndex(name);
return GetField(type, index, converter);
}
///
public virtual object? GetField(Type type, string name, int index, ITypeConverter converter)
{
CheckHasBeenRead();
var fieldIndex = GetFieldIndex(name, index);
return GetField(type, fieldIndex, converter);
}
///
public virtual T? GetField(int index)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter();
return GetField(index, converter);
}
///
public virtual T? GetField(string name)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter();
return GetField(name, converter);
}
///
public virtual T? GetField(string name, int index)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter();
return GetField(name, index, converter);
}
///
public virtual T? GetField(int index, ITypeConverter converter)
{
CheckHasBeenRead();
if (index >= parser.Count || index < 0)
{
currentIndex = index;
var args = new MissingFieldFoundArgs(null, index, context);
missingFieldFound?.Invoke(args);
return default;
}
return (T)GetField(typeof(T), index, converter);
}
///
public virtual T? GetField(string name, ITypeConverter converter)
{
CheckHasBeenRead();
var index = GetFieldIndex(name);
return GetField(index, converter);
}
///
public virtual T? GetField(string name, int index, ITypeConverter converter)
{
CheckHasBeenRead();
var fieldIndex = GetFieldIndex(name, index);
return GetField(fieldIndex, converter);
}
///
public virtual T? GetField(int index) where TConverter : ITypeConverter
{
CheckHasBeenRead();
var converter = ObjectResolver.Current.Resolve();
return GetField(index, converter);
}
///
public virtual T? GetField(string name) where TConverter : ITypeConverter
{
CheckHasBeenRead();
var converter = ObjectResolver.Current.Resolve();
return GetField(name, converter);
}
///
public virtual T? GetField(string name, int index) where TConverter : ITypeConverter
{
CheckHasBeenRead();
var converter = ObjectResolver.Current.Resolve();
return GetField(name, index, converter);
}
///
public virtual bool TryGetField(Type type, int index, out object? field)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter(type);
return TryGetField(type, index, converter, out field);
}
///
public virtual bool TryGetField(Type type, string name, out object? field)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter(type);
return TryGetField(type, name, converter, out field);
}
///
public virtual bool TryGetField(Type type, string name, int index, out object? field)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter(type);
return TryGetField(type, name, index, converter, out field);
}
///
public virtual bool TryGetField(Type type, int index, ITypeConverter converter, out object? field)
{
CheckHasBeenRead();
// TypeConverter.IsValid() just wraps a
// ConvertFrom() call in a try/catch, so lets not
// do it twice and just do it ourselves.
try
{
field = GetField(type, index, converter);
return true;
}
catch
{
field = type.GetTypeInfo().IsValueType ? ObjectResolver.Current.Resolve(type) : null;
return false;
}
}
///
public virtual bool TryGetField(Type type, string name, ITypeConverter converter, out object? field)
{
CheckHasBeenRead();
var index = GetFieldIndex(name, isTryGet: true);
if (index == -1)
{
field = type.GetTypeInfo().IsValueType ? ObjectResolver.Current.Resolve(type) : null;
return false;
}
return TryGetField(type, index, converter, out field);
}
///
public virtual bool TryGetField(Type type, string name, int index, ITypeConverter converter, out object? field)
{
CheckHasBeenRead();
var fieldIndex = GetFieldIndex(name, index, true);
if (fieldIndex == -1)
{
field = type.GetTypeInfo().IsValueType ? ObjectResolver.Current.Resolve(type) : null;
return false;
}
return TryGetField(type, fieldIndex, converter, out field);
}
///
public virtual bool TryGetField(int index, out T? field)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter();
return TryGetField(index, converter, out field);
}
///
public virtual bool TryGetField(string name, out T? field)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter();
return TryGetField(name, converter, out field);
}
///
public virtual bool TryGetField(string name, int index, out T? field)
{
CheckHasBeenRead();
var converter = context.TypeConverterCache.GetConverter();
return TryGetField(name, index, converter, out field);
}
///
public virtual bool TryGetField(int index, ITypeConverter converter, out T? field)
{
CheckHasBeenRead();
// TypeConverter.IsValid() just wraps a
// ConvertFrom() call in a try/catch, so lets not
// do it twice and just do it ourselves.
try
{
field = GetField(index, converter);
return true;
}
catch
{
field = default;
return false;
}
}
///
public virtual bool TryGetField(string name, ITypeConverter converter, out T? field)
{
CheckHasBeenRead();
var index = GetFieldIndex(name, isTryGet: true);
if (index == -1)
{
field = default;
return false;
}
return TryGetField(index, converter, out field);
}
///
public virtual bool TryGetField(string name, int index, ITypeConverter converter, out T? field)
{
CheckHasBeenRead();
var fieldIndex = GetFieldIndex(name, index, true);
if (fieldIndex == -1)
{
field = default;
return false;
}
return TryGetField(fieldIndex, converter, out field);
}
///
public virtual bool TryGetField(int index, out T? field) where TConverter : ITypeConverter
{
CheckHasBeenRead();
var converter = ObjectResolver.Current.Resolve();
return TryGetField(index, converter, out field);
}
///
public virtual bool TryGetField(string name, out T? field) where TConverter : ITypeConverter
{
CheckHasBeenRead();
var converter = ObjectResolver.Current.Resolve();
return TryGetField(name, converter, out field);
}
///
public virtual bool TryGetField(string name, int index, out T? field) where TConverter : ITypeConverter
{
CheckHasBeenRead();
var converter = ObjectResolver.Current.Resolve();
return TryGetField(name, index, converter, out field);
}
///
public virtual T? GetRecord()
{
CheckHasBeenRead();
if (headerRecord == null && hasHeaderRecord)
{
ReadHeader();
ValidateHeader();
if (!Read())
{
return default;
}
}
T record;
try
{
record = recordManager.Value.Create();
}
catch (Exception ex)
{
var csvHelperException = ex as CsvHelperException ?? new ReaderException(context, "An unexpected error occurred.", ex);
var args = new ReadingExceptionOccurredArgs(csvHelperException);
if (readingExceptionOccurred?.Invoke(args) ?? true)
{
if (ex is CsvHelperException)
{
throw;
}
else
{
throw csvHelperException;
}
}
record = default;
}
return record;
}
///
public virtual T? GetRecord(T anonymousTypeDefinition)
{
if (anonymousTypeDefinition == null)
{
throw new ArgumentNullException(nameof(anonymousTypeDefinition));
}
if (!anonymousTypeDefinition.GetType().IsAnonymous())
{
throw new ArgumentException($"Argument is not an anonymous type.", nameof(anonymousTypeDefinition));
}
return GetRecord();
}
///
public virtual object? GetRecord(Type type)
{
CheckHasBeenRead();
if (headerRecord == null && hasHeaderRecord)
{
ReadHeader();
ValidateHeader(type);
if (!Read())
{
return null;
}
}
object record;
try
{
record = recordManager.Value.Create(type);
}
catch (Exception ex)
{
var csvHelperException = ex as CsvHelperException ?? new ReaderException(context, "An unexpected error occurred.", ex);
var args = new ReadingExceptionOccurredArgs(csvHelperException);
if (readingExceptionOccurred?.Invoke(args) ?? true)
{
if (ex is CsvHelperException)
{
throw;
}
else
{
throw csvHelperException;
}
}
record = default;
}
return record;
}
///
public virtual IEnumerable GetRecords()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(CsvReader),
"GetRecords() returns an IEnumerable that yields records. This means that the method isn't actually called until " +
"you try and access the values. e.g. .ToList() Did you create CsvReader inside a using block and are now trying to access " +
"the records outside of that using block?"
);
}
// Don't need to check if it's been read
// since we're doing the reading ourselves.
if (hasHeaderRecord && headerRecord == null)
{
if (!Read())
{
yield break;
}
ReadHeader();
ValidateHeader();
}
while (Read())
{
T record;
try
{
record = recordManager.Value.Create();
}
catch (Exception ex)
{
var csvHelperException = ex as CsvHelperException ?? new ReaderException(context, "An unexpected error occurred.", ex);
var args = new ReadingExceptionOccurredArgs(csvHelperException);
if (readingExceptionOccurred?.Invoke(args) ?? true)
{
if (ex is CsvHelperException)
{
throw;
}
else
{
throw csvHelperException;
}
}
// If the callback doesn't throw, keep going.
continue;
}
yield return record;
}
}
///
public virtual IEnumerable GetRecords(T anonymousTypeDefinition)
{
if (anonymousTypeDefinition == null)
{
throw new ArgumentNullException(nameof(anonymousTypeDefinition));
}
if (!anonymousTypeDefinition.GetType().IsAnonymous())
{
throw new ArgumentException($"Argument is not an anonymous type.", nameof(anonymousTypeDefinition));
}
return GetRecords();
}
///
public virtual IEnumerable