Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,11 @@ public class MapperAttribute : Attribute
/// partial methods are discovered.
/// </summary>
public bool AutoUserMappings { get; set; } = true;

/// <summary>
/// When set to <c>true</c>, only properties with explicit configurations (via attributes like <see cref="MapPropertyAttribute"/>)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapperly also maps fields, use members instead of properties.

/// will be mapped. All other properties will be ignored by default.
/// This is useful when you want to map only a few specific properties from a class with many properties.
/// </summary>
public bool OnlyExplicitMappedMembers { get; set; }
}
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,10 @@ public record MapperConfiguration
/// Can be overwritten on specific enums via mapping method configurations.
/// </summary>
public EnumNamingStrategy? EnumNamingStrategy { get; init; }

/// <summary>
/// When set to <c>true</c>, only properties with explicit configurations (via attributes like <c>MapProperty</c>)
/// will be mapped. All other properties will be ignored by default.
/// </summary>
public bool? OnlyExplicitMappedMembers { get; init; }
}
5 changes: 5 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu
mapper.EnumNamingStrategy =
mapperConfiguration.EnumNamingStrategy ?? defaultMapperConfiguration.EnumNamingStrategy ?? mapper.EnumNamingStrategy;

mapper.OnlyExplicitMappedMembers =
mapperConfiguration.OnlyExplicitMappedMembers
?? defaultMapperConfiguration.OnlyExplicitMappedMembers
?? mapper.OnlyExplicitMappedMembers;

return mapper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ .. GetIgnoredAtMemberMembers(ctx, sourceTarget),
.. GetIgnoredObsoleteMembers(ctx, sourceTarget),
];

if (ctx.Configuration.Mapper.OnlyExplicitMappedMembers)
{
var explicitlyMappedMembers = ctx.Configuration.Members.GetMembersWithExplicitConfigurations(sourceTarget).ToHashSet();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MapNestedPropertiesAttribute also need to be considered… Also add a test for this.
Could probably be included in GetMembersWithExplicitConfigurations.

ignoredMembers.UnionWith(allMembers.Where(m => !explicitlyMappedMembers.Contains(m)));
}

RemoveAndReportConfiguredIgnoredMembers(ctx, sourceTarget, ignoredMembers);
ReportUnmatchedIgnoredMembers(ctx, sourceTarget, ignoredMembers, allMembers);
return ignoredMembers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public MapperAttribute() { }
public Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; }
public Riok.Mapperly.Abstractions.MemberVisibility IncludedConstructors { get; set; }
public Riok.Mapperly.Abstractions.MemberVisibility IncludedMembers { get; set; }
public bool OnlyExplicitMappedMembers { get; set; }
public bool PreferParameterlessConstructors { get; set; }
public Riok.Mapperly.Abstractions.PropertyNameMappingStrategy PropertyNameMappingStrategy { get; set; }
public Riok.Mapperly.Abstractions.RequiredMappingStrategy RequiredEnumMappingStrategy { get; set; }
Expand Down
92 changes: 92 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/ObjectPropertyIgnoreTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,96 @@ public void WithNestedIgnoredSourceAndTargetPropertyShouldDiagnostic()
"""
);
}

[Fact]
public void OnlyExplicitMappedMembersWithNoExplicitMappings()
{
// With OnlyExplicitMappedMembers=true and no [MapProperty], nothing is mapped (no warnings either)
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"partial B Map(A source);",
new TestSourceBuilderOptions(OnlyExplicitMappedMembers: true),
"class A { public int Value1 { get; set; } public int Value2 { get; set; } }",
"class B { public int Value1 { get; set; } public int Value2 { get; set; } }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
return target;
"""
);
}

[Fact]
public void OnlyExplicitMappedMembersWithExplicitMapping()
{
// Only the explicitly declared [MapProperty] member is mapped; Value2 is ignored even though it matches by name
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[MapProperty(nameof(A.Value1), nameof(B.Value1))] partial B Map(A source);",
new TestSourceBuilderOptions(OnlyExplicitMappedMembers: true),
"class A { public int Value1 { get; set; } public int Value2 { get; set; } }",
"class B { public int Value1 { get; set; } public int Value2 { get; set; } }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Value1 = source.Value1;
return target;
"""
);
}

[Fact]
public void OnlyExplicitMappedMembersWithMultipleExplicitMappings()
{
// Only the two explicitly declared [MapProperty] members are mapped; unmatched/extra members are silently ignored
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[MapProperty(nameof(A.Value1), nameof(B.Value1))] [MapProperty(nameof(A.Value2), nameof(B.Value2))] partial B Map(A source);",
new TestSourceBuilderOptions(OnlyExplicitMappedMembers: true),
"class A { public int Value1 { get; set; } public int Value2 { get; set; } public int Value4 { get; set; } }",
"class B { public int Value1 { get; set; } public int Value2 { get; set; } public int Value3 { get; set; } }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Value1 = source.Value1;
target.Value2 = source.Value2;
return target;
"""
);
}

[Fact]
public void OnlyExplicitMappedMembersWithMapPropertyFromSource()
{
// MapPropertyFromSource explicitly maps the whole source to a target member; other target members are ignored
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"[MapPropertyFromSource(nameof(B.Source))] partial B Map(A source);",
new TestSourceBuilderOptions(OnlyExplicitMappedMembers: true),
"class A { public int Value1 { get; set; } }",
"class B { public A Source { get; set; } public int Other { get; set; } }"
);

TestHelper
.GenerateMapper(source)
.Should()
.HaveSingleMethodBody(
"""
var target = new global::B();
target.Source = source;
return target;
"""
);
}
}
5 changes: 3 additions & 2 deletions test/Riok.Mapperly.Tests/TestSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public static string MapperWithBody(
{{body}}
}

{{ additionalNamespaceContent ?? "" }}
{{(options is { Namespace: not null, UseFileScopedNamespace: false } ? "}" : "") }}
{{additionalNamespaceContent ?? ""}}
{{(options is { Namespace: not null, UseFileScopedNamespace: false } ? "}" : "")}}
"""
);
}
Expand Down Expand Up @@ -107,6 +107,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options)
Attribute(options.IncludedConstructors),
Attribute(options.PreferParameterlessConstructors),
Attribute(options.AutoUserMappings),
Attribute(options.OnlyExplicitMappedMembers),
}.WhereNotNull();

return $"[Mapper({string.Join(", ", attrs)})]";
Expand Down
3 changes: 2 additions & 1 deletion test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public record TestSourceBuilderOptions(
MemberVisibility? IncludedConstructors = null,
bool Static = false,
bool PreferParameterlessConstructors = true,
bool AutoUserMappings = true
bool AutoUserMappings = true,
bool OnlyExplicitMappedMembers = false
)
{
public const string DefaultMapperClassName = "Mapper";
Expand Down
Loading