-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Conversation
0571c04
to
26c3f11
Compare
cbbaef0
to
544c00a
Compare
544c00a
to
8c5b980
Compare
|
||
internal static class WellKnownTypeData | ||
{ | ||
public enum WellKnownType |
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.
Does this need to be nested?
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.
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.
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.
Wouldn't leaving this unnested minimize the code delta by removing the using WellKnownType = WellKnownTypeData.WellKnownType;
?
|
||
file static class GeneratedRouteBuilderExtensionsCore | ||
{ | ||
|
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.
super nit: Is there a way to not start the type with a blank line? My OCD is triggered 😄
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.
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" /> |
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.
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)
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.
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> |
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.
Do we need this all the time? VS allows you to see the generate files in the Solution Explorer.
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.
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.
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.
src/Shared/RoslynUtils/CodeWriter.cs
Outdated
|
||
internal class CodeWriter | ||
{ | ||
private readonly StringBuilder _codeBuilder = new(); |
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.
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); |
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.
Why the magic 5
here?
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.
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.
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 Regex source generator uses IndentedTextWriter
. Seems to work really well there. You just writer.Indent++
and --
when you are done.
|
||
private static string EmitResponseWritingCall(this Endpoint endpoint) | ||
{ | ||
var code = new CodeWriter(new StringBuilder()); |
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.
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.
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);"); | ||
} |
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.
EmitResponseWritingCall
handles IsAwaitable()
. So I think this can be refactored to just:
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);"); |
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.
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?
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs
Outdated
Show resolved
Hide resolved
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs
Outdated
Show resolved
Hide resolved
src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs
Show resolved
Hide resolved
src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs
Show resolved
Hide resolved
🆙 📅
|
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.
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 }; |
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.
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.
src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj
Outdated
Show resolved
Hide resolved
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs
Outdated
Show resolved
Hide resolved
Merging to unblock forthcoming PRs. Happy to address any other feedback in a followup PR. |
Addresses #46276 and resolves #45524.
RequestDelegateFactoryOptions.EndpointBuilder