RDG emits invalid code when endpoint handler return type is an IErrorTypeSymbol (cross-generator scenario)
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 276
Description
### Is there an existing issue for this?
- [X] I have searched the existing issues
### Describe the bug
When a minimal API endpoint handler (e.g. `MapGet`) returns a type produced by **another source generator** in the same project, the Request Delegate Generator (RDG) emits syntactically invalid C# code, causing compilation errors.
This happens because **Roslyn source generators cannot see each other's output** — each generator receives only the original user-authored `Compilation`. When the RDG encounters a return type from another generator, it resolves to an `IErrorTypeSymbol`. The RDG has no check for error types, so it proceeds to emit code using the error type's display name (which is empty), producing invalid syntax like `typeof()`, `JsonTypeInfo`, and empty delegate cast expressions.
#### Concrete example
Given a source-generated proxy class `LoremStatic` (generated by the [RazorSlices](https://github.com/DamianEdwards/RazorSlices) source generator) with a `Create()` method that returns `RazorSlice` (which implements `IResult`):
```csharp
// This FAILS — LoremStatic is from another source generator, RDG can't resolve it
app.MapGet("/breaks", () => LoremStatic.Create());
// These WORK — return type is IResult from a pre-compiled library, resolvable by RDG
app.MapGet("/works", () => Results.RazorSlice());
app.MapGet("/works-too", () => (IResult)LoremStatic.Create());
```
The RDG generates broken code for the first endpoint:
```csharp
// Empty type in typeof(), invalid JsonTypeInfo
options.EndpointBuilder.Metadata.Add(new ProducesResponseTypeMetadata(
statusCode: StatusCodes.Status200OK, type: typeof(), contentTypes: GeneratedMetadataConstants.JsonContentType));
// Empty delegate return type
var handler = Cast(del, () => throw null!);
// Invalid type expressions
var responseJsonTypeInfo = (JsonTypeInfo)jsonSerializerOptions.GetTypeInfo(typeof());
```
This produces ~35 compilation errors (CS1031, CS1525, CS0411, CS0119, CS0149, etc.) across all target frameworks.
### Root cause analysis
The issue is in the `EndpointResponse` class and the `DiagnosticEmitter`:
1. **`GetIsIResult()`** ([EndpointResponse.cs](https://github.com/dotnet/aspnetcore/blob/v10.0.3/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/EndpointResponse.cs)) returns `false` for error types because `IErrorTypeSymbol.AllInterfaces` is empty.
2. **`GetIsSerializable()`** returns `true` because the error type passes all the negative checks (`!IsIResult && !HasNoResponse && ResponseType != null && SpecialType != String && SpecialType != Object`).
3. **`EmitRequiredDiagnostics`** ([DiagnosticEmitter.cs](https://github.com/dotnet/aspnetcore/blob/v10.0.3/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/Emitters/DiagnosticEmitter.cs)) only checks for `ITypeParameterSymbol`, private/protected accessibility, and anonymous types — **it never checks for `IErrorTypeSymbol`**. Since no diagnostic is emitted, the endpoint passes through the `Diagnostics.Count == 0` filter and broken code is generated.
4. The emitter calls `ToDisplayString()` on the error type, producing empty strings for type names, which results in syntactically invalid generated C#.
### Suggested fix
Two changes would address this:
**1. Add error type detection in `DiagnosticEmitter.EmitRequiredDiagnostics`:**
```csharp
public static void EmitRequiredDiagnostics(this EndpointResponse response, List diagnostics, Location location)
{
// Add: skip code generation for error types (e.g., types from other source generators)
if (response.ResponseType is IErrorTypeSymbol)
{
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.UnableToResolveMethod, location));
// Or better: a new diagnostic like "Unable to resolve return type '{0}'"
}
// ... existing checks ...
}
```
This would cause the endpoint to be filtered out (`Diagnostics.Count > 0`), falling back to the runtime `RequestDelegateFactory` which works correctly since all generated types are available at runtime.
**2. Guard `GetIsSerializable()` against error types as defense-in-depth:**
```csharp
private bool GetIsSerializable() =>
!IsIResult &&
!HasNoResponse &&
ResponseType != null &&
ResponseType.TypeKind != TypeKind.Error && // Add this
ResponseType.SpecialType != SpecialType.System_String &&
ResponseType.SpecialType != SpecialType.System_Object;
```
### Expected Behavior
When the RDG encounters a return type it cannot resolve (`IErrorTypeSymbol`), it should either:
- Emit a diagnostic and skip code generation for that endpoint, falling back to the runtime `RequestDelegateFactory`
- Or gracefully handle the error type without emitting syntactically invalid code
### Steps To Reproduce
1. Create an ASP.NET Core project with `PublishAot=true` and `EnableRequestDelegateGenerator=true`
2. Add a second source generator to the project that generates a type with a method returning a type that implements `IResult`
3. Use that generated type's method as the return value in a `MapGet` lambda:
```csharp
app.MapGet("/test", () => GeneratedType.Create()); // Create() returns a type implementing IResult
```
4. Build the project — compilation fails with errors in the RDG-generated code
A concrete repro is available at https://github.com/DamianEdwards/RazorSlices — uncomment line 40 of `samples/WebApp/Program.cs` and build.
### Exceptions (if any)
Compilation errors (not runtime exceptions):
```
error CS1031: Type expected
error CS1525: Invalid expression term '?'
error CS0411: The type arguments for method 'Cast(Delegate, T)' cannot be inferred from the usage
error CS0119: 'JsonTypeInfo' is a type, which is not valid in the given context
error CS0149: Method name expected
```
### .NET Version
```
10.0.103
```
### Anything else?
- The RDG source generator version producing the broken code is **10.0.3.0**
- The bug manifests identically across all three target frameworks tested: net8.0, net9.0, and net10.0
- The workaround is to wrap the return value in a method with a known return type (e.g., `Results.RazorSlice()` or `TypedResults.RazorSlice()`) or explicitly cast to `IResult`
- This issue would affect **any** source generator that produces types used as return values in minimal API endpoint handlers when the RDG is enabled
Contributor guide
Assessment
This issue has not been assessed yet.