Skip to content

Commit 0e31a0b

Browse files
authored
Merge pull request #545 from stormaref/master
Fix Issues #536 -> Set properties with reflection if destination has init only properties.
2 parents 65b6466 + 845d112 commit 0e31a0b

File tree

10 files changed

+243
-1
lines changed

10 files changed

+243
-1
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"mapster.tool": {
6+
"version": "8.3.0",
7+
"commands": [
8+
"dotnet-mapster"
9+
]
10+
}
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Linq.Expressions;
2+
3+
namespace Mapster.Tool.Tests.Mappers;
4+
5+
[Mapper]
6+
public interface IUserMapper
7+
{
8+
Expression<Func<_User, _UserDto>> UserProjection { get; }
9+
_UserDto MapTo(_User user);
10+
_UserDto MapTo(_User user, _UserDto userDto);
11+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
using Mapster.Tool.Tests;
4+
using Mapster.Tool.Tests.Mappers;
5+
6+
namespace Mapster.Tool.Tests.Mappers
7+
{
8+
public partial class UserMapper : IUserMapper
9+
{
10+
public Expression<Func<_User, _UserDto>> UserProjection => p1 => new _UserDto()
11+
{
12+
Id = p1.Id,
13+
Name = p1.Name
14+
};
15+
public _UserDto MapTo(_User p2)
16+
{
17+
return p2 == null ? null : new _UserDto()
18+
{
19+
Id = p2.Id,
20+
Name = p2.Name
21+
};
22+
}
23+
public _UserDto MapTo(_User p3, _UserDto p4)
24+
{
25+
if (p3 == null)
26+
{
27+
return null;
28+
}
29+
_UserDto result = p4 ?? new _UserDto();
30+
31+
typeof(_UserDto).GetProperty("Id").SetValue(result, (object)p3.Id);
32+
typeof(_UserDto).GetProperty("Name").SetValue(result, p3.Name);
33+
return result;
34+
35+
}
36+
}
37+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="FluentAssertions" Version="6.9.0" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
14+
<PackageReference Include="Scrutor" Version="4.2.1" />
15+
<PackageReference Include="xunit" Version="2.4.2" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
<PackageReference Include="coverlet.collector" Version="3.1.2">
21+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
22+
<PrivateAssets>all</PrivateAssets>
23+
</PackageReference>
24+
</ItemGroup>
25+
<ItemGroup>
26+
<Generated Include="**\*.g.cs" />
27+
<Generated Remove="obj\**" />
28+
</ItemGroup>
29+
<Target Name="CleanGenerated">
30+
<Delete Files="@(Generated)" />
31+
</Target>
32+
<Target Name="Mapster">
33+
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet build" />
34+
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
35+
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster model -a $(TargetDir)$(ProjectName).dll -n Sample.CodeGen.Models -o Models -r" />
36+
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a $(TargetDir)$(ProjectName).dll -n Sample.CodeGen.Models -o Models" />
37+
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a $(TargetDir)$(ProjectName).dll -n Sample.CodeGen.Mappers -o Mappers" />
38+
</Target>
39+
<ItemGroup>
40+
<Compile Remove="obj\**" />
41+
</ItemGroup>
42+
<ItemGroup>
43+
<EmbeddedResource Remove="obj\**" />
44+
</ItemGroup>
45+
<ItemGroup>
46+
<None Remove="obj\**" />
47+
</ItemGroup>
48+
<ItemGroup>
49+
<ProjectReference Include="..\Mapster.Tool\Mapster.Tool.csproj" />
50+
</ItemGroup>
51+
52+
</Project>

src/Mapster.Tool.Tests/TestBase.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
namespace Mapster.Tool.Tests;
4+
5+
public class TestBase
6+
{
7+
private readonly IServiceScopeFactory _scopeFactory;
8+
9+
public TestBase()
10+
{
11+
var services = ConfigureServiceCollection();
12+
using var scope = services.BuildServiceProvider().CreateScope();
13+
_scopeFactory = scope.ServiceProvider.GetRequiredService<IServiceScopeFactory>();
14+
}
15+
16+
private ServiceCollection ConfigureServiceCollection()
17+
{
18+
ServiceCollection services = new();
19+
services.Scan(selector => selector
20+
.FromCallingAssembly()
21+
.AddClasses()
22+
.AsMatchingInterface()
23+
.WithSingletonLifetime());
24+
return services;
25+
}
26+
27+
protected TInterface GetMappingInterface<TInterface>()
28+
{
29+
using var scope = _scopeFactory.CreateScope();
30+
var service = scope.ServiceProvider.GetService(typeof(TInterface));
31+
if (service == null)
32+
{
33+
throw new Exception($"Service of type {typeof(TInterface).Name} not found!");
34+
}
35+
36+
return (TInterface)service;
37+
}
38+
}

src/Mapster.Tool.Tests/Usings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using Xunit;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Reflection;
2+
using FluentAssertions;
3+
using Mapster.Tool.Tests.Mappers;
4+
5+
namespace Mapster.Tool.Tests;
6+
7+
/// <summary>
8+
/// Tests for https://github.com/MapsterMapper/Mapster/issues/536
9+
/// </summary>
10+
public class WhenMappingWithExistingObjectAndInitProperties : TestBase
11+
{
12+
[Fact]
13+
public void MapWithReflection()
14+
{
15+
TypeAdapterConfig.GlobalSettings
16+
.Scan(Assembly.GetExecutingAssembly());
17+
18+
var userMapper = GetMappingInterface<IUserMapper>();
19+
var expected = "Aref";
20+
var user = new _User { Name = expected, Id = 1 };
21+
var dto = new _UserDto();
22+
userMapper.MapTo(user, dto);
23+
dto.Name.Should().Be(expected);
24+
}
25+
}
26+
27+
public class UserMappingRegister : IRegister
28+
{
29+
public void Register(TypeAdapterConfig config)
30+
{
31+
config.NewConfig<_User, _UserDto>()
32+
.MapToConstructor(true)
33+
.ConstructUsing(s => new _UserDto());
34+
}
35+
}
36+
37+
public class _User
38+
{
39+
public int Id { get; init; }
40+
public string Name { get; init; }
41+
}
42+
43+
public class _UserDto
44+
{
45+
public int Id { get; init; }
46+
public string Name { get; init; }
47+
}

src/Mapster.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExpressionTranslator", "Exp
6161
EndProject
6262
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemplateTest", "TemplateTest\TemplateTest.csproj", "{ED390145-FA22-46BA-86A6-9FA6AC869BA4}"
6363
EndProject
64+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mapster.Tool.Tests", "Mapster.Tool.Tests\Mapster.Tool.Tests.csproj", "{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}"
65+
EndProject
6466
Global
6567
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6668
Debug|Any CPU = Debug|Any CPU
@@ -155,6 +157,10 @@ Global
155157
{ED390145-FA22-46BA-86A6-9FA6AC869BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
156158
{ED390145-FA22-46BA-86A6-9FA6AC869BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
157159
{ED390145-FA22-46BA-86A6-9FA6AC869BA4}.Release|Any CPU.Build.0 = Release|Any CPU
160+
{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
161+
{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
162+
{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
163+
{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Release|Any CPU.Build.0 = Release|Any CPU
158164
EndGlobalSection
159165
GlobalSection(SolutionProperties) = preSolution
160166
HideSolutionNode = FALSE
@@ -175,6 +181,7 @@ Global
175181
{D5DF0FB7-44A5-4326-9EC4-5B8F7FCCE00F} = {D33E5A90-ABCA-4B2D-8758-2873CC5AB847}
176182
{3CB56440-5449-4DE5-A8D3-549C87C1B36A} = {EF7E343F-592E-4EAC-A0A4-92EB4B95CB89}
177183
{ED390145-FA22-46BA-86A6-9FA6AC869BA4} = {D33E5A90-ABCA-4B2D-8758-2873CC5AB847}
184+
{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1} = {D33E5A90-ABCA-4B2D-8758-2873CC5AB847}
178185
EndGlobalSection
179186
GlobalSection(ExtensibilityGlobals) = postSolution
180187
SolutionGuid = {83B87DBA-277C-49F1-B597-E3B78C2C8275}

src/Mapster/Adapters/ClassAdapter.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,18 @@ protected override Expression CreateBlockExpression(Expression source, Expressio
129129
}
130130
else
131131
{
132-
adapt = member.DestinationMember.SetExpression(destination, adapt);
132+
//Todo Try catch block should be removed after pull request approved
133+
try
134+
{
135+
var destinationPropertyInfo = (PropertyInfo)member.DestinationMember.Info!;
136+
adapt = destinationPropertyInfo.IsInitOnly()
137+
? SetValueByReflection(destination, (MemberExpression)adapt, arg.DestinationType)
138+
: member.DestinationMember.SetExpression(destination, adapt);
139+
}
140+
catch (Exception e)
141+
{
142+
adapt = member.DestinationMember.SetExpression(destination, adapt);
143+
}
133144
}
134145
}
135146
else if (!adapt.IsComplex())
@@ -146,6 +157,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio
146157
tuple = Tuple.Create(new List<Expression>(), body);
147158
conditions[member.Ignore.Condition] = tuple;
148159
}
160+
149161
tuple.Item1.Add(adapt);
150162
}
151163
else
@@ -166,6 +178,21 @@ protected override Expression CreateBlockExpression(Expression source, Expressio
166178
return lines.Count > 0 ? (Expression)Expression.Block(lines) : Expression.Empty();
167179
}
168180

181+
private static Expression SetValueByReflection(Expression destination, MemberExpression adapt,
182+
Type destinationType)
183+
{
184+
var memberName = adapt.Member.Name;
185+
var typeofExpression = Expression.Constant(destinationType);
186+
var getPropertyMethod = typeof(Type).GetMethod("GetProperty", new[] { typeof(string) })!;
187+
var getPropertyExpression = Expression.Call(typeofExpression, getPropertyMethod,
188+
Expression.Constant(memberName));
189+
var setValueMethod =
190+
typeof(PropertyInfo).GetMethod("SetValue", new[] { typeof(object), typeof(object) })!;
191+
var memberAsObject = adapt.To(typeof(object));
192+
return Expression.Call(getPropertyExpression, setValueMethod,
193+
new[] { destination, memberAsObject });
194+
}
195+
169196
protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg)
170197
{
171198
//new TDestination {

src/Mapster/Utils/ReflectionUtils.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,5 +374,15 @@ public static IEnumerable<Type> GetAllTypes(this Type type)
374374
type = type.GetTypeInfo().BaseType;
375375
} while (type != null && type != typeof(object));
376376
}
377+
378+
public static bool IsInitOnly(this PropertyInfo propertyInfo)
379+
{
380+
var setMethod = propertyInfo.SetMethod;
381+
if (setMethod == null)
382+
return false;
383+
384+
var isExternalInitType = typeof(System.Runtime.CompilerServices.IsExternalInit);
385+
return setMethod.ReturnParameter.GetRequiredCustomModifiers().Contains(isExternalInitType);
386+
}
377387
}
378388
}

0 commit comments

Comments
 (0)