Skip to content

Extend source generator to auto-register endpoints from referenced assemblies#104

Open
Copilot wants to merge 2 commits intomasterfrom
copilot/extend-source-generator-endpoints-again
Open

Extend source generator to auto-register endpoints from referenced assemblies#104
Copilot wants to merge 2 commits intomasterfrom
copilot/extend-source-generator-endpoints-again

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 6, 2026

The source generator only discovered IEndpointRouteHandlerBuilder implementations in the current compilation, making cross-project endpoint modularisation impossible without falling back to reflection-based MapEndpointsFromAssembly.

Changes

External assembly scanning

  • Added a second incremental pipeline using CompilationProvider.SelectMany that walks the full namespace tree of every referenced assembly symbol, collecting endpoint classes via duck-typing: a public, non-abstract, non-generic class with a public static void MapEndpoints(IEndpointRouteBuilder) method.
  • Duck-typing is intentional — the generated interface is internal (see below), so it is invisible across assembly boundaries.
  • Both pipelines are combined with .Combine() and merged in Execute.

internal generated types (prevents duplicate-type conflicts)

Both generated types were public, causing CS0433 errors when two projects in the same build graph each reference MinimalHelpers.Routing.Analyzers (each assembly compiles its own copy, the consuming project sees two definitions):

Generated type Before After
IEndpointRouteHandlerBuilder public interface internal interface
EndpointRouteBuilderExtensions public static class internal static class

Each assembly now gets its own private copy that never leaks into public metadata.

Usage

An endpoint class in an external library:

// MyEndpointsLibrary — also references MinimalHelpers.Routing.Analyzers
public class PeopleEndpoints : IEndpointRouteHandlerBuilder
{
    public static void MapEndpoints(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("/api/people", () => { /* ... */ });
    }
}

The main API project simply calls app.MapEndpoints() — the generator includes PeopleEndpoints automatically, with no reflection and full Native AOT compatibility.

Original prompt

This section details on the original issue you should resolve

<issue_title>[Source Generator] Registrazione automatica degli endpoint definiti in librerie esterne</issue_title>
<issue_description>Il Source Generator attualmente registra automaticamente gli endpoint tramite il metodo app.MapEndpoints() per tutte le classi presenti nel progetto principale che implementano l'interfaccia IEndpointRouteHandlerBuilder. Tale interfaccia è creata automaticamente dal Source Generator stesso. Tuttavia, non consente di rilevare ed esporre automaticamente endpoint definiti in librerie esterne.

Proposta di feature:
Sarebbe utile estendere il Source Generator in modo che possa rilevare automaticamente le classi che implementano IEndpointRouteHandlerBuilder anche nelle librerie esterne referenziate dal progetto Minimal API (senza ricorrere a soluzioni reflection come MapEndpointsFromAssembly). Ciò permetterebbe di registrare in modo performante e compatibile con Native AOT endpoint definiti in qualsiasi progetto referenziato, migliorando modularità, riuso del codice e organizzazione.

Scenario d'uso desiderato:

  1. Definisco una classe endpoint in una libreria esterna:
    namespace MyEndpointsLibrary.Endpoints;
    
    public class PeopleEndpoints : IEndpointRouteHandlerBuilder
    {
        public static void MapEndpoints(IEndpointRouteBuilder endpoints)
        {
            endpoints.MapGet("/api/people", GetList);
            // ...
        }
    }
  2. Referenzio questa libreria dal progetto principale.
  3. Invocando app.MapEndpoints();, il Source Generator registra automaticamente anche gli endpoint definiti in librerie esterne, senza necessità di ulteriori chiamate, overload o uso della reflection.

Vantaggi:

  • Non è più necessario duplicare la definizione degli endpoint nel progetto principale.
  • Permette progetti più modulari e condivisibili.
  • Mantenimento della compatibilità Native AOT e dei vantaggi prestazionali del Source Generator.

Attualmente è possibile solo con le classi presenti nel progetto principale. Vorrei che il Source Generator lavorasse anche con le classi IEndpointRouteHandlerBuilder definite in progetti esterni referenziati.</issue_description>

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


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

… duplicate-type conflicts; add external assembly scanning via duck-typing

Co-authored-by: marcominerva <3522534+marcominerva@users.noreply.github.com>
Copilot AI changed the title [WIP] Add automatic registration of external endpoint classes Extend source generator to auto-register endpoints from referenced assemblies Mar 6, 2026
@marcominerva marcominerva marked this pull request as ready for review March 6, 2026 11:50
Copilot AI review requested due to automatic review settings March 6, 2026 11:50
Copy link
Copy Markdown

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

