Repository: riok/mapperly (2026-06-22T18:23:19Z)

Issue:
Generic user mapping function overwrites specified function on other parameter
**Please do the checklist before filing an issue:**
- [x] 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

**Describe the bug**
When using generic custom methods and non generic custom methods for different parameter in the same mapping, the generic is used even if a non generic was specified. My actual code uses mapping functions with ref parameters and also does not work.

My simplified tests are with explicit specifying the function as I could better pin the problem down. In the last version (prev 8) using [UseStaticMapper(typeof(....))] instead of MapProperty, works so maybe related to https://github.com/riok/mapperly/issues/2275? 


Please let me know what you think. Thanks!

**Declaration code**
```csharp
public class GenericTestClassIn {
    public string? NullableStr { get; set; }
    public string Str { get; set; }
    public int AdditionalParameter { get; set; }
}
public class GenericTestClassOut {
    public string? NullableStr { get; set; }
    public string Str { get; set; }
    public int AdditionalParameter { get; set; }
}

public static class OtherMapper {
    public static string TrimToNull(string? input) => string.IsNullOrWhiteSpace(input) ? null : input;
    public static string Trim(string input) => input.Trim();
    public static T Identity<T>(T input) => input;
}

[Mapper]
public static partial class OtherTestMapper {
    [MapProperty(nameof(GenericTestClassIn.NullableStr), nameof(GenericTestClassOut.NullableStr), Use = nameof(@OtherMapper.TrimToNull))]
    [MapProperty(nameof(GenericTestClassIn.Str), nameof(GenericTestClassOut.Str), Use = nameof(@OtherMapper.Trim))]
    [MapProperty(nameof(GenericTestClassIn.AdditionalParameter), nameof(GenericTestClassOut.AdditionalParameter), Use = nameof(@OtherMapper.Identity))]
    public static partial GenericTestClassOut Map(GenericTestClassIn input);

    [MapProperty(nameof(GenericTestClassIn.NullableStr), nameof(GenericTestClassOut.NullableStr), Use = nameof(@OtherMapper.TrimToNull))]
    [MapProperty(nameof(GenericTestClassIn.Str), nameof(GenericTestClassOut.Str), Use = nameof(@OtherMapper.Trim))]
    [MapProperty(nameof(GenericTestClassIn.AdditionalParameter), nameof(GenericTestClassOut.AdditionalParameter), Use = nameof(@OtherMapper.Identity))]
    public static partial void Update(GenericTestClassIn input, [MappingTarget] GenericTestClassOut target);
}
```

**Actual relevant generated code**
```csharp
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "5.0.0.0")]
        public static partial global::Tests.Model.Mapper.GenericTestClassOut Map(global::Tests.Model.Mapper.GenericTestClassIn input)
        {
            var target = new global::Tests.Model.Mapper.GenericTestClassOut();
            target.NullableStr = global::Tests.Model.Mapper.OtherMapper.Identity<string>(input.NullableStr);
            target.Str = global::Tests.Model.Mapper.OtherMapper.Identity<string>(input.Str);
            target.AdditionalParameter = global::Tests.Model.Mapper.OtherMapper.Identity<int>(input.AdditionalParameter);
            return target;
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "5.0.0.0")]
        public static partial void Update(global::Tests.Model.Mapper.GenericTestClassIn input, global::Tests.Model.Mapper.GenericTestClassOut target)
        {
            target.NullableStr = global::Tests.Model.Mapper.OtherMapper.Identity<string>(input.NullableStr);
            target.Str = global::Tests.Model.Mapper.OtherMapper.Identity<string>(input.Str);
            target.AdditionalParameter = global::Tests.Model.Mapper.OtherMapper.Identity<int>(input.AdditionalParameter);
        }
```

**Expected relevant generated code**
```csharp
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "5.0.0.0")]
        public static partial global::Tests.Model.Mapper.GenericTestClassOut Map(global::Tests.Model.Mapper.GenericTestClassIn input)
        {
            var target = new global::Tests.Model.Mapper.GenericTestClassOut();
            target.NullableStr = global::Tests.Model.Mapper.OtherMapper.TrimToNull(input.NullableStr);
            target.Str = global::Tests.Model.Mapper.OtherMapper.Trim(input.Str);
            target.AdditionalParameter = global::Tests.Model.Mapper.OtherMapper.Identity<int>(input.AdditionalParameter);
            return target;
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "5.0.0.0")]
        public static partial void Update(global::Tests.Model.Mapper.GenericTestClassIn input, global::Tests.Model.Mapper.GenericTestClassOut target)
        {
            target.NullableStr = global::Tests.Model.Mapper.OtherMapper.TrimToNull(input.NullableStr);
            target.Str = global::Tests.Model.Mapper.OtherMapper.Trim(input.Str);
            target.AdditionalParameter = global::Tests.Model.Mapper.OtherMapper.Identity<int>(input.AdditionalParameter);
        }
```

