Skip to content

Commit 9614139

Browse files
authored
Merge pull request #104 from AutoMapper/ImprovingAnonymousTypeExpressionMapping
Handling property maps for anonymous ParameterExpression.
2 parents 86dd7df + d2f35a8 commit 9614139

File tree

3 files changed

+113
-11
lines changed

3 files changed

+113
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Linq;
3+
4+
namespace AutoMapper.Extensions.ExpressionMapping
5+
{
6+
internal static class TypeMapHelper
7+
{
8+
public static IMemberMap GetMemberMapByDestinationProperty(this TypeMap typeMap, string destinationPropertyName)
9+
{
10+
var propertyMap = typeMap.PropertyMaps.SingleOrDefault(item => item.DestinationName == destinationPropertyName);
11+
if (propertyMap != null)
12+
return propertyMap;
13+
14+
var memberMap = typeMap.MemberMaps.OfType<ConstructorParameterMap>().SingleOrDefault(mm => mm.Parameter.Name == destinationPropertyName);
15+
if (memberMap != null)
16+
return memberMap;
17+
18+
throw PropertyConfigurationException(typeMap, destinationPropertyName);
19+
}
20+
21+
public static TypeMap CheckIfTypeMapExists(this IConfigurationProvider config, Type sourceType, Type destinationType)
22+
{
23+
var typeMap = config.ResolveTypeMap(sourceType, destinationType);
24+
if (typeMap == null)
25+
{
26+
throw MissingMapException(sourceType, destinationType);
27+
}
28+
return typeMap;
29+
}
30+
31+
public static string GetDestinationName(this IMemberMap memberMap)
32+
{
33+
if (memberMap is PropertyMap propertyMap)
34+
return propertyMap.DestinationMember.Name;
35+
36+
if (memberMap is ConstructorParameterMap constructorMap)
37+
return constructorMap.Parameter.Name;
38+
39+
throw new ArgumentException(nameof(memberMap));
40+
}
41+
42+
private static Exception PropertyConfigurationException(TypeMap typeMap, params string[] unmappedPropertyNames)
43+
=> new AutoMapperConfigurationException(new[] { new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, true) });
44+
45+
private static Exception MissingMapException(TypePair types)
46+
=> MissingMapException(types.SourceType, types.DestinationType);
47+
48+
private static Exception MissingMapException(Type sourceType, Type destinationType)
49+
=> new InvalidOperationException($"Missing map from {sourceType} to {destinationType}. Create using CreateMap<{sourceType.Name}, {destinationType.Name}>.");
50+
}
51+
}

src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs

