Skip to content
Draft
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
188 changes: 188 additions & 0 deletions src/Mapster.Tests/WhenImplicitInheritanceMapWithDerivedDestination.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// https://github.com/MapsterMapper/Mapster/issues/947
/// </summary>
[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<AnimalDto947, Animal947>()
.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<Dog947>(config);
var dogInsaider = sourceInsaider.Adapt<DogInsaider947>(config);

dog.ShouldBeOfType<Dog947>();
dog.AnimalValue.ShouldBe("Hello");

dogInsaider.Animal.ShouldBeOfType<Dog947>();
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<AnimalDto947, Animal947>()
.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<AnimalDto947, Dog947>(config);

dog.ShouldBeOfType<Dog947>();
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<AnimalDto947, Animal947>()
.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<Dog947>(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<AnimalDto947, IAnimal>()
.MapWith(src => src.Type == "Bird"
? new ValueTypeBird947 { AnimalValue = src.AnimalValueDto }
: new ValueTypeDog947 { AnimalValue = src.AnimalValueDto });

var dog = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Dog" }.Adapt<ValueTypeDog947>(config);
var defaultdata = new AnimalDto947 { AnimalValueDto = "Hello", Type = "Bird" }.Adapt<ValueTypeDog947>(config);

dog.ShouldBeOfType<ValueTypeDog947>();
dog.AnimalValue.ShouldBe("Hello");

defaultdata.ShouldBeOfType<ValueTypeDog947>();
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<AnimalDto947, IAnimal>()
.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<ValueTypeDog947?>(config);
var Nulldata = invalidSrc.Adapt<ValueTypeDog947?>(config);
var InsaiderNullableDog = new AnimalDtoInsaider947() { Animal = validSrc}.Adapt<DogValueTypeNullableInsaider947>(config);
var NullInsaiderNullableDog = new AnimalDtoInsaider947() { Animal = invalidSrc }.Adapt<DogValueTypeNullableInsaider947>(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
}
}
23 changes: 6 additions & 17 deletions src/Mapster/Adapters/BaseAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
{
Expand Down
36 changes: 36 additions & 0 deletions src/Mapster/Adapters/NullableAdapter.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
76 changes: 71 additions & 5 deletions src/Mapster/TypeAdapterConfig.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -32,6 +33,7 @@ private static List<TypeAdapterRule> CreateRuleTemplate()
new ObjectAdapter().CreateRule(), //-111
new StringAdapter().CreateRule(), //-110
new EnumAdapter().CreateRule(), //-109
new NullableAdapter().CreateRule(), // 0

//fallback rules
new TypeAdapterRule
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -431,14 +437,74 @@ 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)
{
throw new CompileException(arg, ex);
}
}

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>() { 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);
Expand Down
Loading
Loading