diff --git a/src/AutoMapper.Extensions.ExpressionMapping/ExpressionExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/ExpressionExtensions.cs index 1629cf3..803d7a4 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/ExpressionExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/ExpressionExtensions.cs @@ -1,9 +1,11 @@ -using System; +using AutoMapper.Extensions.ExpressionMapping.Structures; +using AutoMapper.Internal; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using AutoMapper.Internal; +using System.Runtime.CompilerServices; namespace AutoMapper.Extensions.ExpressionMapping { @@ -11,36 +13,160 @@ namespace AutoMapper.Extensions.ExpressionMapping internal static class ExpressionExtensions { - public static Expression MemberAccesses(this IEnumerable members, Expression obj) => - members.Aggregate(obj, (expression, member) => MakeMemberAccess(expression, member)); - } + public static Expression ConvertTypeIfNecessary(this Expression expression, Type memberType) + { + if (memberType == expression.Type) + return expression; - internal static class ExpressionHelpers - { - public static MemberExpression MemberAccesses(string members, Expression obj) => - (MemberExpression)GetMemberPath(obj.Type, members).MemberAccesses(obj); + expression = expression.GetUnconvertedExpression(); + if (memberType != expression.Type) + return Expression.Convert(expression, memberType); + + return expression; + } + + /// + /// Returns the first ancestor node that is not a MemberExpression. + /// + /// + /// + public static Expression GetBaseOfMemberExpression(this MemberExpression expression) + { + if (expression.Expression == null) + return null; + + return expression.Expression.NodeType == ExpressionType.MemberAccess + ? GetBaseOfMemberExpression((MemberExpression)expression.Expression) + : expression.Expression; + } + + public static MemberExpression GetMemberExpression(this Expression expr) + => expr?.GetUnconvertedExpression() as MemberExpression; + + public static MemberExpression GetMemberExpression(this LambdaExpression expr) + => expr.Body.GetUnconvertedExpression() as MemberExpression; - public static Expression ReplaceParameters(this LambdaExpression exp, params Expression[] replace) + /// + /// For the given a Lambda Expression, returns the fully qualified name of the member starting with the immediate child member of the parameter + /// + /// + /// + public static string GetMemberFullName(this LambdaExpression expr) { - var replaceExp = exp.Body; - for (var i = 0; i < Math.Min(replace.Length, exp.Parameters.Count); i++) - replaceExp = Replace(replaceExp, exp.Parameters[i], replace[i]); - return replaceExp; + if (expr.Body.NodeType == ExpressionType.Parameter) + return string.Empty; + MemberExpression me = expr.Body.NodeType switch + { + ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs => expr.Body.GetUnconvertedExpression() as MemberExpression, + _ => expr.Body as MemberExpression, + }; + return me.GetPropertyFullName(); } - public static Expression Replace(this Expression exp, Expression old, Expression replace) => new ReplaceExpressionVisitor(old, replace).Visit(exp); + /// + /// Returns the ParameterExpression for the LINQ parameter. + /// + /// + /// + public static ParameterExpression GetParameterExpression(this Expression expression) + { + if (expression == null) + return null; + + //the node represents parameter of the expression + switch (expression.NodeType) + { + case ExpressionType.Parameter: + return (ParameterExpression)expression; + case ExpressionType.Quote: + return GetParameterExpression(((UnaryExpression)expression).Operand); + case ExpressionType.Lambda: + return GetParameterExpression(((LambdaExpression)expression).Body); + case ExpressionType.ConvertChecked: + case ExpressionType.Convert: + var ue = expression as UnaryExpression; + return GetParameterExpression(ue?.Operand); + case ExpressionType.TypeAs: + return ((UnaryExpression)expression).Operand.GetParameterExpression(); + case ExpressionType.TypeIs: + return ((TypeBinaryExpression)expression).Expression.GetParameterExpression(); + case ExpressionType.MemberAccess: + return GetParameterExpression(((MemberExpression)expression).Expression); + case ExpressionType.Call: + var methodExpression = expression as MethodCallExpression; + var parentExpression = methodExpression?.Object;//Method is an instance method + + var isExtension = methodExpression != null && methodExpression.Method.IsDefined(typeof(ExtensionAttribute), true); + if (isExtension && parentExpression == null && methodExpression.Arguments.Count > 0) + parentExpression = methodExpression.Arguments[0];//Method is an extension method based on the type of methodExpression.Arguments[0]. + + if (parentExpression == null) + return null; - private static IEnumerable GetMemberPath(Type type, string fullMemberName) + return GetParameterExpression(parentExpression); + } + + return null; + } + + /// + /// Returns the fully qualified name of the member starting with the immediate child member of the parameter + /// + /// + /// + public static string GetPropertyFullName(this Expression expression) { - MemberInfo property = null; - foreach (var memberName in fullMemberName.Split('.')) + if (expression == null) + return string.Empty; + + const string period = "."; + + //the node represents parameter of the expression + switch (expression.NodeType) { - var currentType = GetCurrentType(property, type); - yield return property = currentType.GetFieldOrProperty(memberName); + case ExpressionType.MemberAccess: + var memberExpression = (MemberExpression)expression; + var parentFullName = memberExpression.Expression.GetPropertyFullName(); + return string.IsNullOrEmpty(parentFullName) + ? memberExpression.Member.Name + : string.Concat(memberExpression.Expression.GetPropertyFullName(), period, memberExpression.Member.Name); + default: + return string.Empty; } } - private static Type GetCurrentType(MemberInfo member, Type type) - => member?.GetMemberType() ?? type; + public static Expression GetUnconvertedExpression(this Expression expression) + { + return expression.NodeType switch + { + ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs => ((UnaryExpression)expression).Operand.GetUnconvertedExpression(), + _ => expression, + }; + } + + /// + /// Determines whether the specified type is an enumeration type. + /// + /// The type to evaluate. This can be a nullable type, in which case the underlying type is checked. + /// true if the specified type is an enumeration; otherwise, false. + public static bool IsEnumType(this Type type) + { + if (type.IsNullableType()) + type = Nullable.GetUnderlyingType(type); + + return type.IsEnum(); + } + + /// + /// Adds member expressions to an existing expression. + /// + /// + /// + /// + public static MemberExpression MemberAccesses(this Expression exp, List list) => + (MemberExpression)list.SelectMany(propertyMapInfo => propertyMapInfo.DestinationPropertyInfos).MemberAccesses(exp); + + public static Expression MemberAccesses(this IEnumerable members, Expression obj) => + members.Aggregate(obj, MakeMemberAccess); } } \ No newline at end of file diff --git a/src/AutoMapper.Extensions.ExpressionMapping/ExpressionHelpers.cs b/src/AutoMapper.Extensions.ExpressionMapping/ExpressionHelpers.cs new file mode 100644 index 0000000..295e456 --- /dev/null +++ b/src/AutoMapper.Extensions.ExpressionMapping/ExpressionHelpers.cs @@ -0,0 +1,37 @@ +using AutoMapper.Internal; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.Extensions.ExpressionMapping +{ + internal static class ExpressionHelpers + { + public static MemberExpression MemberAccesses(string members, Expression obj) => + (MemberExpression)GetMemberPath(obj.Type, members).MemberAccesses(obj); + + public static Expression ReplaceParameters(this LambdaExpression exp, params Expression[] replace) + { + var replaceExp = exp.Body; + for (var i = 0; i < Math.Min(replace.Length, exp.Parameters.Count); i++) + replaceExp = Replace(replaceExp, exp.Parameters[i], replace[i]); + return replaceExp; + } + + public static Expression Replace(this Expression exp, Expression old, Expression replace) => new ReplaceExpressionVisitor(old, replace).Visit(exp); + + private static IEnumerable GetMemberPath(Type type, string fullMemberName) + { + MemberInfo property = null; + foreach (var memberName in fullMemberName.Split('.')) + { + var currentType = GetCurrentType(property, type); + yield return property = currentType.GetFieldOrProperty(memberName); + } + } + + private static Type GetCurrentType(MemberInfo member, Type type) + => member?.GetMemberType() ?? type; + } +} diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs deleted file mode 100644 index 5f9d71c..0000000 --- a/src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; -using AutoMapper.Extensions.ExpressionMapping.Structures; -using AutoMapper.Internal; - -namespace AutoMapper.Extensions.ExpressionMapping.Extensions -{ - internal static class VisitorExtensions - { - /// - /// Returns the fully qualified name of the member starting with the immediate child member of the parameter - /// - /// - /// - public static string GetPropertyFullName(this Expression expression) - { - if (expression == null) - return string.Empty; - - const string period = "."; - - //the node represents parameter of the expression - switch (expression.NodeType) - { - case ExpressionType.MemberAccess: - var memberExpression = (MemberExpression)expression; - var parentFullName = memberExpression.Expression.GetPropertyFullName(); - return string.IsNullOrEmpty(parentFullName) - ? memberExpression.Member.Name - : string.Concat(memberExpression.Expression.GetPropertyFullName(), period, memberExpression.Member.Name); - default: - return string.Empty; - } - } - - public static Expression GetUnconvertedExpression(this Expression expression) - { - return expression.NodeType switch - { - ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs => ((UnaryExpression)expression).Operand.GetUnconvertedExpression(), - _ => expression, - }; - } - - public static Expression ConvertTypeIfNecessary(this Expression expression, Type memberType) - { - if (memberType == expression.Type) - return expression; - - expression = expression.GetUnconvertedExpression(); - if (memberType != expression.Type) - return Expression.Convert(expression, memberType); - - return expression; - } - - public static MemberExpression GetMemberExpression(this LambdaExpression expr) - => expr.Body.GetUnconvertedExpression() as MemberExpression; - - public static MemberExpression GetMemberExpression(this Expression expr) - => expr?.GetUnconvertedExpression() as MemberExpression; - - /// - /// Returns the ParameterExpression for the LINQ parameter. - /// - /// - /// - public static ParameterExpression GetParameterExpression(this Expression expression) - { - if (expression == null) - return null; - - //the node represents parameter of the expression - switch (expression.NodeType) - { - case ExpressionType.Parameter: - return (ParameterExpression)expression; - case ExpressionType.Quote: - return GetParameterExpression(((UnaryExpression)expression).Operand); - case ExpressionType.Lambda: - return GetParameterExpression(((LambdaExpression)expression).Body); - case ExpressionType.ConvertChecked: - case ExpressionType.Convert: - var ue = expression as UnaryExpression; - return GetParameterExpression(ue?.Operand); - case ExpressionType.TypeAs: - return ((UnaryExpression)expression).Operand.GetParameterExpression(); - case ExpressionType.TypeIs: - return ((TypeBinaryExpression)expression).Expression.GetParameterExpression(); - case ExpressionType.MemberAccess: - return GetParameterExpression(((MemberExpression)expression).Expression); - case ExpressionType.Call: - var methodExpression = expression as MethodCallExpression; - var parentExpression = methodExpression?.Object;//Method is an instance method - - var isExtension = methodExpression != null && methodExpression.Method.IsDefined(typeof(ExtensionAttribute), true); - if (isExtension && parentExpression == null && methodExpression.Arguments.Count > 0) - parentExpression = methodExpression.Arguments[0];//Method is an extension method based on the type of methodExpression.Arguments[0]. - - if (parentExpression == null) - return null; - - return GetParameterExpression(parentExpression); - } - - return null; - } - - /// - /// Returns the first ancestor node that is not a MemberExpression. - /// - /// - /// - public static Expression GetBaseOfMemberExpression(this MemberExpression expression) - { - if (expression.Expression == null) - return null; - - return expression.Expression.NodeType == ExpressionType.MemberAccess - ? GetBaseOfMemberExpression((MemberExpression)expression.Expression) - : expression.Expression; - } - - /// - /// Adds member expressions to an existing expression. - /// - /// - /// - /// - public static MemberExpression MemberAccesses(this Expression exp, List list) => - (MemberExpression) list.SelectMany(propertyMapInfo => propertyMapInfo.DestinationPropertyInfos).MemberAccesses(exp); - - /// - /// For the given a Lambda Expression, returns the fully qualified name of the member starting with the immediate child member of the parameter - /// - /// - /// - public static string GetMemberFullName(this LambdaExpression expr) - { - if (expr.Body.NodeType == ExpressionType.Parameter) - return string.Empty; - MemberExpression me = expr.Body.NodeType switch - { - ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs => expr.Body.GetUnconvertedExpression() as MemberExpression, - _ => expr.Body as MemberExpression, - }; - return me.GetPropertyFullName(); - } - - /// - /// Determines whether the specified type is an enumeration type. - /// - /// The type to evaluate. This can be a nullable type, in which case the underlying type is checked. - /// true if the specified type is an enumeration; otherwise, false. - public static bool IsEnumType(this Type type) - { - if (type.IsNullableType()) - type = Nullable.GetUnderlyingType(type); - - return type.IsEnum(); - } - } -} diff --git a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs index 20a5b4f..0b5200d 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs @@ -27,7 +27,7 @@ public static LambdaExpression MapExpression(this IMapper mapper, LambdaExpressi LambdaExpression GetLambdaExpression(ITypeMappingsManager typeMappingsManager) { - Expression mappedBody = new XpressionMapperVisitor(mapper, typeMappingsManager).Visit(expression.Body) ?? throw new InvalidOperationException(Properties.Resources.cantRemapExpression); + Expression mappedBody = new XpressionMapperVisitor(mapper, typeMappingsManager).Visit(expression.Body) ?? throw new InvalidOperationException(Properties.Resources.cantMapExpression); return Lambda ( diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.Designer.cs b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.Designer.cs index 0def712..e249c7d 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.Designer.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.Designer.cs @@ -61,7 +61,7 @@ internal Resources() { } /// - /// Looks up a localized string similar to Argument {0} must be a deledate type.. + /// Looks up a localized string similar to Argument {0} must be a delegate type.. /// internal static string argumentMustBeDelegateFormat { get { @@ -79,11 +79,11 @@ internal static string cannotCreateBinaryExpressionFormat { } /// - /// Looks up a localized string similar to Can't rempa expression. + /// Looks up a localized string similar to Can't map expression. /// - internal static string cantRemapExpression { + internal static string cantMapExpression { get { - return ResourceManager.GetString("cantRemapExpression", resourceCulture); + return ResourceManager.GetString("cantMapExpression", resourceCulture); } } @@ -132,6 +132,15 @@ internal static string invalidExpErr { } } + /// + /// Looks up a localized string similar to Invalid type mappings. Source Type: {0}, Destination Type: {1}.. + /// + internal static string invalidTypeMappingsFormat { + get { + return ResourceManager.GetString("invalidTypeMappingsFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to For members of literal types, use IMappingExpression.ForMember() to make the parent property types an exact match. Parent Source Type: {0}, Parent Destination Type: {1}, Full Member Name "{2}".. /// diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx index d3f05ae..ae9f6ba 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx +++ b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx @@ -102,8 +102,8 @@ Cannot create a binary expression for the following pair. Node: {0}, Type: {1} and Node: {2}, Type: {3}. 0=leftNode; 1=leftNodeType; 2=rightNode; 3=rightNodeType - - Can't rempa expression + + Can't map expression The source and destination types must be the same for expression mapping between literal types. Source Type: {0}, Source Description: {1}, Destination Type: {2}, Destination Property: {3}. @@ -144,7 +144,11 @@ 0=typeSource, 1=typeDestination; 2=sourceFullName - Argument {0} must be a deledate type. + Argument {0} must be a delegate type. 0=argumentType + + + Invalid type mappings. Source Type: {0}, Destination Type: {1}. + 0=sourceType; 1=destinationType \ No newline at end of file diff --git a/src/AutoMapper.Extensions.ExpressionMapping/TypeMappingsManager.cs b/src/AutoMapper.Extensions.ExpressionMapping/TypeMappingsManager.cs index 4db7d4e..684e396 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/TypeMappingsManager.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/TypeMappingsManager.cs @@ -33,6 +33,9 @@ public void AddTypeMapping(Type sourceType, Type destType) { if (sourceType.GetTypeInfo().IsGenericType && sourceType.GetGenericTypeDefinition() == typeof(Expression<>)) { + if (!destType.GetTypeInfo().IsGenericType || destType.GetGenericTypeDefinition() != typeof(Expression<>)) + throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture, Properties.Resources.invalidTypeMappingsFormat, sourceType, destType)); + sourceType = sourceType.GetGenericArguments()[0]; destType = destType.GetGenericArguments()[0]; } @@ -150,8 +153,7 @@ private void DoAddTypeMappings(List sourceArguments, List destArgume for (int i = 0; i < sourceArguments.Count; i++) { - if (!TypeMappings.ContainsKey(sourceArguments[i]) && sourceArguments[i] != destArguments[i]) - AddTypeMapping(sourceArguments[i], destArguments[i]); + AddTypeMapping(sourceArguments[i], destArguments[i]); } } @@ -162,8 +164,7 @@ private void DoAddTypeMappingsFromDelegates(List sourceArguments, List a.Key.Equals(node)); - return !pair.Equals(default(KeyValuePair)) ? pair.Value.NewParameter : base.VisitParameter(node); + return !pair.Equals(default(KeyValuePair)) ? pair.Value.NewParameter : base.VisitParameter(node); } private static object GetConstantValue(object constantObject, string fullName, Type parentType) @@ -557,8 +555,6 @@ protected override Expression VisitMethodCall(MethodCallExpression node) ? node.Method.GetGenericArguments().Select(type => this.TypeMappingsManager.ReplaceType(type)).ToList()//not converting the type it is not in the typeMappings dictionary : null; - ConvertTypesIfNecessary(node.Method.GetParameters(), listOfArgumentsForNewMethod, node.Method); - return node.Method.IsStatic ? GetStaticExpression() : GetInstanceExpression(this.Visit(node.Object)); @@ -584,19 +580,6 @@ MethodCallExpression GetStaticExpression() : Expression.Call(node.Method, [.. listOfArgumentsForNewMethod]); } - static void ConvertTypesIfNecessary(ParameterInfo[] parameters, List listOfArgumentsForNewMethod, MethodInfo mInfo) - { - if (mInfo.IsGenericMethod) - return; - - for (int i = 0; i < listOfArgumentsForNewMethod.Count; i++) - { - if (listOfArgumentsForNewMethod[i].Type != parameters[i].ParameterType - && parameters[i].ParameterType.IsAssignableFrom(listOfArgumentsForNewMethod[i].Type)) - listOfArgumentsForNewMethod[i] = Expression.Convert(listOfArgumentsForNewMethod[i], parameters[i].ParameterType); - } - } - protected static string BuildFullName(List propertyMapInfoList) { var fullName = string.Empty; diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/TypeMappingsManagerTest.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/TypeMappingsManagerTest.cs index bcf842e..d8b4e9b 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/TypeMappingsManagerTest.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/TypeMappingsManagerTest.cs @@ -81,7 +81,7 @@ public void Constructor_NonDelegateSourceType_ThrowsArgumentException() var exception = Assert.Throws(() => new TypeMappingsManager(config, typeof(int), typeof(Func))); - Assert.Contains("must be a deledate type", exception.Message); + Assert.Contains("must be a delegate type", exception.Message); } [Fact] @@ -97,7 +97,7 @@ public void Constructor_NonDelegateDestType_ThrowsArgumentException() var exception = Assert.Throws(() => new TypeMappingsManager(config, typeof(Func), typeof(string))); - Assert.Contains("must be a deledate type", exception.Message); + Assert.Contains("must be a delegate type", exception.Message); } [Fact] @@ -208,6 +208,28 @@ public void AddTypeMapping_DuplicateMapping_DoesNotAddAgain() Assert.Equal(countAfterFirst, manager.TypeMappings.Count); } + [Fact] + public void AddTypeMapping_ExpressionSourceNonExpressionDest_ThrowsArgumentException() + { + // Arrange + var config = ConfigurationHelper.GetMapperConfiguration(cfg => + { + cfg.CreateMap(); + }); + var manager = new TypeMappingsManager( + config, + typeof(Func), + typeof(Func)); + + // Act & Assert + var exception = Assert.Throws(() => + manager.AddTypeMapping( + typeof(Expression>), + typeof(Func))); + + Assert.Contains("Invalid type mappings", exception.Message); + } + [Fact] public void AddTypeMapping_ListTypes_AddsUnderlyingTypeMappings() {