Validation source generator: accept validatable type requests from other source generators
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Is your feature request related to a problem? Please describe the problem.
Endpoint frameworks built on minimal APIs as source generators cannot use Microsoft.Extensions.Validation. The validations generator discovers types from `[ValidatableType]` and from the `Map*` calls it can see in user code; a generator that emits the `Map*` calls, or wants to mark request types validatable from its own output, is invisible to it, because source generators cannot see each other's output. #65161 describes this from the author of one such framework: it generates the endpoint registrations for user-written request classes, and its users have to apply `[ValidatableType]` by hand to get validation at all. ASP0037 now codifies the limit: "Source generators cannot inspect each other's output. Declare the type in a regular .cs file instead."
The consequence is silent. The generated `RuntimeValidatableParameterInfoResolver` handles attributes on parameters only; `TryGetValidatableTypeInfo` returns `false` with a TODO that "the implementation currently relies on static discovery of types". A request type the generator did not see is not validated at all, with no diagnostic at run time. The reflection-based fallback proposed in #61220 would close the gap at the cost of AOT and trimming, which is the reason the generator exists.
The block is in the compiler, and the STJ team ran into the same wall (dotnet/runtime#110428, closed pending "the C# compiler supporting source generator layering"). dotnet/roslyn#85239 now proposes a compiler-side mechanism that does not need layering or additional compilations, and the Roslyn side has asked for a signal from the generators that would consume it. dotnet/runtime#133927 asks the same question of STJ.
### Describe the solution you'd like
The validations generator consumes a small request contract published by other generators. dotnet/roslyn#85239 adds two methods to `IncrementalGeneratorInitializationContext`: a consumer reads `context.ExternalInputsProvider()` as an ordinary `IncrementalValuesProvider`, a producer publishes with `context.RegisterExternalOutput(provider)`. Values are computed by the producer from the input compilation, everything runs against the same compilation every generator sees today, and generators that do not use it are unaffected.
The contract, owned by this generator and shipped as a small `netstandard2.0` package so producers can compile against it:
```csharp
namespace Microsoft.Extensions.Validation
{
// TypeName is a fully qualified metadata name. The generator resolves it and walks
// its members the same way it does for a type marked with [ValidatableType].
public sealed record ValidatableTypeRequest(string TypeName);
}
```
The generator already merges two streams of types, one from `[ValidatableType]` and one from endpoint parameters, with `Concat` before `Distinct` and emit. A request is a third stream in the same place:
```csharp
var validatableTypesFromRequests = context
.ExternalInputsProvider()
.Combine(context.CompilationProvider)
.Select(static (pair, ct) => ResolveValidatableType(pair.Left.TypeName, pair.Right, ct))
.Where(static type => !type.IsDefault);
var allValidatableTypesProviders = validatableTypesFromEndpoints
.Concat(validatableTypesWithAttribute)
.Concat(validatableTypesFromRequests);
```
`ResolveValidatableType` is `GetTypeByMetadataName` followed by the existing `TryExtractValidatableType(ITypeSymbol, ...)`, so accessibility rules, ASP0033-style diagnostics and the member walk are unchanged. An unresolved name gets a diagnostic rather than a silent skip. Everything after the `Concat` is untouched.
On the producer side, an endpoint framework publishes the request types its handlers bind:
```csharp
var requestTypes = endpoints
.SelectMany(static (e, _) => e.RequestTypes)
.Select(static (t, _) => new ValidatableTypeRequest(t.MetadataName));
context.RegisterExternalOutput(requestTypes);
```
What this does not cover: a request must name a type in the input compilation, declared by the user or in a referenced assembly. Types emitted by a generator remain invisible; that is a deliberate limit on the Roslyn side to avoid a second compilation. For the frameworks in question the request types are user-declared classes, and only the code that names them as validatable is generated, which is why the attribute route is closed and the data route is open. Discovery across referenced assemblies (#63626) is a separate question and is not changed by this.
### Additional context
- dotnet/roslyn#85239: the compiler-side proposal.
- dotnet/runtime#133927: the same question for the STJ generator, which would also let RDG populate the `JsonSerializerContext` for endpoint types (#56021).
- #65161, #61220, #63626, ASP0037 (#67993): prior discussion of the same limit.
The ask is a yes or no on whether this generator would take a consumer branch of this shape if dotnet/roslyn#85239 is approved. That is what the Roslyn discussion needs to move; the contract details can follow.
cc @Youssef1313 @oroztocil
Contributor guide
Research direction
Start with the validation generator's existing validatable-type streams, TryGetValidatableTypeInfo, TryExtractValidatableType, and RuntimeValidatableParameterInfoResolver, then read dotnet/roslyn#85239 for the external-input APIs. Done means deciding whether to take the consumer branch and, if approved, integrating ValidatableTypeRequest without changing the existing endpoint and attribute behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend-api-design, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100