diff --git a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs index 2fe725ab05..362214c3ed 100644 --- a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs +++ b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs @@ -8,6 +8,7 @@ namespace AutoMapper.Execution { + using AutoMapper.Mappers.Internal; using static Expression; using static ExpressionFactory; @@ -501,14 +502,14 @@ public Expression MapExpression(TypePair typePair, Expression sourceParameter, P public static Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, TypePair typePair, Expression sourceParameter, Expression contextParameter, PropertyMap propertyMap = null, Expression destinationParameter = null) { - if (destinationParameter == null) + if(destinationParameter == null) { destinationParameter = Default(typePair.DestinationType); } var typeMap = configurationProvider.ResolveTypeMap(typePair); - if (typeMap != null) + if(typeMap != null) { - if (!typeMap.HasDerivedTypesToInclude()) + if(!typeMap.HasDerivedTypesToInclude()) { typeMap.Seal(configurationProvider); @@ -518,8 +519,56 @@ public static Expression MapExpression(IConfigurationProvider configurationProvi } return ContextMap(typePair, sourceParameter, contextParameter, destinationParameter); } + var objectMapperExpression = ObjectMapperExpression(configurationProvider, profileMap, typePair, sourceParameter, contextParameter, propertyMap, destinationParameter); + return NullCheckSource(profileMap, sourceParameter, destinationParameter, objectMapperExpression); + } + + public static Expression NullCheckSource(ProfileMap profileMap, Expression sourceParameter, Expression destinationParameter, Expression objectMapperExpression) + { + var destinationType = destinationParameter.Type; + var defaultDestination = DefaultDestination(destinationType, profileMap); + var ifSourceNull = destinationType.IsCollectionType() ? Block(ClearDestinationCollection(destinationParameter), defaultDestination) : defaultDestination; + return sourceParameter.IfNullElse(ifSourceNull, objectMapperExpression); + } + + private static Expression ClearDestinationCollection(Expression destinationParameter) + { + var destinationElementType = ElementTypeHelper.GetElementType(destinationParameter.Type); + var destinationCollectionType = typeof(ICollection<>).MakeGenericType(destinationElementType); + var clearMethod = destinationCollectionType.GetDeclaredMethod("Clear"); + var collection = ToType(destinationParameter, destinationCollectionType); + var clear = Condition(Property(collection, "IsReadOnly"), Empty(), Call(collection, clearMethod)); + return collection.IfNullElse(Empty(), clear); + } + + private static Expression DefaultDestination(Type destinationType, ProfileMap profileMap) + { + var defaultValue = Default(destinationType); + if(!profileMap.AllowNullCollections) + { + if(destinationType.IsArray) + { + var destinationElementType = ElementTypeHelper.GetElementType(destinationType); + return NewArrayBounds(destinationElementType, Enumerable.Repeat(Constant(0), destinationType.GetArrayRank())); + } + if(destinationType.IsDictionaryType()) + { + return destinationType.IsInterface() ? + DelegateFactory.GenerateNonNullConstructorExpression(typeof(Dictionary<,>).MakeGenericType(destinationType.GetGenericArguments())) : + defaultValue; + } + if(destinationType.IsCollectionType() && !destinationType.IsInterface()) + { + return DelegateFactory.GenerateNonNullConstructorExpression(destinationType); + } + } + return defaultValue; + } + + private static Expression ObjectMapperExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, TypePair typePair, Expression sourceParameter, Expression contextParameter, PropertyMap propertyMap, Expression destinationParameter) + { var match = configurationProvider.GetMappers().FirstOrDefault(m => m.IsMatch(typePair)); - if (match != null) + if(match != null) { var mapperExpression = match.MapExpression(configurationProvider, profileMap, propertyMap, sourceParameter, destinationParameter, contextParameter); diff --git a/src/AutoMapper/Internal/ExpressionFactory.cs b/src/AutoMapper/Internal/ExpressionFactory.cs index e372cc4dc1..1f6151f0de 100644 --- a/src/AutoMapper/Internal/ExpressionFactory.cs +++ b/src/AutoMapper/Internal/ExpressionFactory.cs @@ -138,8 +138,8 @@ public static Expression IfNotNull(Expression expression, Type destinationType) public static Expression IfNullElse(Expression expression, Expression then, Expression @else = null) { - var isNull = expression.Type.IsValueType() ? (Expression) Constant(false) : Equal(expression, Constant(null)); - return Condition(isNull, then, @else ?? Default(then.Type)); + var isNull = expression.Type.IsValueType() && !expression.Type.IsNullableType() ? (Expression) Constant(false) : Equal(expression, Constant(null)); + return Condition(isNull, then, Convert(@else ?? Default(then.Type), then.Type)); } internal class IfNotNullVisitor : ExpressionVisitor diff --git a/src/AutoMapper/MapperConfiguration.cs b/src/AutoMapper/MapperConfiguration.cs index 373c12a384..fb9d71d75e 100644 --- a/src/AutoMapper/MapperConfiguration.cs +++ b/src/AutoMapper/MapperConfiguration.cs @@ -9,7 +9,8 @@ using AutoMapper.QueryableExtensions.Impl; namespace AutoMapper -{ +{ + using AutoMapper.Execution; using static Expression; using static ExpressionFactory; using UntypedMapperFunc = Func; @@ -141,11 +142,11 @@ private LambdaExpression GenerateObjectMapperExpression(MapRequest mapRequest, I var source = Parameter(mapRequest.RequestedTypes.SourceType, "source"); var destination = Parameter(destinationType, "mapperDestination"); var context = Parameter(typeof(ResolutionContext), "context"); - LambdaExpression fullExpression; + Expression fullExpression; if (mapperToUse == null) { var message = Constant("Missing type map configuration or unsupported mapping."); - fullExpression = Lambda(Block(Throw(New(ExceptionConstructor, message, Constant(null, typeof(Exception)), Constant(mapRequest.RequestedTypes))), Default(destinationType)), source, destination, context); + fullExpression = Block(Throw(New(ExceptionConstructor, message, Constant(null, typeof(Exception)), Constant(mapRequest.RequestedTypes))), Default(destinationType)); } else { @@ -153,22 +154,16 @@ private LambdaExpression GenerateObjectMapperExpression(MapRequest mapRequest, I ToType(source, mapRequest.RuntimeTypes.SourceType), ToType(destination, mapRequest.RuntimeTypes.DestinationType), context); - var mapToDestination = Lambda(ToType(map, destinationType), source, destination, context); - fullExpression = TryCatch(mapToDestination, source, destination, context, mapRequest.RequestedTypes); - } - return fullExpression; + var exception = Parameter(typeof(Exception), "ex"); + fullExpression = + TryCatch(ToType(map, destinationType), + MakeCatchBlock(typeof(Exception), exception, Block( + Throw(New(ExceptionConstructor, Constant("Error mapping types."), exception, Constant(mapRequest.RequestedTypes))), + Default(destination.Type)), null)); + } + var nullCheckSource = TypeMapPlanBuilder.NullCheckSource(Configuration, source, destination, fullExpression); + return Lambda(nullCheckSource, source, destination, context); } - - private static LambdaExpression TryCatch(LambdaExpression mapExpression, ParameterExpression source, ParameterExpression destination, ParameterExpression context, TypePair types) - { - var exception = Parameter(typeof(Exception), "ex"); - - return Lambda(Expression.TryCatch(mapExpression.Body, - MakeCatchBlock(typeof(Exception), exception, Block( - Throw(New(ExceptionConstructor, Constant("Error mapping types."), exception, Constant(types))), - Default(destination.Type)), null)), - source, destination, context); - } public TypeMap[] GetAllTypeMaps() => _typeMapRegistry.TypeMaps.ToArray(); diff --git a/src/AutoMapper/Mappers/ArrayMapper.cs b/src/AutoMapper/Mappers/ArrayMapper.cs index 6ba3214c81..dbd7a96abd 100644 --- a/src/AutoMapper/Mappers/ArrayMapper.cs +++ b/src/AutoMapper/Mappers/ArrayMapper.cs @@ -21,10 +21,6 @@ public override Expression MapExpression(IConfigurationProvider configurationPro var sourceElementType = ElementTypeHelper.GetElementType(sourceExpression.Type); var destElementType = ElementTypeHelper.GetElementType(destExpression.Type); - var ifNullExpr = profileMap.AllowNullCollections - ? (Expression) Constant(null, destExpression.Type) - : NewArrayBounds(destElementType, Constant(0)); - var itemExpr = MapItemExpr(configurationProvider, profileMap, propertyMap, sourceExpression.Type, destExpression.Type, contextExpression, out ParameterExpression itemParam); //var count = source.Count(); @@ -36,7 +32,7 @@ public override Expression MapExpression(IConfigurationProvider configurationPro //return array; var countParam = Parameter(typeof(int), "count"); - var arrayParam = Parameter(ifNullExpr.Type, "destinationArray"); + var arrayParam = Parameter(destExpression.Type, "destinationArray"); var indexParam = Parameter(typeof(int), "destinationArrayIndex"); var actions = new List(); @@ -55,10 +51,7 @@ public override Expression MapExpression(IConfigurationProvider configurationPro )); actions.Add(arrayParam); - var mapExpr = Block(parameters, actions); - - // return (source == null) ? ifNullExpr : Map(source, context); - return Condition(Equal(sourceExpression, Constant(null)), ifNullExpr, mapExpr); + return Block(parameters, actions); } } } diff --git a/src/AutoMapper/Mappers/ConvertMapper.cs b/src/AutoMapper/Mappers/ConvertMapper.cs index 07eb7e92fb..249b42cdc5 100644 --- a/src/AutoMapper/Mappers/ConvertMapper.cs +++ b/src/AutoMapper/Mappers/ConvertMapper.cs @@ -34,26 +34,10 @@ from destinationType in primitiveTypes.Concat(from type in primitiveTypes.Where( static LambdaExpression ConvertExpression(Type sourceType, Type destinationType) { - var underlyingDestinationType = UnderlyingType(destinationType, out bool nullableDestination); + var underlyingDestinationType = Nullable.GetUnderlyingType(destinationType) ?? destinationType; var convertMethod = typeof(Convert).GetDeclaredMethod("To" + underlyingDestinationType.Name, new[] { sourceType }); var sourceParameter = Parameter(sourceType, "source"); - Expression convertCall = Call(convertMethod, sourceParameter); - var lambdaBody = nullableDestination && !sourceType.IsValueType() ? - Condition(Equal(sourceParameter, Constant(null)), Constant(null, destinationType), ToType(convertCall, destinationType)) : - convertCall; - return Lambda(lambdaBody, sourceParameter); - } - - private static Type UnderlyingType(Type type, out bool nullable) - { - var underlyingDestinationType = Nullable.GetUnderlyingType(type); - if(underlyingDestinationType == null) - { - nullable = false; - return type; - } - nullable = true; - return underlyingDestinationType; + return Lambda(Call(convertMethod, sourceParameter), sourceParameter); } public bool IsMatch(TypePair types) => _converters.ContainsKey(types); diff --git a/src/AutoMapper/Mappers/EnumToEnumMapper.cs b/src/AutoMapper/Mappers/EnumToEnumMapper.cs index 96a80e8a9a..f53131f269 100644 --- a/src/AutoMapper/Mappers/EnumToEnumMapper.cs +++ b/src/AutoMapper/Mappers/EnumToEnumMapper.cs @@ -11,9 +11,6 @@ public class EnumToEnumMapper : IObjectMapper { public static TDestination Map(TSource source) { - if (source == null) - return default(TDestination); - var sourceEnumType = ElementTypeHelper.GetEnumerationType(typeof(TSource)); var destEnumType = ElementTypeHelper.GetEnumerationType(typeof(TDestination)); diff --git a/src/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs b/src/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs index 5f6643fdd5..0d97d07370 100644 --- a/src/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs +++ b/src/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs @@ -24,14 +24,11 @@ public bool IsMatch(TypePair context) public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) => - Condition( - Equal(ToObject(sourceExpression), Constant(null)), - Default(destExpression.Type), ToType( Call(ChangeTypeMethod, ToObject(sourceExpression), Constant(Nullable.GetUnderlyingType(destExpression.Type) ?? destExpression.Type)), destExpression.Type - )); + ); } } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/FlagsEnumMapper.cs b/src/AutoMapper/Mappers/FlagsEnumMapper.cs index 9bdeec208b..6bd6ddd59a 100644 --- a/src/AutoMapper/Mappers/FlagsEnumMapper.cs +++ b/src/AutoMapper/Mappers/FlagsEnumMapper.cs @@ -28,9 +28,6 @@ public bool IsMatch(TypePair context) public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) => - Condition( - Equal(ToObject(sourceExpression), Constant(null)), - Default(destExpression.Type), ToType( Call(EnumParseMethod, Constant(Nullable.GetUnderlyingType(destExpression.Type) ?? destExpression.Type), @@ -38,6 +35,6 @@ public Expression MapExpression(IConfigurationProvider configurationProvider, Pr Constant(true) ), destExpression.Type - )); + ); } } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs b/src/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs index 7349ceea41..b3fd903a66 100644 --- a/src/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs +++ b/src/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs @@ -37,7 +37,6 @@ public static Expression MapCollectionExpression(IConfigurationProvider configur var mapExpr = Block(addItems, destination); - var ifNullExpr = profileMap.AllowNullCollections ? Constant(null, passedDestination.Type) : (Expression) newExpression; var clearMethod = destinationCollectionType.GetDeclaredMethod("Clear"); var checkNull = Block(new[] {newExpression, passedDestination}, @@ -45,7 +44,7 @@ public static Expression MapCollectionExpression(IConfigurationProvider configur IfThenElse(condition ?? Constant(false), Block(Assign(newExpression, passedDestination), Call(newExpression, clearMethod)), Assign(newExpression, passedDestination.Type.NewExpr(ifInterfaceType))), - sourceExpression.IfNullElse(ToType(ifNullExpr, passedDestination.Type), ToType(mapExpr, passedDestination.Type)) + ToType(mapExpr, passedDestination.Type) ); if (propertyMap != null) return checkNull; diff --git a/src/AutoMapper/Mappers/MultidimensionalArrayMapper.cs b/src/AutoMapper/Mappers/MultidimensionalArrayMapper.cs index 66b8ceaccb..507f2d3afb 100644 --- a/src/AutoMapper/Mappers/MultidimensionalArrayMapper.cs +++ b/src/AutoMapper/Mappers/MultidimensionalArrayMapper.cs @@ -16,19 +16,16 @@ public class MultidimensionalArrayMapper : IObjectMapper private static Array Map(TSource source, ResolutionContext context, ProfileMap profileMap) where TSource : IEnumerable { - if (source == null && profileMap.AllowNullCollections) - return null; - var destElementType = ElementTypeHelper.GetElementType(typeof(TDestination)); - if (source != null && typeof(TDestination).IsAssignableFrom(typeof(TSource))) + if (typeof(TDestination).IsAssignableFrom(typeof(TSource))) { var elementTypeMap = context.ConfigurationProvider.ResolveTypeMap(typeof(TSourceElement), destElementType); if (elementTypeMap == null) return source as Array; } - var sourceList = (IEnumerable)source ?? new List(); + var sourceList = (IEnumerable)source; var sourceArray = source as Array; var destinationArray = sourceArray == null ? Array.CreateInstance(destElementType, sourceList.Cast().Count()) diff --git a/src/AutoMapper/Mappers/NameValueCollectionMapper.cs b/src/AutoMapper/Mappers/NameValueCollectionMapper.cs index 3073953aa1..48c7da0765 100644 --- a/src/AutoMapper/Mappers/NameValueCollectionMapper.cs +++ b/src/AutoMapper/Mappers/NameValueCollectionMapper.cs @@ -10,9 +10,6 @@ public class NameValueCollectionMapper : IObjectMapper { private static NameValueCollection Map(NameValueCollection source) { - if (source == null) - return null; - var nvc = new NameValueCollection(); foreach (var s in source.AllKeys) nvc.Add(s, source[s]); diff --git a/src/AutoMapper/Mappers/NullableSourceMapper.cs b/src/AutoMapper/Mappers/NullableSourceMapper.cs index d790021604..77f00211ef 100644 --- a/src/AutoMapper/Mappers/NullableSourceMapper.cs +++ b/src/AutoMapper/Mappers/NullableSourceMapper.cs @@ -14,17 +14,13 @@ public class NullableSourceMapper : IObjectMapperInfo public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) => - Condition( - Property(sourceExpression, sourceExpression.Type.GetDeclaredProperty("HasValue")), TypeMapPlanBuilder.MapExpression(configurationProvider, profileMap, new TypePair(Nullable.GetUnderlyingType(sourceExpression.Type), destExpression.Type), Property(sourceExpression, sourceExpression.Type.GetDeclaredProperty("Value")), contextExpression, propertyMap, destExpression - ), - DelegateFactory.GenerateConstructorExpression(destExpression.Type, profileMap) - ); + ); public TypePair GetAssociatedTypes(TypePair initialTypes) { diff --git a/src/AutoMapper/Mappers/StringMapper.cs b/src/AutoMapper/Mappers/StringMapper.cs index 85d7433a9d..fe9377c261 100644 --- a/src/AutoMapper/Mappers/StringMapper.cs +++ b/src/AutoMapper/Mappers/StringMapper.cs @@ -10,10 +10,7 @@ public class StringMapper : IObjectMapper public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) { - var toStringCall = Call(sourceExpression, typeof(object).GetDeclaredMethod("ToString")); - return sourceExpression.Type.IsValueType() - ? (Expression) toStringCall - : Condition(Equal(sourceExpression, Constant(null)), Constant(null, typeof(string)), toStringCall); + return Call(sourceExpression, typeof(object).GetDeclaredMethod("ToString")); } } } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs b/src/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs index 97977a83c9..2187675506 100644 --- a/src/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs +++ b/src/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs @@ -23,13 +23,10 @@ public bool IsMatch(TypePair context) public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) => - Condition( - Equal(ToObject(sourceExpression), Constant(null)), - Default(destExpression.Type), ToType( Call(EnumToObject, Constant(Nullable.GetUnderlyingType(destExpression.Type) ?? destExpression.Type), ToObject(sourceExpression)), destExpression.Type - )); + ); } } \ No newline at end of file diff --git a/src/UnitTests/NullBehavior.cs b/src/UnitTests/NullBehavior.cs index a4508b854c..311c062e53 100644 --- a/src/UnitTests/NullBehavior.cs +++ b/src/UnitTests/NullBehavior.cs @@ -3,9 +3,31 @@ using System.Collections.ObjectModel; using Should; using Xunit; +using System.Collections; namespace AutoMapper.UnitTests.NullBehavior { + public class When_resolving_untyped_null : AutoMapperSpecBase + { + class Source + { + public object Value { get; set; } + } + + class Destination + { + public IEnumerable Value { get; set; } + } + + protected override MapperConfiguration Configuration => new MapperConfiguration(c => c.CreateMap()); + + [Fact] + public void Should_map_ok() + { + Mapper.Map(new Source()).Value.ShouldBeNull(); + } + } + public class When_mapping_from_null_interface_and_AllowNullDestinationValues_is_false : AutoMapperSpecBase { ElementDestination _destination;