Skip to content

Request Delegate Generator: handlers that differ only by a parameter's default value share one interceptor, InvalidCastException at startup #69675

Description

@AdCodicem

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Related: #66912 and its fix #67591, which made the grouping of endpoints compare the binding source, the lookup name and optionality, but not the default value.

With EnableRequestDelegateGenerator (on by default under PublishAot and PublishTrimmed), these two endpoints share one interceptor:

app.MapGet("/page-1", (int page = 1) => $"page {page}");
app.MapGet("/page-2", (int page = 2) => $"page {page}");

The interceptor casts the handler to the first lambda's natural type, Cast(handler, global::System.String (global::System.Int32 arg0= 1) => throw null!) (line 182 of the generated file, the MapGet0 frame below), and writes page_parsed_temp = 1; as the fallback. The second lambda's natural type is another anonymous delegate type, so MapGet throws InvalidCastException at startup.

The default value has never been part of the comparison. On main (and release/11.0), EndpointParameter.Equals and GetHashCode compare Source, SymbolName, LookupName, Ordinal, IsOptional, Type and KeyedServiceKey, not HasDefaultValue and DefaultValue (EndpointParameter.cs#L595-L616 at aaec58f). The older EndpointParameter.SignatureEquals on release/10.0 (EndpointParameter.cs#L604-L611 at 55d77e1) ignores it too. Yet the emitted code depends on it: the Cast delegate type is meant to capture default parameter values (StaticRouteHandlerModel.Emitter.cs#L19-L28), and the fallback carries the default value too.

Workaround: give the parameters different names. [FromQuery(Name = "page")] int p = 2 keeps the query key unchanged and gives each handler its own interceptor.

Expected Behavior

GET /page-1 returns page 1 and GET /page-2 returns page 2, as with the generator off.

Steps To Reproduce

Repro project: https://github.com/AdCodicem/dotnet-upstream-repros/tree/main/aspnetcore-rdg-default-value. ./repro.sh builds and runs it with the generator off, then on; REPRO_TFM=net11.0 ./repro.sh does the same on .NET 11.

Repro.csproj (the repository's version takes the target framework from REPRO_TFM; net10.0 is its default):

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  </PropertyGroup>

</Project>

Program.cs:

var builder = WebApplication.CreateSlimBuilder(args);
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:0");
var app = builder.Build();

app.MapGet("/page-1", (int page = 1) => $"page {page}");
app.MapGet("/page-2", (int page = 2) => $"page {page}");

await app.StartAsync();
using var client = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
Console.WriteLine("GET /page-1 -> " + await client.GetStringAsync("/page-1"));
Console.WriteLine("GET /page-2 -> " + await client.GetStringAsync("/page-2"));
await app.StopAsync();
dotnet build -p:EnableRequestDelegateGenerator=false && dotnet run --no-build   # page 1 / page 2
rm -rf bin obj && dotnet build && dotnet run --no-build                         # InvalidCastException

Exceptions (if any)

Unhandled exception. System.InvalidCastException: Unable to cast object of type '<>f__AnonymousDelegate1`2[System.Int32,System.String]' to type '<>f__AnonymousDelegate0`2[System.Int32,System.String]'.
   at Microsoft.AspNetCore.Http.Generated.<GeneratedRouteBuilderExtensions_g>F705AFFC00BBE82D3AAC29F52EDC0EBE8384420EAFA5C35DC66A2FA28704C0DC3__GeneratedRouteBuilderExtensionsCore.Cast[T](Delegate d, T _) in <repro>\obj\Debug\net10.0\generated\Microsoft.AspNetCore.Http.RequestDelegateGenerator\Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator\GeneratedRouteBuilderExtensions.g.cs:line 209
   at Microsoft.AspNetCore.Http.Generated.<GeneratedRouteBuilderExtensions_g>F705AFFC00BBE82D3AAC29F52EDC0EBE8384420EAFA5C35DC66A2FA28704C0DC3__GeneratedRouteBuilderExtensionsCore.MapGet0(IEndpointRouteBuilder endpoints, String pattern, Delegate handler) in <repro>\obj\Debug\net10.0\generated\Microsoft.AspNetCore.Http.RequestDelegateGenerator\Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator\GeneratedRouteBuilderExtensions.g.cs:line 182
   at Program.<Main>$(String[] args) in <repro>\Program.cs:line 7
   at Program.<Main>(String[] args)

The same exception on SDK 11.0.100-rc.1.26425.128 with net11.0.

.NET Version

10.0.401 (also 11.0.100-rc.1.26425.128)

Anything else?

  • ASP.NET Core: 10.0.12 and 11.0.0-rc.1.26425.128 (the generator ships in Microsoft.AspNetCore.App.Ref)
  • No IDE (CLI); Windows 11 (10.0.26200) x64 and Ubuntu x64

Found while adding native AOT checks to AdCodicem.ValueObjects. Investigation and write-up assisted by Claude (Anthropic); the linked repro was run on Windows 11 and Ubuntu, and I reviewed this report.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-rdg

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions