// 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); } } } }