// 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 CsvHelper.Configuration.Attributes;
using CsvHelper.TypeConversion;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace CsvHelper.Configuration
{
///
/// Maps class members to CSV fields.
///
public abstract class ClassMap
{
private static readonly List enumerableConverters = new List
{
typeof(ArrayConverter),
typeof(CollectionGenericConverter),
typeof(EnumerableConverter),
typeof(IDictionaryConverter),
typeof(IDictionaryGenericConverter),
typeof(IEnumerableConverter),
typeof(IEnumerableGenericConverter)
};
///
/// The type of the class this map is for.
///
public virtual Type ClassType { get; private set; }
///
/// The class constructor parameter mappings.
///
public virtual List ParameterMaps { get; } = new List();
///
/// The class member mappings.
///
public virtual MemberMapCollection MemberMaps { get; } = new MemberMapCollection();
///
/// The class member reference mappings.
///
public virtual MemberReferenceMapCollection ReferenceMaps { get; } = new MemberReferenceMapCollection();
///
/// Allow only internal creation of CsvClassMap.
///
/// The type of the class this map is for.
internal ClassMap(Type classType)
{
ClassType = classType;
}
///
/// Maps a member to a CSV field.
///
/// The type of the class this map is for. This may not be the same type
/// as the member.DeclaringType or the current ClassType due to nested member mappings.
/// The member to map.
/// If true, an existing map will be used if available.
/// If false, a new map is created for the same member.
/// The member mapping.
public MemberMap Map(Type classType, MemberInfo member, bool useExistingMap = true)
{
if (useExistingMap)
{
var existingMap = MemberMaps.Find(member);
if (existingMap != null)
{
return existingMap;
}
}
var memberMap = MemberMap.CreateGeneric(classType, member);
memberMap.Data.Index = GetMaxIndex() + 1;
MemberMaps.Add(memberMap);
return memberMap;
}
///
/// Maps a non-member to a CSV field. This allows for writing
/// data that isn't mapped to a class member.
///
/// The member mapping.
public virtual MemberMap Map()
{
var memberMap = new MemberMap(null);
memberMap.Data.Index = GetMaxIndex() + 1;
MemberMaps.Add(memberMap);
return memberMap;
}
///
/// Maps a member to another class map.
///
/// The type of the class map.
/// The member.
/// Constructor arguments used to create the reference map.
/// The reference mapping for the member.
public virtual MemberReferenceMap References(Type classMapType, MemberInfo member, params object[] constructorArgs)
{
if (!typeof(ClassMap).IsAssignableFrom(classMapType))
{
throw new InvalidOperationException($"Argument {nameof(classMapType)} is not a CsvClassMap.");
}
var existingMap = ReferenceMaps.Find(member);
if (existingMap != null)
{
return existingMap;
}
var map = (ClassMap)ObjectResolver.Current.Resolve(classMapType, constructorArgs);
map.ReIndex(GetMaxIndex() + 1);
var reference = new MemberReferenceMap(member, map);
ReferenceMaps.Add(reference);
return reference;
}
///
/// Maps a constructor parameter to a CSV field.
///
/// The name of the constructor parameter.
public virtual ParameterMap Parameter(string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var args = new GetConstructorArgs(ClassType);
return Parameter(() => ConfigurationFunctions.GetConstructor(args), name);
}
///
/// Maps a constructor parameter to a CSV field.
///
/// A function that returns the for the constructor.
/// The name of the constructor parameter.
public virtual ParameterMap Parameter(Func getConstructor, string name)
{
if (getConstructor == null) throw new ArgumentNullException(nameof(getConstructor));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
var constructor = getConstructor();
var parameters = constructor.GetParameters();
var parameter = parameters.SingleOrDefault(p => p.Name == name);
if (parameter == null)
{
throw new ConfigurationException($"Constructor {constructor.GetDefinition()} doesn't contain a paramter with name '{name}'.");
}
return Parameter(constructor, parameter);
}
///
/// Maps a constructor parameter to a CSV field.
///
/// The for the constructor.
/// The for the constructor parameter.
public virtual ParameterMap Parameter(ConstructorInfo constructor, ParameterInfo parameter)
{
if (constructor == null) throw new ArgumentNullException(nameof(constructor));
if (parameter == null) throw new ArgumentNullException(nameof(parameter));
if (!constructor.GetParameters().Contains(parameter))
{
throw new ConfigurationException($"Constructor {constructor.GetDefinition()} doesn't contain parameter '{parameter.GetDefinition()}'.");
}
var parameterMap = new ParameterMap(parameter);
parameterMap.Data.Index = GetMaxIndex(isParameter: true) + 1;
ParameterMaps.Add(parameterMap);
return parameterMap;
}
///
/// Auto maps all members for the given type. If a member
/// is mapped again it will override the existing map.
///
/// The culture.
public virtual void AutoMap(CultureInfo culture)
{
AutoMap(new CsvConfiguration(culture));
}
///
/// Auto maps all members for the given type. If a member
/// is mapped again it will override the existing map.
///
/// The configuration.
public virtual void AutoMap(CsvConfiguration configuration)
{
AutoMap(new CsvContext(configuration));
}
///
/// Auto maps all members for the given type. If a member
/// is mapped again it will override the existing map.
///
/// The context.
public virtual void AutoMap(CsvContext context)
{
var type = GetGenericType();
if (typeof(IEnumerable).IsAssignableFrom(type))
{
throw new ConfigurationException("Types that inherit IEnumerable cannot be auto mapped. " +
"Did you accidentally call GetRecord or WriteRecord which " +
"acts on a single record instead of calling GetRecords or " +
"WriteRecords which acts on a list of records?");
}
var mapParents = new LinkedList();
var args = new ShouldUseConstructorParametersArgs(type);
if (context.Configuration.ShouldUseConstructorParameters(args))
{
// This type doesn't have a parameterless constructor so we can't create an
// instance and set it's member. Constructor parameters need to be created
// instead. Writing only uses getters, so members will also be mapped
// for writing purposes.
AutoMapConstructorParameters(this, context, mapParents);
}
AutoMapMembers(this, context, mapParents);
}
///
/// Get the largest index for the
/// members and references.
///
/// The max index.
public virtual int GetMaxIndex(bool isParameter = false)
{
if (isParameter)
{
return ParameterMaps.Select(parameterMap => parameterMap.GetMaxIndex()).DefaultIfEmpty(-1).Max();
}
if (MemberMaps.Count == 0 && ReferenceMaps.Count == 0)
{
return -1;
}
var indexes = new List();
if (MemberMaps.Count > 0)
{
indexes.Add(MemberMaps.Max(pm => pm.Data.Index));
}
if (ReferenceMaps.Count > 0)
{
indexes.AddRange(ReferenceMaps.Select(referenceMap => referenceMap.GetMaxIndex()));
}
return indexes.Max();
}
///
/// Resets the indexes based on the given start index.
///
/// The index start.
/// The last index + 1.
public virtual int ReIndex(int indexStart = 0)
{
foreach (var parameterMap in ParameterMaps)
{
parameterMap.Data.Index = indexStart + parameterMap.Data.Index;
}
foreach (var memberMap in MemberMaps)
{
if (!memberMap.Data.IsIndexSet)
{
memberMap.Data.Index = indexStart + memberMap.Data.Index;
}
}
foreach (var referenceMap in ReferenceMaps)
{
indexStart = referenceMap.Data.Mapping.ReIndex(indexStart);
}
return indexStart;
}
///
/// Auto maps the given map and checks for circular references as it goes.
///
/// The map to auto map.
/// The context.
/// The list of parents for the map.
/// The index starting point.
protected virtual void AutoMapMembers(ClassMap map, CsvContext context, LinkedList mapParents, int indexStart = 0)
{
var type = map.GetGenericType();
var flags = BindingFlags.Instance | BindingFlags.Public;
if (context.Configuration.IncludePrivateMembers)
{
flags = flags | BindingFlags.NonPublic;
}
var members = new List();
if ((context.Configuration.MemberTypes & MemberTypes.Properties) == MemberTypes.Properties)
{
// We need to go up the declaration tree and find the actual type the property
// exists on and use that PropertyInfo instead. This is so we can get the private
// set method for the property.
var properties = new List();
foreach (var property in ReflectionHelper.GetUniqueProperties(type, flags))
{
if (properties.Any(p => p.Name == property.Name))
{
// Multiple properties could have the same name if a child class property
// is hiding a parent class property by using `new`. It's possible that
// the order of the properties returned
continue;
}
properties.Add(ReflectionHelper.GetDeclaringProperty(type, property, flags));
}
members.AddRange(properties);
}
if ((context.Configuration.MemberTypes & MemberTypes.Fields) == MemberTypes.Fields)
{
// We need to go up the declaration tree and find the actual type the field
// exists on and use that FieldInfo instead.
var fields = new List();
foreach (var field in ReflectionHelper.GetUniqueFields(type, flags))
{
if (fields.Any(p => p.Name == field.Name))
{
// Multiple fields could have the same name if a child class field
// is hiding a parent class field by using `new`. It's possible that
// the order of the fields returned
continue;
}
if (!field.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Any())
{
fields.Add(ReflectionHelper.GetDeclaringField(type, field, flags));
}
}
members.AddRange(fields);
}
foreach (var member in members)
{
if (member.GetCustomAttribute() != null)
{
// Ignore this member including its tree if it's a reference.
continue;
}
var typeConverterType = context.TypeConverterCache.GetConverter(member).GetType();
if (context.Configuration.HasHeaderRecord && enumerableConverters.Contains(typeConverterType))
{
// Enumerable converters can't write the header properly, so skip it.
continue;
}
var memberTypeInfo = member.MemberType().GetTypeInfo();
var isDefaultConverter = typeConverterType == typeof(DefaultTypeConverter);
if (isDefaultConverter)
{
// If the type is not one covered by our type converters
// and it has a parameterless constructor, create a
// reference map for it.
if (context.Configuration.IgnoreReferences)
{
continue;
}
if (CheckForCircularReference(member.MemberType(), mapParents))
{
continue;
}
mapParents.AddLast(type);
var refMapType = typeof(DefaultClassMap<>).MakeGenericType(member.MemberType());
var refMap = (ClassMap)ObjectResolver.Current.Resolve(refMapType);
if (memberTypeInfo.HasConstructor() && !memberTypeInfo.HasParameterlessConstructor() && !memberTypeInfo.IsUserDefinedStruct())
{
AutoMapConstructorParameters(refMap, context, mapParents, Math.Max(map.GetMaxIndex() + 1, indexStart));
}
// Need to use Max here for nested types.
AutoMapMembers(refMap, context, mapParents, Math.Max(map.GetMaxIndex() + 1, indexStart));
mapParents.Drop(mapParents.Find(type));
if (refMap.MemberMaps.Count > 0 || refMap.ReferenceMaps.Count > 0)
{
var referenceMap = new MemberReferenceMap(member, refMap);
if (context.Configuration.ReferenceHeaderPrefix != null)
{
var args = new ReferenceHeaderPrefixArgs(member.MemberType(), member.Name);
referenceMap.Data.Prefix = context.Configuration.ReferenceHeaderPrefix(args);
}
ApplyAttributes(referenceMap);
map.ReferenceMaps.Add(referenceMap);
}
}
else
{
// Only add the member map if it can be converted later on.
// If the member will use the default converter, don't add it because
// we don't want the .ToString() value to be used when auto mapping.
// Use the top of the map tree. This will maps that have been auto mapped
// to later on get a reference to a map by doing map.Map( m => m.A.B.C.Id )
// and it will return the correct parent map type of A instead of C.
var classType = mapParents.First?.Value ?? map.ClassType;
var memberMap = MemberMap.CreateGeneric(classType, member);
// Use global values as the starting point.
memberMap.Data.TypeConverterOptions = TypeConverterOptions.Merge(new TypeConverterOptions(), context.TypeConverterOptionsCache.GetOptions(member.MemberType()), memberMap.Data.TypeConverterOptions);
memberMap.Data.Index = map.GetMaxIndex() + 1;
ApplyAttributes(memberMap);
map.MemberMaps.Add(memberMap);
}
}
map.ReIndex(indexStart);
}
///
/// Auto maps the given map using constructor parameters.
///
/// The map.
/// The context.
/// The list of parents for the map.
/// The index starting point.
protected virtual void AutoMapConstructorParameters(ClassMap map, CsvContext context, LinkedList mapParents, int indexStart = 0)
{
var type = map.GetGenericType();
var args = new GetConstructorArgs(map.ClassType);
var constructor = context.Configuration.GetConstructor(args);
var parameters = constructor.GetParameters();
foreach (var parameter in parameters)
{
var parameterMap = new ParameterMap(parameter);
if (parameter.GetCustomAttributes(true).Any() || parameter.GetCustomAttributes(true).Any())
{
// If there is an IgnoreAttribute or ConstantAttribute, we still need to add a map because a constructor requires
// all parameters to be present. A default value will be used later on.
ApplyAttributes(parameterMap);
map.ParameterMaps.Add(parameterMap);
continue;
}
var typeConverterType = context.TypeConverterCache.GetConverter(parameter.ParameterType).GetType();
var memberTypeInfo = parameter.ParameterType.GetTypeInfo();
var isDefaultConverter = typeConverterType == typeof(DefaultTypeConverter);
if (isDefaultConverter && (memberTypeInfo.HasParameterlessConstructor() || memberTypeInfo.IsUserDefinedStruct()))
{
// If the type is not one covered by our type converters
// and it has a parameterless constructor, create a
// reference map for it.
if (context.Configuration.IgnoreReferences)
{
throw new InvalidOperationException($"Configuration '{nameof(CsvConfiguration.IgnoreReferences)}' can't be true " +
"when using types without a default constructor. Constructor parameters " +
"are used and all members including references must be used.");
}
if (CheckForCircularReference(parameter.ParameterType, mapParents))
{
throw new InvalidOperationException($"A circular reference was detected in constructor paramter '{parameter.Name}'." +
"Since all parameters must be supplied for a constructor, this parameter can't be skipped.");
}
mapParents.AddLast(type);
var refMapType = typeof(DefaultClassMap<>).MakeGenericType(parameter.ParameterType);
var refMap = (ClassMap)ObjectResolver.Current.Resolve(refMapType);
AutoMapMembers(refMap, context, mapParents, Math.Max(map.GetMaxIndex(isParameter: true) + 1, indexStart));
mapParents.Drop(mapParents.Find(type));
var referenceMap = new ParameterReferenceMap(parameter, refMap);
if (context.Configuration.ReferenceHeaderPrefix != null)
{
var referenceHeaderPrefix = new ReferenceHeaderPrefixArgs(memberTypeInfo.MemberType(), memberTypeInfo.Name);
referenceMap.Data.Prefix = context.Configuration.ReferenceHeaderPrefix(referenceHeaderPrefix);
}
ApplyAttributes(referenceMap);
parameterMap.ReferenceMap = referenceMap;
}
else if (isDefaultConverter && context.Configuration.ShouldUseConstructorParameters(new ShouldUseConstructorParametersArgs(parameter.ParameterType)))
{
// If the type is not one covered by our type converters
// and it should use contructor parameters, create a
// constructor map for it.
mapParents.AddLast(type);
var constructorMapType = typeof(DefaultClassMap<>).MakeGenericType(parameter.ParameterType);
var constructorMap = (ClassMap)ObjectResolver.Current.Resolve(constructorMapType);
// Need to use Max here for nested types.
AutoMapConstructorParameters(constructorMap, context, mapParents, Math.Max(map.GetMaxIndex(isParameter: true) + 1, indexStart));
mapParents.Drop(mapParents.Find(type));
parameterMap.ConstructorTypeMap = constructorMap;
}
else
{
parameterMap.Data.TypeConverterOptions = TypeConverterOptions.Merge(new TypeConverterOptions(), context.TypeConverterOptionsCache.GetOptions(parameter.ParameterType), parameterMap.Data.TypeConverterOptions);
parameterMap.Data.Index = map.GetMaxIndex(isParameter: true) + 1;
ApplyAttributes(parameterMap);
}
map.ParameterMaps.Add(parameterMap);
}
map.ReIndex(indexStart);
}
///
/// Checks for circular references.
///
/// The type to check for.
/// The list of parents to check against.
/// A value indicating if a circular reference was found.
/// True if a circular reference was found, otherwise false.
protected virtual bool CheckForCircularReference(Type type, LinkedList mapParents)
{
if (mapParents.Count == 0)
{
return false;
}
var node = mapParents.Last;
while (true)
{
if (node?.Value == type)
{
return true;
}
node = node?.Previous;
if (node == null)
{
break;
}
}
return false;
}
///
/// Gets the generic type for this class map.
///
protected virtual Type GetGenericType()
{
return GetType().GetTypeInfo().BaseType?.GetGenericArguments()[0] ?? throw new ConfigurationException();
}
///
/// Applies attribute configurations to the map.
///
/// The parameter map.
protected virtual void ApplyAttributes(ParameterMap parameterMap)
{
var parameter = parameterMap.Data.Parameter;
var attributes = parameter.GetCustomAttributes().OfType();
foreach (var attribute in attributes)
{
attribute.ApplyTo(parameterMap);
}
}
///
/// Applies attribute configurations to the map.
///
/// The parameter reference map.
protected virtual void ApplyAttributes(ParameterReferenceMap referenceMap)
{
var parameter = referenceMap.Data.Parameter;
var attributes = parameter.GetCustomAttributes().OfType();
foreach (var attribute in attributes)
{
attribute.ApplyTo(referenceMap);
}
}
///
/// Applies attribute configurations to the map.
///
/// The member map.
protected virtual void ApplyAttributes(MemberMap memberMap)
{
if (memberMap.Data.Member == null)
{
return;
}
var member = memberMap.Data.Member;
var attributes = member.GetCustomAttributes().OfType();
foreach (var attribute in attributes)
{
attribute.ApplyTo(memberMap);
}
}
///
/// Applies attribute configurations to the map.
///
/// The member reference map.
protected virtual void ApplyAttributes(MemberReferenceMap referenceMap)
{
var member = referenceMap.Data.Member;
var attributes = member.GetCustomAttributes().OfType();
foreach (var attribute in attributes)
{
attribute.ApplyTo(referenceMap);
}
}
}
}