Skip to content

Add LinqraftMappingGenerate attribute for EFCore query compilation support#245

Merged
arika0093 merged 8 commits intomainfrom
copilot/fix-efcore-query-precompilation
Dec 11, 2025
Merged

Add LinqraftMappingGenerate attribute for EFCore query compilation support#245
arika0093 merged 8 commits intomainfrom
copilot/fix-efcore-query-precompilation

Conversation

Copy link
Contributor

Copilot AI commented Dec 11, 2025

Changes Made to Address Latest Feedback

Moved Generation Logic to Linqraft.Core (comments 2609492424, 2609495116)

  • Line 182: Moved header generation logic to GenerateSourceCodeSnippets.GenerateMappingMethodHeader()
  • Line 126: Moved mapping class generation to GenerateSourceCodeSnippets.BuildMappingClassCode()
  • Line 126: Moved mapping method generation to GenerateSourceCodeSnippets.GenerateMappingMethod()
  • Updated SelectExprGroups.cs to call these methods from GenerateSourceCodeSnippets
  • Removed duplicate code from SelectExprGroups.cs

Enhanced Accessibility Handling (comment 2609490994)

  • Line 212: Now properly retrieves the class's DeclaredAccessibility from Roslyn
  • Supports public, internal, and private modifiers
  • Generates partial keyword for class declarations
  • Handles cases where class is declared with different accessibility

Clarified Attribute Detection (comment 2609500042)

  • Added comment noting that syntax-level check is intentional for performance
  • Semantic validation with full name check using ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) already happens in GetMappingMethodInfo
  • This two-stage approach (syntax filter → semantic validation) is the correct pattern for source generators

Code Organization Benefits

  • Centralized all mapping-related code generation in Linqraft.Core
  • Better separation of concerns: generator orchestrates, Core handles generation
  • Easier to test and maintain generation logic
  • Consistent with existing pattern for DTO and expression generation

Test Results

  • All tests pass (2/2 in mapping tests)
  • No regressions introduced
Original prompt

This section details on the original issue you should resolve

<issue_title>bug: When trying to use EFCore's query (pre)compilation, it fails to parse correctly</issue_title>
<issue_description>This is a complex issue.
EFCore supports query precompilation (pre-compilation) for NativeAOT compatibility.
However, since it relies on the interceptor feature, it conflicts with Linqraft's core concept and does not work correctly.

Specifically, the following error occurs:

