Skip to content

feat: add [MapperNoInlining] attribute and MapperAttribute.NoInlining property#2240

Open
trejjam wants to merge 8 commits intoriok:mainfrom
trejjam:fix/enum-explicit-cast-regression-repro
Open

feat: add [MapperNoInlining] attribute and MapperAttribute.NoInlining property#2240
trejjam wants to merge 8 commits intoriok:mainfrom
trejjam:fix/enum-explicit-cast-regression-repro

Conversation

@trejjam
Copy link
Copy Markdown
Contributor

@trejjam trejjam commented Apr 14, 2026

feat: add [MapperNoInlining] attribute and MapperAttribute.NoInlining property

Description

When a mapper referenced via [UseStaticMapper] is used from a mapper that has IQueryable projection methods, Mapperly attempts to inline or rebuild the referenced mapping methods into expression trees. For enum mappings configured with EnumMappingStrategy.ByName where the source and target enums have different underlying types, this rebuild in expression context produces false RMG037/RMG038 diagnostics — reporting all enum members as unmapped even though they match by name.

I understand the rationale behind the expression-context restrictions for the general case, and the 5.x alpha series has been catching up and tightening these rules further, which makes sense. However, in my project I was relying on [UseStaticMapper] with enum-by-name mappings in queryable projections, and the stricter inlining behavior broke that usage. We've discussed the projection + enum + inlining interaction before.

This PR adds a general opt-out mechanism for expression-tree inlining:

  • [MapperNoInlining] — method-level attribute to prevent a specific mapping method from being inlined/rebuilt in expression context
  • [Mapper(NoInlining = true)] — mapper-level property to prevent all methods of a mapper from being inlined when referenced via [UseStaticMapper]

Both are applied on the callee mapper (the one being referenced), not the consuming mapper.

Example

// Per-method opt-out
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)]
public static partial class EnumMapper
{
    [MapperNoInlining]
    public static partial TargetStatus MapToStatus(this SourceStatus source);
}

// Or per-mapper opt-out
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName, NoInlining = true)]
public static partial class EnumMapper
{
    public static partial TargetStatus MapToStatus(this SourceStatus source);
}

When inlining is disabled, the method call is preserved as-is in the expression tree. The ORM evaluates it client-side instead of attempting to translate it to SQL.

Changes

  • New MapperNoInliningAttribute marker attribute (AttributeTargets.Method)
  • New NoInlining property on MapperAttribute (default: false)
  • Check in InlineExpressionMappingBuilderContext.TryInlineMapping() before dispatching to inline/rebuild
  • Configuration pipeline wired through MapperConfiguration and MapperConfigurationMerger
  • Tests covering: bug repro, method-level attribute, mapper-level property, combined, non-projection (no effect)
  • Documentation in queryable projections guide with runtime behavior comparison

Disclosure: This PR was produced with AI assistance. All code has been manually reviewed, verified, and iteratively refined by me to ensure correctness and adherence to project guidelines.

Checklist

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

Comment thread src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs Outdated
Comment thread src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs Outdated

private bool ShouldSkipInlining(IMethodSymbol method)
{
if (SymbolAccessor.HasAttribute<MapperNoInliningAttribute>(method))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Mapperly usually only reads configuration through ctx.Configuration / from the mapping itself. IMO the correct way here would be passing it through the mapping. ctx.Configuration is available in the user mapping extractor.

TestHelper
.GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
.Should()
.HaveDiagnostics(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should result in RMG032, no? IMO this is somewhat the root cause. We should point to the new NoInliningAttribute in the help of RMG032.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's possible. If I understand the flow correctly, since RMG032 is only a warning. It should produce some code. But since the EnumToEnumMappingBuilder has difficulty producing a mapping as a fallback, we end up with Source/TargetNotMapped.

I can change RMG032 to an error -> no fallback to the EnumToEnumMappingBuilder, and propose using NoInlining. Is this ok for you?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sgtm. Please note this as a breaking change in the 5.0 migration doc.

@latonz latonz added the enhancement New feature or request label Apr 24, 2026
}
```

### Inlining
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd move this into the queryable projection documentation page.

/// will not be inlined or rebuilt in expression context.
/// Defaults to <c>false</c>.
/// </summary>
public bool NoInlining { get; set; }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO we should include the fact that this only applies to expressions / queryable projection mappings in the name. The same for the separate attribute.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants