Skip to content

Implement code fixer to change the code from calling Configure methods to using builder.Configuration #42630

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

Closed
wants to merge 10 commits into from

Conversation

anhthidao
Copy link
Contributor

{Fixer: Replacing builder.Host.ConfigureAppConfiguration/builder.Host.ConfigureHostConfiguration/builder.WebHost.ConfigureAppConfiguration with builder.Configuration directly}

Implement

Description

Add code fixer that calls builder.Configuration instead of using ConfigureAppConfiguration/ConfigureHostConfiguration with builder.Host/builder.WebHost.

Fixes #35815

@anhthidao anhthidao requested a review from Pilchie as a code owner July 8, 2022 15:36
@anhthidao anhthidao requested a review from captainsafia July 8, 2022 15:38
@Pilchie Pilchie 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 Jul 8, 2022
@@ -18,6 +18,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mvc", "Mvc", "{819B136D-B8B6-46AE-8C4F-5469BBBFC46B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc", "src\Mvc\Mvc\src\Microsoft.AspNetCore.Mvc.csproj", "{D4FBDF11-7A65-4205-8AF6-ABC190EFCF50}"
ProjectSection(ProjectDependencies) = postProject
Copy link
Member

Choose a reason for hiding this comment

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

What project is being added as a dependency here? Ideally, there should be no changes to the solution file...

var method = bodyExpression.Name;

expression = expression.WithName(method);
invocationExpression = invocationExpression.WithExpression(expression);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
invocationExpression = invocationExpression.WithExpression(expression);
invocationExpression = invocationExpression.WithExpression(expression).WithArgumentList(argument);

This modifications can all be chained together.


if (invocation is InvocationExpressionSyntax { } invocationExpression)
{
var expression = invocationExpression.Expression as MemberAccessExpressionSyntax;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
var expression = invocationExpression.Expression as MemberAccessExpressionSyntax;
var invokedMethodExpr = invocationExpression.Expression as MemberAccessExpressionSyntax;

To make it a little clearer what this is.

builder = builder.WithName(configuration);
expression = expression.WithExpression(builder);

var initArgument = invocationExpression.ArgumentList.Arguments[0];
Copy link
Member

Choose a reason for hiding this comment

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

Let's add a guard here in case there are no arguments provided to the invocation.

expression = expression.WithExpression(builder);

var initArgument = invocationExpression.ArgumentList.Arguments[0];
var lambdaExpression = initArgument.Expression as SimpleLambdaExpressionSyntax;
Copy link
Member

Choose a reason for hiding this comment

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

It might help to move this to a inline static method called ExtractArgumentBody to make it clearer what this is doing.

invocationExpression = invocationExpression.WithExpression(expression);
invocationExpression = invocationExpression.WithArgumentList(argument);

editor.ReplaceNode(invocation, invocationExpression);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
editor.ReplaceNode(invocation, invocationExpression);
editor.ReplaceNode(invocation, newInvocationExpression);

So it is easier to distinguish between the unmodified and modified invocation.


var initArgument = invocationExpression.ArgumentList.Arguments[0];
var lambdaExpression = initArgument.Expression as SimpleLambdaExpressionSyntax;
var body = lambdaExpression.ExpressionBody as InvocationExpressionSyntax;
Copy link
Member

Choose a reason for hiding this comment

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

Can you add some comments here that show what each of these expressions reflects?

// logging.AddJson();
var body = lambdaExpression.ExpressionBody;

Or what have you.

var expectedDiagnostic = new DiagnosticResult(DiagnosticDescriptors.DisallowConfigureAppConfigureHostBuilder).WithArguments("ConfigureAppConfiguration").WithLocation(0);

//Act
await VerifyCS.VerifyCodeFixAsync(source, expectedDiagnostic, fixedSource);
Copy link
Member

@captainsafia captainsafia Jul 8, 2022

Choose a reason for hiding this comment

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

Let's add a test scenario where the same ConfigureAppConfiguration contains multiple calls inside it.

builder.Host.ConfigureAppConfiguration(builder =>
{
    builder.AddJsonFile("foo.json", optional: true);
    builder.AddEnvironmentVariables();
});

should become

builder.Configuration.
  .AddJsonFile("foo.json", optional: true)
  .AddEnvironmentVariables();

using Microsoft.Extensions.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile(""foo.json"", optional: true);
builder.Configuration.AddJsonFile(""foo.json"", optional: true);
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem like the correct behavior here. I think it's unlikely for users to make the same invocations in different ConfigureX calls, but if they do, it should only produce a single invocation.

return document;
}

var invocation = root.FindNode(diagnostic.Location.SourceSpan); //whole invocation starting from "builder"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
var invocation = root.FindNode(diagnostic.Location.SourceSpan); //whole invocation starting from "builder"
var diagnosticTarget = root.FindNode(diagnostic.Location.SourceSpan); //whole invocation starting from "builder"

Since at this point, we haven't actually verified at runtime that it's an invocation node. That doesn't happen until the if-statement below. This also helps us disambiguate between the different invocations being modified.


var invocation = root.FindNode(diagnostic.Location.SourceSpan); //whole invocation starting from "builder"

if (invocation is InvocationExpressionSyntax { } newInvocationExpr)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (invocation is InvocationExpressionSyntax { } newInvocationExpr)
if (diagnosticTarget is InvocationExpressionSyntax { } originalInvocation)


var configuration = SyntaxFactory.IdentifierName("Configuration");

if (hostBasedInvocationMethodExpr.Expression is not MemberAccessExpressionSyntax subExpression)
Copy link
Member

Choose a reason for hiding this comment

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

We can probably combine the if-statement on L56 and this one into a single on that clearly communicates that we are looking "up" the accessors.

if (newInvocationExpr.Expression is not MemberAccessExpressionSyntax hostBasedInvocationMethodExpr
 && hostBasedInvocationMethodExpr.Expression is not MemberAccessExpressionSyntax hostExpression)
{
}

The values should be assigned correctly here.

{
return editor.OriginalDocument;
}
if (subExpression.Name != null) //subExpression.Name is Host/Webhost
Copy link
Member

Choose a reason for hiding this comment

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

Is Name actually nullable here? Also, it might help to invert the if statement and return early here? I'm having trouble visualizing what happens if we don't hit this case.

var bodyEnum = ExtractParenLambdaBody(parenLambdaExpr).GetEnumerator();
while (bodyEnum.MoveNext())
{
if (bodyEnum.Current is not ExpressionStatementSyntax currentStatement) //builder.AddJsonFile() or builder.AddEnvironmentVariables()
Copy link
Member

Choose a reason for hiding this comment

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

We can combine this if-statement and the one below into one statement with an &&.


if (initArgumentExpr is ParenthesizedLambdaExpressionSyntax { } parenLambdaExpr)
{
var bodyEnum = ExtractParenLambdaBody(parenLambdaExpr).GetEnumerator();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
var bodyEnum = ExtractParenLambdaBody(parenLambdaExpr).GetEnumerator();
var lambdaStatements = ExtractParenLambdaBody(parenLambdaExpr);

if (initArgumentExpr is ParenthesizedLambdaExpressionSyntax { } parenLambdaExpr)
{
var bodyEnum = ExtractParenLambdaBody(parenLambdaExpr).GetEnumerator();
while (bodyEnum.MoveNext())
Copy link
Member

Choose a reason for hiding this comment

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

You can use a foreach here.

foreach (var statement in lambdaStatements)
{
}

}
private static SyntaxList<StatementSyntax> ExtractParenLambdaBody(ParenthesizedLambdaExpressionSyntax initArgumentExpr)
{
var body = initArgumentExpr.Block.Statements;
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 to do any validation on Block here? For example, check if it is null...

@anhthidao
Copy link
Contributor Author

 var method = bodyExpression.Name; // line 119

 hostBasedInvocationMethodExpr = hostBasedInvocationMethodExpr.WithName(method); // line 121
 originalInvocation = originalInvocation.Update(hostBasedInvocationMethodExpr, argument); // line 122
 hostBasedInvocationMethodExpr = hostBasedInvocationMethodExpr.WithExpression(originalInvocation); // line 123

This part, when placed inside a loop, would continuously add the methods called to the final invocation. originalInvocation would be returned as the modified invocation, while hostBasedInvocationMethodExpr would be discarded.

Example:

Before:

 builder.Host.ConfigureHostConfiguration(builder =>
{
     builder.AddJsonFile("foo.json", optional: true);
     builder.AddEnvironmentVariables();
});

After:

 builder.Configuration.AddJsonFile("foo.json", optional: true).AddEnvironmentVariables();

Walkthrough:

First iteration:

method: null -> AddJsonFile

hostBasedInvocationMethodExpr: builder.Configuration.ConfigureHostConfiguration -> builder.Configuration.AddJsonFile

originalInvocation: Before
-> builder.Configuration.AddJsonFile("foo.json", optional: true)

hostBasedInvocationMethodExpr: builder.Configuration.AddJsonFile -> builder.Configuration.AddJsonFile("foo.json", optional: true).AddJsonFile

Second iteration:

method: AddJsonFile -> AddEnvironmentVariables

hostBasedInvocationMethodExpr: builder.Configuration.AddJsonFile("foo.json", optional: true).AddJsonFile -> builder.Configuration.AddJsonFile("foo.json", optional: true).AddEnvironmentVariables

originalInvocation: builder.Configuration.AddJsonFile("foo.json", optional: true) -> builder.Configuration.AddJsonFile("foo.json", optional: true).AddEnvironmentVariables()

hostBasedInvocationMethodExpr: builder.Configuration.AddJsonFile("foo.json", optional: true).AddEnvironmentVariables
-> builder.Configuration.AddJsonFile("foo.json", optional: true).AddEnvironmentVariables().AddEnvironmentVariables

@anhthidao anhthidao closed this Jul 21, 2022
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
3 participants