**Environment (please complete the following information):**
- Mapperly Version: 5.0.0-next.8
- Nullable reference types: enabled
- .NET Version: NET 10.0.203
- Target Framework: .net10.0
- Compiler Version: 5.3.0-2.26219.105 (c23858a6d860abe3ca94f9ac630c3e61b2625664)
- C# Language Version: 14.0
- IDE: Visual Studio v18.5.0
- OS: Win 11



Discussion:
The problem is that matcher of generic templates checks only source/target types ignoring assignment to the property.
If there is at least one generic mapping it overrides all others (including default ones), effectively.

From #2225 [line](https://github.com/riok/mapperly/pull/2225/changes#diff-f8e48bde529aab670e8ed7fa6789428a4235e4ebc82dcecdc3010150e0388d39R187)
```csharp
if (_genericNewInstanceTemplates.Count == 0)
       return null;

foreach (var template in _genericNewInstanceTemplates)
{
     if (template.TryCreateConcreteMapping(genericTypeChecker, mappingKey.Source, mappingKey.Target) is not { } mapping)
```

I'm not sure what should be the desired flow but mappings added by "Use =" must be limited to places where they are explicitly used. 



Patch:
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/UseNamedMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/UseNamedMappingBuilder.cs
index a873a56b55..b9641c5db0 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/UseNamedMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/UseNamedMappingBuilder.cs
@@ -1,6 +1,7 @@
 using Microsoft.CodeAnalysis;
 using Riok.Mapperly.Descriptors.Mappings;
 using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
+using Riok.Mapperly.Descriptors.Mappings.UserMappings;
 using Riok.Mapperly.Diagnostics;
 using Riok.Mapperly.Helpers;
 
@@ -20,6 +21,23 @@ public static class UseNamedMappingBuilder
             return null;
         }
 
