From 4c4f7eb08edcee3b194f6ac16819f4adae02266b Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Sat, 21 Jan 2023 10:48:03 -0500 Subject: [PATCH 1/2] Fixes issue #158 - incorrect arguments passed to Expression.Call. --- .../TypeMapHelper.cs | 44 +++++ .../XpressionMapperVisitor.cs | 7 +- .../CanMapExpressionWithListConstants.cs | 162 ++++++++++++++++++ 3 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs index d559096..e33b215 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs @@ -6,6 +6,50 @@ namespace AutoMapper.Extensions.ExpressionMapping { internal static class TypeMapHelper { + public static bool CanMapConstant(this IConfigurationProvider config, Type sourceType, Type destType) + { + if (sourceType == destType) + return false; + + if (BothTypesAreDictionary()) + { + Type[] sourceGenericTypes = sourceType.GetGenericArguments(); + Type[] destGenericTypes = destType.GetGenericArguments(); + if (sourceGenericTypes.SequenceEqual(destGenericTypes)) + return false; + else if (sourceGenericTypes[0] == destGenericTypes[0]) + return config.CanMapConstant(sourceGenericTypes[1], destGenericTypes[1]); + else if (sourceGenericTypes[1] == destGenericTypes[1]) + return config.CanMapConstant(sourceGenericTypes[0], destGenericTypes[0]); + else + return config.CanMapConstant(sourceGenericTypes[0], destGenericTypes[0]) && config.CanMapConstant(sourceGenericTypes[1], destGenericTypes[1]); + } + else if (BothTypesAreEnumerable()) + return config.CanMapConstant(sourceType.GetGenericArguments()[0], destType.GetGenericArguments()[0]); + else + return config.Internal().ResolveTypeMap(sourceType, destType) != null; + + bool BothTypesAreEnumerable() + { + Type enumerableType = typeof(System.Collections.IEnumerable); + return sourceType.IsGenericType + && destType.IsGenericType + && enumerableType.IsAssignableFrom(sourceType) + && enumerableType.IsAssignableFrom(destType); + } + + bool BothTypesAreDictionary() + { + Type dictionaryType = typeof(System.Collections.IDictionary); + return sourceType.IsGenericType + && destType.IsGenericType + && dictionaryType.IsAssignableFrom(sourceType) + && dictionaryType.IsAssignableFrom(destType) + && sourceType.GetGenericArguments().Length == 2 + && destType.GetGenericArguments().Length == 2; + } + } + public static MemberMap GetMemberMapByDestinationProperty(this TypeMap typeMap, string destinationPropertyName) { var propertyMap = typeMap.PropertyMaps.SingleOrDefault(item => item.DestinationName == destinationPropertyName); diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index ecf33ef..ff354d7 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -511,12 +511,13 @@ Expression DoVisitUnary(Expression updated) protected override Expression VisitConstant(ConstantExpression node) { - if (this.TypeMappings.TryGetValue(node.Type, out Type newType)) + Type newType = this.TypeMappings.ReplaceType(node.Type); + if (newType != node.Type) { if (node.Value == null) return base.VisitConstant(Expression.Constant(null, newType)); - if (ConfigurationProvider.Internal().ResolveTypeMap(node.Type, newType) != null) + if (ConfigurationProvider.CanMapConstant(node.Type, newType)) return base.VisitConstant(Expression.Constant(Mapper.MapObject(node.Value, node.Type, newType), newType)); //Issue 3455 (Non-Generic Mapper.Map failing for structs in v10) //return base.VisitConstant(Expression.Constant(Mapper.Map(node.Value, node.Type, newType), newType)); @@ -553,7 +554,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) MethodCallExpression GetInstanceExpression(Expression instance) => node.Method.IsGenericMethod ? Expression.Call(instance, node.Method.Name, typeArgsForNewMethod.ToArray(), listOfArgumentsForNewMethod.ToArray()) - : Expression.Call(instance, node.Method, listOfArgumentsForNewMethod.ToArray()); + : Expression.Call(instance, instance.Type.GetMethod(node.Method.Name, listOfArgumentsForNewMethod.Select(a => a.Type).ToArray()), listOfArgumentsForNewMethod.ToArray()); MethodCallExpression GetStaticExpression() => node.Method.IsGenericMethod diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs new file mode 100644 index 0000000..5460b43 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class CanMapExpressionWithListConstants + { + [Fact] + public void Map_expression_with_constant_list_using_generic_list_dot_contains() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + List enums = new() { SimpleEnum.Value1, SimpleEnum.Value2 }; + Expression> filter = e => enums.Contains(e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + [Fact] + public void Map_expression_with_constant_list_using_generic_enumerable_dot_contains() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + List enums = new() { SimpleEnum.Value1, SimpleEnum.Value2 }; + Expression> filter = e => Enumerable.Contains(enums, e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + [Fact] + public void Map_expression_with_constant_dictionary() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + Dictionary enumDictionary = new() { ["A"] = SimpleEnum.Value1, ["B"] = SimpleEnum.Value2 }; + Expression> filter = e => enumDictionary.Any(i => i.Value == e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + [Fact] + public void Map_expression_with_constant_dictionary_mapping_both_Key_and_value() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + Dictionary enumDictionary = new() { [SimpleEnum.Value1] = new Entity { SimpleEnum = SimpleEnum.Value1 }, [SimpleEnum.Value2] = new Entity { SimpleEnum = SimpleEnum.Value2 } }; + Expression> filter = e => enumDictionary.Any(i => i.Key == e.SimpleEnum && i.Value.SimpleEnum == e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + public enum SimpleEnum + { + Value1, + Value2, + Value3 + } + + public record Entity + { + public int Id { get; init; } + public SimpleEnum SimpleEnum { get; init; } + } + + public enum SimpleEnumModel + { + Value1, + Value2, + Value3 + } + + public record EntityModel + { + public int Id { get; init; } + public SimpleEnumModel SimpleEnum { get; init; } + } + } +} From 9777d7910ee4fd34267f061c27eba79fa31dd655 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Sun, 22 Jan 2023 07:40:34 -0500 Subject: [PATCH 2/2] Mapping array constant. --- .../MapperExtensions.cs | 25 ++++++++++++--- .../TypeMapHelper.cs | 2 ++ .../CanMapExpressionWithListConstants.cs | 31 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs index acf9c5a..72ba3d0 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs @@ -335,22 +335,39 @@ void AddTypeMaps(TypeMap typeMap) /// public static Type ReplaceType(this Dictionary typeMappings, Type sourceType) { - if (!sourceType.IsGenericType) + if (sourceType.IsArray) { - return typeMappings.TryGetValue(sourceType, out Type destType) ? destType : sourceType; + if (typeMappings.TryGetValue(sourceType, out Type destType)) + return destType; + + if (typeMappings.TryGetValue(sourceType.GetElementType(), out Type destElementType)) + { + int rank = sourceType.GetArrayRank(); + return rank == 1 + ? destElementType.MakeArrayType() + : destElementType.MakeArrayType(rank); + } + + return sourceType; } - else + else if (sourceType.IsGenericType) { if (typeMappings.TryGetValue(sourceType, out Type destType)) return destType; else + { return sourceType.GetGenericTypeDefinition().MakeGenericType ( sourceType .GetGenericArguments() - .Select(type => typeMappings.ReplaceType(type)) + .Select(typeMappings.ReplaceType) .ToArray() ); + } + } + else + { + return typeMappings.TryGetValue(sourceType, out Type destType) ? destType : sourceType; } } diff --git a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs index e33b215..65df684 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs @@ -24,6 +24,8 @@ public static bool CanMapConstant(this IConfigurationProvider config, Type sourc else return config.CanMapConstant(sourceGenericTypes[0], destGenericTypes[0]) && config.CanMapConstant(sourceGenericTypes[1], destGenericTypes[1]); } + else if (sourceType.IsArray && destType.IsArray) + return config.CanMapConstant(sourceType.GetElementType(), destType.GetElementType()); else if (BothTypesAreEnumerable()) return config.CanMapConstant(sourceType.GetGenericArguments()[0], destType.GetGenericArguments()[0]); else diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs index 5460b43..8573476 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs @@ -8,6 +8,37 @@ namespace AutoMapper.Extensions.ExpressionMapping.UnitTests { public class CanMapExpressionWithListConstants { + [Fact] + public void Map_expression_with_constant_array() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + Entity[] entities = new Entity[] { new Entity { SimpleEnum = SimpleEnum.Value1 }, new Entity { SimpleEnum = SimpleEnum.Value2 } }; + Expression> filter = e => entities.Any(en => e.SimpleEnum == en.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + [Fact] public void Map_expression_with_constant_list_using_generic_list_dot_contains() {