Repository: riok/mapperly (2026-04-10T13:29:40Z)

Issue:
Support generic user implemented mappings
Add support for user implemented generic mapping methods:
```c#
class Optional<T> where T : notnull { public T? Value; }
class User { string Id; }
class UserDto { string Id; }
class Document { Optional<User> ModifiedBy; }
class DocumentDto { Optional<UserDto> ModifiedBy; }

[Mapper]
public partial class Mapper
{
    public partial DocumentDto MapDocument(Document source);
    
    private Optional<T> MapOptional<TTarget, TSource>(Optional<TSource> source)
    {
        return new Optional<TTarget> { Value = Map<TTarget>(source.Value) };
    }

    private partial T Map(object source);

    private partial UserDto MapUser(User source);
}
```

Mapperly should use the user implemented `MapOptional` method to map `Document.ModifiedBy` to `DocumentDto.ModifiedBy`.
Depends on https://github.com/riok/mapperly/issues/357.
Discussed in #444.


Discussion:
Would a situation like this fall within what you are describing in this issue:

```csharp
    [Fact]
    public void WithGenericSourceTypeConstraintsAndUserImplemented()
    {
        var source = TestSourceBuilder.MapperWithBodyAndTypes(
            """
                partial To Map(From source);

                public OtherValue PickMeForAAndB<TSource>(TSource source)
                    where TSource : IValue => new OtherValue { Value = source.Value };

                public OtherValue PickMeForIValue(IValue source) => new OtherValue { Value = source.Value };
                """,
            "public interface IValue { public string Value { get; } }",
            "public class OtherValue { public string Value { get; set; } }",

            "public class A: IValue{ public string Value {get; set;} }",
            "public class B: IValue{ public string Value {get; set;} }",

            "public class From { public A AV { get; set;} public B BV { get; set;} public IValue IV { get; set;} }",
            "public class To { public OtherValue AV { get; set; } public OtherValue BV { get; set; } public OtherValue IV { get; set; } }"
        );
        TestHelper
            .GenerateMapper(source)
            .Should()
            .HaveMapMethodBody(
                """
                var target = new global::To();
                target.AV = PickMeForAAndB(source.AV);
                target.BV = PickMeForAAndB(source.BV);
                target.IV = PickMeForIValue(source.IV);
                return target;
                """
            );
    }
```
@Fube that's the idea!
Would it not be possible to implement this by modifying the `MappingCollection.Find` method such that it looks not only for exact matches but also for "type" matches?

I'm able to get that specific test case to pass with this (extremely hacky and specific) solution:
```csharp
    public ITypeMapping? Find(ITypeSymbol sourceType, ITypeSymbol targetType)
    {
        _mappings.TryGetValue(new TypeMappingKey(sourceType, targetType), out var mapping);
        if (mapping != null)
        {
            return mapping;
        }

        var sourceIndirectlyRegistered = _mappings
            .Where(x => x.Key._target.Equals(targetType)) // _target and _source of TypeMappingKey were made public
            .Select(x => (method: x.Key._source.ContainingSymbol as IMethodSymbol, kvp: x))
            .Where(x => x.method is not null)
            .Where(x =>
            {
                if (x.method?.TypeParameters[0].ConstraintTypes[0] is INamedTypeSymbol ns)
                {
                    return sourceType.AllInterfaces.Any(y => SymbolEqualityComparer.Default.Equals(y, ns));
                }

                return false;
            })
            .SingleOrDefault();

        return sourceIndirectlyRegistered.kvp.Value;
    }
```

I know there's a lot more to take into consideration, I'm just wondering if those things can be accounted for within `MappingCollection` or if it is a bigger change 😀
    
@Fube I think this works for methods where the source is the only generic type argument  (would definitely need some cleanup 😄). If the target is (also) generic, not only the lookup side, but also the syntax generation side needs to be adjusted as explicit specification of the generic type arguments is required.
@latonz 

> would definitely need some cleanup 😄

Yes, very much so 😅

> explicit specification of the generic type arguments is required

Could you please go into more detail as to why that would be needed? My understanding of the codebase is too shallow to grasp that


How would addressing this enhancement affect the bug described in issue #421?
The syntax seems close enough that you would not be able to pickup when something is meant as a user mapping and when it is a helper method that just so happens to exist within the mapper
Would we want to introduce something like `[MapperIgnore]` to differentiate them?
https://github.com/riok/mapperly/issues/421 occurred when Mapperly didn't have any support for generic methods, but still tried to call them. If a generic method was implemented by a user, Mapperly handled the generic type as any other type. Therefore it did type checks with `typeof(TGeneric)` which won't work outside the method itself, also methods with a generic target type were called like any other method, without explicit type arguments. But if the return type is a type argument, the c# compiler cannot infer the type. The fix to #421 was to just ignore them for now and open this issue to implement support.
Example of how Mapperly would currently invoke generic user implemented methods if it has a generic return type:
```c#
// user implemented mapping method
TTarget Map<TSource, TTarget>(TSource source)
    where TSource : IMyInterface
    where TTarget : new(), IMyOtherInterface
  => new TTarget { MyValue = source.MyValue };

// would currently be called by Mapperly
var target = Map(source);

// instead of
var target = Map<MySource, MyTarget>(soruce);
```


To support this, as you figured out, Mapperly needs to implement generic mapping support during the mapping resolution in `MappingCollection` and `UserMethodMappingExtractor.BuildUserImplementedMapping`. Additionally the syntax generation needs to be aware of generics and whether they need to be specified explicitly. Probably a new `GenericUserImplementedMethodMapping` is needed or the existing `UserImplementedMethodMapping` could be adjusted. In the `Build` method of the generic mapping, the `Invocation` needs to be a `GenericInvocation` if type arguments need to be specified explicitly.
I have a suggestion to how this could be practially implemented: allow user-mappings to accept `Func<TA, TB>` which acts as "dependency injection" for what nested mappers are required. Example:

```csharp
[UserMapping]
private static Optional<TTarget> MapOptional<TSource, TTarget>(
  Optional<TSource> source,
  Func<TSource, TTarget> mapValue)
  => source.Select(mapValue);
```

