Skip to content

Add generic projection hooks with AsLeftJoin and AsProjectable support#311

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/add-asleftjoin-and-asprojectable
Draft

Add generic projection hooks with AsLeftJoin and AsProjectable support#311
Copilot wants to merge 3 commits intomainfrom
copilot/add-asleftjoin-and-asprojectable

Conversation

Copy link
Contributor

Copilot AI commented Mar 18, 2026

This change adds a reusable projection-hook mechanism for *Expr generation so user-authored anonymous projections can opt into custom rewrites. It introduces built-in support for AsLeftJoin() and AsProjectable(), while enforcing that these hooks are only used inside generated projection contexts.

  • Projection hook infrastructure

    • Added configurable projection hook definitions to the generator options.
    • Support declarations now emit generated no-op extension methods for registered hooks.
    • The mechanism is generic so future hooks can be added without introducing another one-off path.
  • AsLeftJoin() rewrite

    • Added a built-in AsLeftJoin() hook for nullable navigation access inside generated projections.
    • The emitter rewrites x.Child.AsLeftJoin().Foo into an explicit null-guarded form so the generated projection preserves outer rows instead of collapsing into inner-join-like behavior.
  • AsProjectable() rewrite

    • Added a built-in AsProjectable() hook for inlining source-defined computed members into generated projections.
    • Supports expansion of instance properties and methods by substituting the receiver and arguments into the declared body, so the generated *Expr behaves like the query body was written inline.
  • Analyzer enforcement

    • Added a new analyzer error for projection hooks used outside SelectExpr / SelectManyExpr / GroupByExpr / Generate.
    • This keeps the generated hook API intentionally inert unless it is consumed by the projection pipeline.
  • Coverage

    • Added focused source-generator coverage for default and custom hook declaration generation.
    • Added analyzer coverage for valid vs invalid hook usage.
    • Added EF Core coverage for nullable-navigation left-join behavior and projectable member inlining.

Example usage:

query.SelectExpr(x => new
{
    ChildName = x.Child.AsLeftJoin().Name,
    Computed = x.SomeComputedMember.AsProjectable(),
});

This enables the intended flow:

user anonymous projection -> explicit hook -> generated *Expr rewrite


📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.

Copilot AI and others added 2 commits March 18, 2026 10:40
…ting

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] Add AsLeftJoin and AsProjectable methods for EFCore Add generic projection hooks with AsLeftJoin and AsProjectable support Mar 18, 2026
Copilot AI requested a review from arika0093 March 18, 2026 10:44
@arika0093 arika0093 requested a review from Copilot March 18, 2026 10:56
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a generic “projection hook” mechanism to Linqraft’s projection pipeline, enabling explicit, user-authored markers in anonymous projections that the generator rewrites (built-in: AsLeftJoin() and AsProjectable()), plus analyzer enforcement and test coverage.

Changes:

  • Introduces configurable projection hook definitions (defaulting to AsLeftJoin + AsProjectable) and emits corresponding no-op hook extension declarations.
  • Extends the projection expression emitter to rewrite AsLeftJoin into null-guarded access and to inline computed members via AsProjectable.
  • Adds analyzer rule LQRE003 to block projection hook usage outside generated projection contexts, with generator/analyzer/EFCore tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Linqraft.Tests.SG/Program.cs Adds SG coverage for default/custom hook declaration emission.
tests/Linqraft.Tests.EFCore/EfCoreModels.cs Adds a computed query-shaped property for AsProjectable coverage.
tests/Linqraft.Tests.EFCore/EfCoreBasicProjectionTests.cs Adds SQLite EFCore tests for AsLeftJoin and AsProjectable behavior.
tests/Linqraft.Tests.Analyzer/AnalyzerSmokeTests.cs Adds LQRE003 smoke tests for hook usage inside/outside projections.
src/Linqraft.Core/SourceGenerator/ProjectionSupportExtensionClassGenerator.cs Emits generated no-op hook extension methods based on ProjectionHooks.
src/Linqraft.Core/SourceGenerator/ProjectionExpressionEmitter.cs Implements hook rewriting: left-join null-guards + projectable inlining.
src/Linqraft.Core/Configuration/LinqraftGeneratorOptionsCore.cs Adds hook definitions (LinqraftProjectionHookDefinition, Kind) and defaults.
src/Linqraft.Analyzer/LinqraftCompositeAnalyzer.cs Adds projection-hook usage enforcement diagnostic reporting.
src/Linqraft.Analyzer/DiagnosticDescriptors.cs Registers new analyzer error descriptor LQRE003.
src/Linqraft.Analyzer/AnalyzerHelpers.cs Adds helper detection for hook invocations and hook-context detection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +952 to +971
private bool TryEmitProjectableHook(
InvocationExpressionSyntax invocation,
string? overrideReceiver,
out string rewritten
)
{
rewritten = string.Empty;
if (!IsProjectionHookInvocation(invocation, LinqraftProjectionHookKind.Projectable))
{
return false;
}

if (!TryExpandProjectableInvocation(invocation, overrideReceiver, out var expanded))
{
return false;
}

rewritten = Emit(expanded);
return true;
}
Comment on lines +34 to +56
public static bool IsProjectionExprInvocation(InvocationExpressionSyntax invocation)
{
return GetInvocationName(invocation.Expression) is "SelectExpr" or "SelectManyExpr" or "GroupByExpr";
}

public static bool IsProjectionHookInvocation(InvocationExpressionSyntax invocation)
{
return ProjectionHookNames.Contains(GetInvocationName(invocation.Expression));
}

public static bool IsInsideProjectionHookContext(SyntaxNode node)
{
return node.AncestorsAndSelf()
.OfType<InvocationExpressionSyntax>()
.Any(invocation =>
IsProjectionExprInvocation(invocation)
|| string.Equals(
GetInvocationName(invocation.Expression),
"Generate",
StringComparison.Ordinal
)
);
}
Comment on lines +116 to +135
private static void AnalyzeProjectionHookUsage(
SyntaxNodeAnalysisContext context,
InvocationExpressionSyntax invocation
)
{
if (
!AnalyzerHelpers.IsProjectionHookInvocation(invocation)
|| AnalyzerHelpers.IsInsideProjectionHookContext(invocation)
)
{
return;
}

context.ReportDiagnostic(
Diagnostic.Create(
DiagnosticDescriptors.ProjectionHookUsage,
invocation.GetLocation(),
AnalyzerHelpers.GetInvocationName(invocation.Expression)
)
);
Comment on lines +41 to +49
foreach (
var hook in generatorOptions
.ProjectionHooks.GroupBy(
hook => hook.MethodName,
global::System.StringComparer.Ordinal
)
.Select(group => group.First())
)
{
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.

3 participants