Skip to content

Fix Issues #536 -> Set properties with reflection if destination has init only properties. #545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 21, 2023
Merged
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
12 changes: 12 additions & 0 deletions src/Mapster.Tool.Tests/.config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"mapster.tool": {
"version": "8.3.0",
"commands": [
"dotnet-mapster"
]
}
}
}
11 changes: 11 additions & 0 deletions src/Mapster.Tool.Tests/Mappers/IUserMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Linq.Expressions;

namespace Mapster.Tool.Tests.Mappers;

[Mapper]
public interface IUserMapper
{
Expression<Func<_User, _UserDto>> UserProjection { get; }
_UserDto MapTo(_User user);
_UserDto MapTo(_User user, _UserDto userDto);
}
37 changes: 37 additions & 0 deletions src/Mapster.Tool.Tests/Mappers/UserMapper.cs
Original file line number Diff line number Diff line change
@@ -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<Func<_User, _UserDto>> 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;

}
}
}
52 changes: 52 additions & 0 deletions src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Scrutor" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Generated Include="**\*.g.cs" />
<Generated Remove="obj\**" />
</ItemGroup>
<Target Name="CleanGenerated">
<Delete Files="@(Generated)" />
</Target>
<Target Name="Mapster">
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet build" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster model -a $(TargetDir)$(ProjectName).dll -n Sample.CodeGen.Models -o Models -r" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a $(TargetDir)$(ProjectName).dll -n Sample.CodeGen.Models -o Models" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a $(TargetDir)$(ProjectName).dll -n Sample.CodeGen.Mappers -o Mappers" />
</Target>
<ItemGroup>
<Compile Remove="obj\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="obj\**" />
</ItemGroup>
<ItemGroup>
<None Remove="obj\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Mapster.Tool\Mapster.Tool.csproj" />
</ItemGroup>

</Project>
38 changes: 38 additions & 0 deletions src/Mapster.Tool.Tests/TestBase.cs
Original file line number Diff line number Diff line change
@@ -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<IServiceScopeFactory>();
}

private ServiceCollection ConfigureServiceCollection()
{
ServiceCollection services = new();
services.Scan(selector => selector
.FromCallingAssembly()
.AddClasses()
.AsMatchingInterface()
.WithSingletonLifetime());
return services;
}

protected TInterface GetMappingInterface<TInterface>()
{
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;
}
}
1 change: 1 addition & 0 deletions src/Mapster.Tool.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Reflection;
using FluentAssertions;
using Mapster.Tool.Tests.Mappers;

namespace Mapster.Tool.Tests;

/// <summary>
/// Tests for https://github.com/MapsterMapper/Mapster/issues/536
/// </summary>
public class WhenMappingWithExistingObjectAndInitProperties : TestBase
{
[Fact]
public void MapWithReflection()
{
TypeAdapterConfig.GlobalSettings
.Scan(Assembly.GetExecutingAssembly());

var userMapper = GetMappingInterface<IUserMapper>();
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; }
}
7 changes: 7 additions & 0 deletions src/Mapster.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
29 changes: 28 additions & 1 deletion src/Mapster/Adapters/ClassAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -146,6 +157,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio
tuple = Tuple.Create(new List<Expression>(), body);
conditions[member.Ignore.Condition] = tuple;
}

tuple.Item1.Add(adapt);
}
else
Expand All @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions src/Mapster/Utils/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,5 +374,15 @@ public static IEnumerable<Type> 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);
}
}
}