Is there an existing issue for this?
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.
Is there an existing issue for this?
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 underPublishAotandPublishTrimmed), these two endpoints share one interceptor: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, theMapGet0frame below), and writespage_parsed_temp = 1;as the fallback. The second lambda's natural type is another anonymous delegate type, soMapGetthrowsInvalidCastExceptionat startup.The default value has never been part of the comparison. On
main(andrelease/11.0),EndpointParameter.EqualsandGetHashCodecompareSource,SymbolName,LookupName,Ordinal,IsOptional,TypeandKeyedServiceKey, notHasDefaultValueandDefaultValue(EndpointParameter.cs#L595-L616 at aaec58f). The olderEndpointParameter.SignatureEqualsonrelease/10.0(EndpointParameter.cs#L604-L611 at 55d77e1) ignores it too. Yet the emitted code depends on it: theCastdelegate 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 = 2keeps the query key unchanged and gives each handler its own interceptor.Expected Behavior
GET /page-1returnspage 1andGET /page-2returnspage 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.shbuilds and runs it with the generator off, then on;REPRO_TFM=net11.0 ./repro.shdoes the same on .NET 11.Repro.csproj(the repository's version takes the target framework fromREPRO_TFM;net10.0is its default):Program.cs:Exceptions (if any)
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?
Microsoft.AspNetCore.App.Ref)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.