QueryPrecompilationError { SyntaxNode = _dbContext
    .SampleClasses.SelectExpr<SampleClass, LinqraftSampleClassDto>(s => new
    {
        s.Id,
        s.Foo,
        s.Bar,
        Childs = s.Childs.Select(c => new
        {
            c.Id,
            c.Baz,
            ChildId = c.Child?.Id,
            ChildQux = c.Child?.Qux,
        }),
        Child2Id = s.Child2?.Id,
        Child2Quux = s.Child2?.Quux,
        Child3Id = s.Child3.Id,
        Child3Corge = s.Child3.Corge,
        Child3ChildId = s.Child3?.Child?.Id,
        Child3ChildGrault = s.Child3?.Child?.Grault,
    })
    .ToListAsync(), Exception = System.NotSupportedException: Unsupported syntax node of type 'Microsoft.CodeAnalysis.CSharp.Syntax.ConditionalAccessExpressionSyntax': c.Child?.Id

Precompilation is a useful feature and is expected to become mainstream in the future. Therefore, it is desirable to make it available in Linqraft as well.</issue_description>

<agent_instructions>Please try implementing the proposal mentioned in the comments in Issue #238. It will likely involve checking whether it is being executed within [LinqraftMappingGenerateAttribute] in the SelectExpr generation logic and branching accordingly. Depending on the case, it might be better to separate the generation logic into a different file. Please also add a document summarizing the contents described in this Issue (#238) along with the implemented content in docs/library. Additionally, updates to the Playground side may also be necessary.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@arika0093 The terminology is confusing, so let's organize it. * [`Compiled queries`](https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics?tabs=with-di%2Cexpression-api-with-constant#compiled-queries) * This refers to queries that are pre-built "within the program." * Queries are cached in a static field using `EF.CompileAsyncQuery`. * Contributes to improved performance. It is unclear whether it affects NativeAOT. * `Precompiled query`(efcore issues 25009) * This refers to queries that are pre-built "at program build time." * Required for NativeAOT. * Introduced in EF9. * Uses interceptors to hook query calls and inject pre-constructed SQL queries.

Currently, Linqraft does not work with either.

  • For compiled queries:
    • EF.CompileAsyncQuery requires an ExpressionTree as an argument, so ?. access cannot be used.
    • Compilation likely fails due to anonymous type invocation in the arguments.
  • For precompiled queries:
    • Due to interceptor conflicts, Linqraft's functionality is highly likely to not work.
    • It does not yet appear to be stable. While future support may be necessary, it seems premature at this stage.
@arika0093 Here is a possible implementation plan. (I am not sure if it will work well)
// step 1: Define as a static partial class.
public static partial class SampleClassQuery
{
  // step 2: Define a function that calls a dummy SelectExpr.
  // This method is not actually called; it is only used as a base for Linqraft to generate source code.
  // The name of the generated method is specified in the [LinqraftMappingGenerate] attribute.
  // Note: The definition of this method can be anything as long as it internally calls SelectExpr.
  [LinqraftMappingGenerate("ProjectToSampleClassDto")]
  internal static IQueryable<SampleClassDto> DummyQuery(this IQueryable<SampleClass> source) => source
    // Define the mapping using an anonymous type, just like a normal SelectExpr.
    .SelectExpr<SampleClass, SampleClassDto>(x => new
    {
      Id = x.Id,
      CustomerName = x.Customer?.Name,
      Items = x.Items
        .SelectExpr<SampleChildClass, SampleClassItemDto>(item => new
        {
          Calced = item.Value * 2
        }),
    });

  // If necessary, you can also provide an empty definition of the generated method.
  // In this case, you can declare it like [LinqraftMappingGenerate(nameof(ProjectToSampleClassDto))].
  // internal static partial IQueryable<SampleClassDto> ProjectToSampleClassDto(this IQueryable<SampleClass> source);

  // step 3: Linqraft parses the SelectExpr method in the [LinqraftMappingGenerate] attribute and generates the corresponding query method.
  // Instead of the traditional interceptor, it simply generates the same thing within this cla...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes arika0093/Linqraft#238

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 11, 2025

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI and others added 3 commits December 11, 2025 06:08
Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix EFCore query precompilation parsing issue Add LinqraftMappingGenerate attribute for EFCore query compilation support Dec 11, 2025
Copilot AI requested a review from arika0093 December 11, 2025 06:20
Copy link
Owner

@arika0093 arika0093 left a comment

Choose a reason for hiding this comment

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


// With compiled queries (EF Core 2.0+)
private static readonly Func<MyDbContext, Task<List<OrderDto>>> GetOrdersCompiled =
EF.CompileAsyncQuery((MyDbContext db) => db.Orders.ProjectToDto().ToList());
Copy link
Owner

Choose a reason for hiding this comment

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

use ToListAsync if used Compile"Async"Query

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Changed to ToListAsync() for use with CompileAsyncQuery.

| Regular queries | ✅ | ✅ |
| Compiled queries | ❌ | ✅ |
| Precompiled queries | ❌ | ✅ |
| NativeAOT | ❌ | ✅ |
Copy link
Owner

Choose a reason for hiding this comment

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

It is incorrect to say that NativeAot itself works only with the normal Interceptor pattern. The issue arises specifically with EFCore+NativeAOT.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Changed to "EFCore + NativeAOT" to clarify it's specifically the combination that requires this approach.

/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
[EditorBrowsable(EditorBrowsableState.Never)]
Copy link
Owner

Choose a reason for hiding this comment

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

drop editorbrowsable and example documents

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Removed EditorBrowsable attribute and example documentation from XML comments.

|| attrName == "LinqraftMappingGenerateAttribute"
|| attrName == "Linqraft.LinqraftMappingGenerate"
|| attrName == "Linqraft.LinqraftMappingGenerateAttribute";
});
Copy link
Owner

Choose a reason for hiding this comment

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

Try obtaining the Fullname with the RoslynAPI and confirming that it is Linqraft.LinqraftMappingGenerateAttribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Changed to use ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) and check for exact match "global::Linqraft.LinqraftMappingGenerateAttribute".

Copy link
Owner

Choose a reason for hiding this comment

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

It's not applied. Try using the line just below: var fullName = attr.AttributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return fullName == "global::Linqraft.LinqraftMappingGenerateAttribute";.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The syntax-level check at line 444 is intentional for source generator performance - it filters candidate nodes before semantic analysis. The full name verification using ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) is already implemented in GetMappingMethodInfo at line 465 where it checks for exact match "global::Linqraft.LinqraftMappingGenerateAttribute". This two-stage approach (syntax filter → semantic validation) is the recommended pattern for incremental generators.

.FirstOrDefault(attr =>
attr.AttributeClass?.Name == "LinqraftMappingGenerateAttribute"
&& attr.AttributeClass.ContainingNamespace.ToDisplayString() == "Linqraft"
);
Copy link
Owner

