diff --git a/src/Mapster.Tool.Tests/.config/dotnet-tools.json b/src/Mapster.Tool.Tests/.config/dotnet-tools.json new file mode 100644 index 00000000..0c61f2d3 --- /dev/null +++ b/src/Mapster.Tool.Tests/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "mapster.tool": { + "version": "8.3.0", + "commands": [ + "dotnet-mapster" + ] + } + } +} \ No newline at end of file diff --git a/src/Mapster.Tool.Tests/Mappers/IUserMapper.cs b/src/Mapster.Tool.Tests/Mappers/IUserMapper.cs new file mode 100644 index 00000000..5a62a4d4 --- /dev/null +++ b/src/Mapster.Tool.Tests/Mappers/IUserMapper.cs @@ -0,0 +1,11 @@ +using System.Linq.Expressions; + +namespace Mapster.Tool.Tests.Mappers; + +[Mapper] +public interface IUserMapper +{ + Expression> UserProjection { get; } + _UserDto MapTo(_User user); + _UserDto MapTo(_User user, _UserDto userDto); +} \ No newline at end of file diff --git a/src/Mapster.Tool.Tests/Mappers/UserMapper.cs b/src/Mapster.Tool.Tests/Mappers/UserMapper.cs new file mode 100644 index 00000000..a6b18409 --- /dev/null +++ b/src/Mapster.Tool.Tests/Mappers/UserMapper.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq.Expressions; +using Mapster.Tool.Tests; +using Mapster.Tool.Tests.Mappers; + +namespace Mapster.Tool.Tests.Mappers +{ + public partial class UserMapper : IUserMapper + { + public Expression> UserProjection => p1 => new _UserDto() + { + Id = p1.Id, + Name = p1.Name + }; + public _UserDto MapTo(_User p2) + { + return p2 == null ? null : new _UserDto() + { + Id = p2.Id, + Name = p2.Name + }; + } + public _UserDto MapTo(_User p3, _UserDto p4) + { + if (p3 == null) + { + return null; + } + _UserDto result = p4 ?? new _UserDto(); + + typeof(_UserDto).GetProperty("Id").SetValue(result, (object)p3.Id); + typeof(_UserDto).GetProperty("Name").SetValue(result, p3.Name); + return result; + + } + } +} \ No newline at end of file diff --git a/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj new file mode 100644 index 00000000..9ddf4c8d --- /dev/null +++ b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj @@ -0,0 +1,52 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mapster.Tool.Tests/TestBase.cs b/src/Mapster.Tool.Tests/TestBase.cs new file mode 100644 index 00000000..4bc6c6b8 --- /dev/null +++ b/src/Mapster.Tool.Tests/TestBase.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mapster.Tool.Tests; + +public class TestBase +{ + private readonly IServiceScopeFactory _scopeFactory; + + public TestBase() + { + var services = ConfigureServiceCollection(); + using var scope = services.BuildServiceProvider().CreateScope(); + _scopeFactory = scope.ServiceProvider.GetRequiredService(); + } + + private ServiceCollection ConfigureServiceCollection() + { + ServiceCollection services = new(); + services.Scan(selector => selector + .FromCallingAssembly() + .AddClasses() + .AsMatchingInterface() + .WithSingletonLifetime()); + return services; + } + + protected TInterface GetMappingInterface() + { + using var scope = _scopeFactory.CreateScope(); + var service = scope.ServiceProvider.GetService(typeof(TInterface)); + if (service == null) + { + throw new Exception($"Service of type {typeof(TInterface).Name} not found!"); + } + + return (TInterface)service; + } +} \ No newline at end of file diff --git a/src/Mapster.Tool.Tests/Usings.cs b/src/Mapster.Tool.Tests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/src/Mapster.Tool.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Mapster.Tool.Tests/WhenMappingWithExistingObjectAndInitProperties.cs b/src/Mapster.Tool.Tests/WhenMappingWithExistingObjectAndInitProperties.cs new file mode 100644 index 00000000..7908b453 --- /dev/null +++ b/src/Mapster.Tool.Tests/WhenMappingWithExistingObjectAndInitProperties.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using FluentAssertions; +using Mapster.Tool.Tests.Mappers; + +namespace Mapster.Tool.Tests; + +/// +/// Tests for https://github.com/MapsterMapper/Mapster/issues/536 +/// +public class WhenMappingWithExistingObjectAndInitProperties : TestBase +{ + [Fact] + public void MapWithReflection() + { + TypeAdapterConfig.GlobalSettings + .Scan(Assembly.GetExecutingAssembly()); + + var userMapper = GetMappingInterface(); + var expected = "Aref"; + var user = new _User { Name = expected, Id = 1 }; + var dto = new _UserDto(); + userMapper.MapTo(user, dto); + dto.Name.Should().Be(expected); + } +} + +public class UserMappingRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig<_User, _UserDto>() + .MapToConstructor(true) + .ConstructUsing(s => new _UserDto()); + } +} + +public class _User +{ + public int Id { get; init; } + public string Name { get; init; } +} + +public class _UserDto +{ + public int Id { get; init; } + public string Name { get; init; } +} \ No newline at end of file diff --git a/src/Mapster.sln b/src/Mapster.sln index f5df1d5e..befd616f 100644 --- a/src/Mapster.sln +++ b/src/Mapster.sln @@ -61,6 +61,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExpressionTranslator", "Exp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemplateTest", "TemplateTest\TemplateTest.csproj", "{ED390145-FA22-46BA-86A6-9FA6AC869BA4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mapster.Tool.Tests", "Mapster.Tool.Tests\Mapster.Tool.Tests.csproj", "{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -155,6 +157,10 @@ Global {ED390145-FA22-46BA-86A6-9FA6AC869BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU {ED390145-FA22-46BA-86A6-9FA6AC869BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED390145-FA22-46BA-86A6-9FA6AC869BA4}.Release|Any CPU.Build.0 = Release|Any CPU + {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -175,6 +181,7 @@ Global {D5DF0FB7-44A5-4326-9EC4-5B8F7FCCE00F} = {D33E5A90-ABCA-4B2D-8758-2873CC5AB847} {3CB56440-5449-4DE5-A8D3-549C87C1B36A} = {EF7E343F-592E-4EAC-A0A4-92EB4B95CB89} {ED390145-FA22-46BA-86A6-9FA6AC869BA4} = {D33E5A90-ABCA-4B2D-8758-2873CC5AB847} + {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1} = {D33E5A90-ABCA-4B2D-8758-2873CC5AB847} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83B87DBA-277C-49F1-B597-E3B78C2C8275} diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index d1c1febe..cc803d8d 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -129,7 +129,18 @@ protected override Expression CreateBlockExpression(Expression source, Expressio } else { - adapt = member.DestinationMember.SetExpression(destination, adapt); + //Todo Try catch block should be removed after pull request approved + try + { + var destinationPropertyInfo = (PropertyInfo)member.DestinationMember.Info!; + adapt = destinationPropertyInfo.IsInitOnly() + ? SetValueByReflection(destination, (MemberExpression)adapt, arg.DestinationType) + : member.DestinationMember.SetExpression(destination, adapt); + } + catch (Exception e) + { + adapt = member.DestinationMember.SetExpression(destination, adapt); + } } } else if (!adapt.IsComplex()) @@ -146,6 +157,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio tuple = Tuple.Create(new List(), body); conditions[member.Ignore.Condition] = tuple; } + tuple.Item1.Add(adapt); } else @@ -166,6 +178,21 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return lines.Count > 0 ? (Expression)Expression.Block(lines) : Expression.Empty(); } + private static Expression SetValueByReflection(Expression destination, MemberExpression adapt, + Type destinationType) + { + var memberName = adapt.Member.Name; + var typeofExpression = Expression.Constant(destinationType); + var getPropertyMethod = typeof(Type).GetMethod("GetProperty", new[] { typeof(string) })!; + var getPropertyExpression = Expression.Call(typeofExpression, getPropertyMethod, + Expression.Constant(memberName)); + var setValueMethod = + typeof(PropertyInfo).GetMethod("SetValue", new[] { typeof(object), typeof(object) })!; + var memberAsObject = adapt.To(typeof(object)); + return Expression.Call(getPropertyExpression, setValueMethod, + new[] { destination, memberAsObject }); + } + protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg) { //new TDestination { diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index dea925c4..5959daa5 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -374,5 +374,15 @@ public static IEnumerable GetAllTypes(this Type type) type = type.GetTypeInfo().BaseType; } while (type != null && type != typeof(object)); } + + public static bool IsInitOnly(this PropertyInfo propertyInfo) + { + var setMethod = propertyInfo.SetMethod; + if (setMethod == null) + return false; + + var isExternalInitType = typeof(System.Runtime.CompilerServices.IsExternalInit); + return setMethod.ReturnParameter.GetRequiredCustomModifiers().Contains(isExternalInitType); + } } } \ No newline at end of file