+        // if a generic user-implemented mapping is referenced by name,
+        // create a concrete instantiation for the current source and target types
+        if (mapping is GenericUserImplementedNewInstanceMethodMapping genericTemplate)
+        {
+            mapping = genericTemplate.TryCreateConcreteMapping(ctx.GenericTypeChecker, ctx.Source, ctx.Target);
+            if (mapping == null)
+            {
+                ctx.ReportDiagnostic(
+                    DiagnosticDescriptors.ReferencedMappingSourceTypeMismatch,
+                    ctx.MappingKey.Configuration.UseNamedMapping,
+                    genericTemplate.SourceType.ToDisplayString(),
+                    ctx.Source.ToDisplayString()
+                );
+                return null;
+            }
+        }
+
         if (!ctx.ParameterScope.TryUseParameters(mapping))
         {
             ctx.ReportDiagnostic(DiagnosticDescriptors.NamedMappingParametersUnsatisfied, ctx.MappingKey.Configuration.UseNamedMapping);
@@ -55,6 +73,23 @@ public static class UseNamedMappingBuilder
         if (existingTargetMapping is null)
             return null;
 
+        // if a generic user-implemented mapping is referenced by name,
+        // create a concrete instantiation for the current source and target types
+        if (existingTargetMapping is GenericUserImplementedExistingTargetMethodMapping genericTemplate)
+        {
+            existingTargetMapping = genericTemplate.TryCreateConcreteMapping(ctx.GenericTypeChecker, ctx.Source, ctx.Target);
+            if (existingTargetMapping == null)
+            {
+                ctx.ReportDiagnostic(
+                    DiagnosticDescriptors.ReferencedMappingSourceTypeMismatch,
+                    useNamedMapping,
+                    genericTemplate.SourceType.ToDisplayString(),
+                    ctx.Source.ToDisplayString()
+                );
+                return null;
+            }
+        }
+
         if (!ctx.ParameterScope.TryUseParameters(existingTargetMapping))
         {
             ctx.ReportDiagnostic(DiagnosticDescriptors.NamedMappingParametersUnsatisfied, useNamedMapping);
diff --git a/src/Riok.Mapperly/Descriptors/MappingCollection.cs b/src/Riok.Mapperly/Descriptors/MappingCollection.cs
index 3f0feea25e..ec18274a0f 100644
--- a/src/Riok.Mapperly/Descriptors/MappingCollection.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingCollection.cs
@@ -74,16 +74,40 @@ public class MappingCollection(GenericTypeChecker genericTypeChecker)
             .Concat(_newInstanceMappings.UsedDuplicatedNonDefaultNonReferencedUserMappings)
             .Concat(_existingTargetMappings.UsedDuplicatedNonDefaultNonReferencedUserMappings);
 
-    public INewInstanceMapping? FindNewInstanceMapping(TypeMappingKey mappingKey, ParameterScope? scope = null) =>
-        _newInstanceMappings.Find(mappingKey, scope) ?? TryMatchGenericNewInstanceTemplate(mappingKey);
+    public INewInstanceMapping? FindNewInstanceMapping(TypeMappingKey mappingKey, ParameterScope? scope = null)
+    {
+        if (_newInstanceMappings.Find(mappingKey, scope) is { } mapping)
+            return mapping;
+
+        // If an explicit mapping is referenced by name (Use = ...),
+        // do not match generic templates here.
+        // The referenced mapping is resolved later by the UseNamedMappingBuilder,
+        // otherwise a generic template would shadow the explicitly referenced mapping.
+        if (mappingKey.Configuration.UseNamedMapping != null)
+            return null;
+
+        return TryMatchGenericNewInstanceTemplate(mappingKey);
+    }
 
     public INewInstanceUserMapping? FindNewInstanceUserMapping(IMethodSymbol method) => _newInstanceMappings.FindUserMapping(method);
 
     public INewInstanceMapping? FindNamedNewInstanceMapping(string name, out bool ambiguousName) =>
         _newInstanceMappings.FindNamed(name, out ambiguousName);
 
-    public IExistingTargetMapping? FindExistingInstanceMapping(TypeMappingKey mappingKey) =>
-        _existingTargetMappings.Find(mappingKey) ?? TryMatchGenericExistingTargetTemplate(mappingKey);
+    public IExistingTargetMapping? FindExistingInstanceMapping(TypeMappingKey mappingKey)
+    {
+        if (_existingTargetMappings.Find(mappingKey) is { } mapping)
+            return mapping;
+
+        // If an explicit mapping is referenced by name (Use = ...),
+        // do not match generic templates here.
+        // The referenced mapping is resolved later by the UseNamedMappingBuilder,
+        // otherwise a generic template would shadow the explicitly referenced mapping.
+        if (mappingKey.Configuration.UseNamedMapping != null)
+            return null;
+
+        return TryMatchGenericExistingTargetTemplate(mappingKey);
+    }
 
     public IExistingTargetMapping? FindExistingInstanceNamedMapping(string name, out bool ambiguousName) =>
         _existingTargetMappings.FindNamed(name, out ambiguousName);


Test patch:
diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
index 26c4343aaa..e59eb0425e 100644
--- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
@@ -1222,4 +1222,76 @@ private TTarget MapGeneric<TSource, TTarget>(TSource source)
                 """
             );
     }
+
+    [Fact]
+    public void WithGenericUserImplementedDoesNotOverrideExplicitlyReferencedMapping()
+    {
+        // https://github.com/riok/mapperly/issues/2277
+        // a generic user-implemented mapping must not shadow a mapping
+        // which is explicitly referenced via Use on another member.
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            [MapProperty(nameof(A.Value), nameof(B.Value), Use = nameof(MapInt))]
+            [MapProperty(nameof(A.Other), nameof(B.Other), Use = nameof(Identity))]
+            partial B Map(A source);
+
+            [UserMapping(Default = false)]
+            private T Identity<T>(T value) => value;
+
+            [UserMapping(Default = false)]
+            private int MapInt(int value) => value + 1;
+            """,
+            "class A { public int Value { get; set; } public int Other { get; set; } }",
+            "class B { public int Value { get; set; } public int Other { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::B();
+                target.Value = MapInt(source.Value);
+                target.Other = Identity<int>(source.Other);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public void WithGenericUserImplementedExistingTargetDoesNotOverrideExplicitlyReferencedMapping()
+    {
+        // https://github.com/riok/mapperly/issues/2277
+        // same as above but for existing target (void) mappings.
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            [MapProperty(nameof(A.Value), nameof(B.Value), Use = nameof(MapValue))]
+            [MapProperty(nameof(A.Other), nameof(B.Other), Use = nameof(MapGeneric))]
+            partial void Map(A source, B target);
+
+            [UserMapping(Default = false)]
+            private void MapGeneric<TSource, TTarget>(TSource source, [MappingTarget] TTarget target)
+                where TSource : IEntity
+                where TTarget : IDto
+                => target.Id = source.Id;
+
+            [UserMapping(Default = false)]
+            private void MapValue(C source, [MappingTarget] CDto target) => target.Id = source.Id + 1;
+            """,
+            "public interface IEntity { public int Id { get; } }",
+            "public interface IDto { public int Id { get; set; } }",
+            "public class C : IEntity { public int Id { get; set; } }",
+            "public class CDto : IDto { public int Id { get; set; } }",
+            "public class A { public C Value { get; set; } public C Other { get; set; } }",
+            "public class B { public CDto Value { get; set; } public CDto Other { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                MapValue(source.Value, target.Value);
+                MapGeneric<global::C, global::CDto>(source.Other, target.Other);
+                """
+            );
+    }
 }