Add source generator to emit public Program class definition#58199
Add source generator to emit public Program class definition#58199captainsafia merged 4 commits intomainfrom
Conversation
| Private="false" | ||
| ReferenceOutputAssembly="false" /> | ||
|
|
||
| <ProjectReference Include="..\..\AspNetCoreAnalyzers\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj" |
There was a problem hiding this comment.
@wtgodbe Can you sanity check my packaging changes here? I think it's correct given what we had to do in .NEt 8 for the RequestDelegateGenerator but let me know if I missed something.
| { | ||
| var internalGeneratedProgramClass = context.CompilationProvider.Select((compilation, cancellationToken) => | ||
| { | ||
| var program = compilation.GetTypeByMetadataName("Program"); |
|
Just curious, will there be any analyzer or anything that could point out this was added and that any manually added Program declarations can be removed? I have a tonne of these I could delete with this built-in (example), so anything that could point it out rather than me needing to go find them all would be useful 😅 |
|
A different idea / approach: Update the compiler so that it can emit a Or is the process via compiler, sdk, etc. too tedious that this approach is cheaper? |
Oh, good idea! We can definitely author an analyzer + codefix that discovers the
There was a proposal to change the compiler's behavior to emit a public Program class by default for top-level statements but it was rejected because:
With those in mind, it was a bit more straightforward for ASP.NET to control its own destiny here and author a source generator. |
|
(Haven't read the code yet) We also don't want to do this if they've explicitly declared it internal, right? internal partial class Program {} |
src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs
Outdated
Show resolved
Hide resolved
src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs
Outdated
Show resolved
Hide resolved
src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs
Outdated
Show resolved
Hide resolved
|
I'm pretty sure the compilation knows its own entrypoint, but I'm not sure that's exposed anywhere. Edit: this might be a useful reference. |
Yep, this is another good case to cover in tests. Practically, I don't know how frequently users are doing this since the visibility of the
Good reference! I'll incorporate some of the checks here into our code. |
|
I kinda second @gfoidl's question, isn't there a better way to do this, which technically should just be some switch somewhere during project generation or similar, run only once. Instead a source generator which is evaluated more often. |
I started off with this strategy but reached a limitation because there's not a ton of ability to introspect what the Also,
I'm assuming you're referring to a switch that the user would set. I'm keen to have this behavior be on-by-default since that moves the needle for this experience a lot more than users having to switch from a declaration in their source code to another switch in their app code. |
amcasey
left a comment
There was a problem hiding this comment.
The checks that Program is the generated one seem sufficient. I'm ignorant about whether this is an efficient way to write a source generator.
| } | ||
| // If the discovered `Program` type is an interface, struct or generic type then its not | ||
| // generated and has been defined in source, so we can skip it | ||
| if (program.TypeKind == TypeKind.Struct || program.TypeKind == TypeKind.Interface || program.IsGenericType) |
There was a problem hiding this comment.
Out of curiosity, why not program.TypeKind != TypeKind.Class?
There was a problem hiding this comment.
That's....a good idea 😅 I'll update as needed.
src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs
Outdated
Show resolved
Hide resolved
Yeah, something like the implicit usings setting or something similar, could then even be enabled automatically during project creation, but I understand that's a lot more effort than a src gen. Using MSBuild properties to decide if a srcgen should be executed is also not as straight forward as one might think. I admit, that I don't really see any quick win here. |
1e09f50 to
3bc89cd
Compare
amcasey
left a comment
There was a problem hiding this comment.
The selection logic looks correct to me.
...rk/AspNetCoreAnalyzers/src/SourceGenerators/Microsoft.AspNetCore.App.SourceGenerators.csproj
Show resolved
Hide resolved
| { | ||
| var internalGeneratedProgramClass = context.CompilationProvider | ||
| // Get the entry point associated with the compilation, this maps to the Main method definition | ||
| .Select((compilation, cancellationToken) => compilation.GetEntryPoint(cancellationToken)) |
There was a problem hiding this comment.
Iterated selection is very tidy, but it seems like it might be expensive. If this is run repeatedly, I wonder if it would be more efficient to have a single Select with a bigger lambda.
There was a problem hiding this comment.
According to the guidance on writing incremental generators it's better to split things out to multiple Selects so that there's an opportunity to cache more frequently in between steps.
There was a problem hiding this comment.
Surely that's only true for things that change independently? Retrieving and casting the containing type in the first select doesn't seem like something that could affect caching.
There was a problem hiding this comment.
Yes, I suppose the first two selects could be collapsed into one given the things they evaluate are closely related but I don't think there's anything actively harmful about splitting up the transformation.
src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs
Outdated
Show resolved
Hide resolved
|
Merging this now to validate the E2E both perf and functionality wise once it is inserted into the alpha SDK. I'll follow up with building the analyzer after that. |
* Add source generator to emit public Program class definition * Add more checks and test cases * Use GetEntrypoint API and transformations for better caching * Address feedback
* Add source generator to emit public Program class definition * Add more checks and test cases * Use GetEntrypoint API and transformations for better caching * Address feedback
With the introduction of top-level statements in .NET 6, there is not longer an explicit
Programclass defined in user's ASP.NET Core applications. Instead, we rely on theProgramclass generated by the compiler, which has a default visibility ofinternal.This introduces an annoying hurdle for users who want to write integration tests on top of our WebApplicationFactory which requires a public entrypoint as a generic type argument.
To work around this, we document that users can either:
IVTfrom their application code to their test codepublic partial class Program {}declarationThe first approach runs the risk of the user having to expose truly internal types to their test code. The second approach is hard to discover.
To resolve this issue, we introduce a new source generator to the shared framework that will emit the
public partial class Program {}declaration to applications that:Programclass as public in some other fashionProgram.Maindeclarations