Skip to content
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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

captainsafia
Copy link
Member

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 the Program class generated by the compiler, which has a default visibility of internal.

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:

  • Add an IVT from their application code to their test code
  • Add a public partial class Program {} declaration

The 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:

  • Use top-level statements
  • Don't declare the Program class as public in some other fashion
  • Don't use the old-style Program.Main declarations

@captainsafia captainsafia requested review from wtgodbe and a team as code owners October 2, 2024 04:20
@dotnet-issue-labeler dotnet-issue-labeler bot added the area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework label Oct 2, 2024
@captainsafia captainsafia added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Oct 2, 2024
@@ -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"
Copy link
Member Author

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");
Copy link
Member Author

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

Copy link
Contributor

Hey @dotnet/aspnet-build, looks like this PR is something you want to take a look at.

@martincostello
Copy link
Member

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 😅

@gfoidl
Copy link
Member

gfoidl commented Oct 2, 2024

A different idea / approach:

Update the compiler so that it can emit a public class Program that is configurable via MsBuild-property.
So the default remains the same, when the property is set the public class is emitted.
Thus the change here boils down to updating the template csproj.

Or is the process via compiler, sdk, etc. too tedious that this approach is cheaper?

@captainsafia
Copy link
Member Author

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?

Oh, good idea! We can definitely author an analyzer + codefix that discovers the public partial class Program { } and recommends removing them if you're targeting net10 and above.

Update the compiler so that it can emit a public class Program that is configurable via MsBuild-property.

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:

  • At the moment, this ASP.NET Core integration testing scenario seems to be the only one.
  • More importantly, there were questions about what the visibility/accessibility of types defined in the generated Program class should be.

With those in mind, it was a bit more straightforward for ASP.NET to control its own destiny here and author a source generator.

@amcasey
Copy link
Member

amcasey commented Oct 2, 2024

(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 {}

@amcasey
Copy link
Member

amcasey commented Oct 2, 2024

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.

@captainsafia
Copy link
Member Author

captainsafia commented Oct 3, 2024

We also don't want to do this if they've explicitly declared it internal, right?

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 Program class is already internal. I suppose it's also possible for somebody to declare private partial class Program { }. It's not you can't declare private or protected access in top-level classes.

Edit: this might be a useful reference.

Good reference! I'll incorporate some of the checks here into our code.

@Tornhoof
Copy link
Contributor

Tornhoof commented Oct 3, 2024

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.

@captainsafia
Copy link
Member Author

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 Program class that is already defined looks like. We'd end up generating the public partial class Program { } declaration in more situations that is acceptable which might result in surprising behavior.

Also,

some switch somewhere during

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.

Copy link
Member

@amcasey amcasey left a 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)
Copy link
Member

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?

Copy link
Member Author

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.

@Tornhoof
Copy link
Contributor

Tornhoof commented Oct 3, 2024

I'm assuming you're referring to a switch that the user would set.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-infrastructure Includes: MSBuild projects/targets, build scripts, CI, Installers and shared framework area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants