Skip to content

Commit 426a204

Browse files
Add analyzer to suggest top level route registration (#42937)
* Add analyzer to suggest top level route registration * Add tests for other map methods and make the diagnostic descriptor more general * Minor changes to the tests * Update WebApplicationBuilderAnalyzer.cs * Change operationAnalysisContext to context to match the other updated Analyzers.
1 parent 25a5cd8 commit 426a204

File tree

4 files changed

+280
-2
lines changed

4 files changed

+280
-2
lines changed

src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs

+9
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,13 @@ internal static class DiagnosticDescriptors
106106
DiagnosticSeverity.Warning,
107107
isEnabledByDefault: true,
108108
helpLinkUri: "https://aka.ms/aspnet/analyzers");
109+
110+
internal static readonly DiagnosticDescriptor UseTopLevelRouteRegistrationsInsteadOfUseEndpoints = new(
111+
"ASP0014",
112+
"Suggest using top level route registrations",
113+
"Suggest using top level route registrations instead of {0}",
114+
"Usage",
115+
DiagnosticSeverity.Warning,
116+
isEnabledByDefault: true,
117+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
109118
}

src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs

+25-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public class WebApplicationBuilderAnalyzer : DiagnosticAnalyzer
2222
DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder,
2323
DiagnosticDescriptors.DoNotUseHostConfigureLogging,
2424
DiagnosticDescriptors.DoNotUseHostConfigureServices,
25-
DiagnosticDescriptors.DisallowConfigureAppConfigureHostBuilder
25+
DiagnosticDescriptors.DisallowConfigureAppConfigureHostBuilder,
26+
DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints
2627
);
2728

2829
public override void Initialize(AnalysisContext context)
@@ -64,6 +65,11 @@ public override void Initialize(AnalysisContext context)
6465
wellKnownTypes.HostingHostBuilderExtensions,
6566
};
6667
INamedTypeSymbol[] configureHostTypes = { wellKnownTypes.ConfigureHostBuilder };
68+
INamedTypeSymbol[] useEndpointTypes =
69+
{
70+
wellKnownTypes.EndpointRoutingApplicationBuilderExtensions,
71+
wellKnownTypes.WebApplicationBuilder
72+
};
6773

6874
context.RegisterOperationAction(context =>
6975
{
@@ -230,6 +236,24 @@ public override void Initialize(AnalysisContext context)
230236
invocation));
231237
}
232238

239+
//var builder = WebApplication.CreateBuilder(args);
240+
//var app= builder.Build();
241+
//app.UseRouting();
242+
//app.UseEndpoints(x => {})
243+
if (IsDisallowedMethod(
244+
context,
245+
invocation,
246+
targetMethod,
247+
wellKnownTypes.WebApplicationBuilder,
248+
"UseEndpoints",
249+
useEndpointTypes))
250+
{
251+
context.ReportDiagnostic(
252+
CreateDiagnostic(
253+
DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints,
254+
invocation));
255+
}
256+
233257
static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, IInvocationOperation operation)
234258
{
235259
// Take the location for the whole invocation operation as a starting point.

src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs

+17-1
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,28 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We
4848
return false;
4949
}
5050

51+
const string EndpointRoutingApplicationBuilderExtensions = "Microsoft.AspNetCore.Builder.EndpointRoutingApplicationBuilderExtensions";
52+
if (compilation.GetTypeByMetadataName(EndpointRoutingApplicationBuilderExtensions) is not { } endpointRoutingApplicationBuilderExtensions)
53+
{
54+
return false;
55+
}
56+
57+
const string WebApplicationBuilder = "Microsoft.AspNetCore.Builder.WebApplication";
58+
if (compilation.GetTypeByMetadataName(WebApplicationBuilder) is not { } webApplicationBuilder)
59+
{
60+
return false;
61+
}
62+
5163
wellKnownTypes = new WellKnownTypes
5264
{
5365
ConfigureHostBuilder = configureHostBuilder,
5466
ConfigureWebHostBuilder = configureWebHostBuilder,
5567
GenericHostWebHostBuilderExtensions = genericHostWebHostBuilderExtensions,
5668
HostingAbstractionsWebHostBuilderExtensions = hostingAbstractionsWebHostBuilderExtensions,
5769
WebHostBuilderExtensions = webHostBuilderExtensions,
58-
HostingHostBuilderExtensions = hostingHostBuilderExtensions
70+
HostingHostBuilderExtensions = hostingHostBuilderExtensions,
71+
EndpointRoutingApplicationBuilderExtensions = endpointRoutingApplicationBuilderExtensions,
72+
WebApplicationBuilder = webApplicationBuilder
5973
};
6074

