diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs index 08f0860ad7be..f36338eaf879 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs @@ -106,4 +106,13 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Warning, isEnabledByDefault: true, helpLinkUri: "https://aka.ms/aspnet/analyzers"); + + internal static readonly DiagnosticDescriptor UseTopLevelRouteRegistrationsInsteadOfUseEndpoints = new( + "ASP0014", + "Suggest using top level route registrations", + "Suggest using top level route registrations instead of {0}", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: "https://aka.ms/aspnet/analyzers"); } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs index 7e3ba423ccfd..df599d22c6b7 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs @@ -22,7 +22,8 @@ public class WebApplicationBuilderAnalyzer : DiagnosticAnalyzer DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder, DiagnosticDescriptors.DoNotUseHostConfigureLogging, DiagnosticDescriptors.DoNotUseHostConfigureServices, - DiagnosticDescriptors.DisallowConfigureAppConfigureHostBuilder + DiagnosticDescriptors.DisallowConfigureAppConfigureHostBuilder, + DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints ); public override void Initialize(AnalysisContext context) @@ -64,6 +65,11 @@ public override void Initialize(AnalysisContext context) wellKnownTypes.HostingHostBuilderExtensions, }; INamedTypeSymbol[] configureHostTypes = { wellKnownTypes.ConfigureHostBuilder }; + INamedTypeSymbol[] useEndpointTypes = + { + wellKnownTypes.EndpointRoutingApplicationBuilderExtensions, + wellKnownTypes.WebApplicationBuilder + }; context.RegisterOperationAction(context => { @@ -230,6 +236,24 @@ public override void Initialize(AnalysisContext context) invocation)); } + //var builder = WebApplication.CreateBuilder(args); + //var app= builder.Build(); + //app.UseRouting(); + //app.UseEndpoints(x => {}) + if (IsDisallowedMethod( + context, + invocation, + targetMethod, + wellKnownTypes.WebApplicationBuilder, + "UseEndpoints", + useEndpointTypes)) + { + context.ReportDiagnostic( + CreateDiagnostic( + DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, + invocation)); + } + static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, IInvocationOperation operation) { // Take the location for the whole invocation operation as a starting point. diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs index 0e2f1102d4d1..552122334953 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs @@ -48,6 +48,18 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We return false; } + const string EndpointRoutingApplicationBuilderExtensions = "Microsoft.AspNetCore.Builder.EndpointRoutingApplicationBuilderExtensions"; + if (compilation.GetTypeByMetadataName(EndpointRoutingApplicationBuilderExtensions) is not { } endpointRoutingApplicationBuilderExtensions) + { + return false; + } + + const string WebApplicationBuilder = "Microsoft.AspNetCore.Builder.WebApplication"; + if (compilation.GetTypeByMetadataName(WebApplicationBuilder) is not { } webApplicationBuilder) + { + return false; + } + wellKnownTypes = new WellKnownTypes { ConfigureHostBuilder = configureHostBuilder, @@ -55,7 +67,9 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We GenericHostWebHostBuilderExtensions = genericHostWebHostBuilderExtensions, HostingAbstractionsWebHostBuilderExtensions = hostingAbstractionsWebHostBuilderExtensions, WebHostBuilderExtensions = webHostBuilderExtensions, - HostingHostBuilderExtensions = hostingHostBuilderExtensions + HostingHostBuilderExtensions = hostingHostBuilderExtensions, + EndpointRoutingApplicationBuilderExtensions = endpointRoutingApplicationBuilderExtensions, + WebApplicationBuilder = webApplicationBuilder }; return true; @@ -67,4 +81,6 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We public INamedTypeSymbol HostingAbstractionsWebHostBuilderExtensions { get; private init; } public INamedTypeSymbol WebHostBuilderExtensions { get; private init; } public INamedTypeSymbol HostingHostBuilderExtensions { get; private init; } + public INamedTypeSymbol EndpointRoutingApplicationBuilderExtensions { get; private init; } + public INamedTypeSymbol WebApplicationBuilder { get; private init; } } diff --git a/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/UseTopLevelRouteRegistrationInsteadOfUseEndpointsTest.cs b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/UseTopLevelRouteRegistrationInsteadOfUseEndpointsTest.cs new file mode 100644 index 000000000000..791cfc9b37c0 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/UseTopLevelRouteRegistrationInsteadOfUseEndpointsTest.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; +public partial class UseTopLevelRouteRegistrationsInsteadOfUseEndpointsTest +{ + private TestDiagnosticAnalyzerRunner Runner { get; } = new(new WebApplicationBuilderAnalyzer()); + + [Fact] + public async Task DoesNotWarnWhenEndpointRegistrationIsTopLevel() + { + //arrange + var source = @" +using Microsoft.AspNetCore.Builder; +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.UseRouting(); +app.MapGet(""/"", () => ""Hello World!""); +"; + //act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + //assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task DoesNotWarnWhenEnpointRegistrationIsTopLevel_InMain() + { + //arrange + var source = @" +using Microsoft.AspNetCore.Builder; +public static class Program +{ + public static void Main (string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + app.UseRouting(); + app.MapGet(""/"", () => ""Hello World!""); + } +} +"; + //act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + //assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WarnsWhenEndpointRegistrationIsNotTopLevel() + { + //arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.UseRouting(); +app./*MM*/UseEndpoints(endpoints => +{ + endpoints.MapGet(""/"", () => ""Hello World!""); +}); +"); + //act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + //assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_OtherMapMethods() + { + //arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.UseRouting(); +app./*MM1*/UseEndpoints(endpoints => +{ + endpoints.MapGet(""/"", () => ""This is a GET""); +}); +app./*MM2*/UseEndpoints(endpoints => +{ + endpoints.MapPost(""/"", () => ""This is a POST""); +}); +app./*MM3*/UseEndpoints(endpoints => +{ + endpoints.MapPut(""/"", () => ""This is a PUT""); +}); +app./*MM4*/UseEndpoints(endpoints => +{ + endpoints.MapDelete(""/"", () => ""This is a DELETE""); +}); +"); + //act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + //assert + Assert.Equal(4, diagnostics.Length); + var diagnostic1 = diagnostics[0]; + var diagnostic2 = diagnostics[1]; + var diagnostic3 = diagnostics[2]; + var diagnostic4 = diagnostics[3]; + + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic1.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic1.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic1.GetMessage(CultureInfo.InvariantCulture)); + + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM2"], diagnostic2.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture)); + + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic3.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM3"], diagnostic3.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic3.GetMessage(CultureInfo.InvariantCulture)); + + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM4"], diagnostic4.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_InMain_MapControllers() + { + //arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +public static class Program +{ + public static void Main (string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddControllers(); + var app = builder.Build(); + app.UseRouting(); + app./*MM*/UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } +} +"); + //act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + //assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_OnDifferentLine_WithRouteParameters() + { + //arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.UseRouting(); +app. + /*MM*/UseEndpoints(endpoints => +{ + endpoints.MapGet(""/users/{userId}/books/{bookId}"", + (int userId, int bookId) => $""The user id is {userId} and book id is {bookId}""); +}); +"); + //act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + //assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WarnsTwiceWhenEndpointRegistrationIsNotTopLevel_OnDifferentLine() + { + //arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.UseRouting(); +app./*MM1*/UseEndpoints(endpoints => +{ + endpoints.MapGet(""/"", () => ""Hello World!""); +}); +app./*MM2*/UseEndpoints(endpoints => +{ + endpoints.MapGet(""/"", () => ""Hello World!""); +}); +"); + //act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + //assert + Assert.Equal(2, diagnostics.Length); + var diagnostic1 = diagnostics[0]; + var diagnostic2 = diagnostics[1]; + + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic1.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic1.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic1.GetMessage(CultureInfo.InvariantCulture)); + + Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM2"], diagnostic2.Location); + Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture)); + } +}