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 infrastructure for RequestDelegateGenerator #45924

Merged
merged 12 commits into from
Jan 9, 2023
Merged

Conversation

captainsafia
Copy link
Member

@captainsafia captainsafia commented Jan 6, 2023

Part of #45524.

This PR adds a new project to encapsulate the Minimal APIs generator:

  • The generator is off by default and enabled via the EnableRequestDelegateGenerator build property
  • The generator is shipped as part of the Microsoft.AspNetCore.App ref pack
  • The generator is bare-bones as is and only works for emitting endpoints that take no parameters and return a string
  • The generator includes integration and unit tests scaffold

The goal of this PR is to get a barebones steel-thread working so we can validate a generator can be built, tested, shipped, and run from the SDK.

@captainsafia
Copy link
Member Author

Instead of disabling the generator by suppressing the emission of endpoints as in what was removed in 2d8a509, it's better to disable it by removing it from the Analyzers item group in MSBuild. This will require a two-step change where we make a modification in the SDK as well to remove reference to the analyzer if EnableRazorSourceGenerator is not true.

Copy link
Member

@wtgodbe wtgodbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM other than the 2 comments

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a real quick skim over the high-level stuff. This looks really good.

<Suppression>
<DiagnosticId>PKV004</DiagnosticId>
<Target>net7.0</Target>
</Suppression>
<Suppression>
<DiagnosticId>PKV004</DiagnosticId>
<Target>net8.0</Target>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my learning - what are these files for? And why do we need both net7.0 and net8.0 in them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure of the reasons but I ran this delta past @wtgodbe since it was automatically introduced when I ran the GenerateProjectList task after adding the new source generator project to the repo.

From my understanding, it's a requirement of the package validation SDK we use for validating changes across versions.

As to why we have both versions, I'm thinking we might be able to get away with just having net8 in the main branch but I'll let @wtgodbe share.

For now, I just settled with whatever the build produced here 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think net7.0 can be removed now that the source is targeting .NET 8.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think net7.0 can be removed now that the source is targeting .NET 8.

Roger that. It probably makes sense to do this in a separate PR. I assume there's no harm to having net7.0 here since we've had it in the main branch for a while.

We might also want to add removing the older framework target as part of our updating branding instructions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that these files keep changing because the order of the namespaces emitted into the file keep changing. Anyone know the reason for this non-determinism?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ViktorHofer and @smasher164 own the Package Validation SDK - any idea on why there's a net7.0 & and a net8.0 section in the generated file, or why there may nondeterminism in the ordering of the file? It's been somewhat noisy lately

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smasher164 can you please take a look? We might have caused a non-deterministic behavior by ordering the compatibility differences in ApiCompat. Or maybe not and something else is wrong.

