Skip to content

Commit

Permalink
Fixes issue #158 - incorrect arguments passed to Expression.Call. (#159)
Browse files Browse the repository at this point in the history
* Fixes issue #158 - incorrect arguments passed to Expression.Call.

* Mapping array constant.
  • Loading branch information
BlaiseD authored Jan 22, 2023
1 parent 6524c15 commit 2768e95
Show file tree
Hide file tree
Showing 4 changed files with 264 additions and 7 deletions.
25 changes: 21 additions & 4 deletions src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -335,22 +335,39 @@ void AddTypeMaps(TypeMap typeMap)
/// <returns></returns>
public static Type ReplaceType(this Dictionary<Type, Type> 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;
}
}

Expand Down
46 changes: 46 additions & 0 deletions src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@ 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 (sourceType.IsArray && destType.IsArray)
return config.CanMapConstant(sourceType.GetElementType(), destType.GetElementType());
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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_array()
{
//Arrange
var config = new MapperConfiguration
(
cfg =>
{
cfg.CreateMap<EntityModel, Entity>();
cfg.CreateMap<Entity, EntityModel>();
}
);
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
List<EntityModel> source1 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value3 }
};
List<EntityModel> source2 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value1 }
};
Entity[] entities = new Entity[] { new Entity { SimpleEnum = SimpleEnum.Value1 }, new Entity { SimpleEnum = SimpleEnum.Value2 } };
Expression<Func<Entity, bool>> filter = e => entities.Any(en => e.SimpleEnum == en.SimpleEnum);

//act
Expression<Func<EntityModel, bool>> mappedFilter = mapper.MapExpression<Expression<Func<EntityModel, bool>>>(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()
{
//Arrange
var config = new MapperConfiguration
(
cfg =>
{
cfg.CreateMap<EntityModel, Entity>();
cfg.CreateMap<SimpleEnum, SimpleEnumModel>();
}
);
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
List<EntityModel> source1 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value3 }
};
List<EntityModel> source2 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value1 }
};
List<SimpleEnum> enums = new() { SimpleEnum.Value1, SimpleEnum.Value2 };
Expression<Func<Entity, bool>> filter = e => enums.Contains(e.SimpleEnum);

//act
Expression<Func<EntityModel, bool>> mappedFilter = mapper.MapExpression<Expression<Func<EntityModel, bool>>>(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<EntityModel, Entity>();
cfg.CreateMap<SimpleEnum, SimpleEnumModel>();
}
);
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
List<EntityModel> source1 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value3 }
};
List<EntityModel> source2 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value1 }
};
List<SimpleEnum> enums = new() { SimpleEnum.Value1, SimpleEnum.Value2 };
Expression<Func<Entity, bool>> filter = e => Enumerable.Contains(enums, e.SimpleEnum);

//act
Expression<Func<EntityModel, bool>> mappedFilter = mapper.MapExpression<Expression<Func<EntityModel, bool>>>(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<EntityModel, Entity>();
cfg.CreateMap<SimpleEnum, SimpleEnumModel>();
}
);
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
List<EntityModel> source1 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value3 }
};
List<EntityModel> source2 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value1 }
};
Dictionary<string, SimpleEnum> enumDictionary = new() { ["A"] = SimpleEnum.Value1, ["B"] = SimpleEnum.Value2 };
Expression<Func<Entity, bool>> filter = e => enumDictionary.Any(i => i.Value == e.SimpleEnum);

//act
Expression<Func<EntityModel, bool>> mappedFilter = mapper.MapExpression<Expression<Func<EntityModel, bool>>>(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<EntityModel, Entity>();
cfg.CreateMap<Entity, EntityModel>();
cfg.CreateMap<SimpleEnum, SimpleEnumModel>();
}
);
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
List<EntityModel> source1 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value3 }
};
List<EntityModel> source2 = new() {
new EntityModel { SimpleEnum = SimpleEnumModel.Value1 }
};
Dictionary<SimpleEnum, Entity> enumDictionary = new() { [SimpleEnum.Value1] = new Entity { SimpleEnum = SimpleEnum.Value1 }, [SimpleEnum.Value2] = new Entity { SimpleEnum = SimpleEnum.Value2 } };
Expression<Func<Entity, bool>> filter = e => enumDictionary.Any(i => i.Key == e.SimpleEnum && i.Value.SimpleEnum == e.SimpleEnum);

//act
Expression<Func<EntityModel, bool>> mappedFilter = mapper.MapExpression<Expression<Func<EntityModel, bool>>>(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; }
}
}
}

0 comments on commit 2768e95

Please sign in to comment.