This would tell mapperly that if it wants to map a `Optional<int>` to `Optional<string>`, it needs to provide a mapping from `int` to `string` (using it's normal resolution).



Patch:
diff --git a/docs/docs/configuration/user-implemented-methods.mdx b/docs/docs/configuration/user-implemented-methods.mdx
index d61974f179..6fded16a95 100644
--- a/docs/docs/configuration/user-implemented-methods.mdx
+++ b/docs/docs/configuration/user-implemented-methods.mdx
@@ -111,3 +111,46 @@ The mapper will respect the `ref` keyword on the target parameter when using an
       private static void MapArray([MappingTarget] ref int[] target, int[] second) => target = [.. target, .. second.Except(target)];
   }
 ```
+
+## Generic user-implemented mapping methods
+
+User-implemented mapping methods can be generic.
+Mapperly automatically resolves generic user-implemented mapping methods
+by inferring type arguments from the source and target types.
+
+```csharp
+public record Document(string Title, User CreatedBy, Optional<User> ModifiedBy);
+public record DocumentDto(string Title, UserDto CreatedBy, Optional<UserDto> ModifiedBy);
+
+public record User(string Name);
+public record UserDto(string Name);
+
+[Mapper]
+public partial class MyMapper
+{
+    public partial DocumentDto MapDocument(Document source);
+
+    // highlight-start
+    private Optional<TTarget> MapOptional<TSource, TTarget>(Optional<TSource> source)
+        where TSource : notnull
+        where TTarget : notnull
+        => source.HasValue
+           ? Optional.Of(Map(source.Value))
+           : Optional.Empty();
+    // highlight-end
+
+    private partial TTarget Map<TSource, TTarget>(TSource source)
+        where TSource : notnull
+        where TTarget : notnull;
+
+    private partial UserDto MapUser(User source);
+}
+```
+
+In this example, when Mapperly needs to map `Optional<User>` to `Optional<UserDto>`,
+it will match the `MapOptional` method and invoke it as `MapOptional<User, UserDto>(source.ModifiedBy)`.
+
+:::info
+If a non-generic user-implemented mapping method exists for an exact type pair,
+it takes priority over a generic one.
+:::
diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
index 757be84017..c5bffa29ec 100644
--- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
@@ -23,8 +23,8 @@ public class DescriptorBuilder
     private readonly SymbolAccessor _symbolAccessor;
     private readonly ImmutableArray<UseStaticMapperConfiguration> _assemblyScopedStaticMappers;
 
-    private readonly MappingCollection _mappings = new();
-    private readonly InlinedExpressionMappingCollection _inlineMappings = new();
+    private readonly MappingCollection _mappings;
+    private readonly InlinedExpressionMappingCollection _inlineMappings;
 
     private readonly MethodNameBuilder _methodNameBuilder = new();
     private readonly MappingBodyBuilder _mappingBodyBuilder;
@@ -47,11 +47,14 @@ ImmutableArray<UseStaticMapperConfiguration> assemblyScopedStaticMappers
         _symbolAccessor = symbolAccessor;
         _attributeAccessor = attributeDataAccessor;
         _assemblyScopedStaticMappers = assemblyScopedStaticMappers;
+
+        var genericTypeChecker = new GenericTypeChecker(_symbolAccessor, compilationContext.Types);
+        _mappings = new MappingCollection(genericTypeChecker);
+        _inlineMappings = new InlinedExpressionMappingCollection(genericTypeChecker);
         _mappingBodyBuilder = new MappingBodyBuilder(_mappings);
         _unsafeAccessorContext = new UnsafeAccessorContext(_methodNameBuilder, symbolAccessor);
         _diagnostics = new DiagnosticCollection(mapperDeclaration.Syntax.GetLocation());
 
-        var genericTypeChecker = new GenericTypeChecker(_symbolAccessor, compilationContext.Types);
         var configurationReader = new MapperConfigurationReader(
             _attributeAccessor,
             _mappings,
diff --git a/src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs b/src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs
index 27923be681..c94fbd07b3 100644
--- a/src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs
+++ b/src/Riok.Mapperly/Descriptors/InlinedExpressionMappingCollection.cs
@@ -1,16 +1,17 @@
 using Microsoft.CodeAnalysis;
 using Riok.Mapperly.Descriptors.Mappings;
 using Riok.Mapperly.Descriptors.Mappings.UserMappings;
+using Riok.Mapperly.Helpers;
 
 namespace Riok.Mapperly.Descriptors;
 
-public class InlinedExpressionMappingCollection
+public class InlinedExpressionMappingCollection(GenericTypeChecker genericTypeChecker)
 {
     // use the MappingCollection to re-use
     // the mapping default logic.
     // Added mapping bodies are never built
     // and method mappings are not added to the generated mapper.
-    private readonly MappingCollection _delegate = new();
+    private readonly MappingCollection _delegate = new(genericTypeChecker);
 
     private readonly Dictionary<string, INewInstanceMapping> _namedInlinedMappings = new();
     private readonly Dictionary<TypeMappingKey, INewInstanceMapping> _inlinedMappings = new();
diff --git a/src/Riok.Mapperly/Descriptors/MappingCollection.cs b/src/Riok.Mapperly/Descriptors/MappingCollection.cs
index a271e04a29..f479a73df8 100644
--- a/src/Riok.Mapperly/Descriptors/MappingCollection.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingCollection.cs
@@ -7,7 +7,7 @@
 
 namespace Riok.Mapperly.Descriptors;
 
-public class MappingCollection
+public class MappingCollection(GenericTypeChecker genericTypeChecker)
 {
     /// <summary>
     /// A list of all method mappings in order of registration/discovery.
@@ -41,6 +41,18 @@ public class MappingCollection
     /// </summary>
     private readonly MappingCollectionInstance<IExistingTargetMapping, IExistingTargetUserMapping> _existingTargetMappings = new();
 
+    /// <summary>
+    /// Generic user-implemented new instance mapping templates.
+    /// These are matched against concrete type pairs during <see cref="FindNewInstanceMapping"/>.
+    /// </summary>
+    private readonly List<GenericUserImplementedNewInstanceMethodMapping> _genericNewInstanceTemplates = [];
+
+    /// <summary>
+    /// Generic user-implemented existing target mapping templates.
+    /// These are matched against concrete type pairs during <see cref="FindExistingInstanceMapping"/>.
+    /// </summary>
+    private readonly List<GenericUserImplementedExistingTargetMethodMapping> _genericExistingTargetTemplates = [];
+
     /// <inheritdoc cref="_methodMappings"/>
     public IReadOnlyCollection<MethodMapping> MethodMappings => _methodMappings;
 
@@ -63,14 +75,15 @@ public class MappingCollection
             .Concat(_existingTargetMappings.UsedDuplicatedNonDefaultNonReferencedUserMappings);
 
     public INewInstanceMapping? FindNewInstanceMapping(TypeMappingKey mappingKey, ParameterScope? scope = null) =>
-        _newInstanceMappings.Find(mappingKey, scope);
+        _newInstanceMappings.Find(mappingKey, scope) ?? 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);
+    public IExistingTargetMapping? FindExistingInstanceMapping(TypeMappingKey mappingKey) =>
+        _existingTargetMappings.Find(mappingKey) ?? TryMatchGenericExistingTargetTemplate(mappingKey);
 
     public IExistingTargetMapping? FindExistingInstanceNamedMapping(string name, out bool ambiguousName) =>
         _existingTargetMappings.FindNamed(name, out ambiguousName);
@@ -88,6 +101,17 @@ public MappingCollectionAddResult AddUserMapping(IUserMapping userMapping, strin
             _methodMappings.Add(methodMapping);
         }
 
+        // Generic user-implemented mapping templates are stored separately
+        // and matched lazily during Find operations.
+        if (userMapping is GenericUserImplementedNewInstanceMethodMapping genericNewInstance)
+        {
+            _genericNewInstanceTemplates.Add(genericNewInstance);
+        }
+        else if (userMapping is GenericUserImplementedExistingTargetMethodMapping genericExistingTarget)
+        {
+            _genericExistingTargetTemplates.Add(genericExistingTarget);
+        }
+
         return userMapping switch
         {
             INewInstanceUserMapping newInstanceMapping => _newInstanceMappings.AddUserMapping(
@@ -158,6 +182,42 @@ public void AddNamedExistingInstanceUserMapping(string name, IExistingTargetUser
         _existingTargetMappings.AddNamedUserMapping(name, mapping);
     }
 
+    private INewInstanceMapping? TryMatchGenericNewInstanceTemplate(TypeMappingKey mappingKey)
+    {
+        if (_genericNewInstanceTemplates.Count == 0)
+            return null;
+
+        foreach (var template in _genericNewInstanceTemplates)
+        {
+            if (template.TryCreateConcreteMapping(genericTypeChecker, mappingKey.Source, mappingKey.Target) is not { } mapping)
+                continue;
+
+            // Cache the concrete mapping so subsequent lookups find it directly.
+            _newInstanceMappings.TryAddAsDefault(mapping, mappingKey.Configuration);
+            return mapping;
+        }
+
+        return null;
+    }
+
+    private IExistingTargetMapping? TryMatchGenericExistingTargetTemplate(TypeMappingKey mappingKey)
+    {
+        if (_genericExistingTargetTemplates.Count == 0)
+            return null;
+
+        foreach (var template in _genericExistingTargetTemplates)
+        {
+            if (template.TryCreateConcreteMapping(genericTypeChecker, mappingKey.Source, mappingKey.Target) is not { } mapping)
+                continue;
+
+            // Cache the concrete mapping so subsequent lookups find it directly.
+            _existingTargetMappings.TryAddAsDefault(mapping, mappingKey.Configuration);
+            return mapping;
+        }
+
+        return null;
+    }
+
     private class MappingCollectionInstance<T, TUserMapping>
         where T : ITypeMapping
         where TUserMapping : T, IUserMapping
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs
new file mode 100644
index 0000000000..d1654dbacd
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs
@@ -0,0 +1,74 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Helpers;
+using Riok.Mapperly.Symbols;
+
+namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
+
+/// <summary>
+/// A template for a generic user-implemented existing target mapping method.
+/// This is not a concrete mapping but stores the information needed to create
+/// concrete instantiations when a matching type pair is found.
+/// The <see cref="Default"/> is always <c>false</c> so it is not added to the default mappings dictionary.
+/// </summary>
+internal sealed class GenericUserImplementedExistingTargetMethodMapping(
+    string? receiver,
+    IMethodSymbol method,
+    MethodParameter sourceParameter,
+    MethodParameter targetParameter,
+    MethodParameter? referenceHandlerParameter,
+    bool isExternal
+) : IExistingTargetUserMapping
+{
+    public ITypeSymbol SourceType => sourceParameter.Type;
+
+    public ITypeSymbol TargetType => targetParameter.Type;
+
+    public IMethodSymbol Method => method;
+
+    public bool? Default => false;
+
+    public bool IsExternal => isExternal;
+
+    public bool IsSynthetic => false;
+
+    public IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config) => [];
+
+    public IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target) =>
+        throw new InvalidOperationException("Generic mapping template should not be built directly");
+
+    /// <summary>
+    /// Tries to create a concrete mapping for the given source and target types
+    /// by inferring the type arguments from the generic method signature.
+    /// </summary>
+    public UserImplementedGenericExistingTargetMethodMapping? TryCreateConcreteMapping(
+        GenericTypeChecker checker,
+        ITypeSymbol concreteSourceType,
+        ITypeSymbol concreteTargetType
+    )
+    {
+        var result = checker.InferAndCheckTypes(
+            method.TypeParameters,
+            (sourceParameter.Type, concreteSourceType),
+            (targetParameter.Type, concreteTargetType)
+        );
+
+        if (!result.Success)
+            return null;
+
+        var typeArguments = method.TypeParameters.Select(tp => result.InferredTypes[tp]).ToList();
+
+        return new UserImplementedGenericExistingTargetMethodMapping(
+            receiver,
+            method,
+            null,
+            sourceParameter,
+            targetParameter,
+            concreteSourceType,
+            concreteTargetType,
+            typeArguments,
+            referenceHandlerParameter,
+            isExternal
+        );
+    }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs
new file mode 100644
index 0000000000..12c66b7531
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs
@@ -0,0 +1,75 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Helpers;
+using Riok.Mapperly.Symbols;
+
+namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
+
+/// <summary>
+/// A template for a generic user-implemented new instance mapping method.
+/// This is not a concrete mapping but stores the information needed to create
+/// concrete instantiations when a matching type pair is found.
+/// The <see cref="Default"/> is always <c>false</c> so it is not added to the default mappings dictionary.
+/// </summary>
+internal sealed class GenericUserImplementedNewInstanceMethodMapping(
+    string? receiver,
+    IMethodSymbol method,
+    MethodParameter sourceParameter,
+    ITypeSymbol genericTargetType,
+    MethodParameter? referenceHandlerParameter,
+    bool isExternal,
+    UserImplementedMethodMapping.TargetNullability targetNullability
+) : INewInstanceUserMapping
+{
+    public ITypeSymbol SourceType => sourceParameter.Type;
+
+    public ITypeSymbol TargetType => genericTargetType;
+
+    public IMethodSymbol Method => method;
+
+    public bool? Default => false;
+
+    public bool IsExternal => isExternal;
+
+    public bool IsSynthetic => false;
+
+    public IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappingConfiguration config) => [];
+
+    public ExpressionSyntax Build(TypeMappingBuildContext ctx) =>
+        throw new InvalidOperationException("Generic mapping template should not be built directly");
+
+    /// <summary>
+    /// Tries to create a concrete mapping for the given source and target types
+    /// by inferring the type arguments from the generic method signature.
+    /// </summary>
+    public UserImplementedGenericMethodMapping? TryCreateConcreteMapping(
+        GenericTypeChecker checker,
+        ITypeSymbol concreteSourceType,
+        ITypeSymbol concreteTargetType
+    )
+    {
+        var result = checker.InferAndCheckTypes(
+            method.TypeParameters,
+            (sourceParameter.Type, concreteSourceType),
+            (genericTargetType, concreteTargetType)
+        );
+
+        if (!result.Success)
+            return null;
+
+        var typeArguments = method.TypeParameters.Select(tp => result.InferredTypes[tp]).ToList();
+
+        return new UserImplementedGenericMethodMapping(
+            receiver,
+            method,
+            null,
+            sourceParameter,
+            concreteSourceType,
+            concreteTargetType,
+            typeArguments,
+            referenceHandlerParameter,
+            isExternal,
+            targetNullability
+        );
+    }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs
index ea17eb443a..4ef478957c 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs
@@ -1,4 +1,5 @@
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
 using Riok.Mapperly.Symbols;
@@ -16,9 +17,11 @@ public class UserImplementedExistingTargetMethodMapping(
     bool? isDefault,
     MethodParameter sourceParameter,
     MethodParameter targetParameter,
+    ITypeSymbol sourceType,
+    ITypeSymbol targetType,
     MethodParameter? referenceHandlerParameter,
     bool isExternal
-) : ExistingTargetMapping(method.Parameters[0].Type, targetParameter.Type), IExistingTargetUserMapping, IParameterizedMapping
+) : ExistingTargetMapping(sourceType, targetType), IExistingTargetUserMapping, IParameterizedMapping
 {
     public IMethodSymbol Method { get; } = method;
 
@@ -38,6 +41,8 @@ bool isExternal
 
     public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
     {
+        var methodName = BuildMethodName();
+
         // if the user implemented method is on an interface,
         // we explicitly cast to be able to use the default interface implementation or explicit implementations
         if (Method.ReceiverType?.TypeKind != TypeKind.Interface)
@@ -49,7 +54,7 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
                 yield return ctx.SyntaxFactory.DeclareLocalVariable(targetRefVarName, target);
                 yield return ctx.SyntaxFactory.ExpressionStatement(
                     ctx.SyntaxFactory.Invocation(
-                        receiver == null ? IdentifierName(Method.Name) : MemberAccess(receiver, Method.Name),
+                        GetMethodExpr(),
                         ctx.BuildArguments(
                             Method,
                             sourceParameter,
@@ -65,23 +70,34 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
 
             yield return ctx.SyntaxFactory.ExpressionStatement(
                 ctx.SyntaxFactory.Invocation(
-                    receiver == null ? IdentifierName(Method.Name) : MemberAccess(receiver, Method.Name),
+                    GetMethodExpr(),
                     ctx.BuildArguments(Method, sourceParameter, referenceHandlerParameter, targetParameter.WithArgument(target))
                 )
             );
             yield break;
+
+            ExpressionSyntax GetMethodExpr() =>
+                receiver == null
+                    ? methodName
+                    : MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName(receiver), methodName);
         }
 
         var castedThis = CastExpression(
             FullyQualifiedIdentifier(Method.ReceiverType!),
             receiver != null ? IdentifierName(receiver) : ThisExpression()
         );
-        var methodExpr = MemberAccess(ParenthesizedExpression(castedThis), Method.Name);
+        var castedMethodExpr = MemberAccessExpression(
+            SyntaxKind.SimpleMemberAccessExpression,
+            ParenthesizedExpression(castedThis),
+            methodName
+        );
         yield return ctx.SyntaxFactory.ExpressionStatement(
             ctx.SyntaxFactory.Invocation(
-                methodExpr,
+                castedMethodExpr,
                 ctx.BuildArguments(Method, sourceParameter, referenceHandlerParameter, targetParameter.WithArgument(target))
             )
         );
     }
+
+    protected virtual SimpleNameSyntax BuildMethodName() => IdentifierName(Method.Name);
 }
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs
new file mode 100644
index 0000000000..6fd8b3981c
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs
@@ -0,0 +1,42 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Symbols;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
+
+/// <summary>
+/// Represents a generic user-implemented existing target mapping method.
+/// Generates invocations with explicit type arguments.
+/// </summary>
+public class UserImplementedGenericExistingTargetMethodMapping(
+    string? receiver,
+    IMethodSymbol method,
+    bool? isDefault,
+    MethodParameter sourceParameter,
+    MethodParameter targetParameter,
+    ITypeSymbol concreteSourceType,
+    ITypeSymbol concreteTargetType,
+    IReadOnlyList<ITypeSymbol> typeArguments,
+    MethodParameter? referenceHandlerParameter,
+    bool isExternal
+)
+    : UserImplementedExistingTargetMethodMapping(
+        receiver,
+        method,
+        isDefault,
+        sourceParameter,
+        targetParameter,
+        concreteSourceType,
+        concreteTargetType,
+        referenceHandlerParameter,
+        isExternal
+    )
+{
+    protected override SimpleNameSyntax BuildMethodName()
+    {
+        var typeArgs = typeArguments.Select(TypeSyntax (t) => NonNullableIdentifier(t)).ToArray();
+        return GenericName(Method.Name).WithTypeArgumentList(TypeArgumentList(typeArgs));
+    }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs
new file mode 100644
index 0000000000..555c9e581c
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs
@@ -0,0 +1,42 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Riok.Mapperly.Symbols;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;
+
+/// <summary>
+/// Represents a generic user-implemented mapping method.
+/// Generates invocations with explicit type arguments, e.g. <c>MapOptional&lt;UserDto, User&gt;(source.Value)</c>.
+/// </summary>
+public class UserImplementedGenericMethodMapping(
+    string? receiver,
+    IMethodSymbol method,
+    bool? isDefault,
+    MethodParameter sourceParameter,
+    ITypeSymbol concreteSourceType,
+    ITypeSymbol concreteTargetType,
+    IReadOnlyList<ITypeSymbol> typeArguments,
+    MethodParameter? referenceHandlerParameter,
+    bool isExternal,
+    UserImplementedMethodMapping.TargetNullability targetNullability
+)
+    : UserImplementedMethodMapping(
+        receiver,
+        method,
+        isDefault,
+        sourceParameter,
+        concreteSourceType,
+        concreteTargetType,
+        referenceHandlerParameter,
+        isExternal,
+        targetNullability
+    )
+{
+    protected override SimpleNameSyntax BuildMethodName()
+    {
+        var typeArgs = typeArguments.Select(TypeSyntax (t) => NonNullableIdentifier(t)).ToArray();
+        return GenericName(Method.Name).WithTypeArgumentList(TypeArgumentList(typeArgs));
+    }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs
index 1735fe36fc..a28fc1f030 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs
@@ -1,4 +1,5 @@
 using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Riok.Mapperly.Helpers;
 using Riok.Mapperly.Symbols;
@@ -15,11 +16,12 @@ public class UserImplementedMethodMapping(
     IMethodSymbol method,
     bool? isDefault,
     MethodParameter sourceParameter,
+    ITypeSymbol sourceType,
     ITypeSymbol targetType,
     MethodParameter? referenceHandlerParameter,
     bool isExternal,
     UserImplementedMethodMapping.TargetNullability targetNullability
-) : NewInstanceMapping(sourceParameter.Type, targetType), INewInstanceUserMapping, IParameterizedMapping
+) : NewInstanceMapping(sourceType, targetType), INewInstanceUserMapping, IParameterizedMapping
 {
     public enum TargetNullability
     {
@@ -61,21 +63,30 @@ public override IEnumerable<TypeMappingKey> BuildAdditionalMappingKeys(TypeMappi
 
     public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
     {
+        var methodName = BuildMethodName();
+
         // if the user implemented method is on an interface,
         // we explicitly cast to be able to use the default interface implementation or explicit implementations
         if (Method.ReceiverType?.TypeKind != TypeKind.Interface)
         {
-            return ctx.SyntaxFactory.Invocation(
-                receiver == null ? IdentifierName(Method.Name) : MemberAccess(receiver, Method.Name),
-                ctx.BuildArguments(Method, sourceParameter, referenceHandlerParameter)
-            );
+            ExpressionSyntax methodExpr =
+                receiver == null
+                    ? methodName
+                    : MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, IdentifierName(receiver), methodName);
+            return ctx.SyntaxFactory.Invocation(methodExpr, ctx.BuildArguments(Method, sourceParameter, referenceHandlerParameter));
         }
 
         var castedReceiver = CastExpression(
             FullyQualifiedIdentifier(Method.ReceiverType!),
             receiver == null ? ThisExpression() : IdentifierName(receiver)
         );
-        var methodExpr = MemberAccess(ParenthesizedExpression(castedReceiver), Method.Name);
-        return ctx.SyntaxFactory.Invocation(methodExpr, ctx.BuildArguments(Method, sourceParameter, referenceHandlerParameter));
+        var castedMethodExpr = MemberAccessExpression(
+            SyntaxKind.SimpleMemberAccessExpression,
+            ParenthesizedExpression(castedReceiver),
+            methodName
+        );
+        return ctx.SyntaxFactory.Invocation(castedMethodExpr, ctx.BuildArguments(Method, sourceParameter, referenceHandlerParameter));
     }
+
+    protected virtual SimpleNameSyntax BuildMethodName() => IdentifierName(Method.Name);
 }
diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
index 438d9be3c9..c2fce437bb 100644
--- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
+++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs
@@ -167,7 +167,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM
     )
     {
         var userMappingConfig = GetUserMappingConfig(ctx, method, out var hasAttribute);
-        var valid = !method.IsGenericMethod && (allowPartial || !method.IsPartialDefinition) && (!isStatic || method.IsStatic);
+        var valid = (allowPartial || !method.IsPartialDefinition) && (!isStatic || method.IsStatic);
 
         if (!valid || !UserMappingMethodParameterExtractor.BuildParameters(ctx, method, out var parameters))
         {
@@ -182,6 +182,20 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM
         if (userMappingConfig.Ignore == true)
             return null;
 
+        // Generic user-implemented methods are stored as templates
+        // that are matched against concrete type pairs during mapping resolution.
+        if (method.IsGenericMethod)
+        {
+            return BuildGenericUserImplementedMapping(
+                ctx,
+                method,
+                receiver,
+                parameters,
+                isExternal,
+                userMappingConfig.Default ?? isDefault
+            );
+        }
+
         if (method.ReturnsVoid)
         {
             return new UserImplementedExistingTargetMethodMapping(
@@ -190,6 +204,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM
                 userMappingConfig.Default ?? isDefault,
                 parameters.Source,
                 parameters.Target!.Value,
+                parameters.Source.Type,
+                parameters.Target!.Value.Type,
                 parameters.ReferenceHandler,
                 isExternal
             );
@@ -201,6 +217,43 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM
             method,
             userMappingConfig.Default ?? isDefault,
             parameters.Source,
+            parameters.Source.Type,
+            targetType,
+            parameters.ReferenceHandler,
+            isExternal,
+            targetTypeNullability
+        );
+    }
+
+    private static IUserMapping? BuildGenericUserImplementedMapping(
+        SimpleMappingBuilderContext ctx,
+        IMethodSymbol method,
+        string? receiver,
+        MappingMethodParameters parameters,
+        bool isExternal,
+        bool? isDefault
+    )
+    {
+        if (method.ReturnsVoid)
+        {
+            if (parameters.Target is not { } targetParam)
+                return null;
+
+            return new GenericUserImplementedExistingTargetMethodMapping(
+                receiver,
+                method,
+                parameters.Source,
+                targetParam,
+                parameters.ReferenceHandler,
+                isExternal
+            );
+        }
+
+        var (targetType, targetTypeNullability) = BuildTargetType(ctx, method, parameters.Source.Name);
+        return new GenericUserImplementedNewInstanceMethodMapping(
+            receiver,
+            method,
+            parameters.Source,
             targetType,
             parameters.ReferenceHandler,
             isExternal,


Test patch:
diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/DocumentDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/DocumentDto.cs
new file mode 100644
index 0000000000..922e0d5400
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Dto/DocumentDto.cs
@@ -0,0 +1,8 @@
+using Riok.Mapperly.IntegrationTests.Models;
+
+namespace Riok.Mapperly.IntegrationTests.Dto
+{
+    public record DocumentDto(string Title, UserDto CreatedBy, Optional<UserDto> ModifiedBy);
+
+    public record UserDto(string Name);
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/GenericUserMethodMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/GenericUserMethodMapperTest.cs
new file mode 100644
index 0000000000..af5ba33719
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/GenericUserMethodMapperTest.cs
@@ -0,0 +1,46 @@
+using System.Threading.Tasks;
+using Riok.Mapperly.IntegrationTests.Helpers;
+using Riok.Mapperly.IntegrationTests.Mapper;
+using Riok.Mapperly.IntegrationTests.Models;
+using Shouldly;
+using VerifyXunit;
+using Xunit;
+
+namespace Riok.Mapperly.IntegrationTests
+{
+    public class GenericUserMethodMapperTest : BaseMapperTest
+    {
+        [Fact]
+        [VersionedSnapshot(Versions.NET8_0)]
+        public Task SnapshotGeneratedSource()
+        {
+            var path = GetGeneratedMapperFilePath(nameof(GenericUserMethodMapper));
+            return Verifier.VerifyFile(path);
+        }
+
+        [Fact]
+        public void RunMappingShouldWork()
+        {
+            var mapper = new GenericUserMethodMapper();
+            var model = new Document("Design Doc", new User("Alice"), Optional.Of(new User("Bob")));
+            var dto = mapper.MapDocument(model);
+
+            dto.Title.ShouldBe("Design Doc");
+            dto.CreatedBy.Name.ShouldBe("Alice");
+            dto.ModifiedBy.HasValue.ShouldBeTrue();
+            dto.ModifiedBy.Value.Name.ShouldBe("Bob");
+        }
+
+        [Fact]
+        public void RunMappingWithEmptyOptionalShouldWork()
+        {
+            var mapper = new GenericUserMethodMapper();
+            var model = new Document("Draft", new User("Charlie"), Optional.Empty<User>());
+            var dto = mapper.MapDocument(model);
+
+            dto.Title.ShouldBe("Draft");
+            dto.CreatedBy.Name.ShouldBe("Charlie");
+            dto.ModifiedBy.HasValue.ShouldBeFalse();
+        }
+    }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/GenericUserMethodMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/GenericUserMethodMapper.cs
new file mode 100644
index 0000000000..fc6c550414
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Mapper/GenericUserMethodMapper.cs
@@ -0,0 +1,22 @@
+using Riok.Mapperly.Abstractions;
+using Riok.Mapperly.IntegrationTests.Dto;
+using Riok.Mapperly.IntegrationTests.Models;
+
+namespace Riok.Mapperly.IntegrationTests.Mapper
+{
+    [Mapper]
+    public partial class GenericUserMethodMapper
+    {
+        public partial DocumentDto MapDocument(Document source);
+
+        private Optional<TTarget> MapOptional<TSource, TTarget>(Optional<TSource> source)
+            where TSource : notnull
+            where TTarget : notnull => source.HasValue ? Optional.Of(Map<TSource, TTarget>(source.Value)) : Optional.Empty<TTarget>();
+
+        private partial TTarget Map<TSource, TTarget>(TSource source)
+            where TSource : notnull
+            where TTarget : notnull;
+
+        private partial UserDto MapUser(User source);
+    }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/Document.cs b/test/Riok.Mapperly.IntegrationTests/Models/Document.cs
new file mode 100644
index 0000000000..fcdc9e4a91
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Models/Document.cs
@@ -0,0 +1,6 @@
+namespace Riok.Mapperly.IntegrationTests.Models
+{
+    public record Document(string Title, User CreatedBy, Optional<User> ModifiedBy);
+
+    public record User(string Name);
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/Models/Optional.cs b/test/Riok.Mapperly.IntegrationTests/Models/Optional.cs
new file mode 100644
index 0000000000..d06b446b9c
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/Models/Optional.cs
@@ -0,0 +1,30 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Riok.Mapperly.IntegrationTests.Models
+{
+    public static class Optional
+    {
+        public static Optional<T> Of<T>(T value)
+            where T : notnull => new(value);
+
+        public static Optional<T> Empty<T>()
+            where T : notnull => new(default);
+    }
+
+    public class Optional<T>
+        where T : notnull
+    {
+        public Optional(T? value)
+        {
+            HasValue = value is not null;
+            Value = value;
+        }
+
+#if NET5_0_OR_GREATER
+        [MemberNotNullWhen(true, nameof(Value))]
+#endif
+        public bool HasValue { get; }
+
+        public T? Value { get; }
+    }
+}
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/GenericUserMethodMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/GenericUserMethodMapperTest.SnapshotGeneratedSource.verified.cs
new file mode 100644
index 0000000000..55b4b74645
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/GenericUserMethodMapperTest.SnapshotGeneratedSource.verified.cs
@@ -0,0 +1,37 @@
+// <auto-generated />
+#nullable enable annotations
+namespace Riok.Mapperly.IntegrationTests.Mapper
+{
+    public partial class GenericUserMethodMapper
+    {
+        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+        public partial global::Riok.Mapperly.IntegrationTests.Dto.DocumentDto MapDocument(global::Riok.Mapperly.IntegrationTests.Models.Document source)
+        {
+            var target = new global::Riok.Mapperly.IntegrationTests.Dto.DocumentDto(
+                source.Title,
+                MapUser(source.CreatedBy),
+                MapOptional<global::Riok.Mapperly.IntegrationTests.Models.User, global::Riok.Mapperly.IntegrationTests.Dto.UserDto>(source.ModifiedBy)
+            );
+            return target;
+        }
+
+        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+        private partial TTarget Map<TSource, TTarget>(TSource source) where TSource : notnull where TTarget : notnull
+        {
+            return source switch
+            {
+                global::Riok.Mapperly.IntegrationTests.Models.Document x when typeof(TTarget).IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.DocumentDto)) => (TTarget)(object)MapDocument(x),
+                global::Riok.Mapperly.IntegrationTests.Models.User x when typeof(TTarget).IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.UserDto)) => (TTarget)(object)MapUser(x),
+                global::Riok.Mapperly.IntegrationTests.Models.AssemblyScopedModel x when typeof(TTarget).IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.AssemblyScopedDto)) => (TTarget)(object)global::Riok.Mapperly.IntegrationTests.Mapper.AssemblyScopedMappers.ToDto(x),
+                _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)),
+            };
+        }
+
+        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+        private partial global::Riok.Mapperly.IntegrationTests.Dto.UserDto MapUser(global::Riok.Mapperly.IntegrationTests.Models.User source)
+        {
+            var target = new global::Riok.Mapperly.IntegrationTests.Dto.UserDto(source.Name);
+            return target;
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/GenericUserMethodMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/GenericUserMethodMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
new file mode 100644
index 0000000000..7aacc9cce1
--- /dev/null
+++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/GenericUserMethodMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs
@@ -0,0 +1,37 @@
+// <auto-generated />
+#nullable enable
+namespace Riok.Mapperly.IntegrationTests.Mapper
+{
+    public partial class GenericUserMethodMapper
+    {
+        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+        public partial global::Riok.Mapperly.IntegrationTests.Dto.DocumentDto MapDocument(global::Riok.Mapperly.IntegrationTests.Models.Document source)
+        {
+            var target = new global::Riok.Mapperly.IntegrationTests.Dto.DocumentDto(
+                source.Title,
+                MapUser(source.CreatedBy),
+                MapOptional<global::Riok.Mapperly.IntegrationTests.Models.User, global::Riok.Mapperly.IntegrationTests.Dto.UserDto>(source.ModifiedBy)
+            );
+            return target;
+        }
+
+        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+        private partial TTarget Map<TSource, TTarget>(TSource source) where TSource : notnull where TTarget : notnull
+        {
+            return source switch
+            {
+                global::Riok.Mapperly.IntegrationTests.Models.Document x when typeof(TTarget).IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.DocumentDto)) => (TTarget)(object)MapDocument(x),
+                global::Riok.Mapperly.IntegrationTests.Models.User x when typeof(TTarget).IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.UserDto)) => (TTarget)(object)MapUser(x),
+                global::Riok.Mapperly.IntegrationTests.Models.AssemblyScopedModel x when typeof(TTarget).IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.AssemblyScopedDto)) => (TTarget)(object)global::Riok.Mapperly.IntegrationTests.Mapper.AssemblyScopedMappers.ToDto(x),
+                _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)),
+            };
+        }
+
+        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+        private partial global::Riok.Mapperly.IntegrationTests.Dto.UserDto MapUser(global::Riok.Mapperly.IntegrationTests.Models.User source)
+        {
+            var target = new global::Riok.Mapperly.IntegrationTests.Dto.UserDto(source.Name);
+            return target;
+        }
+    }
+}
diff --git a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
index 79dd68fd63..26c4343aaa 100644
--- a/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/UserMethodTest.cs
@@ -804,4 +804,422 @@ public void MappingMethodWithSingleTargetParameterShouldDiagnostic()
             .HaveDiagnostic(DiagnosticDescriptors.UnsupportedMappingMethodSignature, "Map has an unsupported mapping method signature")
             .HaveAssertedAllDiagnostics();
     }
+
+    [Fact]
+    public void WithGenericSourceTypeConstraintAndUserImplemented()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial To Map(From source);
+
+            public OtherValue PickMe<TSource>(TSource source)
+                where TSource : IValue => new OtherValue { Value = source.Value };
+            """,
+            "public interface IValue { public string Value { get; } }",
+            "public class OtherValue { public string Value { get; set; } }",
+            "public class A : IValue { public string Value { get; set; } }",
+            "public class B : IValue { public string Value { get; set; } }",
+            "public class From { public A AV { get; set; } public B BV { get; set; } }",
+            "public class To { public OtherValue AV { get; set; } public OtherValue BV { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::To();
+                target.AV = PickMe<global::A>(source.AV);
+                target.BV = PickMe<global::B>(source.BV);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public void WithGenericSourceAndTargetTypeAndUserImplemented()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial DocumentDto Map(Document source);
+
+            private UserDto MapUser(User source) => new UserDto { Id = source.Id };
+
+            private Optional<TTarget> MapOptional<TSource, TTarget>(Optional<TSource> source)
+                where TSource : notnull
+                where TTarget : notnull
+                => new Optional<TTarget> { Value = MapUser(source.Value) };
+            """,
+            "public class Optional<T> where T : notnull { public T? Value { get; set; } }",
+            "public class User { public string Id { get; set; } }",
+            "public class UserDto { public string Id { get; set; } }",
+            "public class Document { public Optional<User> ModifiedBy { get; set; } }",
+            "public class DocumentDto { public Optional<UserDto> ModifiedBy { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::DocumentDto();
+                target.ModifiedBy = MapOptional<global::User, global::UserDto>(source.ModifiedBy);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public Task WithGenericUserImplementedOptionalSample()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            public partial DocumentDto MapDocument(Document source);
+
+            private Optional<TTarget> MapOptional<TSource, TTarget>(Optional<TSource> source)
+                where TSource : notnull
+                where TTarget : notnull
+                => source.HasValue
+                    ? Optional.Of(Map(source.Value))
+                    : Optional.Empty();
+
+            private partial TTarget Map<TSource, TTarget>(TSource source)
+                where TSource : notnull
+                where TTarget : notnull;
+
+            private partial UserDto MapUser(User source);
+            """,
+            "public record Document(string Title, User CreatedBy, Optional<User> ModifiedBy);",
+            "public record DocumentDto(string Title, UserDto CreatedBy, Optional<UserDto> ModifiedBy);",
+            "public record User(string Name);",
+            "public record UserDto(string Name);",
+            "public readonly record struct EmptyOptional;",
+            "public static class Optional { public static Optional<T> Of<T>(T value) where T : notnull => new(value); public static EmptyOptional Empty() => default; }",
+            "public class Optional<T> where T : notnull { public Optional() {} public Optional(T value) { HasValue = true; Value = value; } public bool HasValue { get; } public T Value { get; } public static implicit operator Optional<T>(EmptyOptional _) => new(); }"
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
+
+    [Fact]
+    public void WithGenericSourceTypeConstraintsAndUserImplementedGenericAndNonGeneric()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial To Map(From source);
+
+            public OtherValue PickMeForAAndB<TSource>(TSource source)
+                where TSource : IValue => new OtherValue { Value = source.Value };
+
+            public OtherValue PickMeForIValue(IValue source) => new OtherValue { Value = source.Value };
+            """,
+            "public interface IValue { public string Value { get; } }",
+            "public class OtherValue { public string Value { get; set; } }",
+            "public class A : IValue { public string Value { get; set; } }",
+            "public class B : IValue { public string Value { get; set; } }",
+            "public class From { public A AV { get; set; } public B BV { get; set; } public IValue IV { get; set; } }",
+            "public class To { public OtherValue AV { get; set; } public OtherValue BV { get; set; } public OtherValue IV { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::To();
+                target.AV = PickMeForAAndB<global::A>(source.AV);
+                target.BV = PickMeForAAndB<global::B>(source.BV);
+                target.IV = PickMeForIValue(source.IV);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public void WithGenericSourceAndTargetTypeParametersAndUserImplemented()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial ADto Map(A source);
+
+            private TTarget MapGeneric<TSource, TTarget>(TSource source)
+                where TSource : IMyInterface
+                where TTarget : new(), IMyOtherInterface
+                => new TTarget { MyValue = source.MyValue };
+            """,
+            "public interface IMyInterface { public string MyValue { get; } }",
+            "public interface IMyOtherInterface { public string MyValue { get; set; } }",
+            "public class B : IMyInterface { public string MyValue { get; set; } }",
+            "public class BDto : IMyOtherInterface { public string MyValue { get; set; } }",
+            "public class A { public B Nested { get; set; } }",
+            "public class ADto { public BDto Nested { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::ADto();
+                target.Nested = MapGeneric<global::B, global::BDto>(source.Nested);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public Task WithGenericUserImplementedAndReferenceHandling()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            private partial B Map(A a);
+
+            private Optional<TTarget> MapOptional<TSource, TTarget>(Optional<TSource> source)
+                where TSource : notnull
+                where TTarget : notnull
+                => new Optional<TTarget>();
+            """,
+            TestSourceBuilderOptions.WithReferenceHandling,
+            "class A { public A Parent { get; set; } public Optional<C> Value { get; set; } }",
+            "class B { public B Parent { get; set; } public Optional<D> Value { get; set; } }",
+            "class C { public string StringValue { get; set; } }",
+            "class D { public string StringValue { get; set; } }",
+            "class Optional<T> where T : notnull { public T Value { get; set; } }"
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
+
+    [Fact]
+    public Task WithGenericUserImplementedWithReferenceHandlerParameter()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            private partial B Map(A a);
+
+            private Optional<TTarget> MapOptional<TSource, TTarget>(Optional<TSource> source, [ReferenceHandler] IReferenceHandler refHandler)
+                where TSource : notnull
+                where TTarget : notnull
+                => new Optional<TTarget>();
+            """,
+            TestSourceBuilderOptions.WithReferenceHandling,
+            "class A { public A Parent { get; set; } public Optional<C> Value { get; set; } }",
+            "class B { public B Parent { get; set; } public Optional<D> Value { get; set; } }",
+            "class C { public string StringValue { get; set; } }",
+            "class D { public string StringValue { get; set; } }",
+            "class Optional<T> where T : notnull { public T Value { get; set; } }"
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
+
+    [Fact]
+    public void WithGenericUserImplementedAndAdditionalParameter()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial ADto Map(A source, int ctx);
+
+            private TTarget MapGeneric<TSource, TTarget>(TSource source)
+                where TSource : IEntity
+                where TTarget : new(), IDto
+                => new TTarget { Id = source.Id };
+            """,
+            "public interface IEntity { public int Id { get; } }",
+            "public interface IDto { public int Id { get; set; } }",
+            "public class B : IEntity { public int Id { get; set; } }",
+            "public class BDto : IDto { public int Id { get; set; } }",
+            "public class A { public B Nested { get; set; } }",
+            "public class ADto { public BDto Nested { get; set; } public int Ctx { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::ADto();
+                target.Nested = MapGeneric<global::B, global::BDto>(source.Nested);
+                target.Ctx = ctx;
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public Task WithGenericUserImplementedWithReferenceHandlerParameterAndRecursiveType()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            private partial B Map(A a);
+
+            private Wrapper<TTarget> MapWrapper<TSource, TTarget>(Wrapper<TSource> source, [ReferenceHandler] IReferenceHandler refHandler)
+                where TSource : notnull
+                where TTarget : notnull
+                => new Wrapper<TTarget>();
+            """,
+            TestSourceBuilderOptions.WithReferenceHandling,
+            "class A { public A Parent { get; set; } public Wrapper<C> Value { get; set; } }",
+            "class B { public B Parent { get; set; } public Wrapper<D> Value { get; set; } }",
+            "class C { public string StringValue { get; set; } }",
+            "class D { public string StringValue { get; set; } }",
+            "class Wrapper<T> where T : notnull { public T Inner { get; set; } }"
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
+
+    [Fact]
+    public void WithGenericUserImplementedExistingTarget()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial ADto Map(A source);
+
+            private void MapGeneric<TSource, TTarget>([MappingTarget] TTarget target, TSource source)
+                where TSource : IEntity
+                where TTarget : IDto
+            {
+                target.Id = source.Id;
+            }
+            """,
+            "public interface IEntity { public int Id { get; } public string Name { get; } }",
+            "public interface IDto { public int Id { get; set; } public string Name { get; set; } }",
+            "public class B : IEntity { public int Id { get; set; } public string Name { get; set; } }",
+            "public class BDto : IDto { public int Id { get; set; } public string Name { get; set; } }",
+            "public class A { public B Nested { get; set; } }",
+            "public class ADto { public BDto Nested { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::ADto();
+                target.Nested = MapToBDto(source.Nested);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public Task WithGenericUserImplementedExistingTargetAndReferenceHandling()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            private partial B Map(A a);
+
+            private void MapGeneric<TSource, TTarget>([MappingTarget] TTarget target, TSource source, [ReferenceHandler] IReferenceHandler refHandler)
+                where TSource : IEntity
+                where TTarget : IDto
+            {
+                target.Id = source.Id;
+            }
+            """,
+            TestSourceBuilderOptions.WithReferenceHandling,
+            "interface IEntity { public int Id { get; } }",
+            "interface IDto { public int Id { get; set; } }",
+            "class C : IEntity { public int Id { get; set; } public C Parent { get; set; } }",
+            "class D : IDto { public int Id { get; set; } public D Parent { get; set; } }",
+            "class A { public A Parent { get; set; } public C Value { get; set; } }",
+            "class B { public B Parent { get; set; } public D Value { get; set; } }"
+        );
+
+        return TestHelper.VerifyGenerator(source);
+    }
+
+    [Fact]
+    public void WithGenericUserImplementedMultipleTypeParametersPartialMatch()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial OuterDto Map(Outer source);
+
+            private Pair<TTarget1, TTarget2> MapPair<TSource1, TSource2, TTarget1, TTarget2>(Pair<TSource1, TSource2> source)
+                where TSource1 : notnull
+                where TSource2 : notnull
+                where TTarget1 : notnull
+                where TTarget2 : notnull
+                => new Pair<TTarget1, TTarget2>();
+            """,
+            "class Pair<T1, T2> where T1 : notnull where T2 : notnull { public T1 Left { get; set; } public T2 Right { get; set; } }",
+            "class X { public string Value { get; set; } }",
+            "class XDto { public string Value { get; set; } }",
+            "class Y { public int Value { get; set; } }",
+            "class YDto { public int Value { get; set; } }",
+            "class Outer { public Pair<X, Y> Items { get; set; } }",
+            "class OuterDto { public Pair<XDto, YDto> Items { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::OuterDto();
+                target.Items = MapPair<global::X, global::Y, global::XDto, global::YDto>(source.Items);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public void WithGenericUserImplementedOnStaticMapper()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            public static partial ADto Map(A source);
+
+            private static TTarget MapGeneric<TSource, TTarget>(TSource source)
+                where TSource : IEntity
+                where TTarget : new(), IDto
+                => new TTarget { Id = source.Id };
+            """,
+            TestSourceBuilderOptions.AsStatic,
+            "public interface IEntity { public int Id { get; } }",
+            "public interface IDto { public int Id { get; set; } }",
+            "public class B : IEntity { public int Id { get; set; } }",
+            "public class BDto : IDto { public int Id { get; set; } }",
+            "public class A { public B Nested { get; set; } }",
+            "public class ADto { public BDto Nested { get; set; } }"
+        );
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::ADto();
+                target.Nested = MapGeneric<global::B, global::BDto>(source.Nested);
+                return target;
+                """
+            );
+    }
+
+    [Fact]
+    public void WithGenericUserImplementedConstraintMismatchShouldNotMatch()
+    {
+        var source = TestSourceBuilder.MapperWithBodyAndTypes(
+            """
+            partial ADto Map(A source);
+
+            private TTarget MapGeneric<TSource, TTarget>(TSource source)
+                where TSource : ISpecialEntity
+                where TTarget : new(), IDto
+                => new TTarget { Id = source.Id };
+            """,
+            "public interface ISpecialEntity { public int Id { get; } }",
+            "public interface IDto { public int Id { get; set; } }",
+            "public class B { public int Id { get; set; } }",
+            "public class BDto : IDto { public int Id { get; set; } }",
+            "public class A { public B Nested { get; set; } }",
+            "public class ADto { public BDto Nested { get; set; } }"
+        );
+
+        // B does not implement ISpecialEntity, so the generic method should NOT match.
+        // Mapperly should fall back to generating a regular mapping.
+        TestHelper
+            .GenerateMapper(source)
+            .Should()
+            .HaveMapMethodBody(
+                """
+                var target = new global::ADto();
+                target.Nested = MapToBDto(source.Nested);
+                return target;
+                """
+            );
+    }
 }
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedAndReferenceHandling#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedAndReferenceHandling#Mapper.g.verified.cs
new file mode 100644
index 0000000000..89f15b3464
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedAndReferenceHandling#Mapper.g.verified.cs
@@ -0,0 +1,26 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::B Map(global::A a)
+    {
+        return MapToB(
+            a,
+            new global::Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler()
+        );
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler)
+    {
+        if (refHandler.TryGetReference<global::A, global::B>(source, out var existingTargetReference))
+            return existingTargetReference;
+        var target = new global::B();
+        refHandler.SetReference<global::A, global::B>(source, target);
+        target.Parent = MapToB(source.Parent, refHandler);
+        target.Value = MapOptional<global::C, global::D>(source.Value);
+        return target;
+    }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedExistingTargetAndReferenceHandling#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedExistingTargetAndReferenceHandling#Mapper.g.verified.cs
new file mode 100644
index 0000000000..22f4c52812
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedExistingTargetAndReferenceHandling#Mapper.g.verified.cs
@@ -0,0 +1,38 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::B Map(global::A a)
+    {
+        return MapToB(
+            a,
+            new global::Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler()
+        );
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler)
+    {
+        if (refHandler.TryGetReference<global::A, global::B>(source, out var existingTargetReference))
+            return existingTargetReference;
+        var target = new global::B();
+        refHandler.SetReference<global::A, global::B>(source, target);
+        target.Parent = MapToB(source.Parent, refHandler);
+        target.Value = MapToD(source.Value, refHandler);
+        return target;
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private global::D MapToD(global::C source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler)
+    {
+        if (refHandler.TryGetReference<global::C, global::D>(source, out var existingTargetReference))
+            return existingTargetReference;
+        var target = new global::D();
+        refHandler.SetReference<global::C, global::D>(source, target);
+        target.Id = source.Id;
+        target.Parent = MapToD(source.Parent, refHandler);
+        return target;
+    }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedOptionalSample#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedOptionalSample#Mapper.g.verified.cs
new file mode 100644
index 0000000000..46225a9663
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedOptionalSample#Mapper.g.verified.cs
@@ -0,0 +1,34 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    public partial global::DocumentDto MapDocument(global::Document source)
+    {
+        var target = new global::DocumentDto(
+            source.Title,
+            MapUser(source.CreatedBy),
+            MapOptional<global::User, global::UserDto>(source.ModifiedBy)
+        );
+        return target;
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial TTarget Map<TSource, TTarget>(TSource source) where TSource : notnull where TTarget : notnull
+    {
+        return source switch
+        {
+            global::Document x when typeof(TTarget).IsAssignableFrom(typeof(global::DocumentDto)) => (TTarget)(object)MapDocument(x),
+            global::User x when typeof(TTarget).IsAssignableFrom(typeof(global::UserDto)) => (TTarget)(object)MapUser(x),
+            _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)),
+        };
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::UserDto MapUser(global::User source)
+    {
+        var target = new global::UserDto(source.Name);
+        return target;
+    }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedWithReferenceHandlerParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedWithReferenceHandlerParameter#Mapper.g.verified.cs
new file mode 100644
index 0000000000..4617f178ef
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedWithReferenceHandlerParameter#Mapper.g.verified.cs
@@ -0,0 +1,26 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::B Map(global::A a)
+    {
+        return MapToB(
+            a,
+            new global::Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler()
+        );
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler)
+    {
+        if (refHandler.TryGetReference<global::A, global::B>(source, out var existingTargetReference))
+            return existingTargetReference;
+        var target = new global::B();
+        refHandler.SetReference<global::A, global::B>(source, target);
+        target.Parent = MapToB(source.Parent, refHandler);
+        target.Value = MapOptional<global::C, global::D>(source.Value, refHandler);
+        return target;
+    }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedWithReferenceHandlerParameterAndRecursiveType#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedWithReferenceHandlerParameterAndRecursiveType#Mapper.g.verified.cs
new file mode 100644
index 0000000000..e713f4d786
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/UserMethodTest.WithGenericUserImplementedWithReferenceHandlerParameterAndRecursiveType#Mapper.g.verified.cs
@@ -0,0 +1,26 @@
+﻿//HintName: Mapper.g.cs
+// <auto-generated />
+#nullable enable
+public partial class Mapper
+{
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private partial global::B Map(global::A a)
+    {
+        return MapToB(
+            a,
+            new global::Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler()
+        );
+    }
+
+    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+    private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler)
+    {
+        if (refHandler.TryGetReference<global::A, global::B>(source, out var existingTargetReference))
+            return existingTargetReference;
+        var target = new global::B();
+        refHandler.SetReference<global::A, global::B>(source, target);
+        target.Parent = MapToB(source.Parent, refHandler);
+        target.Value = MapWrapper<global::C, global::D>(source.Value, refHandler);
+        return target;
+    }
+}
\ No newline at end of file