From 32ee630e74d87be232259cc8517ba2777178fab6 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 13 Apr 2026 13:52:56 +0200 Subject: [PATCH] fix: generic and runtime target type mappings with derived types --- .../RuntimeTargetTypeMappingBodyBuilder.cs | 6 +++++- .../UserDefinedNewInstanceMethodMapping.cs | 5 ++++- ...UserDefinedNewInstanceRuntimeTargetTypeMapping.cs | 12 +++++++++--- .../Descriptors/UserMethodMappingExtractor.cs | 3 ++- .../Symbols/RuntimeTargetTypeMapping.cs | 2 +- ...erTest.SnapshotGeneratedSource_NET8_0.verified.cs | 6 +++--- .../Mapping/GenericDerivedTypeTest.cs | 2 +- .../Mapping/RuntimeTargetTypeMappingTest.cs | 2 +- 8 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs index faab2d9cf3..b87c836544 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/RuntimeTargetTypeMappingBodyBuilder.cs @@ -119,7 +119,11 @@ IEnumerable childMappings .ThenBy(x => x.TargetType.IsNullable()) .GroupBy(x => new TypeMappingKey(x, includeNullability: false)) .Select(x => x.First()) - .Select(x => new RuntimeTargetTypeMapping(x, ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target))) + .Select(x => new RuntimeTargetTypeMapping( + x, + ctx.Compilation.HasImplicitConversion(x.TargetType, ctx.Target), + (x as UserDefinedNewInstanceMethodMapping)?.IsDerivedTypeMapping == true + )) .ToList(); if (runtimeTargetTypeMappings.Count == 0) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index c07acb7f86..5ed1653a7e 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -15,13 +15,16 @@ public class UserDefinedNewInstanceMethodMapping( MethodParameter sourceParameter, MethodParameter? referenceHandlerParameter, ITypeSymbol targetType, - bool enableReferenceHandling + bool enableReferenceHandling, + bool isDerivedTypeMapping ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { private INewInstanceMapping? _delegateMapping; public new IMethodSymbol Method { get; } = method; + public bool IsDerivedTypeMapping => isDerivedTypeMapping; + private MethodMapping? DelegateMethodMapping => _delegateMapping as MethodMapping; public bool? Default { get; } = isDefault; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index a9837969b8..5580b16123 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -91,10 +91,16 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c protected virtual ExpressionSyntax? BuildSwitchArmWhenClause(ExpressionSyntax runtimeTargetType, RuntimeTargetTypeMapping mapping) { - // targetType.IsAssignableFrom(typeof(ADto)) + var mappingTargetType = TypeOfExpression(FullyQualifiedIdentifier(mapping.Mapping.TargetType.NonNullable())); + + // Derived type mapping: A => ADto (runtime target type) or A => BaseDto (runtime target type), BaseDto (mapping target type). + // typeof(BaseDto).IsAssignableFrom(runtimeTargetType) + + // Non-derived type mapping: A => ADto (runtime target type) or A => BaseDto (runtime target type), ADto (mapping target type). + // runtimeTargetType.IsAssignableFrom(ADto) return InvocationWithoutIndention( - MemberAccess(runtimeTargetType, IsAssignableFromMethodName), - TypeOfExpression(FullyQualifiedIdentifier(mapping.Mapping.TargetType.NonNullable())) + MemberAccess(mapping.IsDerivedTypeMapping ? mappingTargetType : runtimeTargetType, IsAssignableFromMethodName), + mapping.IsDerivedTypeMapping ? runtimeTargetType : mappingTargetType ); } diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index c2fce437bb..f4e629b09e 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -338,7 +338,8 @@ string sourceParameterName parameters.Source, parameters.ReferenceHandler, ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), - ctx.Configuration.Mapper.UseReferenceHandling + ctx.Configuration.Mapper.UseReferenceHandling, + ctx.AttributeAccessor.HasAttribute>(methodSymbol) ) { AdditionalSourceParameters = parameters.AdditionalParameters, diff --git a/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMapping.cs index 281cfa8a58..0b8bfd32a4 100644 --- a/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Symbols/RuntimeTargetTypeMapping.cs @@ -2,4 +2,4 @@ namespace Riok.Mapperly.Symbols; -public record RuntimeTargetTypeMapping(INewInstanceMapping Mapping, bool IsAssignableToMethodTargetType); +public record RuntimeTargetTypeMapping(INewInstanceMapping Mapping, bool IsAssignableToMethodTargetType, bool IsDerivedTypeMapping); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs index 9dee2a4f89..fc983811a9 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -790,7 +790,7 @@ string x when targetType.IsAssignableFrom(typeof(int)) => ParseableInt(x), global::Riok.Mapperly.IntegrationTests.Models.AssemblyScopedModel x when targetType.IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.AssemblyScopedDto)) => global::Riok.Mapperly.IntegrationTests.Mapper.AssemblyScopedMappers.ToDto(x), global::System.Collections.Generic.IReadOnlyCollection>> x when targetType.IsAssignableFrom(typeof(global::System.Collections.Generic.IReadOnlyList>>)) => MapNestedLists(x), global::System.Collections.Generic.IEnumerable x when targetType.IsAssignableFrom(typeof(global::System.Collections.Generic.IEnumerable)) => MapAllDtos(x), - object x when targetType.IsAssignableFrom(typeof(object)) => DerivedTypes(x), + object x when typeof(object).IsAssignableFrom(targetType) => DerivedTypes(x), _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; } @@ -817,7 +817,7 @@ string x when targetType.IsAssignableFrom(typeof(int)) => ParseableInt(x), global::Riok.Mapperly.IntegrationTests.Models.AssemblyScopedModel x when targetType.IsAssignableFrom(typeof(global::Riok.Mapperly.IntegrationTests.Dto.AssemblyScopedDto)) => global::Riok.Mapperly.IntegrationTests.Mapper.AssemblyScopedMappers.ToDto(x), global::System.Collections.Generic.IReadOnlyCollection>> x when targetType.IsAssignableFrom(typeof(global::System.Collections.Generic.IReadOnlyList>>)) => MapNestedLists(x), global::System.Collections.Generic.IEnumerable x when targetType.IsAssignableFrom(typeof(global::System.Collections.Generic.IEnumerable)) => MapAllDtos(x), - object x when targetType.IsAssignableFrom(typeof(object)) => DerivedTypes(x), + object x when typeof(object).IsAssignableFrom(targetType) => DerivedTypes(x), null => default, _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; @@ -845,7 +845,7 @@ string x when typeof(TTarget).IsAssignableFrom(typeof(int)) => (TTarget)(object) 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), global::System.Collections.Generic.IReadOnlyCollection>> x when typeof(TTarget).IsAssignableFrom(typeof(global::System.Collections.Generic.IReadOnlyList>>)) => (TTarget)(object)MapNestedLists(x), global::System.Collections.Generic.IEnumerable x when typeof(TTarget).IsAssignableFrom(typeof(global::System.Collections.Generic.IEnumerable)) => (TTarget)(object)MapAllDtos(x), - object x when typeof(TTarget).IsAssignableFrom(typeof(object)) => (TTarget)(object)DerivedTypes(x), + object x when typeof(object).IsAssignableFrom(typeof(TTarget)) => (TTarget)(object)DerivedTypes(x), null => throw new global::System.ArgumentNullException(nameof(source)), _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; diff --git a/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs b/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs index 3b2531e08f..23a6638688 100644 --- a/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/GenericDerivedTypeTest.cs @@ -29,7 +29,7 @@ public void GenericWithDerivedTypes() """ return source switch { - global::Base x when typeof(TTarget).IsAssignableFrom(typeof(global::BaseDto)) => (TTarget)(object)MapDerivedTypes(x), + global::Base x when typeof(global::BaseDto).IsAssignableFrom(typeof(TTarget)) => (TTarget)(object)MapDerivedTypes(x), null => throw new global::System.ArgumentNullException(nameof(source)), _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; diff --git a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs index 398d3082e6..1e072187ba 100644 --- a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs @@ -165,7 +165,7 @@ public void WithDerivedTypesShouldUseBaseType() """ 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), _ => throw new global::System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; """