Skip to content

fix: generic and runtime target type mappings with derived types#2230

Open
justi7n wants to merge 1 commit intoriok:mainfrom
justi7n:fix/polymorphic-mappings
Open

fix: generic and runtime target type mappings with derived types#2230
justi7n wants to merge 1 commit intoriok:mainfrom
justi7n:fix/polymorphic-mappings

Conversation

@justi7n
Copy link
Copy Markdown
Contributor

@justi7n justi7n commented Apr 13, 2026

Fix generic and runtime target type mappings with derived types

Description

If a user defined generic type or runtime target type mapping has a child mapping method which uses the [MapDerivedType] attribute, the parent method switch arm when clause was created wrongly (but only in that specific case). Fixes #1989

This got fixed in this PR

Fixes # (issue)

Checklist

  • I did not use AI tools to generate this PR, or I have manually verified that the code is correct, optimal, and follows the project guidelines and architecture
  • I understand that low-quality, AI-generated PRs will be closed immediately without further explanation
  • The existing code style is followed
  • The commit message follows our guidelines
  • Performed a self-review of my code
  • Hard-to-understand areas of my code are commented
  • The documentation is updated (as applicable)
  • Unit tests are added/updated
  • Integration tests are added/updated (as applicable, especially if feature/bug depends on roslyn or framework version in use)

@justi7n justi7n added the bug Something isn't working label Apr 13, 2026
@justi7n justi7n requested a review from latonz April 13, 2026 09:59
@justi7n justi7n force-pushed the fix/polymorphic-mappings branch from a7e108c to 32ee630 Compare April 13, 2026 11:53
Comment on lines +97 to +98
// If the mapping is a derived type mapping, the runtime target type is the source of the IsAssignableFrom check, otherwise it is the runtime target type.
// For example, when we map to ADto which is a BaseDto, it is typeof(BaseDto).IsAssignableFrom(typeof(ADto)).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't fully get this comment… either it is the runtime target type otherwise it is the runtime target type too?

.Select(x => new RuntimeTargetTypeMapping(
x,
ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target),
(x as UserDefinedNewInstanceMethodMapping)?.IsDerivedTypeMapping == true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Prefer pattern matching x is UserDefinedNewInstanceMethodMapping { IsDerivedTypeMapping: true }

@@ -92,9 +92,13 @@ public override IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext c
protected virtual ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax runtimeTargetType, RuntimeTargetTypeMapping mapping)
{
// targetType.IsAssignableFrom(typeof(ADto))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment isn't 100% accurate anymore, is it?

Comment on lines +100 to +101
MemberAccess(mapping.IsDerivedTypeMapping ? mappingTargetType : runtimeTargetType, IsAssignableFromMethodName),
mapping.IsDerivedTypeMapping ? runtimeTargetType : mappingTargetType
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This probably is a breaking change, isn't it? Document it in the breaking change docs.

return source switch
{
global::Base x when targetType.IsAssignableFrom(typeof(global::BaseDto)) => MapDerivedTypes(x),
global::Base x when typeof(global::BaseDto).IsAssignableFrom(targetType) => MapDerivedTypes(x),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Create a new test case reflecting the exact use-case from the referenced issue.

Comment on lines 150 to 151
[MapDerivedType<A, B>]
[MapDerivedType<C, D>]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add the exact same test but with [MapDerivedType(typeof(A), typeof(B))] instead of the generic one. I don't think it works as expected.

ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType),
ctx.Configuration.Mapper.UseReferenceHandling
ctx.Configuration.Mapper.UseReferenceHandling,
ctx.AttributeAccessor.HasAttribute<MapDerivedTypeAttribute<object, object>>(methodSymbol)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO the correct location to set this would be DerivedTypeMappingBuilder

@justi7n
Copy link
Copy Markdown
Contributor Author

justi7n commented Apr 14, 2026

Example for clarification:

Lets assume we have BananaDto : FruitDto, and FruitDto : OrganicDto. The previously released code in v4.3.1 would generate a code like

    public partial class DerivedTypeMapper
    {
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
        public partial T MapGeneric<T>(object source)
        {
            return source switch
            {
                // --- Relevant Part ---
                global::MapperlyRepro.Fruit x when typeof(T).IsAssignableFrom(typeof(global::MapperlyRepro.FruitDto)) => (T)(object)MapDerivedTypes(x),
                _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {typeof(T)} as there is no known type mapping", nameof(source)),
            };
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
        public partial global::MapperlyRepro.FruitDto MapDerivedTypes(global::MapperlyRepro.Fruit source)
        {
            return source switch
            {
                global::MapperlyRepro.Banana x => MapToBananaDto(x),
                _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to MapperlyRepro.FruitDto as there is no known derived type mapping", nameof(source)),
            };
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
        private global::MapperlyRepro.BananaDto MapToBananaDto(global::MapperlyRepro.Banana source)
        {
               ...
        }
    }

by

[Mapper]
public partial class DerivedTypeMapper
{
    public partial T MapGeneric<T>(object source);

    [MapDerivedType<Banana, BananaDto>]
    public partial FruitDto MapDerivedTypes(Fruit source);
}

So previously, the mapping would work for:

  • Banana => BananaDto.
  • Banana => FruitDto,
  • Banana => OrganicDto

If we reverse the AssignableFrom argument order (like currently in this PR), we would reverse the order of what would work:

  • Banana => BananaDto
  • Banana => FruitDto
  • Banana => OrganicDto

So previously, if someone called derivedTypeMapper.MapGeneric<BananaDto>(new Banana()) he would get an exception. After this PR this works and someone gets an exception after calling derivedTypeMapper.MapGeneric<OrganicDto>(new Banana()) (because the underlying mapping method is for FruitDto, if it should work for OrganicDto it has to be defined as public partial OrganicDto MapDerivedTypes(Organic source)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

polymorphic mappings via [MapDerivedType] on child [UseMapper] classes fail

2 participants