diff --git a/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs b/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs new file mode 100644 index 00000000..d459e1eb --- /dev/null +++ b/src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs @@ -0,0 +1,188 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; +using static Mapster.Tests.DynamicTypeGeneratorTests; + +namespace Mapster.Tests +{ + /// + /// https://github.com/MapsterMapper/Mapster/issues/947 + /// + [TestClass] + public class WhenImplicitInheritanceMapWithDerivedDestination + { + [TestCleanup] + public void Cleanup() + { + TypeAdapterConfig.GlobalSettings.Clear(); + TypeAdapterConfig.GlobalSettings.AllowImplicitDestinationInheritance = false; + } + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_Casts_To_Derived_Destination() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? (Animal947)new Bird947 { AnimalValue = src.AnimalValueDto } + : new Dog947 { AnimalValue = src.AnimalValueDto }); + + var source = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }; + var sourceInsaider = new AnimalDtoInsaider947() { Animal = source }; + + var dog = source.Adapt(config); + var dogInsaider = sourceInsaider.Adapt(config); + + dog.ShouldBeOfType(); + dog.AnimalValue.ShouldBe("Hello"); + + dogInsaider.Animal.ShouldBeOfType(); + dogInsaider.Animal.AnimalValue.ShouldBe("Hello"); + } + + [TestMethod] + public void Inherited_MapWith_Works_For_Explicit_Source_Destination_Pair() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? (Animal947)new Bird947 { AnimalValue = src.AnimalValueDto } + : new Dog947 { AnimalValue = src.AnimalValueDto }); + + var source = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }; + + var dog = source.Adapt(config); + + dog.ShouldBeOfType(); + dog.AnimalValue.ShouldBe("Hello"); + } + + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_ReturnDefault_When_In_Runtime_ResultType_IsNot_Achievable() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? (Animal947)new Bird947 { AnimalValue = src.AnimalValueDto } + : new Dog947 { AnimalValue = src.AnimalValueDto }); + + var source = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Bird" }; + + var dog = source.Adapt(config); + + dog.ShouldBeNull(); + dog.ShouldBe(default); + } + + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_Casts_To_Derived_Destination_UsingInterface() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? new ValueTypeBird947 { AnimalValue = src.AnimalValueDto } + : new ValueTypeDog947 { AnimalValue = src.AnimalValueDto }); + + var dog = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }.Adapt(config); + var defaultdata = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Bird" }.Adapt(config); + + dog.ShouldBeOfType(); + dog.AnimalValue.ShouldBe("Hello"); + + defaultdata.ShouldBeOfType(); + defaultdata.AnimalValue.ShouldBe(default); + } + + [TestMethod] + public void Inherited_MapWith_On_Base_Destination_Casts_To_Derived_Destination_NullableValueType() + { + var config = new TypeAdapterConfig(); + config.AllowImplicitDestinationInheritance = true; + config.NewConfig() + .MapWith(src => src.Type == "Bird" + ? new ValueTypeBird947 { AnimalValue = src.AnimalValueDto } + : new ValueTypeDog947 { AnimalValue = src.AnimalValueDto }); + + var validSrc = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }; + var invalidSrc = new AnimalDto947 { AnimalValueDto = "Tweet", Type = "Bird" }; ; + + var dog = validSrc.Adapt(config); + var Nulldata = invalidSrc.Adapt(config); + var InsaiderNullableDog = new AnimalDtoInsaider947() { Animal = validSrc}.Adapt(config); + var NullInsaiderNullableDog = new AnimalDtoInsaider947() { Animal = invalidSrc }.Adapt(config); + + dog.ShouldNotBeNull(); + dog?.AnimalValue.ShouldBe("Hello"); + InsaiderNullableDog.Animal.ShouldNotBeNull(); + InsaiderNullableDog.Animal?.AnimalValue.ShouldBe("Hello"); + + + Nulldata.ShouldBeNull(); + NullInsaiderNullableDog.Animal.ShouldBeNull(); + } + + + #region TestClases + + public abstract class Animal947 + { + public string AnimalValue { get; set; } = null!; + } + + public class Dog947 : Animal947 + { + } + + public class Bird947 : Animal947 + { + } + + + public class AnimalDto947 + { + public string AnimalValueDto { get; set; } = null!; + + public string Type { get; set; } = null!; + } + + public class AnimalDtoInsaider947 + { + public AnimalDto947 Animal { get; set; } + } + + public class DogInsaider947 + { + public Dog947 Animal { get; set; } + } + + public interface IAnimal + { + public string AnimalValue { get; set; } + } + + public struct ValueTypeBird947 : IAnimal + { + public string AnimalValue { get; set; } + } + + public struct ValueTypeDog947 : IAnimal + { + public string AnimalValue { get; set; } + + } + + public class DogValueTypeNullableInsaider947 + { + public ValueTypeDog947? Animal { get; set; } + } + + #endregion TestClases + } +} diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b31a0dbe..0e9c7923 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -485,12 +485,7 @@ protected Expression CreateAdaptExpression(Expression source, Type destinationTy } internal Expression CreateAdaptExpression(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping, Expression? destination = null) { - Expression _source; - - if (arg.MapType != MapType.Projection) - _source = source.NullableEnumExtractor(); // Extraction Nullable Enum - else - _source = source; + Expression _source = source; if (_source.Type == destinationType && arg.MapType == MapType.Projection) return _source; @@ -506,20 +501,14 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp if (_source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue && rule == null) exp = _source; - else if (source is ConditionalExpression cond && mapping != null) - { - // convert ApplyNullable Propagation for NotPrimitive Nullable types - if (mapping.Getter.Type.IsNotPrimitiveNullableType() && !mapping.DestinationMember.Type.IsNullable()) - { - var adapt = CreateAdaptExpressionCore(cond.IfTrue.GetNotPrimitiveNullableValue(), mapping.DestinationMember.Type, arg, mapping); - exp = Expression.Condition(cond.Test, adapt, mapping.DestinationMember.Type.CreateDefault()); - } - else - exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); - } else exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); + // NullablePropagation when for member using Custom converter MapWith + if (notUsingDestinationValue && arg.MapType != MapType.Projection + && mapping != null && mapping.Getter.CanBeNull()) + exp = mapping.Getter.NotNullReturn(exp); + //transform(adapt(_source)); if (notUsingDestinationValue) { diff --git a/src/Mapster/Adapters/NullableAdapter.cs b/src/Mapster/Adapters/NullableAdapter.cs new file mode 100644 index 00000000..bfc7d836 --- /dev/null +++ b/src/Mapster/Adapters/NullableAdapter.cs @@ -0,0 +1,36 @@ +using Mapster.Utils; +using System.Linq.Expressions; + +namespace Mapster.Adapters +{ + internal class NullableAdapter : BaseAdapter + { + + protected override int Score => 0; //must do first + + protected override bool CanMap(PreCompileArgument arg) + { + return arg.SourceType.IsNullable() || arg.DestinationType.IsNullable(); + } + protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) + { + return true; + } + + protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) + { + var _source = source.Type.IsNullable() + ? Expression.Convert(source, source.Type.GetGenericArguments()[0]) + : source; + + Expression adapt = CreateAdaptExpression(_source, arg.DestinationType.GetNotNullableTypeDefenition(),arg); + + return adapt.ToNullableExp(arg); + } + + protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) + { + return Expression.Empty(); + } + } +} diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 1dce2053..9463072b 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -1,13 +1,14 @@ -using System; +using Mapster.Adapters; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Data.SqlTypes; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; -using Mapster.Adapters; -using Mapster.Models; -using Mapster.Utils; namespace Mapster { @@ -32,6 +33,7 @@ private static List CreateRuleTemplate() new ObjectAdapter().CreateRule(), //-111 new StringAdapter().CreateRule(), //-110 new EnumAdapter().CreateRule(), //-109 + new NullableAdapter().CreateRule(), // 0 //fallback rules new TypeAdapterRule @@ -266,6 +268,10 @@ private static TypeAdapterRule CreateDestinationTypeRule(TypeTuple key) private static int? GetSubclassDistance(Type type1, Type type2, bool allowInheritance) { + //Support for using ValueType mapping configurations of types, for mapping cases on Nulllable ValueType values + if (type1.IsNullable() && !type1.ContainsGenericParameters) + type1 = type1.GetGenericArguments().FirstOrDefault(); + if (type1 == type2) return 50; @@ -431,7 +437,7 @@ private static LambdaExpression CreateMapExpression(CompileArgument arg) throw new CompileException(arg, new InvalidOperationException("ConverterFactory is not found")); try { - return fn(arg); + return AdjustInheritedConverterReturnType(fn(arg), arg); } catch (Exception ex) { @@ -439,6 +445,66 @@ private static LambdaExpression CreateMapExpression(CompileArgument arg) } } + private static LambdaExpression AdjustInheritedConverterReturnType(LambdaExpression lambda, CompileArgument arg) + { + var destinationType = arg.DestinationType; + var returnType = lambda.ReturnType; + var lamdaBody = lambda.Body; + + // Support for using ValueType mapping configurations of types, for mapping cases on Nulllable ValueType values + if (arg.DestinationType.IsNullable() && !returnType.IsNullable() && returnType.IsValueType) + { + lamdaBody = Expression.Convert(lambda.Body, typeof(Nullable<>).MakeGenericType(lambda.ReturnType)); + lambda = Expression.Lambda(lamdaBody, lambda.Parameters); + + returnType = lambda.ReturnType; + } + + if (returnType == destinationType) + return lambda; + + //Support for using ValueType mapping configurations of types, for mapping cases on Nulllable ValueType values + if (destinationType.IsNullable() && lambda.ReturnType.IsInterface) + { + var realDestType = destinationType.GetGenericArguments().FirstOrDefault(); + + if (realDestType is null || !returnType.IsAssignableFrom(realDestType)) + return lambda; + } + // MapWith configured on a base destination type returns the base type, but implicit + // destination inheritance can compile the converter for a derived destination. + else if (!returnType.IsAssignableFrom(destinationType)) + return lambda; + + Expression body; + + if(destinationType.CanBeNull()) + body = Expression.TypeAs(lamdaBody, destinationType); + else + { + var tempDest = Expression.Variable(returnType, "tempDest"); + + var variables = new[] {tempDest}; + + var blockbody = new List() { Expression.Assign(tempDest, lamdaBody) }; + + var condition = Expression.TypeIs(tempDest, destinationType); + UnaryExpression ifTrue = Expression.Convert(tempDest, destinationType); + DefaultExpression ifFalse = Expression.Default(destinationType); + + ConditionalExpression conditionalExpr = Expression.Condition(condition, ifTrue, ifFalse); + blockbody.Add(conditionalExpr); + + BlockExpression body2 = Expression.Block(variables, blockbody); + + return Expression.Lambda(body2,lambda.Parameters); + + } + + + return Expression.Lambda(body, lambda.Parameters); + } + private LambdaExpression CreateDynamicMapExpression(TypeTuple tuple) { var lambda = CreateMapExpression(tuple, MapType.Map); diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index b7ffc365..df0b3058 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -537,22 +537,11 @@ internal static Expression GetNameConverterExpression(Func conve return Expression.Constant(converter); } - public static bool IsNotPrimitiveNullableType(this Type type) + public static Expression ToNullableExp(this Expression adapt, CompileArgument arg) { - return Nullable.GetUnderlyingType(type) != null && !type.IsMapsterPrimitive(); - } - - public static Expression GetNotPrimitiveNullableValue(this Expression exp) - { - if (exp.Type.IsNotPrimitiveNullableType()) - { - var getValueOrDefaultMethod = exp.Type.GetMethod("GetValueOrDefault", Type.EmptyTypes); - var getValue = Expression.Call(exp, getValueOrDefaultMethod); - - return getValue; - } - - return exp; + if (arg.DestinationType.IsNullable()) + return Expression.Convert(adapt, arg.DestinationType); + return adapt; } } diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index 81c3e21e..36a792fc 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -479,5 +479,13 @@ public static bool IsNotCustomConverterFactory(this CompileArgument arg, TypeAda return true; } + + public static Type GetNotNullableTypeDefenition(this Type inputType) + { + if (inputType.IsNullable()) + return inputType.GetGenericArguments()[0]; + + return inputType; + } } }