-
Notifications
You must be signed in to change notification settings - Fork 10.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add source generator to emit public Program class definition #58199
Conversation
@@ -73,6 +73,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant | |||
Private="false" | |||
ReferenceOutputAssembly="false" /> | |||
|
|||
<ProjectReference Include="..\..\AspNetCoreAnalyzers\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc: @amcasey for Roslyn review
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, why not program.TypeKind != TypeKind.Class
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
With the introduction of top-level statements in .NET 6, there is not longer an explicit
Program
class defined in user's ASP.NET Core applications. Instead, we rely on theProgram
class 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:
IVT
from 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:Program
class as public in some other fashionProgram.Main
declarations