From c3a022330317242634ade07ea5cb5a7f6d1a54e7 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 1/8] 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 b3b77942b9a8a3a6fb1e0e312e703881412c1f9f Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 2/8] 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 c9c22b0cc4c9bfdaf4dc7991177e8222aa040ed7 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 3/8] 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 d33552ef66..eb3c0dfafc 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.targets +++ b/src/Riok.Mapperly/Riok.Mapperly.targets @@ -24,5 +24,6 @@ + From aede6a0601ed1954beead90f0fbb9b20302f502b Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 4/8] 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 3ad1c41b2f..2ef5a8a195 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 942c0cd3a7b0917d8faa2f9d8edd90cbe826b522 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 5/8] 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 f431dcf80a15c37e70823b3b6e3a643ce05e5d48 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Tue, 14 Apr 2026 13:49:26 +0200 Subject: [PATCH 6/8] 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 adf6ddb145da563ae2afb9ab26c629caeaba5f9c Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Wed, 15 Apr 2026 20:54:59 +0200 Subject: [PATCH 7/8] 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 258f70f724dab9544cb4cb0613f15145e92dc3a4 Mon Sep 17 00:00:00 2001 From: Jan Trejbal Date: Sat, 18 Apr 2026 22:02:06 +0200 Subject: [PATCH 8/8] 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,