Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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; }
}
}
}