6175
return true;
@@ -67,4 +81,6 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We
6781
public INamedTypeSymbol HostingAbstractionsWebHostBuilderExtensions { get; private init; }
6882
public INamedTypeSymbol WebHostBuilderExtensions { get; private init; }
6983
public INamedTypeSymbol HostingHostBuilderExtensions { get; private init; }
84+
public INamedTypeSymbol EndpointRoutingApplicationBuilderExtensions { get; private init; }
85+
public INamedTypeSymbol WebApplicationBuilder { get; private init; }
7086
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Globalization;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Analyzer.Testing;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
14+
namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder;
15+
public partial class UseTopLevelRouteRegistrationsInsteadOfUseEndpointsTest
16+
{
17+
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new WebApplicationBuilderAnalyzer());
18+
19+
[Fact]
20+
public async Task DoesNotWarnWhenEndpointRegistrationIsTopLevel()
21+
{
22+
//arrange
23+
var source = @"
24+
using Microsoft.AspNetCore.Builder;
25+
var builder = WebApplication.CreateBuilder(args);
26+
var app = builder.Build();
27+
app.UseRouting();
28+
app.MapGet(""/"", () => ""Hello World!"");
29+
";
30+
//act
31+
var diagnostics = await Runner.GetDiagnosticsAsync(source);
32+
33+
//assert
34+
Assert.Empty(diagnostics);
35+
}
36+
37+
[Fact]
38+
public async Task DoesNotWarnWhenEnpointRegistrationIsTopLevel_InMain()
39+
{
40+
//arrange
41+
var source = @"
42+
using Microsoft.AspNetCore.Builder;
43+
public static class Program
44+
{
45+
public static void Main (string[] args)
46+
{
47+
var builder = WebApplication.CreateBuilder(args);
48+
var app = builder.Build();
49+
app.UseRouting();
50+
app.MapGet(""/"", () => ""Hello World!"");
51+
}
52+
}
53+
";
54+
//act
55+
var diagnostics = await Runner.GetDiagnosticsAsync(source);
56+
57+
//assert
58+
Assert.Empty(diagnostics);
59+
}
60+
61+
[Fact]
62+
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel()
63+
{
64+
//arrange
65+
var source = TestSource.Read(@"
66+
using Microsoft.AspNetCore.Builder;
67+
var builder = WebApplication.CreateBuilder(args);
68+
var app = builder.Build();
69+
app.UseRouting();
70+
app./*MM*/UseEndpoints(endpoints =>
71+
{
72+
endpoints.MapGet(""/"", () => ""Hello World!"");
73+
});
74+
");
75+
//act
76+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
77+
78+
//assert
79+
var diagnostic = Assert.Single(diagnostics);
80+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor);
81+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
82+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture));
83+
}
84+
85+
[Fact]
86+
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_OtherMapMethods()
87+
{
88+
//arrange
89+
var source = TestSource.Read(@"
90+
using Microsoft.AspNetCore.Builder;
91+
var builder = WebApplication.CreateBuilder(args);
92+
var app = builder.Build();
93+
app.UseRouting();
94+
app./*MM1*/UseEndpoints(endpoints =>
95+
{
96+
endpoints.MapGet(""/"", () => ""This is a GET"");
97+
});
98+
app./*MM2*/UseEndpoints(endpoints =>
99+
{
100+
endpoints.MapPost(""/"", () => ""This is a POST"");
101+
});
102+
app./*MM3*/UseEndpoints(endpoints =>
103+
{
104+
endpoints.MapPut(""/"", () => ""This is a PUT"");
105+
});
106+
app./*MM4*/UseEndpoints(endpoints =>
107+
{
108+
endpoints.MapDelete(""/"", () => ""This is a DELETE"");
109+
});
110+
");
111+
//act
112+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
113+
114+
//assert
115+
Assert.Equal(4, diagnostics.Length);
116+
var diagnostic1 = diagnostics[0];
117+
var diagnostic2 = diagnostics[1];
118+
var diagnostic3 = diagnostics[2];
119+
var diagnostic4 = diagnostics[3];
120+
121+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic1.Descriptor);
122+
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic1.Location);
123+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic1.GetMessage(CultureInfo.InvariantCulture));
124+
125+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor);
126+
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM2"], diagnostic2.Location);
127+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture));
128+
129+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic3.Descriptor);
130+
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM3"], diagnostic3.Location);
131+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic3.GetMessage(CultureInfo.InvariantCulture));
132+
133+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor);
134+
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM4"], diagnostic4.Location);
135+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture));
136+
}
137+
138+
[Fact]
139+
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_InMain_MapControllers()
140+
{
141+
//arrange
142+
var source = TestSource.Read(@"
143+
using Microsoft.AspNetCore.Builder;
144+
using Microsoft.Extensions.DependencyInjection;
145+
public static class Program
146+
{
147+
public static void Main (string[] args)
148+
{
149+
var builder = WebApplication.CreateBuilder(args);
150+
builder.Services.AddControllers();
151+
var app = builder.Build();
152+
app.UseRouting();
153+
app./*MM*/UseEndpoints(endpoints =>
154+
{
155+
endpoints.MapControllers();
156+
});
157+
}
158+
}
159+
");
160+
//act
161+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
162+
163+
//assert
164+
var diagnostic = Assert.Single(diagnostics);
165+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor);
166+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
167+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture));
168+
}
169+
170+
[Fact]
171+
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_OnDifferentLine_WithRouteParameters()
172+
{
173+
//arrange
174+
var source = TestSource.Read(@"
175+
using Microsoft.AspNetCore.Builder;
176+
var builder = WebApplication.CreateBuilder(args);
177+
var app = builder.Build();
178+
app.UseRouting();
179+
app.
180+
/*MM*/UseEndpoints(endpoints =>
181+
{
182+
endpoints.MapGet(""/users/{userId}/books/{bookId}"",
183+
(int userId, int bookId) => $""The user id is {userId} and book id is {bookId}"");
184+
});
185+
");
186+
//act
187+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
188+
189+
//assert
190+
var diagnostic = Assert.Single(diagnostics);
191+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor);
192+
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
193+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture));
194+
}
195+
196+
[Fact]
197+
public async Task WarnsTwiceWhenEndpointRegistrationIsNotTopLevel_OnDifferentLine()
198+
{
199+
//arrange
200+
var source = TestSource.Read(@"
201+
using Microsoft.AspNetCore.Builder;
202+
var builder = WebApplication.CreateBuilder(args);
203+
var app = builder.Build();
204+
app.UseRouting();
205+
app./*MM1*/UseEndpoints(endpoints =>
206+
{
207+
endpoints.MapGet(""/"", () => ""Hello World!"");
208+
});
209+
app./*MM2*/UseEndpoints(endpoints =>
210+
{
211+
endpoints.MapGet(""/"", () => ""Hello World!"");
212+
});
213+
");
214+
//act
215+
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
216+
//assert
217+
Assert.Equal(2, diagnostics.Length);
218+
var diagnostic1 = diagnostics[0];
219+
var diagnostic2 = diagnostics[1];
220+
221+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic1.Descriptor);
222+
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic1.Location);
223+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic1.GetMessage(CultureInfo.InvariantCulture));
224+
225+
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor);
226+
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM2"], diagnostic2.Location);
227+
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture));
228+
}
229+
}

0 commit comments

Comments
 (0)