Choose a reason for hiding this comment

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

Similarly, try modifying this to determine by Fullname.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Now using full name check with ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).

{
x.Id,
x.Title,
Children = x.Children.Select(c => new
Copy link
Owner

Choose a reason for hiding this comment

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

use .SelectExpr<MappingTestChildClass, MappingTestChildClassDto>(... instead of normal Select.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Changed to use .SelectExpr<MappingTestChildClass, MappingTestChildClassDto>(...) instead of regular Select.


// Test source classes
public class MappingTestSampleClass
{
Copy link
Owner

Choose a reason for hiding this comment

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

Please consolidate class definitions at the end of the file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. All class definitions have been moved to the end of the file.

return new SelectExprMappingInfo
{
MethodDeclaration = method,
TargetMethodName = targetMethodName,
Copy link
Owner

Choose a reason for hiding this comment

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

fix Possible null reference assignment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Added null-forgiving operator to targetMethodName.

Copy link
Owner

Choose a reason for hiding this comment

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

Please change the explanation to focus on "implementing the Select method within a class" without prominently featuring EFCore support. The file name should also be changed accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 7300fec. Renamed file to mapping-methods.md and refocused documentation on "implementing Select methods within a class." EFCore support is now presented as one use case among several (reusability, organization, testability). The documentation now emphasizes general usage patterns first before showing EFCore-specific scenarios.

…ests

Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
Copy link
Owner

@arika0093 arika0093 left a comment

Choose a reason for hiding this comment

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

sb.AppendLine($"/// </summary>");

// Generate method signature as an extension method
sb.AppendLine($"internal static {returnTypePrefix}<{dtoClassName}> {methodName}(");
Copy link
Owner

Choose a reason for hiding this comment

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

Handles cases where a class is declared as public partial ... beforehand. It retrieves the accessibility and adds the partial keyword.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit d40fad6. Now properly retrieves the class's DeclaredAccessibility using Roslyn API and supports public, internal, and private modifiers. The partial keyword is always added to the class declaration.

}

// Generate extension method for mapping methods (without interceptor)
private static string GenerateMappingMethod(SelectExprInfo info)
Copy link
Owner

Choose a reason for hiding this comment

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

This logic should reside in Linqraft.Core / GenerateSourceCodeSnippets. Please obtain headers and common parts from there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit d40fad6. Moved header generation logic to GenerateSourceCodeSnippets.GenerateMappingMethodHeader() in Linqraft.Core. The method uses the existing GenerateCommentHeaderPart() for consistent headers.

}

// Generate the containing class code for mapping methods
private static string GenerateMappingClassCode(INamedTypeSymbol containingClass, List<string> methods)
Copy link
Owner

Choose a reason for hiding this comment

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

This logic should reside in Linqraft.Core.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit d40fad6. Moved both GenerateMappingClassCode() and GenerateMappingMethod() to GenerateSourceCodeSnippets in Linqraft.Core. SelectExprGroups now calls these methods instead of having duplicate logic.

|| attrName == "LinqraftMappingGenerateAttribute"
|| attrName == "Linqraft.LinqraftMappingGenerate"
|| attrName == "Linqraft.LinqraftMappingGenerateAttribute";
});
Copy link
Owner

Choose a reason for hiding this comment

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

It's not applied. Try using the line just below: var fullName = attr.AttributeClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return fullName == "global::Linqraft.LinqraftMappingGenerateAttribute";.

…ization

Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
Copilot AI requested a review from arika0093 December 11, 2025 07:53
@arika0093 arika0093 marked this pull request as ready for review December 11, 2025 08:10
@arika0093 arika0093 merged commit 5521986 into main Dec 11, 2025
4 checks passed
@arika0093 arika0093 deleted the copilot/fix-efcore-query-precompilation branch December 11, 2025 13:33
arika0093 added a commit that referenced this pull request Dec 12, 2025
…pport (#245)

* Initial plan

* Add LinqraftMappingGenerate attribute and detection logic

Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>

* Implement extension method generation for LinqraftMappingGenerate

Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>

* Add documentation and additional tests for LinqraftMappingGenerate

Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>

* Address PR feedback: refocus docs, fix attribute detection, improve tests

Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>

* Move mapping generation logic to Linqraft.Core and improve code organization

Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>

* Refactor mapping class code generation: streamline header and namespace handling

* Fix namespace declaration in Issue_NestedSelectExprTest

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arika0093 <4524647+arika0093@users.noreply.github.com>
Co-authored-by: Arika Ishinami <delete0093@gmail.com>
(cherry picked from commit 5521986)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants