diff --git a/docs/Flattening.md b/docs/Flattening.md index 4cdf74d819..f1d7b3c675 100644 --- a/docs/Flattening.md +++ b/docs/Flattening.md @@ -145,11 +145,11 @@ destination.Name.ShouldBe("name"); destination.Description.ShouldBe("description"); destination.Title.ShouldBe("title"); ``` -So this allows you to reuse the configuration in the existing maps for the child types `InnerSource` and `OtherInnerSource` when mapping the parent types `Source` and `Destination`. It works in a similar way to [mapping inheritance](Mapping-inheritance.html), but it uses composition, not inheritance. +So this allows you to reuse the configuration in the existing map for the child types `InnerSource` and `OtherInnerSource` when mapping the parent types `Source` and `Destination`. It works in a similar way to [mapping inheritance](Mapping-inheritance.html), but it uses composition, not inheritance. The order of the parameters in the `IncludeMembers` call is relevant. When mapping a destination member, the first match wins, starting with the source object itself and then with the included child objects in the order you specified. So in the example above, `Name` is mapped from the source object itself and `Description` from `InnerSource` because it's the first match. -Note that this matching is static, it happens at configuration time, not at `Map` time, and the runtime types of the child objects are not considered. +Note that this matching is static, it happens at configuration time, not at `Map` time, so the runtime types of the child objects are not considered. IncludeMembers integrates with `ReverseMap`. An included member will be reversed to ```c# diff --git a/src/AutoMapper/Configuration/ConfigurationValidator.cs b/src/AutoMapper/Configuration/ConfigurationValidator.cs index d2121de36e..2f1148037e 100644 --- a/src/AutoMapper/Configuration/ConfigurationValidator.cs +++ b/src/AutoMapper/Configuration/ConfigurationValidator.cs @@ -131,7 +131,10 @@ private void DryRunTypeMap(ICollection typeMapsChecked, TypePair types, if(mapperToUse is IObjectMapperInfo mapperInfo) { var newTypePair = mapperInfo.GetAssociatedTypes(types); - DryRunTypeMap(typeMapsChecked, newTypePair, null, memberMap); + if (newTypePair != types) + { + DryRunTypeMap(typeMapsChecked, newTypePair, null, memberMap); + } } } } diff --git a/src/AutoMapper/Configuration/MapperConfiguration.cs b/src/AutoMapper/Configuration/MapperConfiguration.cs index d44b28f705..f05acee714 100644 --- a/src/AutoMapper/Configuration/MapperConfiguration.cs +++ b/src/AutoMapper/Configuration/MapperConfiguration.cs @@ -300,25 +300,15 @@ private TypeMap GetTypeMap(TypePair initialTypes) static List GetTypeInheritance(Type type) { var interfaces = type.GetInterfaces(); - var lastIndex = interfaces.Length - 1; var types = new List(interfaces.Length + 2) { type }; Type baseType = type; while ((baseType = baseType.BaseType) != null) { types.Add(baseType); - foreach (var interfaceType in baseType.GetInterfaces()) - { - var interfaceIndex = Array.LastIndexOf(interfaces, interfaceType); - if (interfaceIndex != lastIndex) - { - interfaces[interfaceIndex] = interfaces[lastIndex]; - interfaces[lastIndex] = interfaceType; - } - } } - foreach (var interfaceType in interfaces) + for(int index = interfaces.Length - 1; index >= 0; index--) { - types.Add(interfaceType); + types.Add(interfaces[index]); } return types; } diff --git a/src/AutoMapper/Internal/TypeExtensions.cs b/src/AutoMapper/Internal/TypeExtensions.cs index a20c97eead..4ac254b435 100644 --- a/src/AutoMapper/Internal/TypeExtensions.cs +++ b/src/AutoMapper/Internal/TypeExtensions.cs @@ -77,8 +77,10 @@ public static Type GetGenericInterface(this Type type, Type genericInterface) { return type; } - foreach (var interfaceType in type.GetInterfaces()) + var interfaces = type.GetInterfaces(); + for(int index = interfaces.Length - 1; index >= 0; index--) { + var interfaceType = interfaces[index]; if (interfaceType.IsGenericType(genericInterface)) { return interfaceType; diff --git a/src/AutoMapper/Mappers/CollectionMapper.cs b/src/AutoMapper/Mappers/CollectionMapper.cs index 21e316afee..b6ff7e320c 100644 --- a/src/AutoMapper/Mappers/CollectionMapper.cs +++ b/src/AutoMapper/Mappers/CollectionMapper.cs @@ -47,13 +47,22 @@ Expression MapReadOnlyCollection(Type genericCollectionType, Type genericReadOnl Expression MapCollectionCore(Expression destExpression) { var destinationType = destExpression.Type; - MethodInfo addMethod; - bool isIList, mustUseDestination = memberMap is { MustUseDestination: true }; - Type destinationCollectionType, destinationElementType; + var sourceType = sourceExpression.Type; + MethodInfo addMethod = null; + bool isIList = false, mustUseDestination = memberMap is { MustUseDestination: true }; + Type destinationCollectionType = null, destinationElementType = null; GetDestinationType(); var passedDestination = Variable(destExpression.Type, "passedDestination"); var newExpression = Variable(passedDestination.Type, "collectionDestination"); - var sourceElementType = sourceExpression.Type.GetICollectionType()?.GenericTypeArguments[0] ?? GetEnumerableElementType(sourceExpression.Type); + var sourceElementType = GetEnumerableElementType(sourceType); + if (destinationCollectionType == null || (sourceType == sourceElementType && destinationType == destinationElementType)) + { + if (destinationType.IsAssignableFrom(sourceType)) + { + return sourceExpression; + } + throw new NotSupportedException($"Unknown collection. Consider a custom type converter from {sourceType} to {destinationType}."); + } var itemParam = Parameter(sourceElementType, "item"); var itemExpr = configurationProvider.MapExpression(profileMap, new TypePair(sourceElementType, destinationElementType), itemParam); Expression destination, assignNewExpression; @@ -78,8 +87,12 @@ Expression MapCollectionCore(Expression destExpression) return CheckContext(); void GetDestinationType() { + var immutableCollection = !mustUseDestination && destinationType.IsValueType; + if (immutableCollection) + { + return; + } destinationCollectionType = destinationType.GetICollectionType(); - destinationElementType = destinationCollectionType?.GenericTypeArguments[0] ?? GetEnumerableElementType(destinationType); isIList = destExpression.Type.IsListType(); if (destinationCollectionType == null) { @@ -87,9 +100,15 @@ void GetDestinationType() { destinationCollectionType = typeof(IList); addMethod = IListAdd; + destinationElementType = GetEnumerableElementType(destinationType); } else { + if (!destinationType.IsInterface) + { + return; + } + destinationElementType = GetEnumerableElementType(destinationType); destinationCollectionType = typeof(ICollection<>).MakeGenericType(destinationElementType); destExpression = Convert(mustUseDestination ? destExpression : Null, destinationCollectionType); addMethod = destinationCollectionType.GetMethod("Add"); @@ -97,6 +116,7 @@ void GetDestinationType() } else { + destinationElementType = destinationCollectionType.GenericTypeArguments[0]; addMethod = destinationCollectionType.GetMethod("Add"); } } diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index bc244923e1..576119f310 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -106,7 +106,7 @@ bool OverMaxDepth() } void ProjectProperties() { - foreach (var propertyMap in typeMap.PropertyMaps.Where(pm => pm.CanResolveValue && pm.DestinationMember.CanBeSet()).OrderBy(pm => pm.DestinationName)) + foreach (var propertyMap in typeMap.PropertyMaps.Where(pm => pm.CanResolveValue && pm.DestinationMember.CanBeSet()).OrderBy(pm => pm.DestinationMember.MetadataToken)) { var propertyProjection = TryProjectMember(propertyMap, propertyMap.ExplicitExpansion); if (propertyProjection != null) diff --git a/src/UnitTests/CollectionMapping.cs b/src/UnitTests/CollectionMapping.cs index d2cf7a0867..21e46b9f7a 100644 --- a/src/UnitTests/CollectionMapping.cs +++ b/src/UnitTests/CollectionMapping.cs @@ -6,11 +6,61 @@ using Xunit; using Shouldly; using System.Collections; -using System.Reflection; using AutoMapper.Internal; +using System.Collections.Immutable; namespace AutoMapper.UnitTests { + public class ImmutableCollection : AutoMapperSpecBase + { + class Source + { + public string Value { get; set; } + } + class Destination + { + public ImmutableArray Value { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(c => + c.CreateMap().ForMember(d=>d.Value, o=>o.MapFrom(_=>ImmutableArray.Create()))); + [Fact] + public void Should_work() => Map(new Source()).Value.ShouldBeOfType>(); + } + public class AssignableCollection : AutoMapperSpecBase + { + class Source + { + public string Value { get; set; } + } + class Destination + { + public MyJObject Value { get; set; } + } + class MyJObject : IEnumerable + { + public IEnumerator GetEnumerator() => throw new NotImplementedException(); + } + protected override MapperConfiguration CreateConfiguration() => new(c => + c.CreateMap().ForMember(d=>d.Value, o=>o.MapFrom(_=>new MyJObject()))); + [Fact] + public void Should_work() => Map(new Source()).Value.ShouldBeOfType(); + } + public class RecursiveCollection : AutoMapperSpecBase + { + class Source + { + public string Value { get; set; } + } + class Destination + { + public MyJObject Value { get; set; } + } + class MyJObject : List{} + protected override MapperConfiguration CreateConfiguration() => new(c => + c.CreateMap().ForMember(d=>d.Value, o=>o.MapFrom(_=>new MyJObject()))); + [Fact] + public void Should_work() => Map(new Source()).Value.ShouldBeOfType(); + } public class AmbigousMethod : AutoMapperSpecBase { public class Source