Extends the MinimalHelpers.Routing.Analyzers source generator so MapEndpoints() can auto-register endpoint classes defined in referenced projects/assemblies (enabling modular endpoint libraries without reflection), and changes generated types to internal to avoid duplicate-type conflicts across build graphs.

Changes:

  • Added a second incremental pipeline that scans referenced assemblies and discovers endpoints via duck-typed public static void MapEndpoints(IEndpointRouteBuilder) methods.
  • Combined current-compilation discovery with external-assembly discovery and updated generator execution accordingly.
  • Made generated IEndpointRouteHandlerBuilder and EndpointRouteBuilderExtensions internal to prevent CS0433 duplicate type conflicts.
Comments suppressed due to low confidence (1)

src/MinimalHelpers.Routing.Analyzers/EndpointRouteHandlerGenerator.cs:149

  • The generated XML doc has an invalid cref due to an extra trailing space (<see cref="IEndpointRouteHandlerBuilder "/>). This can produce XML doc warnings (or failures if warnings are treated as errors). Remove the trailing space so the cref resolves correctly.
                /// Automatically registers all the route endpoints defined in classes that implement the <see cref="IEndpointRouteHandlerBuilder "/> interface.
                /// </summary>

Comment on lines +109 to +113
m.DeclaredAccessibility == Accessibility.Public &&
m.ReturnsVoid &&
m.Parameters.Length == 1 &&
m.Parameters[0].Type.Name == "IEndpointRouteBuilder" &&
m.Parameters[0].Type.ContainingNamespace?.ToDisplayString() == "Microsoft.AspNetCore.Routing");
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

HasMapEndpointsMethod can match methods that will make the generated call uncompilable (e.g., MapEndpoints<T>(IEndpointRouteBuilder)), because it doesn't exclude generic methods or verify the parameter type via symbol equality. Tighten the predicate to require a non-generic method and compare the parameter type against compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Routing.IEndpointRouteBuilder") using SymbolEqualityComparer.Default.

Suggested change
m.DeclaredAccessibility == Accessibility.Public &&
m.ReturnsVoid &&
m.Parameters.Length == 1 &&
m.Parameters[0].Type.Name == "IEndpointRouteBuilder" &&
m.Parameters[0].Type.ContainingNamespace?.ToDisplayString() == "Microsoft.AspNetCore.Routing");
!m.IsGenericMethod &&
m.DeclaredAccessibility == Accessibility.Public &&
m.ReturnsVoid &&
m.Parameters.Length == 1 &&
m.Parameters[0].Type is INamedTypeSymbol parameterType &&
parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder");

Copilot uses AI. Check for mistakes.
var validClasses = input.CurrentCompilation
.Where(static symbol => symbol is not null)
.Cast<INamedTypeSymbol>()
.Concat(input.External)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

validClasses is built by concatenating current + external results without de-duplication or a deterministic order. If the same external assembly is referenced twice (or the symbol enumeration order changes), this can generate duplicate MapEndpoints calls and/or non-deterministic output. Consider Distinct(SymbolEqualityComparer.Default) and ordering by a stable key (e.g., fully-qualified name) before emitting code.

Suggested change
.Concat(input.External)
.Concat(input.External)
.Distinct(SymbolEqualityComparer.Default)
.OrderBy(static symbol => symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +77
var externalEndpointClasses = context.CompilationProvider
.SelectMany(static (compilation, token) =>
{
var results = ImmutableArray.CreateBuilder<INamedTypeSymbol>();

foreach (var reference in compilation.References)
{
token.ThrowIfCancellationRequested();

if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol)
{
CollectEndpointTypesFromNamespace(assemblySymbol.GlobalNamespace, results, token);
}
}

return results.ToImmutable();
})
.Collect();
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The external-assembly pipeline walks every type in every referenced assembly (compilation.References + full namespace recursion). This can significantly slow builds for large projects. Consider filtering references before scanning (e.g., only scan assemblies that reference the assembly containing Microsoft.AspNetCore.Routing.IEndpointRouteBuilder, which you can obtain from compilation.GetTypeByMetadataName(...)) to avoid traversing unrelated framework/NuGet assemblies.

Copilot uses AI. Check for mistakes.
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.

[Source Generator] Registrazione automatica degli endpoint definiti in librerie esterne

3 participants