From 2e3a97ec5a4b8865ae80639b28fb1d53d33f5807 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 01/18] test(enum): reproduce enum by-name mapping with different underlying types --- test/Riok.Mapperly.Tests/Mapping/EnumTest.cs | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs index 112cc18884..897ca8e1e1 100644 --- a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs @@ -43,6 +43,36 @@ public void EnumToOtherEnumTypeShouldCast() .HaveSingleMethodBody("return (global::E2)source;"); } + [Fact] + public void EnumToOtherEnumByNameWithDifferentUnderlyingTypesShouldSwitch() + { + var source = TestSourceBuilder.Mapping( + "E1", + "E2", + TestSourceBuilderOptions.Default with + { + EnumMappingStrategy = EnumMappingStrategy.ByName, + }, + "enum E1 : short {A, B, C}", + "enum E2 : byte {A, B, C}" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E1.A => global::E2.A, + global::E1.B => global::E2.B, + global::E1.C => global::E2.C, + _ => throw new global::System.ArgumentOutOfRangeException(nameof(source), source, "The value of enum E1 is not supported"), + }; + """ + ); + } + [Fact] public void CustomClassToEnumWithBaseTypeCastShouldCast() { From b8b4e15f737fc62b0e80a149e80b150a455ad4c9 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 02/18] feat(abstractions): add MapperNoInliningAttribute --- .../MapperNoInliningAttribute.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Riok.Mapperly.Abstractions/MapperNoInliningAttribute.cs diff --git a/src/Riok.Mapperly.Abstractions/MapperNoInliningAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperNoInliningAttribute.cs new file mode 100644 index 0000000000..fdc66edcba --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperNoInliningAttribute.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace Riok.Mapperly.Abstractions; + +/// +/// Prevents a mapping method from being inlined into expression trees for queryable projection mappings. +/// When applied, the method call is preserved as-is instead of being rebuilt in expression context. +/// This is useful when inlining causes issues such as false enum mapping diagnostics +/// due to expression tree limitations. +/// +[AttributeUsage(AttributeTargets.Method)] +[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] +public sealed class MapperNoInliningAttribute : Attribute; From 02b4f4be63337190f4e6b94874c07387d741fc4c Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 03/18] feat(abstractions): add NoInlining property to MapperAttribute --- src/Riok.Mapperly.Abstractions/MapperAttribute.cs | 9 +++++++++ src/Riok.Mapperly/Configuration/MapperConfiguration.cs | 5 +++++ .../Configuration/MapperConfigurationMerger.cs | 3 +++ src/Riok.Mapperly/Riok.Mapperly.targets | 1 + 4 files changed, 18 insertions(+) diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index f0e1190379..5467f90767 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -142,4 +142,13 @@ public class MapperAttribute : Attribute /// partial methods are discovered. /// public bool AutoUserMappings { get; set; } = true; + + /// + /// Whether to prevent mapping methods of this mapper from being inlined + /// into expression trees for queryable projection mappings. + /// When true, methods from this mapper referenced via + /// will not be inlined or rebuilt in expression context. + /// Defaults to false. + /// + public bool NoInlining { get; set; } } diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 68893d366a..d22acc74da 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -130,6 +130,11 @@ public record MapperConfiguration /// public bool? AutoUserMappings { get; init; } + /// + /// Whether to prevent mapping methods of this mapper from being inlined into expression trees for queryable projection mappings. + /// + public bool? NoInlining { get; init; } + /// /// The default enum naming strategy. /// Can be overwritten on specific enums via mapping method configurations. diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index 4d625f49f3..5e475919fd 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -26,6 +26,7 @@ public static MapperConfiguration Merge(MapperConfiguration highPriority, Mapper IncludedConstructors = highPriority.IncludedConstructors ?? lowPriority.IncludedConstructors, PreferParameterlessConstructors = highPriority.PreferParameterlessConstructors ?? lowPriority.PreferParameterlessConstructors, AutoUserMappings = highPriority.AutoUserMappings ?? lowPriority.AutoUserMappings, + NoInlining = highPriority.NoInlining ?? lowPriority.NoInlining, EnumNamingStrategy = highPriority.EnumNamingStrategy ?? lowPriority.EnumNamingStrategy, }; } @@ -101,6 +102,8 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu mapper.AutoUserMappings = mapperConfiguration.AutoUserMappings ?? defaultMapperConfiguration.AutoUserMappings ?? mapper.AutoUserMappings; + mapper.NoInlining = mapperConfiguration.NoInlining ?? defaultMapperConfiguration.NoInlining ?? mapper.NoInlining; + mapper.EnumNamingStrategy = mapperConfiguration.EnumNamingStrategy ?? defaultMapperConfiguration.EnumNamingStrategy ?? mapper.EnumNamingStrategy; diff --git a/src/Riok.Mapperly/Riok.Mapperly.targets b/src/Riok.Mapperly/Riok.Mapperly.targets index 6a1a0f4e24..4e3eade38f 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.targets +++ b/src/Riok.Mapperly/Riok.Mapperly.targets @@ -23,5 +23,6 @@ + From c234b300b9c9ae64637e0e1a8ba03657d0cfdc15 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 04/18] test: update public API snapshot for MapperNoInlining --- .../PublicApiTest.PublicApiHasNotChanged.verified.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs index f2602782f8..d7dfc3dd50 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs @@ -138,6 +138,7 @@ public MapperAttribute() { } public Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } public Riok.Mapperly.Abstractions.MemberVisibility IncludedConstructors { get; set; } public Riok.Mapperly.Abstractions.MemberVisibility IncludedMembers { get; set; } + public bool NoInlining { get; set; } public bool PreferParameterlessConstructors { get; set; } public Riok.Mapperly.Abstractions.PropertyNameMappingStrategy PropertyNameMappingStrategy { get; set; } public Riok.Mapperly.Abstractions.RequiredMappingStrategy RequiredEnumMappingStrategy { get; set; } @@ -208,6 +209,12 @@ public MapperIgnoreTargetValueAttribute(object target) { } } [System.AttributeUsage(System.AttributeTargets.Method)] [System.Diagnostics.Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] + public sealed class MapperNoInliningAttribute : System.Attribute + { + public MapperNoInliningAttribute() { } + } + [System.AttributeUsage(System.AttributeTargets.Method)] + [System.Diagnostics.Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] public sealed class MapperRequiredMappingAttribute : System.Attribute { public MapperRequiredMappingAttribute(Riok.Mapperly.Abstractions.RequiredMappingStrategy requiredMappingStrategy) { } From 867ec8092a0698e18ce8e45777653b0daf49afa8 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 05/18] feat: implement MapperNoInlining inlining prevention --- .../InlineExpressionMappingBuilderContext.cs | 16 + .../Mapping/UseStaticMapperTest.cs | 422 ++++++++++++++++++ 2 files changed, 438 insertions(+) diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 2efbaf7938..436a62f408 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -199,6 +199,9 @@ protected override MappingBuilderContext ContextForMapping( private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) { + if (mapping is IUserMapping userMappingCheck && ShouldSkipInlining(userMappingCheck.Method)) + return mapping; + return mapping switch { // inline existing mapping @@ -216,6 +219,19 @@ private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) }; } + private bool ShouldSkipInlining(IMethodSymbol method) + { + if (SymbolAccessor.HasAttribute(method)) + return true; + + var containingType = method.ContainingType; + if (containingType == null) + return false; + + var mapperAttribute = AttributeAccessor.AccessFirstOrDefault(containingType); + return mapperAttribute?.NoInlining == true; + } + private INewInstanceMapping? InlineOrRebuild(UserImplementedMethodMapping mapping) { // Try inline first diff --git a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs index 3668ec1ff6..747bfe0bf9 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs @@ -773,4 +773,426 @@ public static partial class Mapper .Should() .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline); } + + [Fact] + public void ProjectionWithUseStaticEnumMapperByNameWithDifferentUnderlyingTypesShouldDiagnose() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostics( + DiagnosticDescriptors.SourceEnumValueNotMapped, + "Enum member Planned (0) on Source.Status not found on target enum Target.Status", + "Enum member Generated (1) on Source.Status not found on target enum Target.Status", + "Enum member Paused (2) on Source.Status not found on target enum Target.Status", + "Enum member Running (3) on Source.Status not found on target enum Target.Status" + ) + .HaveDiagnostics( + DiagnosticDescriptors.TargetEnumValueNotMapped, + "Enum member Planned (0) on Target.Status not found on source enum Source.Status", + "Enum member Generated (1) on Target.Status not found on source enum Source.Status", + "Enum member Paused (2) on Target.Status not found on source enum Source.Status", + "Enum member Running (3) on Target.Status not found on source enum Source.Status" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void ProjectionWithUseStaticEnumMapperByNameWithNoInliningShouldWork() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + [MapperNoInlining] + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } + + [Fact] + public void MapperNoInliningOnNonProjectionMethodShouldHaveNoEffect() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoInlining = true)] + public static partial class EnumMappingExtensions + { + [MapperNoInlining] + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ); + } + + [Fact] + public void ProjectionWithUsedMapperNoInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoInlining = true)] + public static partial class EnumMappingExtensions + { + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } + + [Fact] + public void ProjectionWithCalledMethodNoInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + [MapperNoInlining] + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } } From 230ccdc7a7f2df06be0ccceec6c18076716fef07 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 06/18] docs: document MapperNoInlining and NoInlining --- docs/docs/configuration/mapper.mdx | 14 +++ .../configuration/queryable-projections.mdx | 106 ++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index cfb012f76c..77cda840d6 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -70,6 +70,20 @@ public partial class MyMapper } ``` +### Inlining + +By default, Mapperly inlines mapping methods into expression trees when possible. +Set `NoInlining` to `true` to prevent mapping methods of this mapper from being inlined into expression trees +when referenced via `UseStaticMapper`. Defaults to `false`. + +```csharp +[Mapper(NoInlining = true)] +public partial class CarMapper +{ + // ... +} +``` + ## Properties / fields On each mapping method declaration, property and field mappings can be customized. diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index 55874bdd17..3d0fbd6be0 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -122,6 +122,112 @@ public static partial class CarMapper It is important that the types in the user-implemented mapping method match the types of the objects to be mapped exactly. Otherwise, Mapperly cannot resolve the user-implemented mapping methods. +## Controlling inlining + +Mapperly automatically tries to inline referenced mapping methods into expression trees for queryable projections. +In some cases, this inlining can cause issues — for example, when enum mappings with different underlying types +produce false diagnostics in expression context. + +You can prevent inlining of specific methods using the `MapperNoInlining` attribute, +or prevent inlining of all methods in a mapper using the `NoInlining` property on the `Mapper` attribute. + +### Per-method opt-out + +```csharp +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] +public static partial class EnumMapper +{ + // highlight-start + [MapperNoInlining] + // highlight-end + public static partial TargetStatus MapToStatus(this SourceStatus source); +} + +[Mapper] +[UseStaticMapper(typeof(EnumMapper))] +public static partial class Mapper +{ + public static partial IQueryable ProjectToDto(this IQueryable q); + public static partial CarDto MapToDto(this Car car); +} +``` + +### Per-mapper opt-out + +```csharp +// highlight-start +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoInlining = true)] +// highlight-end +public static partial class EnumMapper +{ + public static partial TargetStatus MapToStatus(this SourceStatus source); +} +``` + +### Runtime behavior + +The `NoInlining` setting changes how the projection is materialized at runtime. + + + + +With inlining enabled, Mapperly rebuilds the mapping logic directly into the expression tree. +The entire projection runs in SQL — but the rebuilt expression may produce incorrect results +for non-trivial type pairs (e.g., enum mappings with different underlying types): + +```csharp +// What runs under the hood: +var dtos = await dbContext.Cars + .Select(x => new CarDto + { + // highlight-start + // Runs entirely in SQL, but the cast may be wrong + // for enums with different underlying types + Status = (TargetStatus)x.Status, + // highlight-end + }) + .ToListAsync(); +``` + + + + +With inlining disabled, the mapping method call is preserved as-is. +The ORM cannot translate it to SQL, so Entity Framework will split the query +into a server-side projection and client-side mapping: + +```csharp +// What runs under the hood: +var dtos = await dbContext.Cars + .Select(x => new + { + // highlight-start + // Only the needed columns are fetched from the database + Status = x.Status, + // highlight-end + }) + // highlight-start + .AsEnumerable() // ← everything before runs in SQL, everything after is client-side + // highlight-end + .Select(x => new CarDto + { + // highlight-start + Status = EnumMapper.MapToStatus(x.Status), // ← runs in C# + // highlight-end + }) + .ToListAsync(); +``` + + + + +:::warning +When inlining is disabled, the mapping method call cannot be translated to SQL. +Entity Framework will evaluate it client-side, which means the relevant columns are still fetched +from the database but the mapping logic runs in C# instead of SQL. +Only use this when the default inlining behavior causes issues. +::: + ## Additional parameters Queryable projection methods can have [additional parameters](./additional-mapping-parameters.mdx). From 9a57c03ab2e8c1519fdfbf68041b4c2a634b4692 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Wed, 15 Apr 2026 20:54:59 +0200 Subject: [PATCH 07/18] fix: remove impossible null check for method containing type --- .../Descriptors/InlineExpressionMappingBuilderContext.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 436a62f408..f53f03086d 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -224,11 +224,7 @@ private bool ShouldSkipInlining(IMethodSymbol method) if (SymbolAccessor.HasAttribute(method)) return true; - var containingType = method.ContainingType; - if (containingType == null) - return false; - - var mapperAttribute = AttributeAccessor.AccessFirstOrDefault(containingType); + var mapperAttribute = AttributeAccessor.AccessFirstOrDefault(method.ContainingType); return mapperAttribute?.NoInlining == true; } From 63ff646af658682b4da8532d0592b98d1f04303b Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Sat, 18 Apr 2026 22:02:06 +0200 Subject: [PATCH 08/18] Refactor TryInlineMapping --- .../Descriptors/InlineExpressionMappingBuilderContext.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index f53f03086d..5841da0be4 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -199,11 +199,11 @@ protected override MappingBuilderContext ContextForMapping( private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) { - if (mapping is IUserMapping userMappingCheck && ShouldSkipInlining(userMappingCheck.Method)) - return mapping; - return mapping switch { + // check if NoInline is requested + IUserMapping userMapping when ShouldSkipInlining(userMapping.Method) => mapping, + // inline existing mapping UserImplementedMethodMapping implementedMapping => InlineOrRebuild(implementedMapping) ?? implementedMapping, From 7e139c10317dac9a1aa558ebce039cced7dc4f56 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Sat, 18 Apr 2026 22:33:34 +0200 Subject: [PATCH 09/18] refactor: pass NoInlining through IUserMapping instead of symbol lookup --- .../InlineExpressionMappingBuilderContext.cs | 11 +-- ...rImplementedExistingTargetMethodMapping.cs | 8 ++- ...UserImplementedNewInstanceMethodMapping.cs | 8 ++- .../Mappings/UserMappings/IUserMapping.cs | 3 + ...DefinedExistingTargetGenericTypeMapping.cs | 5 +- .../UserDefinedExistingTargetMethodMapping.cs | 5 +- .../UserDefinedExpressionMethodMapping.cs | 5 +- ...serDefinedNewInstanceGenericTypeMapping.cs | 6 +- .../UserDefinedNewInstanceMethodMapping.cs | 5 +- ...inedNewInstanceRuntimeTargetTypeMapping.cs | 5 +- ...stanceRuntimeTargetTypeParameterMapping.cs | 6 +- ...rImplementedExistingTargetMethodMapping.cs | 5 +- ...entedGenericExistingTargetMethodMapping.cs | 6 +- .../UserImplementedGenericMethodMapping.cs | 6 +- ...UserImplementedInlinedExpressionMapping.cs | 1 + .../UserImplementedMethodMapping.cs | 5 +- .../Descriptors/UserMethodMappingExtractor.cs | 69 ++++++++++++++----- 17 files changed, 112 insertions(+), 47 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 5841da0be4..e94062fb2a 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -202,7 +202,7 @@ private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) return mapping switch { // check if NoInline is requested - IUserMapping userMapping when ShouldSkipInlining(userMapping.Method) => mapping, + IUserMapping { NoInlining: true } => mapping, // inline existing mapping UserImplementedMethodMapping implementedMapping => InlineOrRebuild(implementedMapping) ?? implementedMapping, @@ -219,15 +219,6 @@ IUserMapping userMapping when ShouldSkipInlining(userMapping.Method) => mapping, }; } - private bool ShouldSkipInlining(IMethodSymbol method) - { - if (SymbolAccessor.HasAttribute(method)) - return true; - - var mapperAttribute = AttributeAccessor.AccessFirstOrDefault(method.ContainingType); - return mapperAttribute?.NoInlining == true; - } - private INewInstanceMapping? InlineOrRebuild(UserImplementedMethodMapping mapping) { // Try inline first diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs index d1654dbacd..c00d525fce 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs @@ -17,7 +17,8 @@ internal sealed class GenericUserImplementedExistingTargetMethodMapping( MethodParameter sourceParameter, MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, - bool isExternal + bool isExternal, + bool noInlining ) : IExistingTargetUserMapping { public ITypeSymbol SourceType => sourceParameter.Type; @@ -30,6 +31,8 @@ bool isExternal public bool IsExternal => isExternal; + public bool NoInlining => noInlining; + public bool IsSynthetic => false; public IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; @@ -68,7 +71,8 @@ ITypeSymbol concreteTargetType concreteTargetType, typeArguments, referenceHandlerParameter, - isExternal + isExternal, + noInlining ); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs index f06a942a3a..44f81ea2dc 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs @@ -19,7 +19,8 @@ internal sealed class GenericUserImplementedNewInstanceMethodMapping( MethodParameter? referenceHandlerParameter, MethodParameter? targetOriginalValueParameter, bool isExternal, - UserImplementedMethodMapping.TargetNullability targetNullability + UserImplementedMethodMapping.TargetNullability targetNullability, + bool noInlining ) : INewInstanceUserMapping { public ITypeSymbol SourceType => sourceParameter.Type; @@ -32,6 +33,8 @@ UserImplementedMethodMapping.TargetNullability targetNullability public bool IsExternal => isExternal; + public bool NoInlining => noInlining; + public bool IsSynthetic => false; public IEnumerable BuildAdditionalMappingKeys(TypeMappingConfiguration config) => []; @@ -77,7 +80,8 @@ ITypeSymbol concreteTargetType referenceHandlerParameter, concreteTargetOriginalValueParameter, isExternal, - targetNullability + targetNullability, + noInlining ); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs index fbb29f9ba4..8edf29c715 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs @@ -18,4 +18,7 @@ public interface IUserMapping : ITypeMapping /// E.g. base class or imported via /// bool IsExternal { get; } + + /// + bool NoInlining { get; } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs index 6a28a721e9..e534542137 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs @@ -24,7 +24,8 @@ public class UserDefinedExistingTargetGenericTypeMapping( MethodParameter sourceParameter, MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, - bool enableReferenceHandling + bool enableReferenceHandling, + bool noInlining ) : MethodMapping(method, sourceParameter, referenceHandlerParameter, targetParameter.Type), IExistingTargetUserMapping { private const string SourceName = "source"; @@ -43,6 +44,8 @@ bool enableReferenceHandling public bool IsExternal => false; + public bool NoInlining => noInlining; + private MethodParameter TargetParameter { get; } = targetParameter; /// diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index 768967f7f1..067d863d2b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -17,7 +17,8 @@ public class UserDefinedExistingTargetMethodMapping( MethodParameter sourceParameter, MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, - bool enableReferenceHandling + bool enableReferenceHandling, + bool noInlining ) : MethodMapping(method, sourceParameter, referenceHandlerParameter, targetParameter.Type), IExistingTargetUserMapping { private IExistingTargetMapping? _delegateMapping; @@ -28,6 +29,8 @@ bool enableReferenceHandling public bool IsExternal => false; + public bool NoInlining => noInlining; + private MethodParameter TargetParameter { get; } = targetParameter; /// diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs index cb37c64c66..9f792db0e4 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs @@ -14,7 +14,8 @@ public class UserDefinedExpressionMethodMapping( IMethodSymbol method, ITypeSymbol expressionSourceType, ITypeSymbol expressionTargetType, - ITypeSymbol returnType + ITypeSymbol returnType, + bool noInlining ) : NewInstanceMethodMapping(method, CreateSourceParameter(expressionSourceType), null, returnType), INewInstanceUserMapping { private INewInstanceMapping? _delegateMapping; @@ -35,6 +36,8 @@ ITypeSymbol returnType public bool IsExternal => false; + public bool NoInlining => noInlining; + public void SetDelegateMapping(INewInstanceMapping mapping) => _delegateMapping = mapping; public override IEnumerable BuildBody(TypeMappingBuildContext ctx) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs index d82a08b206..72b194058b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs @@ -22,7 +22,8 @@ public class UserDefinedNewInstanceGenericTypeMapping( ITypeSymbol targetType, bool enableReferenceHandling, NullFallbackValue? nullArm, - ITypeSymbol objectType + ITypeSymbol objectType, + bool noInlining ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( method, @@ -31,7 +32,8 @@ ITypeSymbol objectType targetType, enableReferenceHandling, nullArm, - objectType + objectType, + noInlining ) { public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 5ed1653a7e..2ec17e7083 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -16,7 +16,8 @@ public class UserDefinedNewInstanceMethodMapping( MethodParameter? referenceHandlerParameter, ITypeSymbol targetType, bool enableReferenceHandling, - bool isDerivedTypeMapping + bool isDerivedTypeMapping, + bool noInlining ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { private INewInstanceMapping? _delegateMapping; @@ -31,6 +32,8 @@ bool isDerivedTypeMapping public bool IsExternal => false; + public bool NoInlining { get; } = noInlining; + /// /// The reference handling is enabled but is only internal to this method. /// No reference handler parameter is passed. diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index 6da754783c..d4f1726eb6 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -20,7 +20,8 @@ public abstract class UserDefinedNewInstanceRuntimeTargetTypeMapping( ITypeSymbol targetType, bool enableReferenceHandling, NullFallbackValue? nullArm, - ITypeSymbol objectType + ITypeSymbol objectType, + bool noInlining ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { private const string IsAssignableFromMethodName = nameof(Type.IsAssignableFrom); @@ -38,6 +39,8 @@ ITypeSymbol objectType public bool IsExternal => false; + public bool NoInlining => noInlining; + /// /// The reference handling is enabled but is only internal to this method. /// No reference handler parameter is passed. diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs index a361c558fa..99bcd29edf 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs @@ -16,7 +16,8 @@ public class UserDefinedNewInstanceRuntimeTargetTypeParameterMapping( bool enableReferenceHandling, ITypeSymbol targetType, NullFallbackValue? nullArm, - ITypeSymbol objectType + ITypeSymbol objectType, + bool noInlining ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( method, @@ -25,7 +26,8 @@ ITypeSymbol objectType targetType, enableReferenceHandling, nullArm, - objectType + objectType, + noInlining ) { protected override ParameterListSyntax BuildParameterList() => diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs index 4ef478957c..1f940366c0 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs @@ -20,7 +20,8 @@ public class UserImplementedExistingTargetMethodMapping( ITypeSymbol sourceType, ITypeSymbol targetType, MethodParameter? referenceHandlerParameter, - bool isExternal + bool isExternal, + bool noInlining ) : ExistingTargetMapping(sourceType, targetType), IExistingTargetUserMapping, IParameterizedMapping { public IMethodSymbol Method { get; } = method; @@ -29,6 +30,8 @@ bool isExternal public bool IsExternal { get; } = isExternal; + public bool NoInlining { get; } = noInlining; + public IReadOnlyCollection AdditionalSourceParameters { get; } = method .Parameters.Where(p => diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs index 6fd8b3981c..7008d604fb 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs @@ -20,7 +20,8 @@ public class UserImplementedGenericExistingTargetMethodMapping( ITypeSymbol concreteTargetType, IReadOnlyList typeArguments, MethodParameter? referenceHandlerParameter, - bool isExternal + bool isExternal, + bool noInlining ) : UserImplementedExistingTargetMethodMapping( receiver, @@ -31,7 +32,8 @@ bool isExternal concreteSourceType, concreteTargetType, referenceHandlerParameter, - isExternal + isExternal, + noInlining ) { protected override SimpleNameSyntax BuildMethodName() diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs index 055ed895cf..6a25d28e5a 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs @@ -21,7 +21,8 @@ public class UserImplementedGenericMethodMapping( MethodParameter? referenceHandlerParameter, MethodParameter? targetOriginalValueParameter, bool isExternal, - UserImplementedMethodMapping.TargetNullability targetNullability + UserImplementedMethodMapping.TargetNullability targetNullability, + bool noInlining ) : UserImplementedMethodMapping( receiver, @@ -33,7 +34,8 @@ UserImplementedMethodMapping.TargetNullability targetNullability referenceHandlerParameter, targetOriginalValueParameter, isExternal, - targetNullability + targetNullability, + noInlining ) { protected override SimpleNameSyntax BuildMethodName() diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs index 7722a32da1..abc0872ef6 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs @@ -25,6 +25,7 @@ ExpressionSyntax mappingBody public IMethodSymbol Method => userMapping.Method; public bool? Default => userMapping.Default; public bool IsExternal => userMapping.IsExternal; + public bool NoInlining => userMapping.NoInlining; public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs index 1ea8959d01..107e4c0591 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs @@ -21,7 +21,8 @@ public class UserImplementedMethodMapping( MethodParameter? referenceHandlerParameter, MethodParameter? targetOriginalValueParameter, bool isExternal, - UserImplementedMethodMapping.TargetNullability targetNullability + UserImplementedMethodMapping.TargetNullability targetNullability, + bool noInlining ) : NewInstanceMapping(sourceType, targetType), INewInstanceUserMapping, IParameterizedMapping { public enum TargetNullability @@ -39,6 +40,8 @@ public enum TargetNullability public MethodParameter? TargetOriginalValueParameter { get; } = targetOriginalValueParameter; + public bool NoInlining { get; } = noInlining; + public IReadOnlyCollection AdditionalSourceParameters { get; } = method .Parameters.Where(p => diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 9c13c957f6..a8fef3bc4d 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -189,6 +189,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM return null; } + var noInlining = GetNoInlining(ctx, method); + // Generic user-implemented methods are stored as templates // that are matched against concrete type pairs during mapping resolution. if (method.IsGenericMethod) @@ -199,7 +201,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM receiver, parameters, isExternal, - userMappingConfig.Default ?? isDefault + userMappingConfig.Default ?? isDefault, + noInlining ); } @@ -214,7 +217,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.Source.Type, parameters.Target!.Value.Type, parameters.ReferenceHandler, - isExternal + isExternal, + noInlining ); } @@ -229,7 +233,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.ReferenceHandler, parameters.TargetOriginalValueParameter, isExternal, - targetTypeNullability + targetTypeNullability, + noInlining ); } @@ -239,7 +244,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM string? receiver, MappingMethodParameters parameters, bool isExternal, - bool? isDefault + bool? isDefault, + bool noInlining ) { if (method.ReturnsVoid) @@ -253,7 +259,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.Source, targetParam, parameters.ReferenceHandler, - isExternal + isExternal, + noInlining ); } @@ -266,7 +273,8 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.ReferenceHandler, parameters.TargetOriginalValueParameter, isExternal, - targetTypeNullability + targetTypeNullability, + noInlining ); } @@ -310,10 +318,12 @@ string sourceParameterName return null; } - if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol) is { } userMapping) + var noInlining = GetNoInlining(ctx, methodSymbol); + + if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol, noInlining) is { } userMapping) return userMapping; - if (TryBuildExpressionMapping(ctx, methodSymbol) is { } expressionMapping) + if (TryBuildExpressionMapping(ctx, methodSymbol, noInlining) is { } expressionMapping) return expressionMapping; if (!UserMappingMethodParameterExtractor.BuildParameters(ctx, methodSymbol, out var parameters)) @@ -329,7 +339,7 @@ string sourceParameterName } if (methodSymbol.IsGenericMethod) - return BuildGenericTypeMapping(ctx, methodSymbol, parameters); + return BuildGenericTypeMapping(ctx, methodSymbol, parameters, noInlining); if (parameters.Target.HasValue) { @@ -338,7 +348,8 @@ string sourceParameterName parameters.Source, parameters.Target.Value, parameters.ReferenceHandler, - ctx.Configuration.Mapper.UseReferenceHandling + ctx.Configuration.Mapper.UseReferenceHandling, + noInlining ) { AdditionalSourceParameters = parameters.AdditionalParameters, @@ -363,7 +374,8 @@ string sourceParameterName ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), ctx.Configuration.Mapper.UseReferenceHandling, ctx.AttributeAccessor.HasAttribute>(methodSymbol) - || ctx.AttributeAccessor.HasAttribute(methodSymbol) + || ctx.AttributeAccessor.HasAttribute(methodSymbol), + noInlining ) { AdditionalSourceParameters = parameters.AdditionalParameters, @@ -374,7 +386,8 @@ string sourceParameterName private static IUserMapping BuildGenericTypeMapping( SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, - MappingMethodParameters parameters + MappingMethodParameters parameters, + bool noInlining ) { if (parameters.Target.HasValue) @@ -384,7 +397,8 @@ MappingMethodParameters parameters parameters.Source, parameters.Target.Value, parameters.ReferenceHandler, - ctx.Configuration.Mapper.UseReferenceHandling + ctx.Configuration.Mapper.UseReferenceHandling, + noInlining ); } @@ -394,13 +408,15 @@ MappingMethodParameters parameters ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), ctx.Configuration.Mapper.UseReferenceHandling, GetTypeSwitchNullArm(methodSymbol, parameters), - ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) + ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated), + noInlining ); } private static UserDefinedNewInstanceRuntimeTargetTypeParameterMapping? TryBuildRuntimeTargetTypeMapping( SimpleMappingBuilderContext ctx, - IMethodSymbol methodSymbol + IMethodSymbol methodSymbol, + bool noInlining ) { if (methodSymbol.IsGenericMethod) @@ -419,13 +435,15 @@ IMethodSymbol methodSymbol ctx.Configuration.Mapper.UseReferenceHandling, ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), GetTypeSwitchNullArm(methodSymbol, runtimeTargetTypeParams), - ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated) + ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated), + noInlining ); } private static UserDefinedExpressionMethodMapping? TryBuildExpressionMapping( SimpleMappingBuilderContext ctx, - IMethodSymbol methodSymbol + IMethodSymbol methodSymbol, + bool noInlining ) { if (methodSymbol.IsGenericMethod) @@ -455,7 +473,13 @@ IMethodSymbol methodSymbol var sourceType = ctx.SymbolAccessor.UpgradeNullable(funcTypeArgs.TypeArguments[0]); var targetType = ctx.SymbolAccessor.UpgradeNullable(funcTypeArgs.TypeArguments[1]); - return new UserDefinedExpressionMethodMapping(methodSymbol, sourceType, targetType, ctx.SymbolAccessor.UpgradeNullable(returnType)); + return new UserDefinedExpressionMethodMapping( + methodSymbol, + sourceType, + targetType, + ctx.SymbolAccessor.UpgradeNullable(returnType), + noInlining + ); } private static bool HasTargetOriginalValueParameter(SimpleMappingBuilderContext ctx, IMethodSymbol method) => @@ -490,6 +514,15 @@ out bool hasAttribute return userMappingAttr ?? new UserMappingConfiguration(); } + private static bool GetNoInlining(SimpleMappingBuilderContext ctx, IMethodSymbol method) + { + if (ctx.SymbolAccessor.HasAttribute(method)) + return true; + + var mapperAttribute = ctx.AttributeAccessor.AccessFirstOrDefault(method.ContainingType); + return mapperAttribute?.NoInlining == true; + } + private static IEnumerable ExtractNamedUserImplementedMappings( SimpleMappingBuilderContext ctx, ITypeSymbol mapperSymbol, From 73f1bff316010a9fb62d4bb51dc0b2dfa6460955 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Fri, 24 Apr 2026 11:52:24 +0200 Subject: [PATCH 10/18] refactor: rename NoInlining to NoExpressionInlining Make the expression-only scope explicit in the public API name, as requested in PR review feedback: both the attribute and the mapper property only affect expression/queryable projection inlining, not regular mappings. - MapperNoInliningAttribute -> MapperNoExpressionInliningAttribute - MapperAttribute.NoInlining -> NoExpressionInlining - MapperlyNoInlining build property -> MapperlyNoExpressionInlining - Internal IUserMapping.NoInlining and locals renamed accordingly - XML docs clarify the expression-only scope --- docs/docs/configuration/mapper.mdx | 4 +- .../configuration/queryable-projections.mdx | 12 ++--- .../MapperAttribute.cs | 4 +- ...=> MapperNoExpressionInliningAttribute.cs} | 4 +- .../Configuration/MapperConfiguration.cs | 2 +- .../MapperConfigurationMerger.cs | 5 +- .../InlineExpressionMappingBuilderContext.cs | 2 +- ...rImplementedExistingTargetMethodMapping.cs | 6 +-- ...UserImplementedNewInstanceMethodMapping.cs | 6 +-- .../Mappings/UserMappings/IUserMapping.cs | 4 +- ...DefinedExistingTargetGenericTypeMapping.cs | 4 +- .../UserDefinedExistingTargetMethodMapping.cs | 4 +- .../UserDefinedExpressionMethodMapping.cs | 4 +- ...serDefinedNewInstanceGenericTypeMapping.cs | 4 +- .../UserDefinedNewInstanceMethodMapping.cs | 4 +- ...inedNewInstanceRuntimeTargetTypeMapping.cs | 4 +- ...stanceRuntimeTargetTypeParameterMapping.cs | 4 +- ...rImplementedExistingTargetMethodMapping.cs | 4 +- ...entedGenericExistingTargetMethodMapping.cs | 4 +- .../UserImplementedGenericMethodMapping.cs | 4 +- ...UserImplementedInlinedExpressionMapping.cs | 2 +- .../UserImplementedMethodMapping.cs | 4 +- .../Descriptors/UserMethodMappingExtractor.cs | 46 +++++++++---------- src/Riok.Mapperly/Riok.Mapperly.targets | 2 +- ...ApiTest.PublicApiHasNotChanged.verified.cs | 8 ++-- .../Mapping/UseStaticMapperTest.cs | 18 ++++---- 26 files changed, 87 insertions(+), 82 deletions(-) rename src/Riok.Mapperly.Abstractions/{MapperNoInliningAttribute.cs => MapperNoExpressionInliningAttribute.cs} (73%) diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index 77cda840d6..e42cac7119 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -73,11 +73,11 @@ public partial class MyMapper ### Inlining By default, Mapperly inlines mapping methods into expression trees when possible. -Set `NoInlining` to `true` to prevent mapping methods of this mapper from being inlined into expression trees +Set `NoExpressionInlining` to `true` to prevent mapping methods of this mapper from being inlined into expression trees when referenced via `UseStaticMapper`. Defaults to `false`. ```csharp -[Mapper(NoInlining = true)] +[Mapper(NoExpressionInlining = true)] public partial class CarMapper { // ... diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index 3d0fbd6be0..c52e70f177 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -128,8 +128,8 @@ Mapperly automatically tries to inline referenced mapping methods into expressio In some cases, this inlining can cause issues — for example, when enum mappings with different underlying types produce false diagnostics in expression context. -You can prevent inlining of specific methods using the `MapperNoInlining` attribute, -or prevent inlining of all methods in a mapper using the `NoInlining` property on the `Mapper` attribute. +You can prevent inlining of specific methods using the `MapperNoExpressionInlining` attribute, +or prevent inlining of all methods in a mapper using the `NoExpressionInlining` property on the `Mapper` attribute. ### Per-method opt-out @@ -138,7 +138,7 @@ or prevent inlining of all methods in a mapper using the `NoInlining` property o public static partial class EnumMapper { // highlight-start - [MapperNoInlining] + [MapperNoExpressionInlining] // highlight-end public static partial TargetStatus MapToStatus(this SourceStatus source); } @@ -156,7 +156,7 @@ public static partial class Mapper ```csharp // highlight-start -[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoInlining = true)] +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] // highlight-end public static partial class EnumMapper { @@ -166,7 +166,7 @@ public static partial class EnumMapper ### Runtime behavior -The `NoInlining` setting changes how the projection is materialized at runtime. +The `NoExpressionInlining` setting changes how the projection is materialized at runtime. @@ -190,7 +190,7 @@ var dtos = await dbContext.Cars ``` - + With inlining disabled, the mapping method call is preserved as-is. The ORM cannot translate it to SQL, so Entity Framework will split the query diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 5467f90767..d3e23c4504 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -148,7 +148,9 @@ public class MapperAttribute : Attribute /// into expression trees for queryable projection mappings. /// When true, methods from this mapper referenced via /// will not be inlined or rebuilt in expression context. + /// This only applies to expression / queryable projection mappings; + /// regular (non-expression) mappings are unaffected. /// Defaults to false. /// - public bool NoInlining { get; set; } + public bool NoExpressionInlining { get; set; } } diff --git a/src/Riok.Mapperly.Abstractions/MapperNoInliningAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperNoExpressionInliningAttribute.cs similarity index 73% rename from src/Riok.Mapperly.Abstractions/MapperNoInliningAttribute.cs rename to src/Riok.Mapperly.Abstractions/MapperNoExpressionInliningAttribute.cs index fdc66edcba..29e4c35a21 100644 --- a/src/Riok.Mapperly.Abstractions/MapperNoInliningAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperNoExpressionInliningAttribute.cs @@ -5,9 +5,11 @@ namespace Riok.Mapperly.Abstractions; /// /// Prevents a mapping method from being inlined into expression trees for queryable projection mappings. /// When applied, the method call is preserved as-is instead of being rebuilt in expression context. +/// This only applies to expression / queryable projection mappings; +/// regular (non-expression) mappings are unaffected. /// This is useful when inlining causes issues such as false enum mapping diagnostics /// due to expression tree limitations. /// [AttributeUsage(AttributeTargets.Method)] [Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] -public sealed class MapperNoInliningAttribute : Attribute; +public sealed class MapperNoExpressionInliningAttribute : Attribute; diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index d22acc74da..eae3842b31 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -133,7 +133,7 @@ public record MapperConfiguration /// /// Whether to prevent mapping methods of this mapper from being inlined into expression trees for queryable projection mappings. /// - public bool? NoInlining { get; init; } + public bool? NoExpressionInlining { get; init; } /// /// The default enum naming strategy. diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index 5e475919fd..750f76f043 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -26,7 +26,7 @@ public static MapperConfiguration Merge(MapperConfiguration highPriority, Mapper IncludedConstructors = highPriority.IncludedConstructors ?? lowPriority.IncludedConstructors, PreferParameterlessConstructors = highPriority.PreferParameterlessConstructors ?? lowPriority.PreferParameterlessConstructors, AutoUserMappings = highPriority.AutoUserMappings ?? lowPriority.AutoUserMappings, - NoInlining = highPriority.NoInlining ?? lowPriority.NoInlining, + NoExpressionInlining = highPriority.NoExpressionInlining ?? lowPriority.NoExpressionInlining, EnumNamingStrategy = highPriority.EnumNamingStrategy ?? lowPriority.EnumNamingStrategy, }; } @@ -102,7 +102,8 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu mapper.AutoUserMappings = mapperConfiguration.AutoUserMappings ?? defaultMapperConfiguration.AutoUserMappings ?? mapper.AutoUserMappings; - mapper.NoInlining = mapperConfiguration.NoInlining ?? defaultMapperConfiguration.NoInlining ?? mapper.NoInlining; + mapper.NoExpressionInlining = + mapperConfiguration.NoExpressionInlining ?? defaultMapperConfiguration.NoExpressionInlining ?? mapper.NoExpressionInlining; mapper.EnumNamingStrategy = mapperConfiguration.EnumNamingStrategy ?? defaultMapperConfiguration.EnumNamingStrategy ?? mapper.EnumNamingStrategy; diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index e94062fb2a..26ce6325e4 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -202,7 +202,7 @@ private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) return mapping switch { // check if NoInline is requested - IUserMapping { NoInlining: true } => mapping, + IUserMapping { NoExpressionInlining: true } => mapping, // inline existing mapping UserImplementedMethodMapping implementedMapping => InlineOrRebuild(implementedMapping) ?? implementedMapping, diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs index c00d525fce..2dd656c216 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedExistingTargetMethodMapping.cs @@ -18,7 +18,7 @@ internal sealed class GenericUserImplementedExistingTargetMethodMapping( MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, bool isExternal, - bool noInlining + bool noExpressionInlining ) : IExistingTargetUserMapping { public ITypeSymbol SourceType => sourceParameter.Type; @@ -31,7 +31,7 @@ bool noInlining public bool IsExternal => isExternal; - public bool NoInlining => noInlining; + public bool NoExpressionInlining => noExpressionInlining; public bool IsSynthetic => false; @@ -72,7 +72,7 @@ ITypeSymbol concreteTargetType typeArguments, referenceHandlerParameter, isExternal, - noInlining + noExpressionInlining ); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs index 44f81ea2dc..a4a398da18 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/GenericUserImplementedNewInstanceMethodMapping.cs @@ -20,7 +20,7 @@ internal sealed class GenericUserImplementedNewInstanceMethodMapping( MethodParameter? targetOriginalValueParameter, bool isExternal, UserImplementedMethodMapping.TargetNullability targetNullability, - bool noInlining + bool noExpressionInlining ) : INewInstanceUserMapping { public ITypeSymbol SourceType => sourceParameter.Type; @@ -33,7 +33,7 @@ bool noInlining public bool IsExternal => isExternal; - public bool NoInlining => noInlining; + public bool NoExpressionInlining => noExpressionInlining; public bool IsSynthetic => false; @@ -81,7 +81,7 @@ ITypeSymbol concreteTargetType concreteTargetOriginalValueParameter, isExternal, targetNullability, - noInlining + noExpressionInlining ); } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs index 8edf29c715..2f985a4cdf 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/IUserMapping.cs @@ -19,6 +19,6 @@ public interface IUserMapping : ITypeMapping /// bool IsExternal { get; } - /// - bool NoInlining { get; } + /// + bool NoExpressionInlining { get; } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs index e534542137..c0fcd451c5 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetGenericTypeMapping.cs @@ -25,7 +25,7 @@ public class UserDefinedExistingTargetGenericTypeMapping( MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, bool enableReferenceHandling, - bool noInlining + bool noExpressionInlining ) : MethodMapping(method, sourceParameter, referenceHandlerParameter, targetParameter.Type), IExistingTargetUserMapping { private const string SourceName = "source"; @@ -44,7 +44,7 @@ bool noInlining public bool IsExternal => false; - public bool NoInlining => noInlining; + public bool NoExpressionInlining => noExpressionInlining; private MethodParameter TargetParameter { get; } = targetParameter; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index 067d863d2b..c21b83018b 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -18,7 +18,7 @@ public class UserDefinedExistingTargetMethodMapping( MethodParameter targetParameter, MethodParameter? referenceHandlerParameter, bool enableReferenceHandling, - bool noInlining + bool noExpressionInlining ) : MethodMapping(method, sourceParameter, referenceHandlerParameter, targetParameter.Type), IExistingTargetUserMapping { private IExistingTargetMapping? _delegateMapping; @@ -29,7 +29,7 @@ bool noInlining public bool IsExternal => false; - public bool NoInlining => noInlining; + public bool NoExpressionInlining => noExpressionInlining; private MethodParameter TargetParameter { get; } = targetParameter; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs index 9f792db0e4..fd8b707054 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExpressionMethodMapping.cs @@ -15,7 +15,7 @@ public class UserDefinedExpressionMethodMapping( ITypeSymbol expressionSourceType, ITypeSymbol expressionTargetType, ITypeSymbol returnType, - bool noInlining + bool noExpressionInlining ) : NewInstanceMethodMapping(method, CreateSourceParameter(expressionSourceType), null, returnType), INewInstanceUserMapping { private INewInstanceMapping? _delegateMapping; @@ -36,7 +36,7 @@ bool noInlining public bool IsExternal => false; - public bool NoInlining => noInlining; + public bool NoExpressionInlining => noExpressionInlining; public void SetDelegateMapping(INewInstanceMapping mapping) => _delegateMapping = mapping; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs index 72b194058b..a3fe3fd61d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs @@ -23,7 +23,7 @@ public class UserDefinedNewInstanceGenericTypeMapping( bool enableReferenceHandling, NullFallbackValue? nullArm, ITypeSymbol objectType, - bool noInlining + bool noExpressionInlining ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( method, @@ -33,7 +33,7 @@ bool noInlining enableReferenceHandling, nullArm, objectType, - noInlining + noExpressionInlining ) { public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 2ec17e7083..76683faaf6 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -17,7 +17,7 @@ public class UserDefinedNewInstanceMethodMapping( ITypeSymbol targetType, bool enableReferenceHandling, bool isDerivedTypeMapping, - bool noInlining + bool noExpressionInlining ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { private INewInstanceMapping? _delegateMapping; @@ -32,7 +32,7 @@ bool noInlining public bool IsExternal => false; - public bool NoInlining { get; } = noInlining; + public bool NoExpressionInlining { get; } = noExpressionInlining; /// /// The reference handling is enabled but is only internal to this method. diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index d4f1726eb6..1b33e2a901 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -21,7 +21,7 @@ public abstract class UserDefinedNewInstanceRuntimeTargetTypeMapping( bool enableReferenceHandling, NullFallbackValue? nullArm, ITypeSymbol objectType, - bool noInlining + bool noExpressionInlining ) : NewInstanceMethodMapping(method, sourceParameter, referenceHandlerParameter, targetType), INewInstanceUserMapping { private const string IsAssignableFromMethodName = nameof(Type.IsAssignableFrom); @@ -39,7 +39,7 @@ bool noInlining public bool IsExternal => false; - public bool NoInlining => noInlining; + public bool NoExpressionInlining => noExpressionInlining; /// /// The reference handling is enabled but is only internal to this method. diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs index 99bcd29edf..7de21de74d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeParameterMapping.cs @@ -17,7 +17,7 @@ public class UserDefinedNewInstanceRuntimeTargetTypeParameterMapping( ITypeSymbol targetType, NullFallbackValue? nullArm, ITypeSymbol objectType, - bool noInlining + bool noExpressionInlining ) : UserDefinedNewInstanceRuntimeTargetTypeMapping( method, @@ -27,7 +27,7 @@ bool noInlining enableReferenceHandling, nullArm, objectType, - noInlining + noExpressionInlining ) { protected override ParameterListSyntax BuildParameterList() => diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs index 1f940366c0..27b0aa77dd 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedExistingTargetMethodMapping.cs @@ -21,7 +21,7 @@ public class UserImplementedExistingTargetMethodMapping( ITypeSymbol targetType, MethodParameter? referenceHandlerParameter, bool isExternal, - bool noInlining + bool noExpressionInlining ) : ExistingTargetMapping(sourceType, targetType), IExistingTargetUserMapping, IParameterizedMapping { public IMethodSymbol Method { get; } = method; @@ -30,7 +30,7 @@ bool noInlining public bool IsExternal { get; } = isExternal; - public bool NoInlining { get; } = noInlining; + public bool NoExpressionInlining { get; } = noExpressionInlining; public IReadOnlyCollection AdditionalSourceParameters { get; } = method diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs index 7008d604fb..a35b7dea0c 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericExistingTargetMethodMapping.cs @@ -21,7 +21,7 @@ public class UserImplementedGenericExistingTargetMethodMapping( IReadOnlyList typeArguments, MethodParameter? referenceHandlerParameter, bool isExternal, - bool noInlining + bool noExpressionInlining ) : UserImplementedExistingTargetMethodMapping( receiver, @@ -33,7 +33,7 @@ bool noInlining concreteTargetType, referenceHandlerParameter, isExternal, - noInlining + noExpressionInlining ) { protected override SimpleNameSyntax BuildMethodName() diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs index 6a25d28e5a..92c25f17b1 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedGenericMethodMapping.cs @@ -22,7 +22,7 @@ public class UserImplementedGenericMethodMapping( MethodParameter? targetOriginalValueParameter, bool isExternal, UserImplementedMethodMapping.TargetNullability targetNullability, - bool noInlining + bool noExpressionInlining ) : UserImplementedMethodMapping( receiver, @@ -35,7 +35,7 @@ bool noInlining targetOriginalValueParameter, isExternal, targetNullability, - noInlining + noExpressionInlining ) { protected override SimpleNameSyntax BuildMethodName() diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs index abc0872ef6..20996912e8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs @@ -25,7 +25,7 @@ ExpressionSyntax mappingBody public IMethodSymbol Method => userMapping.Method; public bool? Default => userMapping.Default; public bool IsExternal => userMapping.IsExternal; - public bool NoInlining => userMapping.NoInlining; + public bool NoExpressionInlining => userMapping.NoExpressionInlining; public override ExpressionSyntax Build(TypeMappingBuildContext ctx) { diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs index 107e4c0591..faee97f7f5 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedMethodMapping.cs @@ -22,7 +22,7 @@ public class UserImplementedMethodMapping( MethodParameter? targetOriginalValueParameter, bool isExternal, UserImplementedMethodMapping.TargetNullability targetNullability, - bool noInlining + bool noExpressionInlining ) : NewInstanceMapping(sourceType, targetType), INewInstanceUserMapping, IParameterizedMapping { public enum TargetNullability @@ -40,7 +40,7 @@ public enum TargetNullability public MethodParameter? TargetOriginalValueParameter { get; } = targetOriginalValueParameter; - public bool NoInlining { get; } = noInlining; + public bool NoExpressionInlining { get; } = noExpressionInlining; public IReadOnlyCollection AdditionalSourceParameters { get; } = method diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index a8fef3bc4d..1141285b40 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -189,7 +189,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM return null; } - var noInlining = GetNoInlining(ctx, method); + var noExpressionInlining = GetNoExpressionInlining(ctx, method); // Generic user-implemented methods are stored as templates // that are matched against concrete type pairs during mapping resolution. @@ -202,7 +202,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters, isExternal, userMappingConfig.Default ?? isDefault, - noInlining + noExpressionInlining ); } @@ -218,7 +218,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.Target!.Value.Type, parameters.ReferenceHandler, isExternal, - noInlining + noExpressionInlining ); } @@ -234,7 +234,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM parameters.TargetOriginalValueParameter, isExternal, targetTypeNullability, - noInlining + noExpressionInlining ); } @@ -245,7 +245,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM MappingMethodParameters parameters, bool isExternal, bool? isDefault, - bool noInlining + bool noExpressionInlining ) { if (method.ReturnsVoid) @@ -260,7 +260,7 @@ bool noInlining targetParam, parameters.ReferenceHandler, isExternal, - noInlining + noExpressionInlining ); } @@ -274,7 +274,7 @@ bool noInlining parameters.TargetOriginalValueParameter, isExternal, targetTypeNullability, - noInlining + noExpressionInlining ); } @@ -318,12 +318,12 @@ string sourceParameterName return null; } - var noInlining = GetNoInlining(ctx, methodSymbol); + var noExpressionInlining = GetNoExpressionInlining(ctx, methodSymbol); - if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol, noInlining) is { } userMapping) + if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol, noExpressionInlining) is { } userMapping) return userMapping; - if (TryBuildExpressionMapping(ctx, methodSymbol, noInlining) is { } expressionMapping) + if (TryBuildExpressionMapping(ctx, methodSymbol, noExpressionInlining) is { } expressionMapping) return expressionMapping; if (!UserMappingMethodParameterExtractor.BuildParameters(ctx, methodSymbol, out var parameters)) @@ -339,7 +339,7 @@ string sourceParameterName } if (methodSymbol.IsGenericMethod) - return BuildGenericTypeMapping(ctx, methodSymbol, parameters, noInlining); + return BuildGenericTypeMapping(ctx, methodSymbol, parameters, noExpressionInlining); if (parameters.Target.HasValue) { @@ -349,7 +349,7 @@ string sourceParameterName parameters.Target.Value, parameters.ReferenceHandler, ctx.Configuration.Mapper.UseReferenceHandling, - noInlining + noExpressionInlining ) { AdditionalSourceParameters = parameters.AdditionalParameters, @@ -375,7 +375,7 @@ string sourceParameterName ctx.Configuration.Mapper.UseReferenceHandling, ctx.AttributeAccessor.HasAttribute>(methodSymbol) || ctx.AttributeAccessor.HasAttribute(methodSymbol), - noInlining + noExpressionInlining ) { AdditionalSourceParameters = parameters.AdditionalParameters, @@ -387,7 +387,7 @@ private static IUserMapping BuildGenericTypeMapping( SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, MappingMethodParameters parameters, - bool noInlining + bool noExpressionInlining ) { if (parameters.Target.HasValue) @@ -398,7 +398,7 @@ bool noInlining parameters.Target.Value, parameters.ReferenceHandler, ctx.Configuration.Mapper.UseReferenceHandling, - noInlining + noExpressionInlining ); } @@ -409,14 +409,14 @@ bool noInlining ctx.Configuration.Mapper.UseReferenceHandling, GetTypeSwitchNullArm(methodSymbol, parameters), ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated), - noInlining + noExpressionInlining ); } private static UserDefinedNewInstanceRuntimeTargetTypeParameterMapping? TryBuildRuntimeTargetTypeMapping( SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, - bool noInlining + bool noExpressionInlining ) { if (methodSymbol.IsGenericMethod) @@ -436,14 +436,14 @@ bool noInlining ctx.SymbolAccessor.UpgradeNullable(methodSymbol.ReturnType), GetTypeSwitchNullArm(methodSymbol, runtimeTargetTypeParams), ctx.Compilation.ObjectType.WithNullableAnnotation(NullableAnnotation.NotAnnotated), - noInlining + noExpressionInlining ); } private static UserDefinedExpressionMethodMapping? TryBuildExpressionMapping( SimpleMappingBuilderContext ctx, IMethodSymbol methodSymbol, - bool noInlining + bool noExpressionInlining ) { if (methodSymbol.IsGenericMethod) @@ -478,7 +478,7 @@ bool noInlining sourceType, targetType, ctx.SymbolAccessor.UpgradeNullable(returnType), - noInlining + noExpressionInlining ); } @@ -514,13 +514,13 @@ out bool hasAttribute return userMappingAttr ?? new UserMappingConfiguration(); } - private static bool GetNoInlining(SimpleMappingBuilderContext ctx, IMethodSymbol method) + private static bool GetNoExpressionInlining(SimpleMappingBuilderContext ctx, IMethodSymbol method) { - if (ctx.SymbolAccessor.HasAttribute(method)) + if (ctx.SymbolAccessor.HasAttribute(method)) return true; var mapperAttribute = ctx.AttributeAccessor.AccessFirstOrDefault(method.ContainingType); - return mapperAttribute?.NoInlining == true; + return mapperAttribute?.NoExpressionInlining == true; } private static IEnumerable ExtractNamedUserImplementedMappings( diff --git a/src/Riok.Mapperly/Riok.Mapperly.targets b/src/Riok.Mapperly/Riok.Mapperly.targets index 4e3eade38f..e46aa1f1d4 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.targets +++ b/src/Riok.Mapperly/Riok.Mapperly.targets @@ -23,6 +23,6 @@ - + diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs index d7dfc3dd50..c790dc75b0 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs @@ -1,4 +1,4 @@ -[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] +[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] namespace Riok.Mapperly.Abstractions { public enum EnumMappingStrategy @@ -138,7 +138,7 @@ public MapperAttribute() { } public Riok.Mapperly.Abstractions.IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } public Riok.Mapperly.Abstractions.MemberVisibility IncludedConstructors { get; set; } public Riok.Mapperly.Abstractions.MemberVisibility IncludedMembers { get; set; } - public bool NoInlining { get; set; } + public bool NoExpressionInlining { get; set; } public bool PreferParameterlessConstructors { get; set; } public Riok.Mapperly.Abstractions.PropertyNameMappingStrategy PropertyNameMappingStrategy { get; set; } public Riok.Mapperly.Abstractions.RequiredMappingStrategy RequiredEnumMappingStrategy { get; set; } @@ -209,9 +209,9 @@ public MapperIgnoreTargetValueAttribute(object target) { } } [System.AttributeUsage(System.AttributeTargets.Method)] [System.Diagnostics.Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] - public sealed class MapperNoInliningAttribute : System.Attribute + public sealed class MapperNoExpressionInliningAttribute : System.Attribute { - public MapperNoInliningAttribute() { } + public MapperNoExpressionInliningAttribute() { } } [System.AttributeUsage(System.AttributeTargets.Method)] [System.Diagnostics.Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] diff --git a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs index 747bfe0bf9..d5a3ad71cb 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs @@ -854,7 +854,7 @@ public static partial class Mapper } [Fact] - public void ProjectionWithUseStaticEnumMapperByNameWithNoInliningShouldWork() + public void ProjectionWithUseStaticEnumMapperByNameWithNoExpressionInliningShouldWork() { var source = TestSourceBuilder.CSharp( """ @@ -898,7 +898,7 @@ public class Domain [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] public static partial class EnumMappingExtensions { - [MapperNoInlining] + [MapperNoExpressionInlining] public static partial Target.Status MapToStatus(this Source.Status source); } @@ -945,7 +945,7 @@ public static partial class Mapper } [Fact] - public void MapperNoInliningOnNonProjectionMethodShouldHaveNoEffect() + public void MapperNoExpressionInliningOnNonProjectionMethodShouldHaveNoEffect() { var source = TestSourceBuilder.CSharp( """ @@ -985,10 +985,10 @@ public class Domain public Target.Status Status { get; set; } } - [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoInlining = true)] + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] public static partial class EnumMappingExtensions { - [MapperNoInlining] + [MapperNoExpressionInlining] public static partial Target.Status MapToStatus(this Source.Status source); } @@ -1016,7 +1016,7 @@ public static partial class Mapper } [Fact] - public void ProjectionWithUsedMapperNoInliningShouldNotInline() + public void ProjectionWithUsedMapperNoExpressionInliningShouldNotInline() { var source = TestSourceBuilder.CSharp( """ @@ -1057,7 +1057,7 @@ public class Domain public Target.Status Status { get; set; } } - [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoInlining = true)] + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] public static partial class EnumMappingExtensions { public static partial Target.Status MapToStatus(this Source.Status source); @@ -1106,7 +1106,7 @@ public static partial class Mapper } [Fact] - public void ProjectionWithCalledMethodNoInliningShouldNotInline() + public void ProjectionWithCalledMethodNoExpressionInliningShouldNotInline() { var source = TestSourceBuilder.CSharp( """ @@ -1150,7 +1150,7 @@ public class Domain [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] public static partial class EnumMappingExtensions { - [MapperNoInlining] + [MapperNoExpressionInlining] public static partial Target.Status MapToStatus(this Source.Status source); } From 7f5d54d2e7afe912a95832fe9b2611c7692c422b Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Fri, 24 Apr 2026 12:03:17 +0200 Subject: [PATCH 11/18] feat: promote RMG032 to error and short-circuit enum fallback Address PR review feedback: RMG032 is the root cause when enum projections are not translatable to expressions. Previously it was emitted as a warning and Mapperly fell back to EnumToEnumMappingBuilder, which produced cascading false RMG037/RMG038 diagnostics when the source/target enums had different underlying types (e.g. sbyte vs byte) due to boxed value equality failing across primitive types. - Change RMG032 severity: Warning -> Error - Extend the help message to point users to the new [MapperNoExpressionInlining] attribute and Mapper(NoExpressionInlining) as the supported escape hatch for projection mappings. - When RMG032 applies, bail out (return null) instead of producing a cast-based fallback mapping, eliminating the RMG037/RMG038 cascade. - Also report RMG032 for expression-context enum-to-enum mappings when the underlying types differ, catching the original bug path regardless of the ambient EnumMappingStrategy. - Record the severity change in AnalyzerReleases.Unshipped.md. - Update affected test expectations in QueryableProjectionEnumTest and UseStaticMapperTest. --- src/Riok.Mapperly/AnalyzerReleases.Unshipped.md | 6 ++++++ .../MappingBuilders/EnumToEnumMappingBuilder.cs | 17 ++++++++++++----- .../Diagnostics/DiagnosticDescriptors.cs | 4 ++-- .../Mapping/QueryableProjectionEnumTest.cs | 15 +++++++++++---- .../Mapping/UseStaticMapperTest.cs | 17 ++++------------- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md b/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md index 17d4678ce3..fa78d6b449 100644 --- a/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md +++ b/src/Riok.Mapperly/AnalyzerReleases.Unshipped.md @@ -1,2 +1,8 @@ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### Changed Rules + +Rule ID | New Category | New Severity | Old Category | Old Severity | Notes +--------|--------------|--------------|--------------|--------------|------ +RMG032 | Mapper | Error | Mapper | Warning | The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs index 8298275c86..fde6661d8c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs @@ -36,27 +36,34 @@ public static class EnumToEnumMappingBuilder if (!ctx.IsConversionEnabled(MappingConversionType.EnumToEnum)) return null; + // A cast-based enum mapping in expression context fails to translate + // members between enums with different underlying types (e.g. sbyte vs byte) + // because boxed value equality across primitive types returns false. + // Report RMG032 and bail out instead of cascading RMG037/RMG038. + if (ctx.IsExpression && !SymbolEqualityComparer.Default.Equals(sourceEnumType, targetEnumType)) + return ReportNotSupportedInProjectionMappings(ctx); + // map enums by strategy return ctx.Configuration.Enum.Strategy switch { - EnumMappingStrategy.ByName when ctx.IsExpression => BuildCastMappingAndDiagnostic(ctx), + EnumMappingStrategy.ByName when ctx.IsExpression => ReportNotSupportedInProjectionMappings(ctx), EnumMappingStrategy.ByValue when ctx is { IsExpression: true, Configuration.Enum.HasExplicitConfigurations: true } => - BuildCastMappingAndDiagnostic(ctx), - EnumMappingStrategy.ByValueCheckDefined when ctx.IsExpression => BuildCastMappingAndDiagnostic(ctx), + ReportNotSupportedInProjectionMappings(ctx), + EnumMappingStrategy.ByValueCheckDefined when ctx.IsExpression => ReportNotSupportedInProjectionMappings(ctx), EnumMappingStrategy.ByName => BuildNameMapping(ctx), EnumMappingStrategy.ByValueCheckDefined => BuildEnumToEnumCastMapping(ctx, checkTargetDefined: true), _ => BuildEnumToEnumCastMapping(ctx), }; } - private static INewInstanceMapping BuildCastMappingAndDiagnostic(MappingBuilderContext ctx) + private static INewInstanceMapping? ReportNotSupportedInProjectionMappings(MappingBuilderContext ctx) { ctx.ReportDiagnostic( DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, ctx.Source.ToDisplayString(), ctx.Target.ToDisplayString() ); - return BuildEnumToEnumCastMapping(ctx, true); + return null; } private static INewInstanceMapping BuildEnumToEnumCastMapping( diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs index e14e23f0d6..9083fdc309 100644 --- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs @@ -233,9 +233,9 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor EnumMappingNotSupportedInProjectionMappings = new( "RMG032", "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings", - "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from {0} to {1}", + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from {0} to {1}, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper", DiagnosticCategories.Mapper, - DiagnosticSeverity.Warning, + DiagnosticSeverity.Error, true ); diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs index d0ad6e6688..67d12462b4 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs @@ -41,7 +41,16 @@ TestSourceBuilderOptions.Default with .Should() .HaveDiagnostic( DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, - "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D" + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper" + ) + .HaveDiagnostic(DiagnosticDescriptors.CouldNotMapMember, "Could not map member A.Value of type C to B.Value of type D") + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotMapped, + "The member Value on the mapping source type A is not mapped to any member on the mapping target type B" + ) + .HaveDiagnostic( + DiagnosticDescriptors.SourceMemberNotFound, + "The member Value on the mapping target type B was not found on the mapping source type A" ) .HaveAssertedAllDiagnostics(); } @@ -71,10 +80,8 @@ TestSourceBuilderOptions.Default with .Should() .HaveDiagnostic( DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, - "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D" + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from C to D, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper" ) - .HaveDiagnostic(DiagnosticDescriptors.TargetEnumValueNotMapped, "Enum member Value2 (200) on D not found on source enum C") - .HaveDiagnostic(DiagnosticDescriptors.SourceEnumValueNotMapped, "Enum member Value2 (100) on C not found on target enum D") .HaveAssertedAllDiagnostics(); } diff --git a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs index d5a3ad71cb..3c83318b37 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs @@ -836,20 +836,11 @@ public static partial class Mapper TestHelper .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) .Should() - .HaveDiagnostics( - DiagnosticDescriptors.SourceEnumValueNotMapped, - "Enum member Planned (0) on Source.Status not found on target enum Target.Status", - "Enum member Generated (1) on Source.Status not found on target enum Target.Status", - "Enum member Paused (2) on Source.Status not found on target enum Target.Status", - "Enum member Running (3) on Source.Status not found on target enum Target.Status" - ) - .HaveDiagnostics( - DiagnosticDescriptors.TargetEnumValueNotMapped, - "Enum member Planned (0) on Target.Status not found on source enum Source.Status", - "Enum member Generated (1) on Target.Status not found on source enum Source.Status", - "Enum member Paused (2) on Target.Status not found on source enum Source.Status", - "Enum member Running (3) on Target.Status not found on source enum Source.Status" + .HaveDiagnostic( + DiagnosticDescriptors.EnumMappingNotSupportedInProjectionMappings, + "The enum mapping strategy ByName, ByValueCheckDefined, explicit enum mappings and ignored enum values cannot be used in projection mappings to map from Source.Status to Target.Status, consider applying [MapperNoExpressionInlining] to the mapping method or Mapper(NoExpressionInlining = true) to the containing mapper" ) + .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline) .HaveAssertedAllDiagnostics(); } From 7181f99bbc8ebb81b1caa350a01939d6b30d03b6 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Fri, 24 Apr 2026 12:04:02 +0200 Subject: [PATCH 12/18] docs: note RMG032 promotion to error in 5.0 migration guide Add the RMG032 severity change to the 5.0 breaking-change list and a dedicated section explaining the migration path via the new [MapperNoExpressionInlining] attribute and Mapper(NoExpressionInlining) property. --- docs/docs/breaking-changes/5-0.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/docs/breaking-changes/5-0.md b/docs/docs/breaking-changes/5-0.md index dc21c2fef4..edd80df66b 100644 --- a/docs/docs/breaking-changes/5-0.md +++ b/docs/docs/breaking-changes/5-0.md @@ -22,6 +22,7 @@ description: How to upgrade to Mapperly 5.0 and a list of all its breaking chang - Copy constructors are no longer selected over constructors annotated with `[MapperConstructor]` or when `[MapProperty]` configurations are present. - Generic and runtime target type mappings now correctly match derived types handled by a child `[MapDerivedType]` mapping. - The default severity of `RMG068` (cannot inline user implemented queryable expression mapping) has been raised from info to warning, see below. +- `RMG032` is now an error instead of a warning. Queryable projection mappings that relied on the previous fallback behavior must opt out of inlining, see below. ## Inaccessible members from other assemblies included @@ -179,6 +180,33 @@ public partial Dto Map(int[]? source) } ``` +## `RMG032` promoted to error + +Mapperly previously emitted `RMG032` as a warning when a queryable projection required an enum mapping strategy that cannot be translated to an expression tree +(`ByName`, `ByValueCheckDefined`, explicit enum mappings, ignored enum values, or cast-based mappings between enums with different underlying types). +When this happened, Mapperly silently fell back to a cast-based mapping which produced incorrect code at runtime and cascading false `RMG037` / `RMG038` diagnostics. + +Starting with v5.0, `RMG032` is reported as an error and no fallback mapping is produced. +To opt out of inlining and keep the referenced mapping method as-is in the expression tree, apply the new `[MapperNoExpressionInlining]` attribute or +set `Mapper(NoExpressionInlining = true)` on the callee mapper. +See the [queryable projections documentation](../configuration/queryable-projections.mdx) for more details. + +```csharp +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] +public static partial class EnumMapper +{ + [MapperNoExpressionInlining] + public static partial TargetStatus MapToStatus(this SourceStatus source); +} + +// or, per mapper +[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] +public static partial class EnumMapper +{ + public static partial TargetStatus MapToStatus(this SourceStatus source); +} +``` + ## Constructor selection Previously, when a type had a copy constructor (a single-parameter constructor accepting the same type), From 5c3c4b3c9fd9e256175ba13ea1c7ca92c1b7b69d Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Fri, 24 Apr 2026 12:04:36 +0200 Subject: [PATCH 13/18] docs: move inlining section from mapper.mdx to queryable-projections.mdx The NoExpressionInlining option only applies to queryable projection mappings, so document it under the queryable projections page where users looking for projection-specific guidance will find it. The queryable-projections page already covers the attribute, the property, per-method and per-mapper opt-outs, and the runtime behavior comparison, so the duplicated section in mapper.mdx can simply be removed. --- docs/docs/configuration/mapper.mdx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index e42cac7119..cfb012f76c 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -70,20 +70,6 @@ public partial class MyMapper } ``` -### Inlining - -By default, Mapperly inlines mapping methods into expression trees when possible. -Set `NoExpressionInlining` to `true` to prevent mapping methods of this mapper from being inlined into expression trees -when referenced via `UseStaticMapper`. Defaults to `false`. - -```csharp -[Mapper(NoExpressionInlining = true)] -public partial class CarMapper -{ - // ... -} -``` - ## Properties / fields On each mapping method declaration, property and field mappings can be customized. From 005f39dd716f4b978f42382c0a270f48c778f27e Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Fri, 12 Jun 2026 15:07:08 +0200 Subject: [PATCH 14/18] refactor: read mapper-level NoExpressionInlining through ctx.Configuration --- .../Descriptors/UserMethodMappingExtractor.cs | 11 +++-- .../Mapping/QueryableProjectionEnumTest.cs | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 1141285b40..954a7853f4 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -189,7 +189,7 @@ private static bool IsMappingMethodCandidate(SimpleMappingBuilderContext ctx, IM return null; } - var noExpressionInlining = GetNoExpressionInlining(ctx, method); + var noExpressionInlining = GetNoExpressionInlining(ctx, method, isExternal); // Generic user-implemented methods are stored as templates // that are matched against concrete type pairs during mapping resolution. @@ -318,7 +318,7 @@ string sourceParameterName return null; } - var noExpressionInlining = GetNoExpressionInlining(ctx, methodSymbol); + var noExpressionInlining = GetNoExpressionInlining(ctx, methodSymbol, isExternal: false); if (TryBuildRuntimeTargetTypeMapping(ctx, methodSymbol, noExpressionInlining) is { } userMapping) return userMapping; @@ -514,11 +514,16 @@ out bool hasAttribute return userMappingAttr ?? new UserMappingConfiguration(); } - private static bool GetNoExpressionInlining(SimpleMappingBuilderContext ctx, IMethodSymbol method) + private static bool GetNoExpressionInlining(SimpleMappingBuilderContext ctx, IMethodSymbol method, bool isExternal) { if (ctx.SymbolAccessor.HasAttribute(method)) return true; + if (!isExternal) + return ctx.Configuration.Mapper.NoExpressionInlining; + + // the configuration of external mappers is not merged into ctx.Configuration, + // read the MapperAttribute of the containing type instead var mapperAttribute = ctx.AttributeAccessor.AccessFirstOrDefault(method.ContainingType); return mapperAttribute?.NoExpressionInlining == true; } diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs index 67d12462b4..52b3161673 100644 --- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionEnumTest.cs @@ -85,6 +85,51 @@ TestSourceBuilderOptions.Default with .HaveAssertedAllDiagnostics(); } + [Fact] + public void EnumToAnotherEnumByNameWithMapperNoExpressionInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + public class A { public C Value { get; set; } } + + public class B { public D Value { get; set; } } + + public enum C { Value1, Value2 } + + public enum D { Value1, Value2 } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoExpressionInlining = true)] + public static partial class Mapper + { + public static partial IQueryable Map(IQueryable q); + + public static partial D MapEnum(C src); + } + """ + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveMethodBody( + "Map", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + q, + x => new global::B() + { + Value = MapEnum(x.Value), + } + ); + #nullable enable + """ + ); + } + [Fact] public Task EnumToString() { From f3de520ef840381f13985a03fe14b6c4fff52596 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Sat, 13 Jun 2026 11:19:15 +0200 Subject: [PATCH 15/18] Update ProjectionMapper to use the NoExpressionInlining --- .../Mapper/ProjectionMapper.cs | 13 ++++++++++++- ...st.ExpressionShouldMapObject_NET8_0.verified.txt | 2 +- ...sionShouldMapObject_NETFRAMEWORK4_8.verified.txt | 2 +- ...essionShouldTranslateToQuery_NET8_0.verified.sql | 2 +- ...essionShouldTranslateToQuery_NET9_0.verified.sql | 2 +- ...t.ExpressionShouldTranslateToResult.verified.txt | 4 ++-- ...ectionShouldTranslateToQuery_NET8_0.verified.sql | 2 +- ...ectionShouldTranslateToQuery_NET9_0.verified.sql | 2 +- ...t.ProjectionShouldTranslateToResult.verified.txt | 4 ++-- ...rTest.SnapshotGeneratedSource_NET8_0.verified.cs | 12 ++++++------ ...pshotGeneratedSource_NETFRAMEWORK4_8.verified.cs | 12 ++++++------ 11 files changed, 34 insertions(+), 23 deletions(-) diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs index edfab900ee..62105e5d15 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ProjectionMapper.cs @@ -8,7 +8,18 @@ namespace Riok.Mapperly.IntegrationTests.Mapper { - [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByValue, AutoUserMappings = false)] + [Mapper(NoExpressionInlining = true)] + public static partial class EnumProjectionMapper + { + [MapEnum(EnumMappingStrategy.ByValue)] + public static partial TestEnumDtoByValue MapToTestEnumDtoByValue(TestEnum source); + + [MapEnum(EnumMappingStrategy.ByName)] + public static partial TestEnumDtoByName MapToTestEnumDtoByName(TestEnum source); + } + + [UseStaticMapper(typeof(EnumProjectionMapper))] + [Mapper(AutoUserMappings = false)] public static partial class ProjectionMapper { public static partial IQueryable ProjectToDto(this IQueryable q); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt index 8ed74a080b..45aca6d760 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NET8_0.verified.txt @@ -9,7 +9,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt index 83f8d4d3d9..ee60f58fc9 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldMapObject_NETFRAMEWORK4_8.verified.txt @@ -9,7 +9,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql index 481f89469c..3dc53a70b3 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET8_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" FROM "Objects" AS "o" INNER JOIN "IdObject" AS "i" ON "o"."FlatteningIdValue" = "i"."IdValue" LEFT JOIN "IdObject" AS "i0" ON "o"."NullableFlatteningIdValue" = "i0"."IdValue" diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql index 49bb79e593..b2c1853d1b 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToQuery_NET9_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" WHEN 10 THEN 'Value10' WHEN 20 THEN 'Value20' WHEN 30 THEN 'Value30' diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt index fe59dd382c..7ce990a727 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ExpressionShouldTranslateToResult.verified.txt @@ -61,7 +61,7 @@ ] }, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumStringValue: 0, EnumReverseStringValue: Value10, ManuallyMapped: { @@ -82,7 +82,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql index 481f89469c..3dc53a70b3 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET8_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), "o"."EnumStringValue", "o"."EnumReverseStringValue", "i1"."SubIntValue" IS NOT NULL, "i1"."BaseIntValue", date("o"."DateTimeValueTargetDateOnly"), "o"."DateTimeValueTargetTimeOnly", "o"."ManuallyMapped", "o"."ManuallyMappedModified" + 10, COALESCE("o"."ManuallyMappedNullableToNonNullable", -1), "t3"."Value", "t3"."Id", "i2"."Id", "i2"."TestObjectProjectionId", "i2"."Value", "l"."Value", "l"."Id" FROM "Objects" AS "o" INNER JOIN "IdObject" AS "i" ON "o"."FlatteningIdValue" = "i"."IdValue" LEFT JOIN "IdObject" AS "i0" ON "o"."NullableFlatteningIdValue" = "i0"."IdValue" diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql index 49bb79e593..b2c1853d1b 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToQuery_NET9_0.verified.sql @@ -1,7 +1,7 @@ SELECT "o"."CtorValue", "o"."IntValue", "o"."IntInitOnlyValue", "o"."RequiredValue", "o"."StringValue", "o"."RenamedStringValue", "i"."IdValue", "i0"."IdValue", CASE WHEN "t"."IntValue" IS NOT NULL THEN "t"."IntValue" ELSE 0 -END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", CAST("o"."EnumValue" AS INTEGER), CAST("o"."EnumName" AS INTEGER), CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" +END, "t"."IntValue" IS NOT NULL, "t"."IntValue", "t0"."IntValue" IS NOT NULL, "t0"."IntValue", COALESCE("o"."StringNullableTargetNotNullable", ''), "o0"."Id", "o0"."CtorValue", "o0"."DateTimeValueTargetDateOnly", "o0"."DateTimeValueTargetTimeOnly", "o0"."EnumName", "o0"."EnumRawValue", "o0"."EnumReverseStringValue", "o0"."EnumStringValue", "o0"."EnumValue", "o0"."FlatteningIdValue", "o0"."IgnoredIntValue", "o0"."IgnoredStringValue", "o0"."IntInitOnlyValue", "o0"."IntValue", "o0"."ManuallyMapped", "o0"."ManuallyMappedModified", "o0"."ManuallyMappedNullableToNonNullable", "o0"."NestedNullableIntValue", "o0"."NestedNullableTargetNotNullableIntValue", "o0"."NullableFlatteningIdValue", "o0"."NullableUnflatteningIdValue", "o0"."RecursiveObjectId", "o0"."RenamedStringValue", "o0"."RequiredValue", "o0"."StringNullableTargetNotNullable", "o0"."StringValue", "o0"."SubObjectSubIntValue", "o0"."UnflatteningIdValue", "o"."Id", "i1"."SubIntValue", "t1"."IntValue", "t1"."TestObjectProjectionId", "t2"."IntValue", "o"."EnumValue", "o"."EnumName", CAST("o"."EnumRawValue" AS INTEGER), CASE "o"."EnumStringValue" WHEN 10 THEN 'Value10' WHEN 20 THEN 'Value20' WHEN 30 THEN 'Value30' diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt index fe59dd382c..7ce990a727 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.ProjectionShouldTranslateToResult.verified.txt @@ -61,7 +61,7 @@ ] }, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumStringValue: 0, EnumReverseStringValue: Value10, ManuallyMapped: { @@ -82,7 +82,7 @@ NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar, EnumValue: DtoValue2, - EnumName: 10, + EnumName: Value10, EnumRawValue: 30, EnumStringValue: Value10, EnumReverseStringValue: Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs index 72abee8f9c..8d126fa198 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -39,8 +39,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = global::System.Enum.Parse(x.EnumReverseStringValue, false), @@ -121,8 +121,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = global::System.Enum.Parse(x.EnumReverseStringValue, false), @@ -187,8 +187,8 @@ public static partial class ProjectionMapper { target.NullableReadOnlyObjectCollection = null; } - target.EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; - target.EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)testObject.EnumName; + target.EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(testObject.EnumValue); + target.EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(testObject.EnumName); target.EnumRawValue = (byte)testObject.EnumRawValue; target.EnumStringValue = MapToString(testObject.EnumStringValue); target.EnumReverseStringValue = MapToTestEnumDtoByName(testObject.EnumReverseStringValue); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs index be69037d66..c1c98f9e85 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ProjectionMapperTest.SnapshotGeneratedSource_NETFRAMEWORK4_8.verified.cs @@ -39,8 +39,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)global::System.Enum.Parse( @@ -125,8 +125,8 @@ public static partial class ProjectionMapper } ) ) : default, - EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)x.EnumValue, - EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)x.EnumName, + EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(x.EnumValue), + EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(x.EnumName), EnumRawValue = (byte)x.EnumRawValue, EnumStringValue = (string)x.EnumStringValue.ToString(), EnumReverseStringValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)global::System.Enum.Parse( @@ -195,8 +195,8 @@ public static partial class ProjectionMapper { target.NullableReadOnlyObjectCollection = null; } - target.EnumValue = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue)testObject.EnumValue; - target.EnumName = (global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName)testObject.EnumName; + target.EnumValue = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByValue(testObject.EnumValue); + target.EnumName = global::Riok.Mapperly.IntegrationTests.Mapper.EnumProjectionMapper.MapToTestEnumDtoByName(testObject.EnumName); target.EnumRawValue = (byte)testObject.EnumRawValue; target.EnumStringValue = MapToString(testObject.EnumStringValue); target.EnumReverseStringValue = MapToTestEnumDtoByName(testObject.EnumReverseStringValue); From 58eacaab59ab8404e2c5f5a32442d7d24f8446e4 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Sat, 18 Jul 2026 15:00:15 +0200 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=93=9D=20docs:=20use=20AsAsyncEnume?= =?UTF-8?q?rable=20in=20non-inlined=20projection=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs/configuration/queryable-projections.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index c52e70f177..57820613a5 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -207,7 +207,7 @@ var dtos = await dbContext.Cars // highlight-end }) // highlight-start - .AsEnumerable() // ← everything before runs in SQL, everything after is client-side + .AsAsyncEnumerable() // ← everything before runs in SQL, everything after is client-side // highlight-end .Select(x => new CarDto { From 83d002f421de418de6908c0583867415aa9b9db9 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Sat, 18 Jul 2026 15:50:07 +0200 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20never=20inline=20ma?= =?UTF-8?q?ppings=20declared=20in=20other=20assemblies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InlineExpressionMappingBuilderContext.cs | 14 +++++++++ .../Mapping/UseStaticMapperTest.cs | 30 ++++++++++--------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs index 26ce6325e4..a38b9e1b03 100644 --- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs @@ -204,6 +204,14 @@ private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) // check if NoInline is requested IUserMapping { NoExpressionInlining: true } => mapping, + // methods declared in another assembly are never inlined: + // whether their syntax is accessible depends on the compilation stage + // (available for compilation references, unavailable for compiled assembly references), + // treat them always as non-inlinable to generate deterministic code + IUserMapping externalAssemblyMapping + when !SymbolEqualityComparer.Default.Equals(externalAssemblyMapping.Method.ContainingAssembly, Compilation.Assembly) => + ReportCannotInline(mapping, externalAssemblyMapping.Method), + // inline existing mapping UserImplementedMethodMapping implementedMapping => InlineOrRebuild(implementedMapping) ?? implementedMapping, @@ -219,6 +227,12 @@ private INewInstanceMapping TryInlineMapping(INewInstanceMapping mapping) }; } + private INewInstanceMapping ReportCannotInline(INewInstanceMapping mapping, IMethodSymbol method) + { + ReportDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline, method); + return mapping; + } + private INewInstanceMapping? InlineOrRebuild(UserImplementedMethodMapping mapping) { // Try inline first diff --git a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs index 3c83318b37..caec7399d5 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs @@ -429,20 +429,22 @@ public void UseStaticGenericMapperStaticMethodFromAnotherAssemblyAsReference() { var result = ExecuteStaticGenericMapperStaticMethodFromAnotherAssemblyCompilation(asCompilationReference: true); - result.HaveMethodBody( - "ProjectToTarget", - """ - #nullable disable - return global::System.Linq.Queryable.Select( - source, - x => new global::Mapper.Target() - { - DateTime = new global::System.DateTimeOffset(x.DateTime, global::System.TimeSpan.Zero), - } - ); - #nullable enable - """ - ); + result + .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingCannotInline) + .HaveMethodBody( + "ProjectToTarget", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + source, + x => new global::Mapper.Target() + { + DateTime = global::Riok.Mapperly.TestDependency.Mapper.DateTimeMapper.MapToDateTimeOffset(x.DateTime), + } + ); + #nullable enable + """ + ); } /// From 9aeaf610bc1f23902855166155267a4d9459efe3 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Sat, 18 Jul 2026 15:50:25 +0200 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=90=9B=20fix:=20honor=20MapperDefau?= =?UTF-8?q?lts=20NoExpressionInlining=20for=20external=20mappers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MapperConfigurationReader.cs | 25 ++++- .../SimpleMappingBuilderContext.cs | 2 + .../Descriptors/UserMethodMappingExtractor.cs | 5 +- .../Mapping/UseStaticMapperTest.cs | 92 +++++++++++++++++++ 4 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index 3968e59d0f..f2d351e861 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -13,11 +13,13 @@ namespace Riok.Mapperly.Configuration; public class MapperConfigurationReader { private readonly Dictionary _resolvedConfigurations = new(); + private readonly Dictionary _resolvedMapperConfigurations = new(SymbolEqualityComparer.Default); private readonly AttributeDataAccessor _dataAccessor; private readonly MappingCollection _mappings; private readonly GenericTypeChecker _genericTypeChecker; private readonly DiagnosticCollection _diagnostics; private readonly WellKnownTypes _types; + private readonly MapperConfiguration _defaultMapperConfiguration; public MapperConfigurationReader( AttributeDataAccessor dataAccessor, @@ -35,9 +37,9 @@ SupportedFeatures supportedFeatures _genericTypeChecker = genericTypeChecker; _diagnostics = diagnostics; _types = types; + _defaultMapperConfiguration = defaultMapperConfiguration; - var mapperConfiguration = _dataAccessor.AccessSingle(mapperSymbol); - var mapper = MapperConfigurationMerger.MergeToAttribute(mapperConfiguration, defaultMapperConfiguration); + var mapper = BuildMapperConfiguration(mapperSymbol); MapperConfiguration = new MappingConfiguration( mapper, @@ -61,6 +63,25 @@ SupportedFeatures supportedFeatures public MappingConfiguration MapperConfiguration { get; } + /// + /// Builds the effective configuration for a mapper symbol + /// by merging its with the default mapper configuration + /// ( and MSBuild options). + /// + public MapperAttribute BuildMapperConfiguration(ISymbol mapperSymbol) + { + if (_resolvedMapperConfigurations.TryGetValue(mapperSymbol, out var mapper)) + return mapper; + + var mapperConfiguration = + _dataAccessor.AccessFirstOrDefault(mapperSymbol) ?? new MapperConfiguration(); + + mapper = MapperConfigurationMerger.MergeToAttribute(mapperConfiguration, _defaultMapperConfiguration); + _resolvedMapperConfigurations[mapperSymbol] = mapper; + + return mapper; + } + public MappingConfiguration BuildFor(MappingConfigurationReference reference, bool supportsDeepCloning) { if (_resolvedConfigurations.TryGetValue(reference, out var resolved)) diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index 771be5d94d..a152d9934d 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -77,6 +77,8 @@ protected SimpleMappingBuilderContext(SimpleMappingBuilderContext ctx, Location? public SemanticModel? GetSemanticModel(SyntaxTree syntaxTree) => _compilationContext.GetSemanticModel(syntaxTree); + public MapperAttribute BuildMapperConfiguration(ISymbol mapperSymbol) => _configurationReader.BuildMapperConfiguration(mapperSymbol); + public virtual bool IsConversionEnabled(MappingConversionType conversionType) => Configuration.Mapper.EnabledConversions.HasFlag(conversionType); diff --git a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs index 954a7853f4..53ae928ffd 100644 --- a/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs +++ b/src/Riok.Mapperly/Descriptors/UserMethodMappingExtractor.cs @@ -523,9 +523,8 @@ private static bool GetNoExpressionInlining(SimpleMappingBuilderContext ctx, IMe return ctx.Configuration.Mapper.NoExpressionInlining; // the configuration of external mappers is not merged into ctx.Configuration, - // read the MapperAttribute of the containing type instead - var mapperAttribute = ctx.AttributeAccessor.AccessFirstOrDefault(method.ContainingType); - return mapperAttribute?.NoExpressionInlining == true; + // build it from the MapperAttribute of the containing type and the default configuration + return ctx.BuildMapperConfiguration(method.ContainingType).NoExpressionInlining; } private static IEnumerable ExtractNamedUserImplementedMappings( diff --git a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs index caec7399d5..f6b5dc7288 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs @@ -1098,6 +1098,98 @@ public static partial class Mapper ); } + [Fact] + public void ProjectionWithUsedMapperDefaultsNoExpressionInliningShouldNotInline() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + using System.Linq; + + [assembly: MapperDefaults(NoExpressionInlining = true)] + + namespace Source + { + public enum Status : sbyte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + namespace Target + { + public enum Status : byte + { + Planned = 0, + Generated = 1, + Paused = 2, + Running = 3, + } + } + + public class Entity + { + public int Id { get; set; } + public Source.Status Status { get; set; } + } + + public class Domain + { + public int Id { get; set; } + public Target.Status Status { get; set; } + } + + [Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)] + public static partial class EnumMappingExtensions + { + public static partial Target.Status MapToStatus(this Source.Status source); + } + + [Mapper(NoExpressionInlining = false)] + [UseStaticMapper(typeof(EnumMappingExtensions))] + public static partial class Mapper + { + public static partial IQueryable ProjectToDomain(this IQueryable queryable); + + public static partial Domain MapToDomain(this Entity entity); + } + """ + ); + + var generated = TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics); + + generated.Diagnostics.ShouldNotContain(x => x.Descriptor.Id == DiagnosticDescriptors.QueryableProjectionMappingCannotInline.Id); + generated + .Should() + .HaveMethodBody( + "MapToDomain", + """ + var target = new global::Domain(); + target.Id = entity.Id; + target.Status = global::EnumMappingExtensions.MapToStatus(entity.Status); + return target; + """ + ) + .HaveMethodBody( + "ProjectToDomain", + """ + #nullable disable + return global::System.Linq.Queryable.Select( + queryable, + x => new global::Domain() + { + Id = x.Id, + Status = global::EnumMappingExtensions.MapToStatus(x.Status), + } + ); + #nullable enable + """ + ); + } + [Fact] public void ProjectionWithCalledMethodNoExpressionInliningShouldNotInline() {