+42-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ public XpressionMapperVisitor(IMapper mapper, IConfigurationProvider configurati
3030

3131
protected IMapper Mapper { get; }
3232

33+
private IConfigurationProvider anonymousTypesConfigurationProvider;
34+
private Configuration.MapperConfigurationExpression anonymousTypesBaseMappings = new Configuration.MapperConfigurationExpression();
35+
3336
protected override Expression VisitParameter(ParameterExpression node)
3437
{
3538
InfoDictionary.Add(node, TypeMappings);
@@ -172,7 +175,7 @@ protected override Expression VisitNew(NewExpression node)
172175
for (int i = 0; i < parameters.Length; i++)
173176
bindingExpressions.Add(parameters[i].Name, this.Visit(node.Arguments[i]));
174177

175-
return GetMemberInitExpression(bindingExpressions, node.Type);
178+
return GetAnonymousTypeMemberInitExpression(bindingExpressions, node.Type);
176179
}
177180

178181
return base.VisitNew(node);
@@ -196,7 +199,7 @@ protected override Expression VisitMemberInit(MemberInitExpression node)
196199
{
197200
if (this.TypeMappings.TryGetValue(node.Type, out Type newType))
198201
{
199-
var typeMap = ConfigurationProvider.CheckIfMapExists(sourceType: newType, destinationType: node.Type);
202+
var typeMap = ConfigurationProvider.CheckIfTypeMapExists(sourceType: newType, destinationType: node.Type);
200203
//The destination becomes the source because to map a source expression to a destination expression,
201204
//we need the expressions used to create the source from the destination
202205

@@ -221,7 +224,7 @@ protected override Expression VisitMemberInit(MemberInitExpression node)
221224
}
222225
else if (IsAnonymousType(node.Type))
223226
{
224-
return GetMemberInitExpression
227+
return GetAnonymousTypeMemberInitExpression
225228
(
226229
node.Bindings
227230
.OfType<MemberAssignment>()
@@ -237,11 +240,36 @@ protected override Expression VisitMemberInit(MemberInitExpression node)
237240
return base.VisitMemberInit(node);
238241
}
239242

240-
private MemberInitExpression GetMemberInitExpression(Dictionary<string, Expression> bindingExpressions, Type oldType)
243+
private void ConfigureAnonymousTypeMaps(Type oldType, Type newAnonymousType)
244+
{
245+
anonymousTypesBaseMappings.CreateMap(newAnonymousType, oldType);
246+
Dictionary<Type, Type> memberTypeMaps = new Dictionary<Type, Type>();
247+
newAnonymousType.GetMembers()
248+
.OfType<PropertyInfo>()
249+
.ToList()
250+
.ForEach(member =>
251+
{
252+
Type sourceType = member.GetMemberType();
253+
Type destMember = oldType.GetMember(member.Name)[0].GetMemberType();
254+
if (sourceType == destMember)
255+
return;
256+
257+
if (!memberTypeMaps.ContainsKey(sourceType))
258+
{
259+
memberTypeMaps.Add(sourceType, destMember);
260+
anonymousTypesBaseMappings.CreateMap(sourceType, destMember);
261+
}
262+
});
263+
264+
anonymousTypesConfigurationProvider = new MapperConfiguration(anonymousTypesBaseMappings);
265+
}
266+
267+
private MemberInitExpression GetAnonymousTypeMemberInitExpression(Dictionary<string, Expression> bindingExpressions, Type oldType)
241268
{
242269
Type newAnonymousType = AnonymousTypeFactory.CreateAnonymousType(bindingExpressions.ToDictionary(a => a.Key, a => a.Value.Type));
243270
TypeMappings.AddTypeMapping(ConfigurationProvider, oldType, newAnonymousType);
244271

272+
ConfigureAnonymousTypeMaps(oldType, newAnonymousType);
245273
return Expression.MemberInit
246274
(
247275
Expression.New(newAnonymousType),
@@ -593,7 +621,7 @@ protected void FindDestinationFullName(Type typeSource, Type typeDestination, st
593621
bool BothTypesAreAnonymous()
594622
=> IsAnonymousType(typeSource) && IsAnonymousType(typeDestination);
595623

596-
if (typeSource == typeDestination || BothTypesAreAnonymous())
624+
if (typeSource == typeDestination)
597625
{
598626
var sourceFullNameArray = sourceFullName.Split(new[] { period[0] }, StringSplitOptions.RemoveEmptyEntries);
599627
sourceFullNameArray.Aggregate(propertyMapInfoList, (list, next) =>
@@ -644,7 +672,10 @@ bool BothTypesAreAnonymous()
644672
}
645673
}
646674

647-
var typeMap = ConfigurationProvider.CheckIfMapExists(sourceType: typeDestination, destinationType: typeSource);//The destination becomes the source because to map a source expression to a destination expression,
675+
var typeMap = BothTypesAreAnonymous()
676+
? anonymousTypesConfigurationProvider.CheckIfTypeMapExists(sourceType: typeDestination, destinationType: typeSource)
677+
: ConfigurationProvider.CheckIfTypeMapExists(sourceType: typeDestination, destinationType: typeSource);
678+
//The destination becomes the source because to map a source expression to a destination expression,
648679
//we need the expressions used to create the source from the destination
649680

650681
PathMap pathMap = typeMap.FindPathMapByDestinationPath(destinationFullPath: sourceFullName);
@@ -657,8 +688,8 @@ bool BothTypesAreAnonymous()
657688

658689
if (sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) < 0)
659690
{
660-
var propertyMap = typeMap.GetPropertyMapByDestinationProperty(sourceFullName);
661-
var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.DestinationMember.Name);
691+
var propertyMap = typeMap.GetMemberMapByDestinationProperty(sourceFullName);
692+
var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.GetDestinationName());
662693
if (propertyMap.ValueResolverConfig != null)
663694
{
664695
throw new InvalidOperationException(Resource.customResolversNotSupported);
@@ -678,7 +709,7 @@ void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedProperty
678709
{
679710
//switch from IsValueType to IsLiteralType because we do not want to throw an exception for all structs
680711
if ((mappedPropertyType.IsLiteralType() || sourceMemberType.IsLiteralType()) && sourceMemberType != mappedPropertyType)
681-
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.expressionMapValueTypeMustMatchFormat, mappedPropertyType.Name, mappedPropertyDescription, sourceMemberType.Name, propertyMap.DestinationMember.Name));
712+
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.expressionMapValueTypeMustMatchFormat, mappedPropertyType.Name, mappedPropertyDescription, sourceMemberType.Name, propertyMap.GetDestinationName()));
682713
}
683714

684715
if (propertyMap.ProjectToCustomSource != null)
@@ -689,9 +720,9 @@ void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedProperty
689720
else
690721
{
691722
var propertyName = sourceFullName.Substring(0, sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase));
692-
var propertyMap = typeMap.GetPropertyMapByDestinationProperty(propertyName);
723+
var propertyMap = typeMap.GetMemberMapByDestinationProperty(propertyName);
693724

694-
var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.DestinationMember.Name);
725+
var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.GetDestinationName());
695726
if (propertyMap.CustomMapExpression == null && !propertyMap.SourceMembers.Any())//If sourceFullName has a period then the SourceMember cannot be null. The SourceMember is required to find the ProertyMap of its child object.
696727
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.srcMemberCannotBeNullFormat, typeSource.Name, typeDestination.Name, propertyName));
697728

tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs

+20
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,26 @@ public void Map_to_anonymous_type_when_init_member_is_not_a_literal()
655655
Assert.True(users[0].Account.Balance == 150000);
656656
}
657657

658+
[Fact]
659+
public void Map_to_anonymous_type_when_init_member_is_not_a_literal_and_parameter_is_anonymous_type()
660+
{
661+
//Arrange
662+
Expression<Func<IQueryable<UserModel>, IEnumerable<object>>> expression = q => q.OrderBy(s => s.Id).Select(u => new { UserId = u.Id, Account = u.AccountModel }).Where(a => a.Account.Bal != -1);
663+
664+
//Act
665+
Expression<Func<IQueryable<User>, IEnumerable<object>>> expMapped = (Expression<Func<IQueryable<User>, IEnumerable<object>>>)mapper.MapExpression
666+
(
667+
expression,
668+
typeof(Expression<Func<IQueryable<UserModel>, IEnumerable<object>>>),
669+
typeof(Expression<Func<IQueryable<User>, IEnumerable<object>>>)
670+
);
671+
672+
List<dynamic> users = expMapped.Compile().Invoke(Users).ToList();
673+
674+
Assert.True(users[0].UserId == 11);
675+
Assert.True(users[0].Account.Balance == 150000);
676+
}
677+
658678
[Fact]
659679
public void Map_to_anonymous_type_when_init_member_is_not_a_literal_with_navigation_property()
660680
{

0 commit comments

Comments
 (0)