// 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; using CsvHelper.TypeConversion; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace CsvHelper.Expressions { /// /// Manages expression creation. /// public class ExpressionManager { private readonly CsvReader reader; private readonly CsvWriter writer; /// /// Initializes a new instance using the given reader. /// /// The reader. public ExpressionManager(CsvReader reader) { this.reader = reader; } /// /// Initializes a new instance using the given writer. /// /// The writer. public ExpressionManager(CsvWriter writer) { this.writer = writer; } /// /// Creates the constructor arguments used to create a type. /// /// The mapping to create the arguments for. /// The arguments that will be added to the mapping. public virtual void CreateConstructorArgumentExpressionsForMapping(ClassMap map, List argumentExpressions) { foreach (var parameterMap in map.ParameterMaps) { if (parameterMap.Data.IsConstantSet) { var constantExpression = Expression.Convert(Expression.Constant(parameterMap.Data.Constant), parameterMap.Data.Parameter.ParameterType); argumentExpressions.Add(constantExpression); continue; } if (parameterMap.Data.Ignore) { Expression defaultExpression; if (parameterMap.Data.IsDefaultSet) { defaultExpression = Expression.Convert(Expression.Constant(parameterMap.Data.Default), parameterMap.Data.Parameter.ParameterType); } else if (parameterMap.Data.Parameter.HasDefaultValue) { defaultExpression = Expression.Convert(Expression.Constant(parameterMap.Data.Parameter.DefaultValue), parameterMap.Data.Parameter.ParameterType); } else { defaultExpression = Expression.Default(parameterMap.Data.Parameter.ParameterType); } argumentExpressions.Add(defaultExpression); continue; } if (parameterMap.ConstructorTypeMap != null) { // Constructor parameter type. var arguments = new List(); CreateConstructorArgumentExpressionsForMapping(parameterMap.ConstructorTypeMap, arguments); var args = new GetConstructorArgs(parameterMap.ConstructorTypeMap.ClassType); var constructorExpression = Expression.New(reader.Configuration.GetConstructor(args), arguments); argumentExpressions.Add(constructorExpression); } else if (parameterMap.ReferenceMap != null) { // Reference type. var referenceAssignments = new List(); CreateMemberAssignmentsForMapping(parameterMap.ReferenceMap.Data.Mapping, referenceAssignments); var referenceBody = CreateInstanceAndAssignMembers(parameterMap.ReferenceMap.Data.Parameter.ParameterType, referenceAssignments); argumentExpressions.Add(referenceBody); } else { // Value type. int index; if (reader.Configuration.HasHeaderRecord && (parameterMap.Data.IsNameSet || !parameterMap.Data.IsIndexSet)) { // Use name. index = reader.GetFieldIndex(parameterMap.Data.Names, parameterMap.Data.NameIndex, parameterMap.Data.IsOptional); if (index == -1) { if (parameterMap.Data.IsDefaultSet || parameterMap.Data.IsOptional) { var defaultExpression = CreateDefaultExpression(parameterMap, Expression.Constant(string.Empty)); argumentExpressions.Add(defaultExpression); continue; } // Skip if the index was not found. continue; } } else if (!parameterMap.Data.IsIndexSet && parameterMap.Data.IsOptional) { // If there wasn't an index explicitly, use a default value since constructors need all // arguments to be created. var defaultExpression = CreateDefaultExpression(parameterMap, Expression.Constant(string.Empty)); argumentExpressions.Add(defaultExpression); continue; } else { // Use index. index = parameterMap.Data.Index; } // Get the field using the field index. var method = typeof(IReaderRow).GetProperty("Item", typeof(string), new[] { typeof(int) }).GetGetMethod(); Expression fieldExpression = Expression.Call(Expression.Constant(reader), method, Expression.Constant(index, typeof(int))); if (parameterMap.Data.IsDefaultSet) { fieldExpression = CreateDefaultExpression(parameterMap, fieldExpression); } else { fieldExpression = CreateTypeConverterExpression(parameterMap, fieldExpression); } argumentExpressions.Add(fieldExpression); } } } /// /// Creates the member assignments for the given . /// /// The mapping to create the assignments for. /// The assignments that will be added to from the mapping. public virtual void CreateMemberAssignmentsForMapping(ClassMap mapping, List assignments) { foreach (var memberMap in mapping.MemberMaps) { var fieldExpression = CreateGetFieldExpression(memberMap); if (fieldExpression == null) { continue; } assignments.Add(Expression.Bind(memberMap.Data.Member, fieldExpression)); } foreach (var referenceMap in mapping.ReferenceMaps) { if (!reader.CanRead(referenceMap)) { continue; } Expression referenceBody; if (referenceMap.Data.Mapping.ParameterMaps.Count > 0) { var arguments = new List(); CreateConstructorArgumentExpressionsForMapping(referenceMap.Data.Mapping, arguments); var args = new GetConstructorArgs(referenceMap.Data.Mapping.ClassType); referenceBody = Expression.New(reader.Configuration.GetConstructor(args), arguments); } else { var referenceAssignments = new List(); CreateMemberAssignmentsForMapping(referenceMap.Data.Mapping, referenceAssignments); referenceBody = CreateInstanceAndAssignMembers(referenceMap.Data.Member.MemberType(), referenceAssignments); } assignments.Add(Expression.Bind(referenceMap.Data.Member, referenceBody)); } } /// /// Creates an expression the represents getting the field for the given /// member and converting it to the member's type. /// /// The mapping for the member. public virtual Expression? CreateGetFieldExpression(MemberMap memberMap) { if (memberMap.Data.ReadingConvertExpression != null) { // The user is providing the expression to do the conversion. Expression exp = Expression.Invoke(memberMap.Data.ReadingConvertExpression, Expression.Constant(new ConvertFromStringArgs(reader))); return Expression.Convert(exp, memberMap.Data.Member.MemberType()); } if (!reader.CanRead(memberMap)) { return null; } if (memberMap.Data.IsConstantSet) { return Expression.Convert(Expression.Constant(memberMap.Data.Constant), memberMap.Data.Member.MemberType()); } if (memberMap.Data.TypeConverter == null) { // Skip if the type isn't convertible. return null; } int index; if (reader.Configuration.HasHeaderRecord && (memberMap.Data.IsNameSet || !memberMap.Data.IsIndexSet)) { // Use the name. index = reader.GetFieldIndex(memberMap.Data.Names, memberMap.Data.NameIndex, memberMap.Data.IsOptional); if (index == -1) { if (memberMap.Data.IsDefaultSet) { return CreateDefaultExpression(memberMap, Expression.Constant(string.Empty)); } // Skip if the index was not found. return null; } } else { // Use the index. index = memberMap.Data.Index; } // Get the field using the field index. var method = typeof(IReaderRow).GetProperty("Item", typeof(string), new[] { typeof(int) }).GetGetMethod(); Expression fieldExpression = Expression.Call(Expression.Constant(reader), method, Expression.Constant(index, typeof(int))); // Validate the field. if (memberMap.Data.ValidateExpression != null) { var constructor = typeof(ValidateArgs).GetConstructor(new Type[] { typeof(string), typeof(IReaderRow) }); var args = Expression.New(constructor, fieldExpression, Expression.Constant(reader)); var validateExpression = Expression.IsFalse(Expression.Invoke(memberMap.Data.ValidateExpression, args)); var validationExceptionConstructor = typeof(FieldValidationException).GetConstructor(new Type[] { typeof(CsvContext), typeof(string), typeof(string) }); var messageExpression = Expression.Invoke(memberMap.Data.ValidateMessageExpression, args); var newValidationExceptionExpression = Expression.New(validationExceptionConstructor, Expression.Constant(reader.Context), fieldExpression, messageExpression); var throwExpression = Expression.Throw(newValidationExceptionExpression); fieldExpression = Expression.Block( // If the validate method returns false, throw an exception. Expression.IfThen(validateExpression, throwExpression), fieldExpression ); } if (memberMap.Data.IsDefaultSet) { return CreateDefaultExpression(memberMap, fieldExpression); } fieldExpression = CreateTypeConverterExpression(memberMap, fieldExpression); return fieldExpression; } /// /// Creates a member expression for the given member on the record. /// This will recursively traverse the mapping to find the member /// and create a safe member accessor for each level as it goes. /// /// The current member expression. /// The mapping to look for the member to map on. /// The member map to look for on the mapping. /// An Expression to access the given member. public virtual Expression? CreateGetMemberExpression(Expression recordExpression, ClassMap mapping, MemberMap memberMap) { if (mapping.MemberMaps.Any(mm => mm == memberMap)) { // The member is on this level. if (memberMap.Data.Member is PropertyInfo) { return Expression.Property(recordExpression, (PropertyInfo)memberMap.Data.Member); } if (memberMap.Data.Member is FieldInfo) { return Expression.Field(recordExpression, (FieldInfo)memberMap.Data.Member); } } // The member isn't on this level of the mapping. // We need to search down through the reference maps. foreach (var refMap in mapping.ReferenceMaps) { var wrapped = refMap.Data.Member.GetMemberExpression(recordExpression); var memberExpression = CreateGetMemberExpression(wrapped, refMap.Data.Mapping, memberMap); if (memberExpression == null) { continue; } if (refMap.Data.Member.MemberType().GetTypeInfo().IsValueType) { return memberExpression; } var nullCheckExpression = Expression.Equal(wrapped, Expression.Constant(null)); var isValueType = memberMap.Data.Member.MemberType().GetTypeInfo().IsValueType; var isGenericType = isValueType && memberMap.Data.Member.MemberType().GetTypeInfo().IsGenericType; Type memberType; if (isValueType && !isGenericType && !writer.Configuration.UseNewObjectForNullReferenceMembers) { memberType = typeof(Nullable<>).MakeGenericType(memberMap.Data.Member.MemberType()); memberExpression = Expression.Convert(memberExpression, memberType); } else { memberType = memberMap.Data.Member.MemberType(); } var defaultValueExpression = isValueType && !isGenericType ? (Expression)Expression.New(memberType) : Expression.Constant(null, memberType); var conditionExpression = Expression.Condition(nullCheckExpression, defaultValueExpression, memberExpression); return conditionExpression; } return null; } /// /// Creates an instance of the given type using , then assigns /// the given member assignments to that instance. /// /// The type of the record we're creating. /// The member assignments that will be assigned to the created instance. /// A representing the instance creation and assignments. public virtual BlockExpression CreateInstanceAndAssignMembers(Type recordType, List assignments) { var expressions = new List(); var createInstanceMethod = typeof(IObjectResolver).GetMethod(nameof(IObjectResolver.Resolve), new Type[] { typeof(Type), typeof(object[]) }); var instanceExpression = Expression.Convert(Expression.Call(Expression.Constant(ObjectResolver.Current), createInstanceMethod, Expression.Constant(recordType), Expression.Constant(new object[0])), recordType); var variableExpression = Expression.Variable(instanceExpression.Type, "instance"); expressions.Add(Expression.Assign(variableExpression, instanceExpression)); expressions.AddRange(assignments.Select(b => Expression.Assign(Expression.MakeMemberAccess(variableExpression, b.Member), b.Expression))); expressions.Add(variableExpression); var variables = new ParameterExpression[] { variableExpression }; var blockExpression = Expression.Block(variables, expressions); return blockExpression; } /// /// Creates an expression that converts the field expression using a type converter. /// /// The mapping for the member. /// The field expression. public virtual Expression CreateTypeConverterExpression(MemberMap memberMap, Expression fieldExpression) { memberMap.Data.TypeConverterOptions = TypeConverterOptions.Merge(new TypeConverterOptions { CultureInfo = reader.Configuration.CultureInfo }, reader.Context.TypeConverterOptionsCache.GetOptions(memberMap.Data.Member.MemberType()), memberMap.Data.TypeConverterOptions); Expression typeConverterFieldExpression = Expression.Call(Expression.Constant(memberMap.Data.TypeConverter), nameof(ITypeConverter.ConvertFromString), null, fieldExpression, Expression.Constant(reader), Expression.Constant(memberMap.Data)); typeConverterFieldExpression = Expression.Convert(typeConverterFieldExpression, memberMap.Data.Member.MemberType()); return typeConverterFieldExpression; } /// /// Creates an expression that converts the field expression using a type converter. /// /// The mapping for the parameter. /// The field expression. public virtual Expression CreateTypeConverterExpression(ParameterMap parameterMap, Expression fieldExpression) { parameterMap.Data.TypeConverterOptions = TypeConverterOptions.Merge ( new TypeConverterOptions { CultureInfo = reader.Configuration.CultureInfo }, reader.Context.TypeConverterOptionsCache.GetOptions(parameterMap.Data.Parameter.ParameterType), parameterMap.Data.TypeConverterOptions ); var memberMapData = new MemberMapData(null) { Constant = parameterMap.Data.Constant, Default = parameterMap.Data.Default, Ignore = parameterMap.Data.Ignore, Index = parameterMap.Data.Index, IsConstantSet = parameterMap.Data.IsConstantSet, IsDefaultSet = parameterMap.Data.IsDefaultSet, IsIndexSet = parameterMap.Data.IsIndexSet, IsNameSet = parameterMap.Data.IsNameSet, NameIndex = parameterMap.Data.NameIndex, TypeConverter = parameterMap.Data.TypeConverter, TypeConverterOptions = parameterMap.Data.TypeConverterOptions }; memberMapData.Names.AddRange(parameterMap.Data.Names); Expression typeConverterFieldExpression = Expression.Call(Expression.Constant(parameterMap.Data.TypeConverter), nameof(ITypeConverter.ConvertFromString), null, fieldExpression, Expression.Constant(reader), Expression.Constant(memberMapData)); typeConverterFieldExpression = Expression.Convert(typeConverterFieldExpression, parameterMap.Data.Parameter.ParameterType); return typeConverterFieldExpression; } /// /// Creates a default expression if field expression is empty. /// /// The mapping for the member. /// The field expression. public virtual Expression CreateDefaultExpression(MemberMap memberMap, Expression fieldExpression) { var typeConverterExpression = CreateTypeConverterExpression(memberMap, fieldExpression); // Create default value expression. Expression defaultValueExpression; if (memberMap.Data.Member.MemberType() != typeof(string) && memberMap.Data.Default != null && memberMap.Data.Default.GetType() == typeof(string)) { // The default is a string but the member type is not. Use a converter. defaultValueExpression = Expression.Call(Expression.Constant(memberMap.Data.TypeConverter), nameof(ITypeConverter.ConvertFromString), null, Expression.Constant(memberMap.Data.Default), Expression.Constant(reader), Expression.Constant(memberMap.Data)); } else { // The member type and default type match. defaultValueExpression = Expression.Constant(memberMap.Data.Default); } defaultValueExpression = Expression.Convert(defaultValueExpression, memberMap.Data.Member.MemberType()); // If null, use string.Empty. var coalesceExpression = Expression.Coalesce(fieldExpression, Expression.Constant(string.Empty)); // Check if the field is an empty string. var checkFieldEmptyExpression = Expression.Equal(Expression.Convert(coalesceExpression, typeof(string)), Expression.Constant(string.Empty, typeof(string))); // Use a default value if the field is an empty string. fieldExpression = Expression.Condition(checkFieldEmptyExpression, defaultValueExpression, typeConverterExpression); return fieldExpression; } /// /// Creates a default expression if field expression is empty. /// /// The mapping for the parameter. /// The field expression. public virtual Expression CreateDefaultExpression(ParameterMap parameterMap, Expression fieldExpression) { var typeConverterExpression = CreateTypeConverterExpression(parameterMap, fieldExpression); // Create default value expression. Expression defaultValueExpression; if (parameterMap.Data.Parameter.ParameterType != typeof(string) && parameterMap.Data.Default != null && parameterMap.Data.Default.GetType() == typeof(string)) { // The default is a string but the member type is not. Use a converter. //defaultValueExpression = Expression.Call(Expression.Constant(parameterMap.Data.TypeConverter), nameof(ITypeConverter.ConvertFromString), null, Expression.Constant(parameterMap.Data.Default), Expression.Constant(reader), Expression.Constant(memberMap.Data)); defaultValueExpression = CreateTypeConverterExpression(parameterMap, Expression.Constant(parameterMap.Data.Default)); } else { // The member type and default type match. defaultValueExpression = Expression.Convert(Expression.Constant(parameterMap.Data.Default), parameterMap.Data.Parameter.ParameterType); } // If null, use string.Empty. var coalesceExpression = Expression.Coalesce(fieldExpression, Expression.Constant(string.Empty)); // Check if the field is an empty string. var checkFieldEmptyExpression = Expression.Equal(Expression.Convert(coalesceExpression, typeof(string)), Expression.Constant(string.Empty, typeof(string))); // Use a default value if the field is an empty string. fieldExpression = Expression.Condition(checkFieldEmptyExpression, defaultValueExpression, typeConverterExpression); return fieldExpression; } } }