Repository: riok/mapperly (2026-04-13T12:43:45Z)

Issue:
Wrong code generated when mapping method has additional parameter
**Please do the checklist before filing an issue:**
- [ ] I have read the [documentation](https://mapperly.riok.app), including the [FAQ](https://mapperly.riok.app/docs/getting-started/faq/)
- [x] I can reproduce the bug using the latest prerelease version
- [x] I have searched existing [discussion](https://github.com/riok/mapperly/discussions) and [issue](https://github.com/riok/mapperly/issues) to avoid duplicates

<!-- Feel free to remove sections of this template that do not apply including the checklist at the top. -->

**Describe the bug**
Code generation is broken in Riok.Mapperly versions 5.0.0-next.4 and 5.0.0-next.5 when mapping method contains additional parameter. Earlier versions generate proper code.

**Repro**
This repository demonstrates the issue: https://github.com/kockicica/mapperly-test

**Actual relevant generated code**
```csharp
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "5.0.0.0")]
    private global::Data.ScenarioAction MapToScenarioAction(global::Model.ScenarioAction source)
    {
        var target = new global::Data.ScenarioAction();
        if (id != null)
        {
            target.Id = id.Value;
        }
        target.Name = source.Name;
        target.Type = source.Type;
        target.Required = source.Required;
        target.Duration = source.Duration;
        if (source.Params != null)
        {
            target.Params = new global::System.Collections.Generic.Dictionary<string, object>(source.Params);
        }
        else
        {
            target.Params = null;
        }
        return target;
    }
```

**Expected relevant generated code**
```csharp
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "5.0.0.0")]
    private global::Data.ScenarioAction MapToScenarioAction(global::Model.ScenarioAction source)
    {
        var target = new global::Data.ScenarioAction();
        target.Id = source.Id;
        target.Name = source.Name;
        target.Type = source.Type;
        target.Required = source.Required;
        target.Duration = source.Duration;
        if (source.Params != null)
        {
            target.Params = new global::System.Collections.Generic.Dictionary<string, object>(source.Params);
        }
        else
        {
            target.Params = null;
        }
        return target;
    }

```

**Environment (please complete the following information):**
- Mapperly Version: 5.0.0-next.4 and 5.0.0-next.5
- Target Framework: net10.0
- Compiler Version: 5.3.0-2.26153.122 (4d3023de605a78ba3e59e50c657eed70f125c68a)
- C# Language Version: 14.0
- OS: Windows 11



Discussion:

Patch:
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs
index 632dd0bd65..f484aff7b9 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingStateBuilder.cs
@@ -51,6 +51,14 @@ public static MembersMappingState Build(MappingBuilderContext ctx, IMapping mapp
             .Keys.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
             .ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase);
         var unmappedTargetMemberNames = targetMembers.Keys.ToHashSet();
+
+        // Only expose additional parameters as source members when:
+        // 1. The scope is root (this mapping declared the parameters), or
+        // 2. We're in an expression context (inline within the same method scope).
+        // For separate auto-generated method mappings, inherited parent parameters
+        // must not shadow source type members.
+        var parameterScope = ctx.ParameterScope.IsRoot || ctx.IsExpression ? ctx.ParameterScope : ParameterScope.Empty;
+
         return new MembersMappingState(
             unmappedSourceMemberNames,
             unmappedTargetMemberNames,
@@ -60,7 +68,7 @@ public static MembersMappingState Build(MappingBuilderContext ctx, IMapping mapp
             memberConfigsByRootTargetName,
             configuredTargetMembersByRootName.AsDictionary(),
             ignoredSourceMemberNames,
-            ctx.ParameterScope
+            parameterScope
         );
     }
 


Test patch:
diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParameterForwardingTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParameterForwardingTest.cs
index de4c7a362c..2d7973165c 100644
--- a/test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParameterForwardingTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodAdditionalParameterForwardingTest.cs
@@ -897,4 +897,41 @@ public void DuplicateAdditionalParameterNamesCaseInsensitiveShouldDiagnostic()
             )
             .HaveAssertedAllDiagnostics();
     }
+
+    [Fact]
+    public Task AdditionalParameterShouldNotLeakIntoNestedAutoGeneratedMapping()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            [MapperIgnoreSource(nameof(A.Id))]
+            partial B Map(A src, int id);
+            """,
+            """
+            class A
+            {
+                public int Id { get; set; }
+                public string Name { get; set; }
+                public ANested Nested { get; set; }
+            }
+            class B
+            {
+                public int Id { get; set; }
+                public string Name { get; set; }
+                public BNested Nested { get; set; }
+            }
+            class ANested
+            {
+                public int Id { get; set; }
+                public string Value { get; set; }
+            }
+            class BNested
+            {
+                public int Id { get; set; }
+                public string Value { get; set; }
+            }
+            """
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
 }
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParameterForwardingTest.AdditionalParameterShouldNotLeakIntoNestedAutoGeneratedMapping#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParameterForwardingTest.AdditionalParameterShouldNotLeakIntoNestedAutoGeneratedMapping#Mapper.g.verified.cs
new file mode 100644
index 0000000000..de2133053f
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodAdditionalParameterForwardingTest.AdditionalParameterShouldNotLeakIntoNestedAutoGeneratedMapping#Mapper.g.verified.cs
@@ -0,0 +1,24 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    partial global::B Map(global::A src, int id)
+    {
+        var target = new global::B();
+        target.Id = id;
+        target.Name = src.Name;
+        target.Nested = MapToBNested(src.Nested);
+        return target;
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private global::BNested MapToBNested(global::ANested source)
+    {
+        var target = new global::BNested();
+        target.Id = source.Id;
+        target.Value = source.Value;
+        return target;
+    }
+}
\ No newline at end of file