Skip to content

Add support for response writing to RequestDelegateGenerator #46291

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

Merged
merged 4 commits into from
Feb 3, 2023

Conversation

captainsafia
Copy link
Member

@captainsafia captainsafia commented Jan 27, 2023

Addresses #46276 and resolves #45524.

  • Share WellKnownTypes infrastructure between analyzers and generator
  • Emit diagnostics from RequestDelegateGenerator for unsupported syntax
  • Add support for writing more response types to generator
  • Dedupe strongly-typed endpoints generated from the same delegate type
  • Use RequestDelegateGenerator in MinimalSample
  • Fix nullability warnings in generated source when referencing RequestDelegateFactoryOptions.EndpointBuilder

@captainsafia captainsafia force-pushed the safia/rdg-responses branch 3 times, most recently from 0571c04 to 26c3f11 Compare January 27, 2023 17:58
@mkArtakMSFT mkArtakMSFT added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Jan 27, 2023
@captainsafia captainsafia force-pushed the safia/rdg-responses branch 2 times, most recently from cbbaef0 to 544c00a Compare January 30, 2023 17:44

internal static class WellKnownTypeData
{
public enum WellKnownType
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be nested?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not necessarily. It was largely a stylistic choice to put the associated data in the same class. To minimize the code delta, I ended up using an alias in the existing analyzers/codefixers but it felt nicer to capture these types in class instead of having them on the top-level namespace.

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't leaving this unnested minimize the code delta by removing the using WellKnownType = WellKnownTypeData.WellKnownType;?


file static class GeneratedRouteBuilderExtensionsCore
{

Copy link
Member

Choose a reason for hiding this comment

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

super nit: Is there a way to not start the type with a blank line? My OCD is triggered 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, I think we might be able to remove the newline in the interpolated string {GetGenericThunks()}{GetThunks()} and change the strings emitted from those methods to get around it.

In the long-term, we might be able to take advantage of the SyntaxTree.NormalizeWhitespace feature to correct the indentation for the outputted source. I wasn't able to use it successfully here because it has a really hard time formatting the Dictionary of source key to tuples of funcs pairing. As we evolve the code, we might be able to get rid of this (e.g. once the interceptors feature lands) and we can do all the formatting at once instead of doing the bookkeeping for the indents here.

A related issue on the Roslyn side is: dotnet/roslyn#43821

<ItemGroup>
<Compile Include="$(SharedSourceRoot)IsExternalInit.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\BoundedCacheWithFactory.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\CodeWriter.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)RoslynUtils\WellKnownTypes.cs" LinkBase="Shared" />
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 sharing code is ok for now, but doing this results in two caches. I think we should investigate whether we can have a shared library project output to the analyzers directory that generators, analyzers, and code fixers can reference.

For route pattern parsing, there is a lot more code, and we don't want to duplicate parsing and caching route patterns twice (once in analyzer assembly, once in generators assembly)

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 sharing code is ok for now, but doing this results in two caches. I think we should investigate whether we can have a shared library project output to the analyzers directory that generators, analyzers, and code fixers can reference.

This is an interesting proposal. @chsienki do you happen to know if there is a pattern for sharing a symbol cache across analyzers/generators in the same compilation process?

For route pattern parsing, there is a lot more code, and we don't want to duplicate parsing and caching route patterns twice (once in analyzer assembly, once in generators assembly)

Roger that! I captured that in #46342 so we can start discussing what that might look like.

@@ -3,6 +3,8 @@
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
Copy link
Member

Choose a reason for hiding this comment

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

Do we need this all the time? VS allows you to see the generate files in the Solution Explorer.

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 don't use Visual Studio so I'm not able to take advantage of any shortcuts there.

Do we need this all the time?

I found it helpful to have on while debugging as it makes it easier to examine the generated code in another format. There's no con to having it so I figured it was helpful.

Copy link
Member

Choose a reason for hiding this comment

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

I don't use Visual Studio

image


internal class CodeWriter
{
private readonly StringBuilder _codeBuilder = new();
Copy link
Member

Choose a reason for hiding this comment

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

This allocation seems unnecessary since this class also takes the StringBuilder stringBuilder in its only ctor.

}
""";
var code = new CodeWriter(new StringBuilder());
code.Indent(5);
Copy link
Member

Choose a reason for hiding this comment

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

Why the magic 5 here?

Copy link
Member

Choose a reason for hiding this comment

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

When @captainsafia was getting @brunolins16 up to speed yesterday we were discussing the CodeWriter in general. My feedback was that using the CodeWriter rather than simple string concatenation/interpolation actually made it harder to reason over the generated code. I'd rather see us drop the CodeWriter (at the expense of indentation) and then either do some code formatting at the end before handing it back to Roslyn or just leave it a little messy. We can also do a Format Document on the generated code after build if we need to read it.

Copy link
Member

Choose a reason for hiding this comment

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

The Regex source generator uses IndentedTextWriter. Seems to work really well there. You just writer.Indent++ and -- when you are done.

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs#L30


private static string EmitResponseWritingCall(this Endpoint endpoint)
{
var code = new CodeWriter(new StringBuilder());
Copy link
Member

Choose a reason for hiding this comment

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

Allocating these intermediate strings seems unnecessary. Why not just pass the existing CodeWriter into this method? It would also help with the WriteNoIndent method calls. You wouldn't need those if you were just writing to the "outer" CodeWriter.

Comment on lines 80 to 89
if (endpoint.Response.IsAwaitable)
{
code.WriteLine("var result = await handler();");
code.WriteLine(endpoint.EmitResponseWritingCall());
}
else
{
code.WriteLine("var result = handler();");
code.WriteLine("return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);");
}
Copy link
Member

Choose a reason for hiding this comment

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

EmitResponseWritingCall handles IsAwaitable(). So I think this can be refactored to just:

Suggested change
if (endpoint.Response.IsAwaitable)
{
code.WriteLine("var result = await handler();");
code.WriteLine(endpoint.EmitResponseWritingCall());
}
else
{
code.WriteLine("var result = handler();");
code.WriteLine("return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);");
}
code.WriteLine(endpoint.Response.IsAwaitable
? "var result = await handler();"
: "var result = handler();");
code.WriteLine(endpoint.EmitResponseWritingCall());

else
{
code.WriteLine("var result = handler();");
code.WriteLine("return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);");
Copy link
Member

Choose a reason for hiding this comment

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

Nit: we should be able special-case string and IResult in the generated RequestHandler, because unlike RequestHandlerFiltered, this is only called for non-filtered handlers, right?

@captainsafia
Copy link
Member Author

🆙 📅

  • Removed CodeWriter in favor of string literals with indentation in code sources
  • Added incrementality tests to validate behavior if Endpoint's comparable implementation
  • Added and consume Microsoft.Bcl.HashCode API as a dependency
  • Tweaked smaller pieces of feedback

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

I've got a draft PR based on this branch open at #46407. Now I just want to minimize my merge conflicts! 😆 Looks good though!

{
if (options == null || options.EndpointBuilder == null)
{
return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection<object>.Empty };
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this branch is possible given the only caller to this callback is RouteEndpointDataSource, so there will always be an EndpointBuilder.

If this branch were possible, we'd probably want to add the SourceKey here anyway. But since I think it's not, it's probably better to add an assert or throw instead.

@captainsafia
Copy link
Member Author

Merging to unblock forthcoming PRs. Happy to address any other feedback in a followup PR. :shipit:

@captainsafia captainsafia merged commit b138bde into main Feb 3, 2023
@captainsafia captainsafia deleted the safia/rdg-responses branch February 3, 2023 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
old-area-web-frameworks-do-not-use *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.

Support response writing in RequestDelegateGenerator Generate route handler-endpoints at compile time via source generator
6 participants