dotnet / dotnet/aspnetcore

RDG emits invalid code when endpoint handler return type is an IErrorTypeSymbol (cross-generator scenario)

Open
#65,452 0 comments 0 reactions 0 assignees View on GitHub
area-minimal NativeAOT
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.