@@ -4,17 +4,28 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<!-- This is needed to support codepaths that use NullabilityInfoContext. -->
<Features>$(Features.Replace('nullablePublicOnly', '')</Features>
<PreserveCompilationContext>true</PreserveCompilationContext>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? I thought this wasn't even used anymore.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can test with reference assemblies, not runtime assemblies.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, you can view that code here.

This also means that we can run tests with changes in the repo incorporated. Say we add a new API in the framework that affects the output of the generator, this infrastructure allows us to reference those changes even if they aren't shipped yet.


internal static partial class GeneratedRouteBuilderExtensions
{{
internal class GenericThunks<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider using the new file class feature for classes that we don't expect user code to need to interact with. That way code inside the user's project can't see these "implementation details" of our source generation.

See https://learn.microsoft.com/En-Us/dotnet/csharp/language-reference/proposals/csharp-11.0/file-local-types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will disappear anyways. @captainsafia I think on the issue for the source generator, we should output the current generated code and what the north start will look like.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would benefit the most from this in the SourceGeneratedRouteEndpointDataSource type that is defined here but since the plan is to remove it eventually, I think the benefit is marginally and we can keep it as a nested private type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just something to keep in mind when writing a source generator - any internal code here will be visible to the user's project. And people will definitely start using it in ways we don't expect. On the runtime team, we treat the internal code more or less like public API. If we change it in the future, it would be considered a breaking change. Thus the reason we made the language feature.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just something to keep in mind when writing a source generator - any internal code here will be visible to the user's project.

Yeah, we tend to exploit that aspect of the generator for our purposes here, we want user code to be able to see the strongly-typed MapAction overloads so those overloads can be chosen and the user can opt-in to our code-generated logic.

I've added a sample of what the generator currently emits and some explanation of why it works and how it might evolve in the future in the issue here: #45524

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we tend to exploit that aspect of the generator for our purposes here, we want user code to be able to see the strongly-typed MapAction overloads so those overloads can be chosen and the user can opt-in to our code-generated logic.

Right - for the code we want the user to be able to call, we should use internal. But for the implementation details, we don't want it visible to the user.

In your sample code, the SourceKey class is an example of this. This is an implementation detail we don't want visible to the user. So we should use file class SourceKey there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an implementation detail we don't want visible to the user. So we should use file class SourceKey there.

we're actually going to expose SourceKey or something like it at runtime publicly. Though, not in this PR.

Copy link
Member

@Youssef1313 Youssef1313 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a detailed review, but I'm concerned about proper incrementality here.

@Youssef1313
Copy link
Member

Youssef1313 commented Jan 7, 2023

This will require a two-step change where we make a modification in the SDK as well to remove reference to the analyzer if EnableRazorSourceGenerator is not true.

Why? isn't it possible that you ship an MSBuild targets file in the source generator package as follows <Analyzer Remove="@(Analyzer)" Condition="'%(Analyzer.NugetPackageId)'=='PackageIdGoesHere' and 'EnableRazorSourceGenerator'!='true'"/>

@eerhardt
Copy link
Member

eerhardt commented Jan 7, 2023

isn't it possible that you ship an MSBuild targets file in the source generator package

There is no NuGet package here. This source generator ships in the ASP.NET ref pack.

@JamesNK
Copy link
Member

JamesNK commented Jan 8, 2023

Should we be adding GeneratedCodeAttribute to generated types and methods?

For example, from generating log source:

partial class Log
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.8.5613")]
    private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::Microsoft.AspNetCore.Http.Endpoint, global::System.Exception?> __ExecutingEndpointCallback =
        global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::Microsoft.AspNetCore.Http.Endpoint>(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, "ExecutingEndpoint"), "Executing endpoint '{EndpointName}'", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); 

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.8.5613")]
    public static partial void ExecutingEndpoint(global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.AspNetCore.Http.Endpoint endpointName)
    {
        if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
        {
            __ExecutingEndpointCallback(logger, endpointName, null);
        }
    }
}

@JamesNK
Copy link
Member

JamesNK commented Jan 8, 2023

Also, generated source tends to use fully qualified type names with global:: prefix to avoid type conflicts.

@JamesNK
Copy link
Member

JamesNK commented Jan 8, 2023

Also, what is the usual suffix for source generators projects? We currently have Microsoft.AspNetCore.Http.SourceGeneration. The logging generator lives in Microsoft.Extensions.Logging.Generators. Is there a standard we should follow?

@captainsafia
Copy link
Member Author

Also, what is the usual suffix for source generators projects? We currently have Microsoft.AspNetCore.Http.SourceGeneration. The logging generator lives in Microsoft.Extensions.Logging.Generators. Is there a standard we should follow?

The naming here was partially inspired by the fact that the JSON source generator is under the System.Text.Json.SourceGeneration namespace (ref). It does look like logging uses a different namespace (ref) and the regular expression generator as well with a singular instead of plural name scheme. (ref).

With all that in mind, I actually think that the plural "Generators" is the way to go to match with the naming for "Analyzers" and "CodeFixers".

Also, generated source tends to use fully qualified type names with global:: prefix to avoid type conflicts.

Good call!

Should we be adding GeneratedCodeAttribute to generated types and methods?

I discovered this thread that talks about this a little bit further. It seems like the auto-generated header provides decent coverage but the GeneratedCodeAttribute is helpful for scenarios where compiled binaries need to be audited for generated code.

@mkArtakMSFT mkArtakMSFT added the area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Jan 8, 2023
@eerhardt
Copy link
Member

eerhardt commented Jan 9, 2023

Also, generated source tends to use fully qualified type names with global:: prefix to avoid type conflicts.

See the strategy employed in the Regex source generator and discussion in dotnet/runtime#66432 and dotnet/runtime#63512 for a way to not have to fully qualify everything with global:: prefix.

With this PR, we instead move all of the types into a separate namespace and parent class, which then enables a) having usings inside the namespace declaration so that we can avoid fully-qualifying everything

Comment on lines +12 to +13
<Reference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<Reference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="all" />
Copy link
Member

@Youssef1313 Youssef1313 Jan 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a reference to Microsoft.CodeAnalysis.Analyzers. It currently comes as a transitive package from Microsoft.CodeAnalysis.Common. The direct dependency will ensure dotnet/roslyn-analyzers#6229 doesn't happen in future (I'm not sure why it doesn't happen currently).

@captainsafia
Copy link
Member Author

Where's everyone barometer at with merging this once the build is green? I've addressed the major points of feedback, and there are some outstanding comments:

I want to make sure we can validate the steel thread with the generator landing in the SDK and supporting the EnableRequestDelegateGenerator property sooner rather than later.

@eerhardt
Copy link
Member

eerhardt commented Jan 9, 2023

Where's everyone barometer at with merging this once the build is green?

I'm good with merging it, as long as you have follow up issues tracking the "TODO" pieces. We should make progress where we can.

@captainsafia
Copy link
Member Author

Where's everyone barometer at with merging this once the build is green?

I'm good with merging it, as long as you have follow up issues tracking the "TODO" pieces. We should make progress where we can.

TODOs tracked in #45524. Let me know if i missed anything.

@captainsafia captainsafia enabled auto-merge (squash) January 9, 2023 19:12
@captainsafia captainsafia merged commit b110c98 into main Jan 9, 2023
@captainsafia captainsafia deleted the safia/minapi-sgen branch January 9, 2023 19:25
@ghost ghost added this to the 8.0-preview1 milestone Jan 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-web-frameworks *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants