diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
index f0e1190379..1b01daca76 100644
--- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
@@ -142,4 +142,11 @@ public class MapperAttribute : Attribute
/// partial methods are discovered.
///
public bool AutoUserMappings { get; set; } = true;
+
+ ///
+ /// When set to true, only properties with explicit configurations (via attributes like )
+ /// 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.
+ ///
+ public bool OnlyExplicitMappedMembers { get; set; }
}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
index 68893d366a..7da6edad77 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
@@ -135,4 +135,10 @@ public record MapperConfiguration
/// Can be overwritten on specific enums via mapping method configurations.
///
public EnumNamingStrategy? EnumNamingStrategy { get; init; }
+
+ ///
+ /// When set to true, only properties with explicit configurations (via attributes like MapProperty)
+ /// will be mapped. All other properties will be ignored by default.
+ ///
+ public bool? OnlyExplicitMappedMembers { get; init; }
}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
index 4d625f49f3..5591041854 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
@@ -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;
}
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs
index b41a73a2d5..b814807c43 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/IgnoredMembersBuilder.cs
@@ -28,6 +28,12 @@ .. GetIgnoredAtMemberMembers(ctx, sourceTarget),
.. GetIgnoredObsoleteMembers(ctx, sourceTarget),
];
+ if (ctx.Configuration.Mapper.OnlyExplicitMappedMembers)
+ {
+ var explicitlyMappedMembers = ctx.Configuration.Members.GetMembersWithExplicitConfigurations(sourceTarget).ToHashSet();
+ ignoredMembers.UnionWith(allMembers.Where(m => !explicitlyMappedMembers.Contains(m)));
+ }
+
RemoveAndReportConfiguredIgnoredMembers(ctx, sourceTarget, ignoredMembers);
ReportUnmatchedIgnoredMembers(ctx, sourceTarget, ignoredMembers, allMembers);
return ignoredMembers;
diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs
index 3ad1c41b2f..e7acaa9b3e 100644
--- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs
+++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs
@@ -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; }
diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyIgnoreTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyIgnoreTest.cs
index 7dc1b69dea..84d42fc0c9 100644
--- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyIgnoreTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyIgnoreTest.cs
@@ -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;
+ """
+ );
+ }
}
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
index 5861f4b988..fc4cfde873 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
@@ -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 } ? "}" : "")}}
"""
);
}
@@ -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)})]";
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
index 6e166ff415..5b5a4acce8 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
@@ -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";