From 8c5b980e066a822216d4400c66cf4c5cfd7da3cf Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 12 Jan 2023 14:24:10 -0800 Subject: [PATCH 1/4] Add support for response writing to RequestDelegateGenerator --- .../Http/HeaderDictionaryAddAnalyzer.cs | 2 + .../Http/RequestDelegateReturnTypeAnalyzer.cs | 2 + .../Infrastructure/ParsabilityHelper.cs | 4 +- .../Infrastructure/WellKnownTypeData.cs | 203 +++++++++++ .../Infrastructure/WellKnownTypes.cs | 314 ------------------ .../Microsoft.AspNetCore.App.Analyzers.csproj | 2 + .../Mvc/DetectAmbiguousActionRoutes.cs | 2 + .../src/Analyzers/Mvc/MvcAnalyzer.cs | 2 + .../RenderTreeBuilderAnalyzer.cs | 2 + .../FrameworkParametersCompletionProvider.cs | 2 + .../Infrastructure/MvcDetector.cs | 2 + .../RoutePatternParametersDetector.cs | 2 + .../Infrastructure/RouteUsageDetector.cs | 2 + .../Infrastructure/RouteWellKnownTypes.cs | 2 + .../RouteHandlers/DetectAmbiguousRoutes.cs | 2 + .../DisallowMvcBindArgumentsOnParameters.cs | 2 + ...llowNonParsableComplexTypesOnParameters.cs | 2 + ...llowReturningActionResultFromMapMethods.cs | 2 + .../RouteHandlers/RouteHandlerAnalyzer.cs | 4 +- .../WebApplicationBuilderAnalyzer.cs | 2 + .../Http/HeaderDictionaryAddFixer.cs | 2 + .../Infrastructure/WellKnownTypesTests.cs | 2 + .../gen/DiagnosticDescriptors.cs | 27 ++ .../Http.Extensions/gen/GeneratorSteps.cs | 9 + ...icrosoft.AspNetCore.Http.Generators.csproj | 6 +- .../gen/RequestDelegateGenerator.cs | 164 ++++++--- .../gen/RequestDelegateGeneratorSources.cs | 18 +- src/Http/Http.Extensions/gen/Resources.resx | 132 ++++++++ .../gen/StaticRouteHandlerModel/Endpoint.cs | 82 +++++ .../EndpointResponse.cs | 136 ++++++++ .../StaticRouteHandlerModel/EndpointRoute.cs | 48 +++ .../InvocationOperationExtensions.cs | 68 ++++ .../StaticRouteHandlerModel.Emitter.cs | 118 +++++-- .../StaticRouteHandlerModel.Parser.cs | 121 ------- .../StaticRouteHandlerModel.cs | 20 -- .../WellKnownTypeData.cs | 25 ++ ...ft.AspNetCore.Http.Extensions.Tests.csproj | 4 +- ...aram_StringReturn_WithFilter.generated.txt | 88 +++-- ...pAction_NoParam_StringReturn.generated.txt | 225 +++++++++++++ .../RequestDelegateGeneratorTestBase.cs | 42 ++- .../RequestDelegateGeneratorTests.cs | 229 ++++++++++++- .../MinimalSample/MinimalSample.csproj | 6 + .../RoslynUtils}/BoundedCacheWithFactory.cs | 0 src/Shared/RoslynUtils/CodeWriter.cs | 82 +++++ src/Shared/RoslynUtils/LambdaComparer.cs | 37 +++ src/Shared/RoslynUtils/WellKnownTypes.cs | 130 ++++++++ 46 files changed, 1763 insertions(+), 615 deletions(-) create mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs delete mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs create mode 100644 src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs create mode 100644 src/Http/Http.Extensions/gen/GeneratorSteps.cs create mode 100644 src/Http/Http.Extensions/gen/Resources.resx create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointRoute.cs create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs delete mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs delete mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt rename src/{Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure => Shared/RoslynUtils}/BoundedCacheWithFactory.cs (100%) create mode 100644 src/Shared/RoslynUtils/CodeWriter.cs create mode 100644 src/Shared/RoslynUtils/LambdaComparer.cs create mode 100644 src/Shared/RoslynUtils/WellKnownTypes.cs diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs index 64c859541f7d..adf6e40cf0cb 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.Http; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class HeaderDictionaryAddAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs index fc710b8a4cce..9ce7c69a9ecb 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.Http; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class RequestDelegateReturnTypeAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs index 654aa2a38707..124832884c96 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.Analyzers.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class ParsabilityHelper { private static bool IsTypeAlwaysParsableOrBindable(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes) @@ -160,7 +162,7 @@ internal static Bindability GetBindability(INamedTypeSymbol typeSymbol, WellKnow { return Bindability.InvalidReturnType; } - + } return Bindability.NotBindable; diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs new file mode 100644 index 000000000000..d22c74e2d029 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; + +internal static class WellKnownTypeData +{ + public enum WellKnownType + { + Microsoft_AspNetCore_Components_Rendering_RenderTreeBuilder, + Microsoft_AspNetCore_Http_IHeaderDictionary, + Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata, + Microsoft_AspNetCore_Http_HeaderDictionaryExtensions, + Microsoft_AspNetCore_Routing_IEndpointRouteBuilder, + Microsoft_AspNetCore_Mvc_ControllerAttribute, + Microsoft_AspNetCore_Mvc_NonControllerAttribute, + Microsoft_AspNetCore_Mvc_NonActionAttribute, + Microsoft_AspNetCore_Http_AsParametersAttribute, + System_Threading_CancellationToken, + Microsoft_AspNetCore_Http_HttpContext, + Microsoft_AspNetCore_Http_HttpRequest, + Microsoft_AspNetCore_Http_HttpResponse, + System_Security_Claims_ClaimsPrincipal, + Microsoft_AspNetCore_Http_IFormFileCollection, + Microsoft_AspNetCore_Http_IFormFile, + System_IO_Stream, + System_IO_Pipelines_PipeReader, + System_IFormatProvider, + System_Uri, + Microsoft_AspNetCore_Builder_ConfigureHostBuilder, + Microsoft_AspNetCore_Builder_ConfigureWebHostBuilder, + Microsoft_Extensions_Hosting_GenericHostWebHostBuilderExtensions, + Microsoft_AspNetCore_Hosting_WebHostBuilderExtensions, + Microsoft_AspNetCore_Hosting_HostingAbstractionsWebHostBuilderExtensions, + Microsoft_Extensions_Hosting_HostingHostBuilderExtensions, + Microsoft_AspNetCore_Builder_EndpointRoutingApplicationBuilderExtensions, + Microsoft_AspNetCore_Builder_WebApplication, + Microsoft_AspNetCore_Builder_EndpointRouteBuilderExtensions, + System_Delegate, + Microsoft_AspNetCore_Mvc_ModelBinding_IBinderTypeProviderMetadata, + Microsoft_AspNetCore_Mvc_BindAttribute, + Microsoft_AspNetCore_Http_IResult, + Microsoft_AspNetCore_Mvc_IActionResult, + Microsoft_AspNetCore_Mvc_Infrastructure_IConvertToActionResult, + Microsoft_AspNetCore_Http_RequestDelegate, + System_Threading_Tasks_Task_T, + System_Threading_Tasks_ValueTask_T, + System_Reflection_ParameterInfo, + Microsoft_AspNetCore_Http_IBindableFromHttpContext_T, + System_IParsable_T, + Microsoft_AspNetCore_Builder_AuthorizationEndpointConventionBuilderExtensions, + Microsoft_AspNetCore_Http_OpenApiRouteHandlerBuilderExtensions, + Microsoft_AspNetCore_Builder_CorsEndpointConventionBuilderExtensions, + Microsoft_Extensions_DependencyInjection_OutputCacheConventionBuilderExtensions, + Microsoft_AspNetCore_Builder_RateLimiterEndpointConventionBuilderExtensions, + Microsoft_AspNetCore_Builder_RoutingEndpointConventionBuilderExtensions, + Microsoft_AspNetCore_Mvc_RouteAttribute, + Microsoft_AspNetCore_Mvc_HttpDeleteAttribute, + Microsoft_AspNetCore_Mvc_HttpGetAttribute, + Microsoft_AspNetCore_Mvc_HttpHeadAttribute, + Microsoft_AspNetCore_Mvc_HttpOptionsAttribute, + Microsoft_AspNetCore_Mvc_HttpPatchAttribute, + Microsoft_AspNetCore_Mvc_HttpPostAttribute, + Microsoft_AspNetCore_Mvc_HttpPutAttribute, + Microsoft_AspNetCore_Http_EndpointDescriptionAttribute, + Microsoft_AspNetCore_Http_EndpointSummaryAttribute, + Microsoft_AspNetCore_Http_TagsAttribute, + Microsoft_AspNetCore_Routing_EndpointGroupNameAttribute, + Microsoft_AspNetCore_Routing_EndpointNameAttribute, + Microsoft_AspNetCore_Routing_ExcludeFromDescriptionAttribute, + Microsoft_AspNetCore_Cors_DisableCorsAttribute, + Microsoft_AspNetCore_Cors_EnableCorsAttribute, + Microsoft_AspNetCore_OutputCaching_OutputCacheAttribute, + Microsoft_AspNetCore_RateLimiting_DisableRateLimitingAttribute, + Microsoft_AspNetCore_RateLimiting_EnableRateLimitingAttribute, + Microsoft_AspNetCore_Mvc_ActionNameAttribute, + Microsoft_AspNetCore_Mvc_DisableRequestSizeLimitAttribute, + Microsoft_AspNetCore_Mvc_FormatFilterAttribute, + Microsoft_AspNetCore_Mvc_ProducesAttribute, + Microsoft_AspNetCore_Mvc_ProducesDefaultResponseTypeAttribute, + Microsoft_AspNetCore_Mvc_ProducesErrorResponseTypeAttribute, + Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute, + Microsoft_AspNetCore_Mvc_RequestFormLimitsAttribute, + Microsoft_AspNetCore_Mvc_RequestSizeLimitAttribute, + Microsoft_AspNetCore_Mvc_RequireHttpsAttribute, + Microsoft_AspNetCore_Mvc_ResponseCacheAttribute, + Microsoft_AspNetCore_Mvc_ServiceFilterAttribute, + Microsoft_AspNetCore_Mvc_TypeFilterAttribute, + Microsoft_AspNetCore_Mvc_ApiExplorer_ApiConventionNameMatchAttribute, + Microsoft_AspNetCore_Mvc_Filters_ResultFilterAttribute, + Microsoft_AspNetCore_Mvc_Infrastructure_DefaultStatusCodeAttribute, + Microsoft_AspNetCore_Mvc_AutoValidateAntiforgeryTokenAttribute, + Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute, + Microsoft_AspNetCore_Mvc_ViewFeatures_SaveTempDataAttribute, + Microsoft_AspNetCore_Mvc_SkipStatusCodePagesAttribute, + Microsoft_AspNetCore_Mvc_ValidateAntiForgeryTokenAttribute, + Microsoft_AspNetCore_Authorization_AllowAnonymousAttribute, + Microsoft_AspNetCore_Authorization_AuthorizeAttribute + } + + public static string[] WellKnownTypeNames = new[] + { + "Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder", + "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata", + "Microsoft.AspNetCore.Http.HeaderDictionaryExtensions", + "Microsoft.AspNetCore.Routing.IEndpointRouteBuilder", + "Microsoft.AspNetCore.Mvc.ControllerAttribute", + "Microsoft.AspNetCore.Mvc.NonControllerAttribute", + "Microsoft.AspNetCore.Mvc.NonActionAttribute", + "Microsoft.AspNetCore.Http.AsParametersAttribute", + "System.Threading.CancellationToken", + "Microsoft.AspNetCore.Http.HttpContext", + "Microsoft.AspNetCore.Http.HttpRequest", + "Microsoft.AspNetCore.Http.HttpResponse", + "System.Security.Claims.ClaimsPrincipal", + "Microsoft.AspNetCore.Http.IFormFileCollection", + "Microsoft.AspNetCore.Http.IFormFile", + "System.IO.Stream", + "System.IO.Pipelines.PipeReader", + "System.IFormatProvider", + "System.Uri", + "Microsoft.AspNetCore.Builder.ConfigureHostBuilder", + "Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder", + "Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions", + "Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions", + "Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions", + "Microsoft.Extensions.Hosting.HostingHostBuilderExtensions", + "Microsoft.AspNetCore.Builder.EndpointRoutingApplicationBuilderExtensions", + "Microsoft.AspNetCore.Builder.WebApplication", + "Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions", + "System.Delegate", + "Microsoft.AspNetCore.Mvc.ModelBinding.IBinderTypeProviderMetadata", + "Microsoft.AspNetCore.Mvc.BindAttribute", + "Microsoft.AspNetCore.Http.IResult", + "Microsoft.AspNetCore.Mvc.IActionResult", + "Microsoft.AspNetCore.Mvc.Infrastructure.IConvertToActionResult", + "Microsoft.AspNetCore.Http.RequestDelegate", + "System.Threading.Tasks.Task`1", + "System.Threading.Tasks.ValueTask`1", + "System.Reflection.ParameterInfo", + "Microsoft.AspNetCore.Http.IBindableFromHttpContext`1", + "System.IParsable`1", + "Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions", + "Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions", + "Microsoft.AspNetCore.Builder.CorsEndpointConventionBuilderExtensions", + "Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions", + "Microsoft.AspNetCore.Builder.RateLimiterEndpointConventionBuilderExtensions", + "Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions", + "Microsoft.AspNetCore.Mvc.RouteAttribute", + "Microsoft.AspNetCore.Mvc.HttpDeleteAttribute", + "Microsoft.AspNetCore.Mvc.HttpGetAttribute", + "Microsoft.AspNetCore.Mvc.HttpHeadAttribute", + "Microsoft.AspNetCore.Mvc.HttpOptionsAttribute", + "Microsoft.AspNetCore.Mvc.HttpPatchAttribute", + "Microsoft.AspNetCore.Mvc.HttpPostAttribute", + "Microsoft.AspNetCore.Mvc.HttpPutAttribute", + "Microsoft.AspNetCore.Http.EndpointDescriptionAttribute", + "Microsoft.AspNetCore.Http.EndpointSummaryAttribute", + "Microsoft.AspNetCore.Http.TagsAttribute", + "Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute", + "Microsoft.AspNetCore.Routing.EndpointNameAttribute", + "Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute", + "Microsoft.AspNetCore.Cors.DisableCorsAttribute", + "Microsoft.AspNetCore.Cors.EnableCorsAttribute", + "Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute", + "Microsoft.AspNetCore.RateLimiting.DisableRateLimitingAttribute", + "Microsoft.AspNetCore.RateLimiting.EnableRateLimitingAttribute", + "Microsoft.AspNetCore.Mvc.ActionNameAttribute", + "Microsoft.AspNetCore.Mvc.DisableRequestSizeLimitAttribute", + "Microsoft.AspNetCore.Mvc.FormatFilterAttribute", + "Microsoft.AspNetCore.Mvc.ProducesAttribute", + "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute", + "Microsoft.AspNetCore.Mvc.ProducesErrorResponseTypeAttribute", + "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute", + "Microsoft.AspNetCore.Mvc.RequestFormLimitsAttribute", + "Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute", + "Microsoft.AspNetCore.Mvc.RequireHttpsAttribute", + "Microsoft.AspNetCore.Mvc.ResponseCacheAttribute", + "Microsoft.AspNetCore.Mvc.ServiceFilterAttribute", + "Microsoft.AspNetCore.Mvc.TypeFilterAttribute", + "Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchAttribute", + "Microsoft.AspNetCore.Mvc.Filters.ResultFilterAttribute", + "Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute", + "Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute", + "Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute", + "Microsoft.AspNetCore.Mvc.ViewFeatures.SaveTempDataAttribute", + "Microsoft.AspNetCore.Mvc.SkipStatusCodePagesAttribute", + "Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute", + "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute", + "Microsoft.AspNetCore.Authorization.AuthorizeAttribute" + }; +} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs deleted file mode 100644 index efa48b32700b..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs +++ /dev/null @@ -1,314 +0,0 @@ -// 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.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using Microsoft.AspNetCore.Analyzers.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; - -internal enum WellKnownType -{ - Microsoft_AspNetCore_Components_Rendering_RenderTreeBuilder, - Microsoft_AspNetCore_Http_IHeaderDictionary, - Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata, - Microsoft_AspNetCore_Http_HeaderDictionaryExtensions, - Microsoft_AspNetCore_Routing_IEndpointRouteBuilder, - Microsoft_AspNetCore_Mvc_ControllerAttribute, - Microsoft_AspNetCore_Mvc_NonControllerAttribute, - Microsoft_AspNetCore_Mvc_NonActionAttribute, - Microsoft_AspNetCore_Http_AsParametersAttribute, - System_Threading_CancellationToken, - Microsoft_AspNetCore_Http_HttpContext, - Microsoft_AspNetCore_Http_HttpRequest, - Microsoft_AspNetCore_Http_HttpResponse, - System_Security_Claims_ClaimsPrincipal, - Microsoft_AspNetCore_Http_IFormFileCollection, - Microsoft_AspNetCore_Http_IFormFile, - System_IO_Stream, - System_IO_Pipelines_PipeReader, - System_IFormatProvider, - System_Uri, - Microsoft_AspNetCore_Builder_ConfigureHostBuilder, - Microsoft_AspNetCore_Builder_ConfigureWebHostBuilder, - Microsoft_Extensions_Hosting_GenericHostWebHostBuilderExtensions, - Microsoft_AspNetCore_Hosting_WebHostBuilderExtensions, - Microsoft_AspNetCore_Hosting_HostingAbstractionsWebHostBuilderExtensions, - Microsoft_Extensions_Hosting_HostingHostBuilderExtensions, - Microsoft_AspNetCore_Builder_EndpointRoutingApplicationBuilderExtensions, - Microsoft_AspNetCore_Builder_WebApplication, - Microsoft_AspNetCore_Builder_EndpointRouteBuilderExtensions, - System_Delegate, - Microsoft_AspNetCore_Mvc_ModelBinding_IBinderTypeProviderMetadata, - Microsoft_AspNetCore_Mvc_BindAttribute, - Microsoft_AspNetCore_Http_IResult, - Microsoft_AspNetCore_Mvc_IActionResult, - Microsoft_AspNetCore_Mvc_Infrastructure_IConvertToActionResult, - Microsoft_AspNetCore_Http_RequestDelegate, - System_Threading_Tasks_Task_T, - System_Threading_Tasks_ValueTask_T, - System_Reflection_ParameterInfo, - Microsoft_AspNetCore_Http_IBindableFromHttpContext_T, - System_IParsable_T, - Microsoft_AspNetCore_Builder_AuthorizationEndpointConventionBuilderExtensions, - Microsoft_AspNetCore_Http_OpenApiRouteHandlerBuilderExtensions, - Microsoft_AspNetCore_Builder_CorsEndpointConventionBuilderExtensions, - Microsoft_Extensions_DependencyInjection_OutputCacheConventionBuilderExtensions, - Microsoft_AspNetCore_Builder_RateLimiterEndpointConventionBuilderExtensions, - Microsoft_AspNetCore_Builder_RoutingEndpointConventionBuilderExtensions, - Microsoft_AspNetCore_Mvc_RouteAttribute, - Microsoft_AspNetCore_Mvc_HttpDeleteAttribute, - Microsoft_AspNetCore_Mvc_HttpGetAttribute, - Microsoft_AspNetCore_Mvc_HttpHeadAttribute, - Microsoft_AspNetCore_Mvc_HttpOptionsAttribute, - Microsoft_AspNetCore_Mvc_HttpPatchAttribute, - Microsoft_AspNetCore_Mvc_HttpPostAttribute, - Microsoft_AspNetCore_Mvc_HttpPutAttribute, - Microsoft_AspNetCore_Http_EndpointDescriptionAttribute, - Microsoft_AspNetCore_Http_EndpointSummaryAttribute, - Microsoft_AspNetCore_Http_TagsAttribute, - Microsoft_AspNetCore_Routing_EndpointGroupNameAttribute, - Microsoft_AspNetCore_Routing_EndpointNameAttribute, - Microsoft_AspNetCore_Routing_ExcludeFromDescriptionAttribute, - Microsoft_AspNetCore_Cors_DisableCorsAttribute, - Microsoft_AspNetCore_Cors_EnableCorsAttribute, - Microsoft_AspNetCore_OutputCaching_OutputCacheAttribute, - Microsoft_AspNetCore_RateLimiting_DisableRateLimitingAttribute, - Microsoft_AspNetCore_RateLimiting_EnableRateLimitingAttribute, - Microsoft_AspNetCore_Mvc_ActionNameAttribute, - Microsoft_AspNetCore_Mvc_DisableRequestSizeLimitAttribute, - Microsoft_AspNetCore_Mvc_FormatFilterAttribute, - Microsoft_AspNetCore_Mvc_ProducesAttribute, - Microsoft_AspNetCore_Mvc_ProducesDefaultResponseTypeAttribute, - Microsoft_AspNetCore_Mvc_ProducesErrorResponseTypeAttribute, - Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute, - Microsoft_AspNetCore_Mvc_RequestFormLimitsAttribute, - Microsoft_AspNetCore_Mvc_RequestSizeLimitAttribute, - Microsoft_AspNetCore_Mvc_RequireHttpsAttribute, - Microsoft_AspNetCore_Mvc_ResponseCacheAttribute, - Microsoft_AspNetCore_Mvc_ServiceFilterAttribute, - Microsoft_AspNetCore_Mvc_TypeFilterAttribute, - Microsoft_AspNetCore_Mvc_ApiExplorer_ApiConventionNameMatchAttribute, - Microsoft_AspNetCore_Mvc_Filters_ResultFilterAttribute, - Microsoft_AspNetCore_Mvc_Infrastructure_DefaultStatusCodeAttribute, - Microsoft_AspNetCore_Mvc_AutoValidateAntiforgeryTokenAttribute, - Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute, - Microsoft_AspNetCore_Mvc_ViewFeatures_SaveTempDataAttribute, - Microsoft_AspNetCore_Mvc_SkipStatusCodePagesAttribute, - Microsoft_AspNetCore_Mvc_ValidateAntiForgeryTokenAttribute, - Microsoft_AspNetCore_Authorization_AllowAnonymousAttribute, - Microsoft_AspNetCore_Authorization_AuthorizeAttribute -} - -internal sealed class WellKnownTypes -{ - private static readonly BoundedCacheWithFactory LazyWellKnownTypesCache = new(); - private static readonly string[] WellKnownTypeNames = new[] - { - "Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder", - "Microsoft.AspNetCore.Http.IHeaderDictionary", - "Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata", - "Microsoft.AspNetCore.Http.HeaderDictionaryExtensions", - "Microsoft.AspNetCore.Routing.IEndpointRouteBuilder", - "Microsoft.AspNetCore.Mvc.ControllerAttribute", - "Microsoft.AspNetCore.Mvc.NonControllerAttribute", - "Microsoft.AspNetCore.Mvc.NonActionAttribute", - "Microsoft.AspNetCore.Http.AsParametersAttribute", - "System.Threading.CancellationToken", - "Microsoft.AspNetCore.Http.HttpContext", - "Microsoft.AspNetCore.Http.HttpRequest", - "Microsoft.AspNetCore.Http.HttpResponse", - "System.Security.Claims.ClaimsPrincipal", - "Microsoft.AspNetCore.Http.IFormFileCollection", - "Microsoft.AspNetCore.Http.IFormFile", - "System.IO.Stream", - "System.IO.Pipelines.PipeReader", - "System.IFormatProvider", - "System.Uri", - "Microsoft.AspNetCore.Builder.ConfigureHostBuilder", - "Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder", - "Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions", - "Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions", - "Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions", - "Microsoft.Extensions.Hosting.HostingHostBuilderExtensions", - "Microsoft.AspNetCore.Builder.EndpointRoutingApplicationBuilderExtensions", - "Microsoft.AspNetCore.Builder.WebApplication", - "Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions", - "System.Delegate", - "Microsoft.AspNetCore.Mvc.ModelBinding.IBinderTypeProviderMetadata", - "Microsoft.AspNetCore.Mvc.BindAttribute", - "Microsoft.AspNetCore.Http.IResult", - "Microsoft.AspNetCore.Mvc.IActionResult", - "Microsoft.AspNetCore.Mvc.Infrastructure.IConvertToActionResult", - "Microsoft.AspNetCore.Http.RequestDelegate", - "System.Threading.Tasks.Task`1", - "System.Threading.Tasks.ValueTask`1", - "System.Reflection.ParameterInfo", - "Microsoft.AspNetCore.Http.IBindableFromHttpContext`1", - "System.IParsable`1", - "Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions", - "Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions", - "Microsoft.AspNetCore.Builder.CorsEndpointConventionBuilderExtensions", - "Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions", - "Microsoft.AspNetCore.Builder.RateLimiterEndpointConventionBuilderExtensions", - "Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions", - "Microsoft.AspNetCore.Mvc.RouteAttribute", - "Microsoft.AspNetCore.Mvc.HttpDeleteAttribute", - "Microsoft.AspNetCore.Mvc.HttpGetAttribute", - "Microsoft.AspNetCore.Mvc.HttpHeadAttribute", - "Microsoft.AspNetCore.Mvc.HttpOptionsAttribute", - "Microsoft.AspNetCore.Mvc.HttpPatchAttribute", - "Microsoft.AspNetCore.Mvc.HttpPostAttribute", - "Microsoft.AspNetCore.Mvc.HttpPutAttribute", - "Microsoft.AspNetCore.Http.EndpointDescriptionAttribute", - "Microsoft.AspNetCore.Http.EndpointSummaryAttribute", - "Microsoft.AspNetCore.Http.TagsAttribute", - "Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute", - "Microsoft.AspNetCore.Routing.EndpointNameAttribute", - "Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute", - "Microsoft.AspNetCore.Cors.DisableCorsAttribute", - "Microsoft.AspNetCore.Cors.EnableCorsAttribute", - "Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute", - "Microsoft.AspNetCore.RateLimiting.DisableRateLimitingAttribute", - "Microsoft.AspNetCore.RateLimiting.EnableRateLimitingAttribute", - "Microsoft.AspNetCore.Mvc.ActionNameAttribute", - "Microsoft.AspNetCore.Mvc.DisableRequestSizeLimitAttribute", - "Microsoft.AspNetCore.Mvc.FormatFilterAttribute", - "Microsoft.AspNetCore.Mvc.ProducesAttribute", - "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute", - "Microsoft.AspNetCore.Mvc.ProducesErrorResponseTypeAttribute", - "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute", - "Microsoft.AspNetCore.Mvc.RequestFormLimitsAttribute", - "Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute", - "Microsoft.AspNetCore.Mvc.RequireHttpsAttribute", - "Microsoft.AspNetCore.Mvc.ResponseCacheAttribute", - "Microsoft.AspNetCore.Mvc.ServiceFilterAttribute", - "Microsoft.AspNetCore.Mvc.TypeFilterAttribute", - "Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchAttribute", - "Microsoft.AspNetCore.Mvc.Filters.ResultFilterAttribute", - "Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute", - "Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute", - "Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute", - "Microsoft.AspNetCore.Mvc.ViewFeatures.SaveTempDataAttribute", - "Microsoft.AspNetCore.Mvc.SkipStatusCodePagesAttribute", - "Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute", - "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute", - "Microsoft.AspNetCore.Authorization.AuthorizeAttribute" - }; - - public static WellKnownTypes GetOrCreate(Compilation compilation) => - LazyWellKnownTypesCache.GetOrCreateValue(compilation, static c => new WellKnownTypes(c)); - - private readonly INamedTypeSymbol?[] _lazyWellKnownTypes; - private readonly Compilation _compilation; - - static WellKnownTypes() - { - AssertEnumAndTableInSync(); - } - - [Conditional("DEBUG")] - private static void AssertEnumAndTableInSync() - { - for (var i = 0; i < WellKnownTypeNames.Length; i++) - { - var name = WellKnownTypeNames[i]; - var typeId = (WellKnownType)i; - - var typeIdName = typeId.ToString().Replace("__", "+").Replace('_', '.'); - - var separator = name.IndexOf('`'); - if (separator >= 0) - { - // Ignore type parameter qualifier for generic types. - name = name.Substring(0, separator); - typeIdName = typeIdName.Substring(0, separator); - } - - Debug.Assert(name == typeIdName, $"Enum name ({typeIdName}) and type name ({name}) must match at {i}"); - } - } - - private WellKnownTypes(Compilation compilation) - { - _lazyWellKnownTypes = new INamedTypeSymbol?[WellKnownTypeNames.Length]; - _compilation = compilation; - } - - public INamedTypeSymbol Get(SpecialType type) - { - return _compilation.GetSpecialType(type); - } - - public INamedTypeSymbol Get(WellKnownType type) - { - var index = (int)type; - var symbol = _lazyWellKnownTypes[index]; - if (symbol is not null) - { - return symbol; - } - - // Symbol hasn't been added to the cache yet. - // Resolve symbol from name, cache, and return. - return GetAndCache(index); - } - - private INamedTypeSymbol GetAndCache(int index) - { - var result = _compilation.GetTypeByMetadataName(WellKnownTypeNames[index]); - if (result == null) - { - throw new InvalidOperationException($"Failed to resolve well-known type '{WellKnownTypeNames[index]}'."); - } - Interlocked.CompareExchange(ref _lazyWellKnownTypes[index], result, null); - - // GetTypeByMetadataName should always return the same instance for a name. - // To ensure we have a consistent value, for thread safety, return symbol set in the array. - return _lazyWellKnownTypes[index]!; - } - - public bool IsType(ITypeSymbol type, WellKnownType[] wellKnownTypes) => IsType(type, wellKnownTypes, out var _); - - public bool IsType(ITypeSymbol type, WellKnownType[] wellKnownTypes, [NotNullWhen(true)] out WellKnownType? match) - { - foreach (var wellKnownType in wellKnownTypes) - { - if (SymbolEqualityComparer.Default.Equals(type, Get(wellKnownType))) - { - match = wellKnownType; - return true; - } - } - - match = null; - return false; - } - - public bool Implements(ITypeSymbol type, WellKnownType[] interfaceWellKnownTypes) - { - foreach (var wellKnownType in interfaceWellKnownTypes) - { - if (type.Implements(Get(wellKnownType))) - { - return true; - } - } - - return false; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj index 1481d546fbf6..5e6cbf836ff8 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj @@ -23,6 +23,8 @@ + + diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs index d16e9eff140e..37916d9514c2 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Analyzers.Mvc; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class MvcAnalyzer { private static void DetectAmbiguousActionRoutes(SymbolAnalysisContext context, WellKnownTypes wellKnownTypes, List actionRoutes) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs index 6a94f4480f73..e460f90f4686 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Analyzers.Mvc; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class MvcAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs index d09f4f6c0551..76c41aa8f066 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs @@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Analyzers.RenderTreeBuilder; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class RenderTreeBuilderAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs index 50d074b77c84..708c6f5baf4d 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs @@ -27,6 +27,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; +using WellKnownType = WellKnownTypeData.WellKnownType; + [ExportCompletionProvider(nameof(RoutePatternCompletionProvider), LanguageNames.CSharp)] [Shared] public sealed class FrameworkParametersCompletionProvider : CompletionProvider diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs index a4b661ee8b9a..1f9b1eb041c6 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class MvcDetector { public static bool IsController(INamedTypeSymbol? typeSymbol, WellKnownTypes wellKnownTypes) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs index dabd6b18c90e..dfabd214ccbc 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class RoutePatternParametersDetector { public static ImmutableArray ResolvedParameters(ISymbol symbol, WellKnownTypes wellKnownTypes) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs index 8e7be9d99f16..e5f47a84c0ca 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal enum RouteUsageType { Other, diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs index d070903f5bad..d11517ecacc1 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs @@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class RouteWellKnownTypes { // Cache well known type keys rather than symbol instances. diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs index f76cf3fedd0d..bbab616e7f7e 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs @@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DetectAmbiguousRoutes(in OperationBlockAnalysisContext context, WellKnownTypes wellKnownTypes, ConcurrentDictionary mapOperations) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs index b6a5fa10e287..ab8fc9d4c760 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DisallowMvcBindArgumentsOnParameters( diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs index 0e90cbc31b45..6f6d8c8cb71f 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DisallowNonParsableComplexTypesOnParameters( diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs index 96fe35f2cfd0..4cdc651d291d 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DisallowReturningActionResultFromMapMethods( diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs index a763446ee7b0..18eba86d37dc 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { @@ -218,7 +220,7 @@ private record struct MapOperation(IOperation? Builder, IInvocationOperation Ope public static MapOperation Create(IInvocationOperation operation, RouteUsageModel routeUsageModel) { IOperation? builder = null; - + var builderArgument = operation.Arguments.SingleOrDefault(a => a.Parameter?.Ordinal == 0); if (builderArgument != null) { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs index 1f296aa9cf46..95f596817c4a 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class WebApplicationBuilderAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs index b21282c6c5eb..3d11de0e1f97 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Analyzers.Http.Fixers; +using WellKnownType = WellKnownTypeData.WellKnownType; + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] public sealed class HeaderDictionaryAddFixer : CodeFixProvider { diff --git a/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs b/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs index 6e515b67fc37..8c02e10752d8 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class WellKnownTypesTests { private TestDiagnosticAnalyzerRunner Runner { get; } = new(new TestAnalyzer()); diff --git a/src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs b/src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs new file mode 100644 index 000000000000..d6a54e65fd00 --- /dev/null +++ b/src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Http.Generators; + +internal static class DiagnosticDescriptors +{ + public static DiagnosticDescriptor UnableToResolveRoutePattern { get; } = new( + "RDG001", + new LocalizableResourceString(nameof(Resources.UnableToResolveRoutePattern_Title), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.UnableToResolveRoutePattern_Message), Resources.ResourceManager, typeof(Resources)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static DiagnosticDescriptor UnableToResolveMethod { get; } = new( + "RDG002", + new LocalizableResourceString(nameof(Resources.UnableToResolveMethod_Title), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.UnableToResolveMethod_Message), Resources.ResourceManager, typeof(Resources)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); +} diff --git a/src/Http/Http.Extensions/gen/GeneratorSteps.cs b/src/Http/Http.Extensions/gen/GeneratorSteps.cs new file mode 100644 index 000000000000..d007c5e11f69 --- /dev/null +++ b/src/Http/Http.Extensions/gen/GeneratorSteps.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +namespace Microsoft.AspNetCore.Http.Generators; + +internal class GeneratorSteps +{ + internal const string EndpointsStep = "EndpointModel"; + internal const string EndpointsWithoutDiagnosicsStep = "EndpointsWithoutDiagnostics"; +} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj index 171d742689ab..19e93a74d651 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj @@ -20,9 +20,13 @@ - + + + + + diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 529e7e6f4b4d..89dcd551591d 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Text; +using Microsoft.AspNetCore.Analyzers.Infrastructure; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; @@ -24,7 +26,7 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { - var endpoints = context.SyntaxProvider.CreateSyntaxProvider( + var endpointsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider( predicate: (node, _) => node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax @@ -39,83 +41,141 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: (context, token) => { var operation = context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation; - return StaticRouteHandlerModelParser.GetEndpointFromOperation(operation); + var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation); + return new Endpoint(operation, wellKnownTypes); }) - .Where(endpoint => endpoint.Response.ResponseType == "string") - .WithTrackingName("EndpointModel"); + .WithTrackingName(GeneratorSteps.EndpointsStep); - var thunks = endpoints.Select((endpoint, _) => $$""" -[{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( - (methodInfo, options) => + context.RegisterSourceOutput(endpointsWithDiagnostics, (context, endpoint) => + { + var (filePath, _) = endpoint.Location; + foreach (var diagnostic in endpoint.Diagnostics) { - if (options == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } - options.EndpointBuilder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); - return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; - }, - (del, options, inferredMetadataResult) => + context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath)); + } + foreach (var diagnostic in endpoint.Response.Diagnostics) { - var handler = ({{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}})del; - EndpointFilterDelegate? filteredInvocation = null; + context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath)); + } + foreach (var diagnostic in endpoint.Route.Diagnostics) + { + context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath)); + } + }); - if (options.EndpointBuilder.FilterFactories.Count > 0) + var endpoints = endpointsWithDiagnostics + .Where(endpoint => endpoint.Diagnostics.Count == 0 && + endpoint.Response.Diagnostics.Count == 0 && + endpoint.Route.Diagnostics.Count == 0) + .WithTrackingName(GeneratorSteps.EndpointsWithoutDiagnosicsStep); + + var thunks = endpoints.Select((endpoint, _) => $$""" + [{{endpoint.EmitSourceKey()}}] = ( + (methodInfo, options) => + { + if (options == null || options.EndpointBuilder == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey{{endpoint.EmitSourceKey()}}); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => { - filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + var handler = ({{endpoint.EmitHandlerDelegateType()}})del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) { - if (ic.HttpContext.Response.StatusCode == 400) + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => { - return ValueTask.FromResult(Results.Empty); - } - {{StaticRouteHandlerModelEmitter.EmitFilteredInvocation()}} - }, - options.EndpointBuilder, - handler.Method); - } + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } +{{endpoint.EmitFilteredInvocation()}} + }, + options.EndpointBuilder, + handler.Method); + } - {{StaticRouteHandlerModelEmitter.EmitRequestHandler()}} - {{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} +{{endpoint.EmitRequestHandler()}} +{{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} - RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; - var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; - return new RequestDelegateResult(targetDelegate, metadata); - }), + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), """); - var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => $$""" - internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( - this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, - [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, - global::{{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, - [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", - [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) - { - return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, {{StaticRouteHandlerModelEmitter.EmitVerb(endpoint)}}, filePath, lineNumber); - } -"""); + var stronglyTypedEndpointDefinitions = endpoints + .Collect() + .Select((endpoints, _) => + { + var dedupedByDelegate = endpoints.Distinct(new LambdaComparer((a, b) => + { + if (a.Response.IsAwaitable == b.Response.IsAwaitable && + a.Response.IsVoid == b.Response.IsVoid && + SymbolEqualityComparer.Default.Equals(a.Response.ResponseType, b.Response.ResponseType) && + a.HttpMethod == b.HttpMethod) + { + return 0; + } + return -1; + }, (endpoint) => + { + unchecked + { + var hashCode = SymbolEqualityComparer.Default.GetHashCode(endpoint.Response.ResponseType); + hashCode = (hashCode * 397) ^ endpoint.Response.IsAwaitable.GetHashCode(); + hashCode = (hashCode * 397) ^ endpoint.Response.IsVoid.GetHashCode(); + hashCode = (hashCode * 397) ^ endpoint.HttpMethod.GetHashCode(); + return hashCode; + } + })); + var code = new CodeWriter(new StringBuilder()); + code.Indent(2); + foreach (var endpoint in dedupedByDelegate) + { + code.WriteLine($"internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {endpoint.HttpMethod}("); + code.Indent(); + code.WriteLine("this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,"); + code.WriteLine(@"[global::System.Diagnostics.CodeAnalysis.StringSyntax(""Route"")] string pattern,"); + code.WriteLine($"global::{endpoint.EmitHandlerDelegateType()} handler,"); + code.WriteLine(@"[global::System.Runtime.CompilerServices.CallerFilePath] string filePath = """","); + code.WriteLine("[global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)"); + code.Unindent(); + code.StartBlock(); + code.WriteLine($"return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, {endpoint.EmitVerb()}, filePath, lineNumber);"); + code.EndBlock(); + } - var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect()); + return code.ToString(); + }); + + var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions); context.RegisterSourceOutput(thunksAndEndpoints, (context, sources) => { - var (thunks, endpoints) = sources; + var (thunks, endpointsCode) = sources; - var endpointsCode = new StringBuilder(); - var thunksCode = new StringBuilder(); - foreach (var endpoint in endpoints) + if (thunks.IsDefaultOrEmpty || string.IsNullOrEmpty(endpointsCode)) { - endpointsCode.AppendLine(endpoint); + return; } + + var thunksCode = new CodeWriter(new StringBuilder()); + foreach (var thunk in thunks) { - thunksCode.AppendLine(thunk); + thunksCode.WriteLine(thunk); } var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource( genericThunks: string.Empty, thunks: thunksCode.ToString(), - endpoints: endpointsCode.ToString()); + endpoints: endpointsCode); + context.AddSource("GeneratedRouteBuilderExtensions.g.cs", code); }); } diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index 6cb436e4d9e9..0183e9d0b9f6 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -85,16 +85,6 @@ private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate return filteredInvocation; } - private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider - { - T.PopulateMetadata(method, builder); - } - - private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameter, builder); - } - private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) { if (obj is IResult r) @@ -115,10 +105,10 @@ private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) """; private static string GetGenericThunks(string genericThunks) => genericThunks != string.Empty ? $$""" private static class GenericThunks + { + public static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { - public static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { - {{genericThunks}} + {{genericThunks}} }; } @@ -138,7 +128,7 @@ internal static RouteHandlerBuilder MapCore( private static string GetThunks(string thunks) => thunks != string.Empty ? $$""" private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { - {{thunks}} +{{thunks}} }; internal static RouteHandlerBuilder MapCore( diff --git a/src/Http/Http.Extensions/gen/Resources.resx b/src/Http/Http.Extensions/gen/Resources.resx new file mode 100644 index 000000000000..6880e701b101 --- /dev/null +++ b/src/Http/Http.Extensions/gen/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unable to resolve route pattern + + + Unable to statically resolve route pattern for endpoint. Compile-time endpoint generation will skip this endpoint. + + + Unable to resolve endpoint handler + + + Unable to statically resolve endpoint handler method. Only method groups, lambda expressions or readonly fields/variables are allowed. Compile-time endpoint generation will skip this endpoint. + + diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs new file mode 100644 index 000000000000..3761ed895147 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs @@ -0,0 +1,82 @@ +// 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 Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +internal class Endpoint +{ + public string HttpMethod { get; } + public EndpointRoute Route { get; } + public EndpointResponse Response { get; } + public List Diagnostics { get; } = new List(); + + public (string, int) Location { get; } + public IInvocationOperation Operation { get; } + + private WellKnownTypes WellKnownTypes { get; } + + public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes) + { + Operation = operation; + WellKnownTypes = wellKnownTypes; + Location = GetLocation(); + HttpMethod = GetHttpMethod(); + Response = new EndpointResponse(Operation, wellKnownTypes); + Route = new EndpointRoute(Operation); + } + + private (string, int) GetLocation() + { + var filePath = Operation.Syntax.SyntaxTree.FilePath; + var span = Operation.Syntax.SyntaxTree.GetLineSpan(Operation.Syntax.Span); + var lineNumber = span.EndLinePosition.Line + 1; + return (filePath, lineNumber); + } + + private string GetHttpMethod() + { + var syntax = (InvocationExpressionSyntax)Operation.Syntax; + var expression = (MemberAccessExpressionSyntax)syntax.Expression; + var name = (IdentifierNameSyntax)expression.Name; + var identifier = name.Identifier; + return identifier.ValueText; + } + + public override bool Equals(object o) + { + if (o is null) + { + return false; + } + + if (o is Endpoint endpoint) + { + return endpoint.HttpMethod.Equals(HttpMethod, StringComparison.OrdinalIgnoreCase) || + endpoint.Location.Item1.Equals(Location.Item1, StringComparison.OrdinalIgnoreCase) || + endpoint.Location.Item2.Equals(Location.Item2) || + endpoint.Response.Equals(Response); + } + + return false; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = HttpMethod.GetHashCode(); + hashCode = (hashCode * 397) ^ Route.GetHashCode(); + hashCode = (hashCode * 397) ^ Response.GetHashCode(); + hashCode = (hashCode * 397) ^ Diagnostics.GetHashCode(); + hashCode = (hashCode * 397) ^ Location.GetHashCode(); + hashCode = (hashCode * 397) ^ Operation.GetHashCode(); + return hashCode; + } + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs new file mode 100644 index 000000000000..f42447842a5a --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs @@ -0,0 +1,136 @@ +// 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.Linq; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +using WellKnownType = WellKnownTypeData.WellKnownType; + +public class EndpointResponse +{ + public ITypeSymbol ResponseType { get; set; } + public string WrappedResponseType { get; set; } + public string ContentType { get; set; } + public bool IsAwaitable { get; set; } + public bool IsVoid { get; set; } + public bool IsIResult { get; set; } + + private WellKnownTypes WellKnownTypes { get; init; } + + public List Diagnostics { get; init; } = new List(); + + internal EndpointResponse(IInvocationOperation operation, WellKnownTypes wellKnownTypes) + { + if (!operation.TryGetRouteHandlerMethod(out var method)) + { + Diagnostics.Add(DiagnosticDescriptors.UnableToResolveMethod); + return; + } + + WellKnownTypes = wellKnownTypes; + ResponseType = UnwrapResponseType(method); + WrappedResponseType = method.ReturnType.ToString(); + IsAwaitable = GetIsAwaitable(method); + IsVoid = method.ReturnsVoid; + IsIResult = GetIsIResult(); + ContentType = GetContentType(method); + } + + private ITypeSymbol UnwrapResponseType(IMethodSymbol method) + { + var returnType = method.ReturnType; + var task = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_Task); + var taskOfT = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_Task_T); + var valueTask = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_ValueTask); + var valueTaskOfT = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_ValueTask_T); + if (returnType.OriginalDefinition.Equals(taskOfT, SymbolEqualityComparer.Default) || + returnType.OriginalDefinition.Equals(valueTaskOfT, SymbolEqualityComparer.Default)) + { + return ((INamedTypeSymbol)returnType).TypeArguments[0]; + } + + if (returnType.OriginalDefinition.Equals(task, SymbolEqualityComparer.Default) || + returnType.OriginalDefinition.Equals(valueTask, SymbolEqualityComparer.Default)) + { + return null; + } + + return returnType; + } + + private bool GetIsIResult() + { + var resultType = WellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IResult); + return WellKnownTypes.Implements(ResponseType, resultType) || + SymbolEqualityComparer.Default.Equals(ResponseType, resultType); + } + + private static bool GetIsAwaitable(IMethodSymbol method) + { + var potentialGetAwaiters = method.ReturnType.OriginalDefinition.GetMembers(WellKnownMemberNames.GetAwaiter); + var getAwaiters = potentialGetAwaiters.OfType().Where(x => !x.Parameters.Any()); + return getAwaiters.Any(symbol => symbol.Name == WellKnownMemberNames.GetAwaiter && VerifyGetAwaiter(symbol)); + + static bool VerifyGetAwaiter(IMethodSymbol getAwaiter) + { + var returnType = getAwaiter.ReturnType; + + // bool IsCompleted { get } + if (!returnType.GetMembers() + .OfType() + .Any(p => p.Name == WellKnownMemberNames.IsCompleted && + p.Type.SpecialType == SpecialType.System_Boolean && p.GetMethod != null)) + { + return false; + } + + var methods = returnType.GetMembers().OfType(); + + if (!methods.Any(x => x.Name == WellKnownMemberNames.OnCompleted && + x.ReturnsVoid && + x.Parameters.Length == 1 && + x.Parameters.First().Type.TypeKind == TypeKind.Delegate)) + { + return false; + } + + // void GetResult() || T GetResult() + return methods.Any(m => m.Name == WellKnownMemberNames.GetResult && !m.Parameters.Any()); + } + } + + private string? GetContentType(IMethodSymbol method) + { + // `void` returning methods do not have a Content-Type. + // We don't have a strategy for resolving a Content-Type + // from an IResult. Typically, this would be done via an + // IEndpointMetadataProvider so we don't need to set a + // Content-Type here. + if (method.ReturnsVoid || IsIResult) + { + return null; + } + return method.ReturnType.SpecialType is SpecialType.System_String ? "text/plain" : "application/json"; + } + + public override bool Equals(object obj) + { + return obj is EndpointResponse otherEndpointResponse && + SymbolEqualityComparer.Default.Equals(otherEndpointResponse.ResponseType, ResponseType) && + otherEndpointResponse.IsAwaitable == IsAwaitable && + otherEndpointResponse.IsVoid == IsVoid && + otherEndpointResponse.IsIResult == IsIResult && + otherEndpointResponse.ContentType.Equals(ContentType, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointRoute.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointRoute.cs new file mode 100644 index 000000000000..66de4f9d88f6 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointRoute.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +public class EndpointRoute +{ + private const int RoutePatternArgumentOrdinal = 1; + + public string RoutePattern { get; init; } + + public List Diagnostics { get; init; } = new List(); + + public EndpointRoute(IInvocationOperation operation) + { + if (!TryGetRouteHandlerPattern(operation, out var routeToken)) + { + Diagnostics.Add(DiagnosticDescriptors.UnableToResolveRoutePattern); + } + + RoutePattern = routeToken.ValueText; + } + + private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, out SyntaxToken token) + { + IArgumentOperation? argumentOperation = null; + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal) + { + argumentOperation = argument; + } + } + if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax || + routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax) + { + token = default; + return false; + } + token = routePatternArgumentLiteralSyntax.Token; + return true; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs new file mode 100644 index 000000000000..6a0ee6783a49 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +public static class InvocationOperationExtensions +{ + private const int RouteHandlerArgumentOrdinal = 2; + + public static bool TryGetRouteHandlerMethod(this IInvocationOperation invocation, out IMethodSymbol method) + { + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal) + { + method = ResolveMethodFromOperation(argument); + return true; + } + } + method = null; + return false; + } + + private static IMethodSymbol ResolveMethodFromOperation(IOperation operation) => operation switch + { + IArgumentOperation argument => ResolveMethodFromOperation(argument.Value), + IConversionOperation conv => ResolveMethodFromOperation(conv.Operand), + IDelegateCreationOperation del => ResolveMethodFromOperation(del.Target), + IFieldReferenceOperation { Field.IsReadOnly: true } f when ResolveDeclarationOperation(f.Field, operation.SemanticModel) is IOperation op => + ResolveMethodFromOperation(op), + IAnonymousFunctionOperation anon => anon.Symbol, + ILocalFunctionOperation local => local.Symbol, + IMethodReferenceOperation method => method.Method, + IParenthesizedOperation parenthesized => ResolveMethodFromOperation(parenthesized.Operand), + _ => null + }; + + private static IOperation ResolveDeclarationOperation(ISymbol symbol, SemanticModel semanticModel) + { + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + var syn = syntaxReference.GetSyntax(); + + if (syn is VariableDeclaratorSyntax + { + Initializer: + { + Value: var expr + } + }) + { + // Use the correct semantic model based on the syntax tree + var operation = semanticModel.GetOperation(expr); + + if (operation is not null) + { + return operation; + } + } + } + + return null; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 996cec75e731..864f7f653a74 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Text; +using Microsoft.AspNetCore.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; @@ -20,17 +23,25 @@ internal static class StaticRouteHandlerModelEmitter * that do return a value, `System.Func` will * be emitted to indicate a `string`return type. */ - public static string EmitHandlerDelegateType(Endpoint endpoint) + public static string EmitHandlerDelegateType(this Endpoint endpoint) { + if (endpoint.Response.IsVoid) + { + return $"System.Action"; + } + if (endpoint.Response.IsAwaitable) + { + return $"System.Func<{endpoint.Response.WrappedResponseType}>"; + } return $"System.Func<{endpoint.Response.ResponseType}>"; } - public static string EmitSourceKey(Endpoint endpoint) + public static string EmitSourceKey(this Endpoint endpoint) { return $@"(@""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; } - public static string EmitVerb(Endpoint endpoint) + public static string EmitVerb(this Endpoint endpoint) { return endpoint.HttpMethod switch { @@ -49,15 +60,65 @@ public static string EmitVerb(Endpoint endpoint) * their validity (optionality), invoke the underlying handler with * the arguments bound from HTTP context, and write out the response. */ - public static string EmitRequestHandler() + public static string EmitRequestHandler(this Endpoint endpoint) { - return """ -Task RequestHandler(HttpContext httpContext) - { - var result = handler(); - return httpContext.Response.WriteAsync(result); - } -"""; + var code = new CodeWriter(new StringBuilder()); + code.Indent(5); + code.WriteLine(endpoint.Response.IsAwaitable + ? "async Task RequestHandler(HttpContext httpContext)" + : "Task RequestHandler(HttpContext httpContext)"); + code.StartBlock(); + + if (endpoint.Response.IsVoid) + { + code.WriteLine("handler();"); + code.WriteLine("return Task.CompletedTask;"); + } + else + { + code.WriteLine($"""httpContext.Response.ContentType ??= "{endpoint.Response.ContentType}";"""); + 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.EndBlock(); + return code.ToString(); + } + + private static string EmitResponseWritingCall(this Endpoint endpoint) + { + var code = new CodeWriter(new StringBuilder()); + code.WriteNoIndent(endpoint.Response.IsAwaitable ? "await " : "return "); + + if (endpoint.Response.IsIResult) + { + code.WriteNoIndent("result.ExecuteAsync(httpContext);"); + } + else if (endpoint.Response.ResponseType.SpecialType == SpecialType.System_String) + { + code.WriteNoIndent("httpContext.Response.WriteAsync(result);"); + } + else if (endpoint.Response.ResponseType.SpecialType == SpecialType.System_Object) + { + code.WriteNoIndent("GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);"); + } + else if (!endpoint.Response.IsVoid) + { + code.WriteNoIndent("httpContext.Response.WriteAsJsonAsync(result);"); + } + else if (!endpoint.Response.IsAwaitable && endpoint.Response.IsVoid) + { + code.WriteNoIndent("Task.CompletedTask;"); + } + + return code.ToString(); } /* @@ -69,13 +130,14 @@ Task RequestHandler(HttpContext httpContext) */ public static string EmitFilteredRequestHandler() { - return """ -async Task RequestHandlerFiltered(HttpContext httpContext) - { - var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); - await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); - } -"""; + var code = new CodeWriter(new StringBuilder()); + code.Indent(5); + code.WriteLine("async Task RequestHandlerFiltered(HttpContext httpContext)"); + code.StartBlock(); + code.WriteLine("var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext));"); + code.WriteLine("await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);"); + code.EndBlock(); + return code.ToString(); } /* @@ -83,7 +145,7 @@ async Task RequestHandlerFiltered(HttpContext httpContext) * the appropriate arguments processed via the parameter binding. * * ``` - * return System.Threading.Tasks.ValueTask.FromResult(handler(name, age)); + * return ValueTask.FromResult(handler(name, age)); * ``` * * If the handler returns void, it will be invoked and an `EmptyHttpResult` @@ -91,11 +153,23 @@ async Task RequestHandlerFiltered(HttpContext httpContext) * * ``` * handler(name, age); - * return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + * return ValueTask.FromResult(Results.Empty); * ``` */ - public static string EmitFilteredInvocation() + public static string EmitFilteredInvocation(this Endpoint endpoint) { - return "return ValueTask.FromResult(handler());"; + var code = new CodeWriter(new StringBuilder()); + code.Indent(7); + if (endpoint.Response.IsVoid) + { + code.WriteLine("handler();"); + code.WriteLine("return ValueTask.FromResult(Results.Empty);"); + } + else + { + code.WriteLine("return ValueTask.FromResult(handler());"); + } + + return code.ToString(); } } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs deleted file mode 100644 index b7a53d418fca..000000000000 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Operations; - -namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; - -internal static class StaticRouteHandlerModelParser -{ - private const int RoutePatternArgumentOrdinal = 1; - private const int RouteHandlerArgumentOrdinal = 2; - - private static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern) - { - return new EndpointRoute(routePattern.ValueText); - } - - private static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method) - { - return new EndpointResponse(method.ReturnType.ToString(), "plain/text"); - } - - public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) - { - if (!TryGetRouteHandlerPattern(operation, out var routeToken)) - { - return null; - } - if (!TryGetRouteHandlerMethod(operation, out var method)) - { - return null; - } - var filePath = operation.Syntax.SyntaxTree.FilePath; - var span = operation.Syntax.SyntaxTree.GetLineSpan(operation.Syntax.Span); - - var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; - var httpMethod = ((IdentifierNameSyntax)((MemberAccessExpressionSyntax)invocationExpression.Expression).Name).Identifier.ValueText; - - return new Endpoint(httpMethod, - GetEndpointRouteFromArgument(routeToken), - GetEndpointResponseFromMethod(method), - (filePath, span.EndLinePosition.Line + 1)); - } - - private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, out SyntaxToken token) - { - IArgumentOperation? argumentOperation = null; - foreach (var argument in invocation.Arguments) - { - if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal) - { - argumentOperation = argument; - } - } - if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax || - routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax) - { - token = default; - return false; - } - token = routePatternArgumentLiteralSyntax.Token; - return true; - } - - private static bool TryGetRouteHandlerMethod(IInvocationOperation invocation, out IMethodSymbol method) - { - foreach (var argument in invocation.Arguments) - { - if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal) - { - method = ResolveMethodFromOperation(argument); - return true; - } - } - method = null; - return false; - } - - private static IMethodSymbol ResolveMethodFromOperation(IOperation operation) => operation switch - { - IArgumentOperation argument => ResolveMethodFromOperation(argument.Value), - IConversionOperation conv => ResolveMethodFromOperation(conv.Operand), - IDelegateCreationOperation del => ResolveMethodFromOperation(del.Target), - IFieldReferenceOperation { Field.IsReadOnly: true } f when ResolveDeclarationOperation(f.Field, operation.SemanticModel) is IOperation op => - ResolveMethodFromOperation(op), - IAnonymousFunctionOperation anon => anon.Symbol, - ILocalFunctionOperation local => local.Symbol, - IMethodReferenceOperation method => method.Method, - IParenthesizedOperation parenthesized => ResolveMethodFromOperation(parenthesized.Operand), - _ => null - }; - - private static IOperation ResolveDeclarationOperation(ISymbol symbol, SemanticModel semanticModel) - { - foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) - { - var syn = syntaxReference.GetSyntax(); - - if (syn is VariableDeclaratorSyntax - { - Initializer: - { - Value: var expr - } - }) - { - // Use the correct semantic model based on the syntax tree - var operation = semanticModel.GetOperation(expr); - - if (operation is not null) - { - return operation; - } - } - } - - return null; - } -} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs deleted file mode 100644 index fb3a60c63bbf..000000000000 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; - -internal enum RequestParameterSource -{ - Query, - Route, - Header, - Form, - Service, - BodyOrService, -} - -internal record RequestParameter(string Name, string Type, RequestParameterSource Source, bool IsOptional, object? DefaultValue); -internal record EndpointRoute(string RoutePattern); -internal record EndpointResponse(string ResponseType, string ContentType); -internal record Endpoint(string HttpMethod, EndpointRoute Route, EndpointResponse Response, (string, int) Location); diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs new file mode 100644 index 000000000000..d889acca30f0 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; + +internal static class WellKnownTypeData +{ + public enum WellKnownType + { + Microsoft_AspNetCore_Http_IResult, + System_Threading_Tasks_Task, + System_Threading_Tasks_Task_T, + System_Threading_Tasks_ValueTask, + System_Threading_Tasks_ValueTask_T + } + + public static string[] WellKnownTypeNames = new[] + { + "Microsoft.AspNetCore.Http.IResult", + "System.Threading.Tasks.Task", + "System.Threading.Tasks.Task`1", + "System.Threading.Tasks.ValueTask", + "System.Threading.Tasks.ValueTask`1" + }; +} diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index 95942fd9ed66..42b8310af5da 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -20,11 +20,11 @@ - + - + PreserveNewest diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt index 8309dc890cee..233cb85ac8f7 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt @@ -79,49 +79,53 @@ namespace Microsoft.AspNetCore.Http.Generated private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { [(@"TestMapActions.cs", 15)] = ( - (methodInfo, options) => - { - if (options == null) + (methodInfo, options) => { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } - options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); - return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; - }, - (del, options, inferredMetadataResult) => - { - var handler = (System.Func)del; - EndpointFilterDelegate? filteredInvocation = null; - - if (options.EndpointBuilder.FilterFactories.Count > 0) + if (options == null || options.EndpointBuilder == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => { - filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) { - if (ic.HttpContext.Response.StatusCode == 400) + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => { - return ValueTask.FromResult(Results.Empty); - } - return ValueTask.FromResult(handler()); - }, - options.EndpointBuilder, - handler.Method); - } - - Task RequestHandler(HttpContext httpContext) - { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "text/plain"; var result = handler(); - return httpContext.Response.WriteAsync(result); - } - async Task RequestHandlerFiltered(HttpContext httpContext) - { - var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); - await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); - } + return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } - RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; - var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; - return new RequestDelegateResult(targetDelegate, metadata); - }), + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), }; @@ -154,16 +158,6 @@ namespace Microsoft.AspNetCore.Http.Generated return filteredInvocation; } - private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider - { - T.PopulateMetadata(method, builder); - } - - private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameter, builder); - } - private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) { if (obj is IResult r) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt new file mode 100644 index 000000000000..cfd7014beac6 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt @@ -0,0 +1,225 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace Microsoft.AspNetCore.Builder +{ + %GENERATEDCODEATTRIBUTE% + internal class SourceKey + { + public string Path { get; init; } + public int Line { get; init; } + + public SourceKey(string path, int line) + { + Path = path; + Line = line; + } + } + + // This class needs to be internal so that the compiled application + // has access to the strongly-typed endpoint definitions that are + // generated by the compiler so that they will be favored by + // overload resolution and opt the runtime in to the code generated + // implementation produced here. + %GENERATEDCODEATTRIBUTE% + internal static class GenerateRouteBuilderEndpoints + { + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch }; + + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + } + + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Primitives; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + file static class GeneratedRouteBuilderExtensionsCore + { + + private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + [(@"TestMapActions.cs", 15)] = ( + (methodInfo, options) => + { + if (options == null || options.EndpointBuilder == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "text/plain"; + var result = handler(); + return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 16)] = ( + (methodInfo, options) => + { + if (options == null || options.EndpointBuilder == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 16)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "text/plain"; + var result = handler(); + return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + + }; + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return httpContext.Response.WriteAsJsonAsync(obj); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 87fe135e619b..10c83cd038b1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -11,15 +11,17 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.DependencyModel.Resolution; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Http.Generators.Tests; -public class RequestDelegateGeneratorTestBase +public class RequestDelegateGeneratorTestBase : LoggedTest { internal static (ImmutableArray, Compilation) RunGenerator(string sources) { @@ -58,7 +60,7 @@ internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArra return null; } - internal static Endpoint GetEndpointFromCompilation(Compilation compilation) + internal static Endpoint GetEndpointFromCompilation(Compilation compilation, bool checkSourceKey = true) { var assemblyName = compilation.AssemblyName!; var symbolsName = Path.ChangeExtension(assemblyName, "pdb"); @@ -100,7 +102,6 @@ internal static Endpoint GetEndpointFromCompilation(Compilation compilation) var handler = assembly.GetType("TestMapActions") ?.GetMethod("MapTestEndpoints", BindingFlags.Public | BindingFlags.Static) ?.CreateDelegate>(); - var sourceKeyType = assembly.GetType("Microsoft.AspNetCore.Builder.SourceKey"); Assert.NotNull(handler); @@ -111,12 +112,29 @@ internal static Endpoint GetEndpointFromCompilation(Compilation compilation) // Trigger Endpoint build by calling getter. var endpoint = Assert.Single(dataSource.Endpoints); - var sourceKeyMetadata = endpoint.Metadata.Single(metadata => metadata.GetType() == sourceKeyType); - Assert.NotNull(sourceKeyMetadata); + if (checkSourceKey) + { + var sourceKeyType = assembly.GetType("Microsoft.AspNetCore.Builder.SourceKey"); + var sourceKeyMetadata = endpoint.Metadata.Single(metadata => metadata.GetType() == sourceKeyType); + Assert.NotNull(sourceKeyMetadata); + } return endpoint; } + internal HttpContext CreateHttpContext() + { + var httpContext = new DefaultHttpContext(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + return httpContext; + } private static Compilation CreateCompilation(string sources) { var source = $$""" @@ -137,6 +155,20 @@ public static IEndpointRouteBuilder MapTestEndpoints(this IEndpointRouteBuilder {{sources}} return app; } + + public interface ITodo + { + public int Id { get; } + public string? Name { get; } + public bool IsComplete { get; } + } + + public class Todo + { + public int Id { get; set; } + public string? Name { get; set; } = "Todo"; + public bool IsComplete { get; set; } + } } """; diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index 18db6f48d6bf..41b332b724f4 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -6,23 +6,24 @@ namespace Microsoft.AspNetCore.Http.Generators.Tests; public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase { [Theory] - [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPost(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapDelete(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPut(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapGet(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPost(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] - [InlineData(@"app.MapDelete(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPut(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] - public async Task MapAction_NoParam_StringReturn(string source, string expectedBody) + [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "MapGet", "Hello world!")] + [InlineData(@"app.MapPost(""/hello"", () => ""Hello world!"");", "MapPost", "Hello world!")] + [InlineData(@"app.MapDelete(""/hello"", () => ""Hello world!"");", "MapDelete", "Hello world!")] + [InlineData(@"app.MapPut(""/hello"", () => ""Hello world!"");", "MapPut", "Hello world!")] + [InlineData(@"app.MapGet(pattern: ""/hello"", handler: () => ""Hello world!"");", "MapGet", "Hello world!")] + [InlineData(@"app.MapPost(handler: () => ""Hello world!"", pattern: ""/hello"");", "MapPost", "Hello world!")] + [InlineData(@"app.MapDelete(pattern: ""/hello"", handler: () => ""Hello world!"");", "MapDelete", "Hello world!")] + [InlineData(@"app.MapPut(handler: () => ""Hello world!"", pattern: ""/hello"");", "MapPut", "Hello world!")] + public async Task MapAction_NoParam_StringReturn(string source, string httpMethod, string expectedBody) { var (results, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; Assert.Equal("/hello", endpointModel.Route.RoutePattern); + Assert.Equal(httpMethod, endpointModel.HttpMethod); var httpContext = new DefaultHttpContext(); @@ -76,13 +77,211 @@ public async Task MapGet_NoParam_StringReturn_WithFilter() } [Theory] - [InlineData("""app.MapGet("/hello", () => 2);""")] - [InlineData("""app.MapGet("/hello", () => new System.DateTime());""")] - public void MapGet_UnsupportedSignature_DoesNotEmit(string source) + [InlineData(@"app.MapGet(""/"", () => 123456);", "123456")] + [InlineData(@"app.MapGet(""/"", () => true);", "true")] + [InlineData(@"app.MapGet(""/"", () => new DateTime(2023, 1, 1));", @"""2023-01-01T00:00:00""")] + public async Task MapAction_NoParam_AnyReturn(string source, string expectedBody) { var (results, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, "EndpointModel"); - Assert.Null(endpointModel); + var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + + var httpContext = new DefaultHttpContext(); + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => new Todo() { Name = ""Test Item""});")] + [InlineData(""" +object GetTodo() => new Todo() { Name = "Test Item"}; +app.MapGet("/", GetTodo); +""")] + [InlineData(@"app.MapGet(""/"", () => TypedResults.Ok(new Todo() { Name = ""Test Item""}));")] + public async Task MapAction_NoParam_ComplexReturn(string source) + { + var expectedBody = """{"id":0,"name":"Test Item","isComplete":false}"""; + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => Console.WriteLine(""Returns void""));", null)] + [InlineData(@"app.MapGet(""/"", () => TypedResults.Ok(""Alright!""));", null)] + [InlineData(@"app.MapGet(""/"", () => Results.NotFound(""Oops!""));", null)] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", "application/json")] + [InlineData(@"app.MapGet(""/"", () => ""Hello world!"");", "text/plain")] + public void MapAction_ProducesCorrectContentType(string source, string expectedContentType) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.Equal(expectedContentType, endpointModel.Response.ContentType); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + public async Task MapAction_NoParam_TaskOfTReturn(string source, string expectedBody) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => ValueTask.FromResult(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => ValueTask.FromResult(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => ValueTask.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + public async Task MapAction_NoParam_ValueTaskOfTReturn(string source, string expectedBody) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => new ValueTask(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => new ValueTask(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => new ValueTask(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + public async Task MapAction_NoParam_TaskLikeOfObjectReturn(string source, string expectedBody) + { + var (results, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Fact] + public async Task Multiple_MapAction_NoParam_StringReturn() + { + var source = """ +app.MapGet("/en", () => "Hello world!"); +app.MapGet("/es", () => "Hola mundo!"); +"""; + var (_, compilation) = RunGenerator(source); + + await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_VariableRoutePattern_EmitsDiagnostic_NoSource() + { + var expectedBody = "Hello world!"; + var source = """ +var route = "/en"; +app.MapGet(route, () => "Hello world!"); +"""; + var (results, compilation) = RunGenerator(source); + + // Emits diagnostic but generates no source + var result = Assert.Single(results); + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal(DiagnosticDescriptors.UnableToResolveRoutePattern.Id,diagnostic.Id); + Assert.Empty(result.GeneratedSources); + + // Falls back to runtime-generated endpoint + var endpoint = GetEndpointFromCompilation(compilation, checkSourceKey: false); + var requestDelegate = endpoint.RequestDelegate; + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); } } diff --git a/src/Http/samples/MinimalSample/MinimalSample.csproj b/src/Http/samples/MinimalSample/MinimalSample.csproj index 8c5e9e6306e3..f057142bca29 100644 --- a/src/Http/samples/MinimalSample/MinimalSample.csproj +++ b/src/Http/samples/MinimalSample/MinimalSample.csproj @@ -3,6 +3,8 @@ $(DefaultNetCoreTargetFramework) enable + true + true @@ -15,4 +17,8 @@ + + + + diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/BoundedCacheWithFactory.cs b/src/Shared/RoslynUtils/BoundedCacheWithFactory.cs similarity index 100% rename from src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/BoundedCacheWithFactory.cs rename to src/Shared/RoslynUtils/BoundedCacheWithFactory.cs diff --git a/src/Shared/RoslynUtils/CodeWriter.cs b/src/Shared/RoslynUtils/CodeWriter.cs new file mode 100644 index 000000000000..4aec51ba3ce9 --- /dev/null +++ b/src/Shared/RoslynUtils/CodeWriter.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.AspNetCore.Analyzers.Infrastructure; + +internal class CodeWriter +{ + private readonly StringBuilder _codeBuilder = new(); + private int _indent; + + public CodeWriter(StringBuilder stringBuilder) + { + _codeBuilder = stringBuilder; + } + + public void StartBlock() + { + WriteLine("{"); + Indent(); + } + + public void EndBlock() + { + Unindent(); + WriteLine("}"); + } + + public void Indent() + { + _indent++; + } + + public void Unindent() + { + _indent--; + } + + public void Indent(int tabs) + { + _indent += tabs; + } + + public void Unindent(int tabs) + { + _indent -= tabs; + } + + public void WriteLineNoIndent(string value) + { + _codeBuilder.AppendLine(value); + } + + public void WriteNoIndent(string value) + { + _codeBuilder.Append(value); + } + + public void Write(string value) + { + if (_indent > 0) + { + _codeBuilder.Append(new string(' ', _indent * 4)); + } + _codeBuilder.Append(value); + } + + public void WriteLine(string value) + { + if (_indent > 0) + { + _codeBuilder.Append(new string(' ', _indent * 4)); + } + _codeBuilder.AppendLine(value); + } + + public override string ToString() + { + return _codeBuilder.ToString(); + } +} diff --git a/src/Shared/RoslynUtils/LambdaComparer.cs b/src/Shared/RoslynUtils/LambdaComparer.cs new file mode 100644 index 000000000000..214bf3412262 --- /dev/null +++ b/src/Shared/RoslynUtils/LambdaComparer.cs @@ -0,0 +1,37 @@ +// 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; + +internal class LambdaComparer : IEqualityComparer, IComparer +{ + readonly Func lambdaComparer; + readonly Func? lambdaHash; + public LambdaComparer(Func lambdaComparer) : + this(lambdaComparer, null) + { + } + public LambdaComparer(Func lambdaComparer, Func? lambdaHash) + { + this.lambdaComparer = lambdaComparer; + this.lambdaHash = lambdaHash; + } + + public int Compare(T x, T y) + { + return lambdaComparer(x, y); + } + + public bool Equals(T x, T y) + { + return lambdaComparer(x, y) == 0; + } + + public int GetHashCode(T obj) + { + return lambdaHash != null + ? lambdaHash(obj) + : obj?.GetHashCode() ?? 0; + } +} diff --git a/src/Shared/RoslynUtils/WellKnownTypes.cs b/src/Shared/RoslynUtils/WellKnownTypes.cs new file mode 100644 index 000000000000..3d8c22d17b10 --- /dev/null +++ b/src/Shared/RoslynUtils/WellKnownTypes.cs @@ -0,0 +1,130 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.AspNetCore.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; + +internal class WellKnownTypes +{ + private static readonly BoundedCacheWithFactory LazyWellKnownTypesCache = new(); + + public static WellKnownTypes GetOrCreate(Compilation compilation) => + LazyWellKnownTypesCache.GetOrCreateValue(compilation, static c => new WellKnownTypes(c)); + + private readonly INamedTypeSymbol?[] _lazyWellKnownTypes; + private readonly Compilation _compilation; + + static WellKnownTypes() + { + AssertEnumAndTableInSync(); + } + + [Conditional("DEBUG")] + private static void AssertEnumAndTableInSync() + { + for (var i = 0; i < WellKnownTypeData.WellKnownTypeNames.Length; i++) + { + var name = WellKnownTypeData.WellKnownTypeNames[i]; + var typeId = (WellKnownTypeData.WellKnownType)i; + + var typeIdName = typeId.ToString().Replace("__", "+").Replace('_', '.'); + + var separator = name.IndexOf('`'); + if (separator >= 0) + { + // Ignore type parameter qualifier for generic types. + name = name.Substring(0, separator); + typeIdName = typeIdName.Substring(0, separator); + } + + Debug.Assert(name == typeIdName, $"Enum name ({typeIdName}) and type name ({name}) must match at {i}"); + } + } + + private WellKnownTypes(Compilation compilation) + { + _lazyWellKnownTypes = new INamedTypeSymbol?[WellKnownTypeData.WellKnownTypeNames.Length]; + _compilation = compilation; + } + + public INamedTypeSymbol Get(SpecialType type) + { + return _compilation.GetSpecialType(type); + } + + public INamedTypeSymbol Get(WellKnownTypeData.WellKnownType type) + { + var index = (int)type; + var symbol = _lazyWellKnownTypes[index]; + if (symbol is not null) + { + return symbol; + } + + // Symbol hasn't been added to the cache yet. + // Resolve symbol from name, cache, and return. + return GetAndCache(index); + } + + private INamedTypeSymbol GetAndCache(int index) + { + var result = _compilation.GetTypeByMetadataName(WellKnownTypeData.WellKnownTypeNames[index]); + if (result == null) + { + throw new InvalidOperationException($"Failed to resolve well-known type '{WellKnownTypeData.WellKnownTypeNames[index]}'."); + } + Interlocked.CompareExchange(ref _lazyWellKnownTypes[index], result, null); + + // GetTypeByMetadataName should always return the same instance for a name. + // To ensure we have a consistent value, for thread safety, return symbol set in the array. + return _lazyWellKnownTypes[index]!; + } + + public bool IsType(ITypeSymbol type, WellKnownTypeData.WellKnownType[] wellKnownTypes) => IsType(type, wellKnownTypes, out var _); + + public bool IsType(ITypeSymbol type, WellKnownTypeData.WellKnownType[] wellKnownTypes, [NotNullWhen(true)] out WellKnownTypeData.WellKnownType? match) + { + foreach (var wellKnownType in wellKnownTypes) + { + if (SymbolEqualityComparer.Default.Equals(type, Get(wellKnownType))) + { + match = wellKnownType; + return true; + } + } + + match = null; + return false; + } + + public bool Implements(ITypeSymbol type, WellKnownTypeData.WellKnownType[] interfaceWellKnownTypes) + { + foreach (var wellKnownType in interfaceWellKnownTypes) + { + if (Implements(type, Get(wellKnownType))) + { + return true; + } + } + + return false; + } + + public static bool Implements(ITypeSymbol type, ITypeSymbol interfaceType) + { + foreach (var t in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(t, interfaceType)) + { + return true; + } + } + return false; + } +} From 4da1b3ac0e73fc8a5a6ffb85496e9264a7f336c3 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 1 Feb 2023 18:57:32 -0800 Subject: [PATCH 2/4] React to feedback and add incrementality tests --- .../Http.Extensions/gen/GeneratorSteps.cs | 4 +- ...icrosoft.AspNetCore.Http.Generators.csproj | 15 +- .../gen/RequestDelegateGenerator.cs | 64 ++++----- .../gen/StaticRouteHandlerModel/Endpoint.cs | 27 ++-- .../EndpointResponse.cs | 17 ++- .../EndpointShapeComparer.cs | 31 ++++ .../StaticRouteHandlerModel.Emitter.cs | 97 +++++-------- .../WellKnownTypeData.cs | 2 +- ...aram_StringReturn_WithFilter.generated.txt | 13 +- ...pAction_NoParam_StringReturn.generated.txt | 134 +++++++++++++++++- ...estDelegateGeneratorIncrementalityTests.cs | 45 ++++++ .../RequestDelegateGeneratorTestBase.cs | 28 ++-- .../RequestDelegateGeneratorTests.cs | 51 ++++--- src/Shared/RoslynUtils/CodeWriter.cs | 82 ----------- 14 files changed, 353 insertions(+), 257 deletions(-) create mode 100644 src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs create mode 100644 src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorIncrementalityTests.cs delete mode 100644 src/Shared/RoslynUtils/CodeWriter.cs diff --git a/src/Http/Http.Extensions/gen/GeneratorSteps.cs b/src/Http/Http.Extensions/gen/GeneratorSteps.cs index d007c5e11f69..3783ddfc7378 100644 --- a/src/Http/Http.Extensions/gen/GeneratorSteps.cs +++ b/src/Http/Http.Extensions/gen/GeneratorSteps.cs @@ -4,6 +4,6 @@ namespace Microsoft.AspNetCore.Http.Generators; internal class GeneratorSteps { - internal const string EndpointsStep = "EndpointModel"; - internal const string EndpointsWithoutDiagnosicsStep = "EndpointsWithoutDiagnostics"; + internal const string EndpointModelStep = nameof(EndpointModelStep); + internal const string EndpointsWithoutDiagnosicsStep = nameof(EndpointsWithoutDiagnosicsStep); } diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj index 19e93a74d651..a29ac7cbb482 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj @@ -9,13 +9,21 @@ + - - - + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + @@ -24,7 +32,6 @@ - diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 89dcd551591d..8f8bfbb0c8f7 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -1,9 +1,9 @@ // 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.Linq; using System.Text; -using Microsoft.AspNetCore.Analyzers.Infrastructure; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -44,7 +44,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation); return new Endpoint(operation, wellKnownTypes); }) - .WithTrackingName(GeneratorSteps.EndpointsStep); + .WithTrackingName(GeneratorSteps.EndpointModelStep); context.RegisterSourceOutput(endpointsWithDiagnostics, (context, endpoint) => { @@ -93,7 +93,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { return ValueTask.FromResult(Results.Empty); } -{{endpoint.EmitFilteredInvocation()}} + {{endpoint.EmitFilteredInvocation()}} }, options.EndpointBuilder, handler.Method); @@ -112,42 +112,27 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Collect() .Select((endpoints, _) => { - var dedupedByDelegate = endpoints.Distinct(new LambdaComparer((a, b) => - { - if (a.Response.IsAwaitable == b.Response.IsAwaitable && - a.Response.IsVoid == b.Response.IsVoid && - SymbolEqualityComparer.Default.Equals(a.Response.ResponseType, b.Response.ResponseType) && - a.HttpMethod == b.HttpMethod) - { - return 0; - } - return -1; - }, (endpoint) => - { - unchecked - { - var hashCode = SymbolEqualityComparer.Default.GetHashCode(endpoint.Response.ResponseType); - hashCode = (hashCode * 397) ^ endpoint.Response.IsAwaitable.GetHashCode(); - hashCode = (hashCode * 397) ^ endpoint.Response.IsVoid.GetHashCode(); - hashCode = (hashCode * 397) ^ endpoint.HttpMethod.GetHashCode(); - return hashCode; - } - })); - var code = new CodeWriter(new StringBuilder()); - code.Indent(2); + var dedupedByDelegate = endpoints.Distinct(EndpointShapeComparer.Instance); + var code = new StringBuilder(); foreach (var endpoint in dedupedByDelegate) { - code.WriteLine($"internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {endpoint.HttpMethod}("); - code.Indent(); - code.WriteLine("this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,"); - code.WriteLine(@"[global::System.Diagnostics.CodeAnalysis.StringSyntax(""Route"")] string pattern,"); - code.WriteLine($"global::{endpoint.EmitHandlerDelegateType()} handler,"); - code.WriteLine(@"[global::System.Runtime.CompilerServices.CallerFilePath] string filePath = """","); - code.WriteLine("[global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0)"); - code.Unindent(); - code.StartBlock(); - code.WriteLine($"return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, {endpoint.EmitVerb()}, filePath, lineNumber);"); - code.EndBlock(); + code.AppendLine($$""" + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::{{endpoint.EmitHandlerDelegateType()}} handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + {{endpoint.EmitVerb()}}, + filePath, + lineNumber); + } +"""); } return code.ToString(); @@ -164,11 +149,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return; } - var thunksCode = new CodeWriter(new StringBuilder()); - + var thunksCode = new StringBuilder(); foreach (var thunk in thunks) { - thunksCode.WriteLine(thunk); + thunksCode.AppendLine(thunk); } var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource( diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs index 3761ed895147..3f6520f1dede 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -15,7 +16,6 @@ internal class Endpoint public EndpointRoute Route { get; } public EndpointResponse Response { get; } public List Diagnostics { get; } = new List(); - public (string, int) Location { get; } public IInvocationOperation Operation { get; } @@ -57,26 +57,17 @@ public override bool Equals(object o) if (o is Endpoint endpoint) { - return endpoint.HttpMethod.Equals(HttpMethod, StringComparison.OrdinalIgnoreCase) || - endpoint.Location.Item1.Equals(Location.Item1, StringComparison.OrdinalIgnoreCase) || - endpoint.Location.Item2.Equals(Location.Item2) || - endpoint.Response.Equals(Response); + return endpoint.HttpMethod.Equals(HttpMethod, StringComparison.OrdinalIgnoreCase) && + endpoint.Route.Equals(Route) && + endpoint.Location.Item1.Equals(Location.Item1, StringComparison.OrdinalIgnoreCase) && + endpoint.Location.Item2.Equals(Location.Item2) && + endpoint.Response.Equals(Response) && + endpoint.Diagnostics.SequenceEqual(Diagnostics); } return false; } - public override int GetHashCode() - { - unchecked - { - var hashCode = HttpMethod.GetHashCode(); - hashCode = (hashCode * 397) ^ Route.GetHashCode(); - hashCode = (hashCode * 397) ^ Response.GetHashCode(); - hashCode = (hashCode * 397) ^ Diagnostics.GetHashCode(); - hashCode = (hashCode * 397) ^ Location.GetHashCode(); - hashCode = (hashCode * 397) ^ Operation.GetHashCode(); - return hashCode; - } - } + public override int GetHashCode() => + HashCode.Combine(HttpMethod, Route, Location, Response, Diagnostics); } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs index f42447842a5a..4f27b0700ce2 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs @@ -122,15 +122,14 @@ static bool VerifyGetAwaiter(IMethodSymbol getAwaiter) public override bool Equals(object obj) { return obj is EndpointResponse otherEndpointResponse && - SymbolEqualityComparer.Default.Equals(otherEndpointResponse.ResponseType, ResponseType) && - otherEndpointResponse.IsAwaitable == IsAwaitable && - otherEndpointResponse.IsVoid == IsVoid && - otherEndpointResponse.IsIResult == IsIResult && - otherEndpointResponse.ContentType.Equals(ContentType, StringComparison.OrdinalIgnoreCase); + SymbolEqualityComparer.Default.Equals(otherEndpointResponse.ResponseType, ResponseType) && + otherEndpointResponse.WrappedResponseType.Equals(WrappedResponseType, StringComparison.Ordinal) && + otherEndpointResponse.IsAwaitable == IsAwaitable && + otherEndpointResponse.IsVoid == IsVoid && + otherEndpointResponse.IsIResult == IsIResult && + otherEndpointResponse.ContentType.Equals(ContentType, StringComparison.OrdinalIgnoreCase); } - public override int GetHashCode() - { - return base.GetHashCode(); - } + public override int GetHashCode() => + HashCode.Combine(SymbolEqualityComparer.Default.GetHashCode(ResponseType), WrappedResponseType, IsAwaitable, IsVoid, IsIResult, ContentType); } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs new file mode 100644 index 000000000000..c2a211bc1e13 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs @@ -0,0 +1,31 @@ +// 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; +using System.Collections.Generic; +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +internal sealed class EndpointShapeComparer : IEqualityComparer, IComparer +{ + public static readonly EndpointShapeComparer Instance = new EndpointShapeComparer(); + + public bool Equals(Endpoint a, Endpoint b) => Compare(a, b) == 0; + + public int GetHashCode(Endpoint endpoint) => HashCode.Combine( + endpoint.Response.WrappedResponseType, + endpoint.Response.IsVoid, + endpoint.Response.IsAwaitable, + endpoint.HttpMethod); + + public int Compare(Endpoint a, Endpoint b) + { + if (a.Response.IsAwaitable == b.Response.IsAwaitable && + a.Response.IsVoid == b.Response.IsVoid && + a.Response.WrappedResponseType.Equals(b.Response.WrappedResponseType, StringComparison.Ordinal) && + a.HttpMethod.Equals(b.HttpMethod, StringComparison.Ordinal)) + { + return 0; + } + return -1; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 864f7f653a74..a167d600bb62 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Text; -using Microsoft.AspNetCore.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; @@ -62,63 +60,48 @@ public static string EmitVerb(this Endpoint endpoint) */ public static string EmitRequestHandler(this Endpoint endpoint) { - var code = new CodeWriter(new StringBuilder()); - code.Indent(5); - code.WriteLine(endpoint.Response.IsAwaitable - ? "async Task RequestHandler(HttpContext httpContext)" - : "Task RequestHandler(HttpContext httpContext)"); - code.StartBlock(); - - if (endpoint.Response.IsVoid) - { - code.WriteLine("handler();"); - code.WriteLine("return Task.CompletedTask;"); - } - else - { - code.WriteLine($"""httpContext.Response.ContentType ??= "{endpoint.Response.ContentType}";"""); - 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.EndBlock(); - return code.ToString(); + var handlerSignature = endpoint.Response.IsAwaitable ? "async Task RequestHandler(HttpContext httpContext)" : "Task RequestHandler(HttpContext httpContext)"; + var resultAssignment = endpoint.Response.IsVoid ? string.Empty : "var result = "; + var awaitHandler = endpoint.Response.IsAwaitable ? "await " : string.Empty; + var setContentType = endpoint.Response.IsVoid ? string.Empty : $@"httpContext.Response.ContentType ??= ""{endpoint.Response.ContentType}"";"; + return $$""" + {{handlerSignature}} + { + {{setContentType}} + {{resultAssignment}}{{awaitHandler}}handler(); + {{(endpoint.Response.IsVoid ? "return Task.CompletedTask;" : endpoint.EmitResponseWritingCall())}} + } +"""; } private static string EmitResponseWritingCall(this Endpoint endpoint) { - var code = new CodeWriter(new StringBuilder()); - code.WriteNoIndent(endpoint.Response.IsAwaitable ? "await " : "return "); + var returnOrAwait = endpoint.Response.IsAwaitable ? "await" : "return"; if (endpoint.Response.IsIResult) { - code.WriteNoIndent("result.ExecuteAsync(httpContext);"); + return $"{returnOrAwait} result.ExecuteAsync(httpContext);"; } else if (endpoint.Response.ResponseType.SpecialType == SpecialType.System_String) { - code.WriteNoIndent("httpContext.Response.WriteAsync(result);"); + return $"{returnOrAwait} httpContext.Response.WriteAsync(result);"; } else if (endpoint.Response.ResponseType.SpecialType == SpecialType.System_Object) { - code.WriteNoIndent("GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);"); + return $"{returnOrAwait} GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);"; } else if (!endpoint.Response.IsVoid) { - code.WriteNoIndent("httpContext.Response.WriteAsJsonAsync(result);"); + return $"{returnOrAwait} httpContext.Response.WriteAsJsonAsync(result);"; } else if (!endpoint.Response.IsAwaitable && endpoint.Response.IsVoid) { - code.WriteNoIndent("Task.CompletedTask;"); + return $"{returnOrAwait} Task.CompletedTask;"; + } + else + { + return $"{returnOrAwait} httpContext.Response.WriteAsync(result);"; } - - return code.ToString(); } /* @@ -130,14 +113,13 @@ private static string EmitResponseWritingCall(this Endpoint endpoint) */ public static string EmitFilteredRequestHandler() { - var code = new CodeWriter(new StringBuilder()); - code.Indent(5); - code.WriteLine("async Task RequestHandlerFiltered(HttpContext httpContext)"); - code.StartBlock(); - code.WriteLine("var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext));"); - code.WriteLine("await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);"); - code.EndBlock(); - return code.ToString(); + return """ + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } +"""; } /* @@ -158,18 +140,13 @@ public static string EmitFilteredRequestHandler() */ public static string EmitFilteredInvocation(this Endpoint endpoint) { - var code = new CodeWriter(new StringBuilder()); - code.Indent(7); - if (endpoint.Response.IsVoid) - { - code.WriteLine("handler();"); - code.WriteLine("return ValueTask.FromResult(Results.Empty);"); - } - else - { - code.WriteLine("return ValueTask.FromResult(handler());"); - } - - return code.ToString(); + // Note: This string does not need indentation since it is + // handled when we generate the output string in the `thunks` pipeline. + return endpoint.Response.IsVoid ? """ +handler(); +return ValueTask.FromResult(Results.Empty); +""" : """ +return ValueTask.FromResult(handler()); +"""; } } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs index d889acca30f0..72657181113a 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs @@ -14,7 +14,7 @@ public enum WellKnownType System_Threading_Tasks_ValueTask_T } - public static string[] WellKnownTypeNames = new[] + public static readonly string[] WellKnownTypeNames = new[] { "Microsoft.AspNetCore.Http.IResult", "System.Threading.Tasks.Task", diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt index 233cb85ac8f7..6e0354e6d028 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt @@ -44,7 +44,13 @@ namespace Microsoft.AspNetCore.Builder [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) { - return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); } } @@ -102,7 +108,6 @@ namespace Microsoft.AspNetCore.Http.Generated return ValueTask.FromResult(Results.Empty); } return ValueTask.FromResult(handler()); - }, options.EndpointBuilder, handler.Method); @@ -112,16 +117,14 @@ namespace Microsoft.AspNetCore.Http.Generated { httpContext.Response.ContentType ??= "text/plain"; var result = handler(); - return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + return httpContext.Response.WriteAsync(result); } - async Task RequestHandlerFiltered(HttpContext httpContext) { var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); } - RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; return new RequestDelegateResult(targetDelegate, metadata); diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt index cfd7014beac6..4141c1cdd83e 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt @@ -44,7 +44,43 @@ namespace Microsoft.AspNetCore.Builder [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) { - return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); + } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func> handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); + } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func> handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); } } @@ -102,7 +138,6 @@ namespace Microsoft.AspNetCore.Http.Generated return ValueTask.FromResult(Results.Empty); } return ValueTask.FromResult(handler()); - }, options.EndpointBuilder, handler.Method); @@ -112,16 +147,14 @@ namespace Microsoft.AspNetCore.Http.Generated { httpContext.Response.ContentType ??= "text/plain"; var result = handler(); - return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + return httpContext.Response.WriteAsync(result); } - async Task RequestHandlerFiltered(HttpContext httpContext) { var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); } - RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; return new RequestDelegateResult(targetDelegate, metadata); @@ -150,7 +183,6 @@ namespace Microsoft.AspNetCore.Http.Generated return ValueTask.FromResult(Results.Empty); } return ValueTask.FromResult(handler()); - }, options.EndpointBuilder, handler.Method); @@ -160,15 +192,103 @@ namespace Microsoft.AspNetCore.Http.Generated { httpContext.Response.ContentType ??= "text/plain"; var result = handler(); - return GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + return httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 17)] = ( + (methodInfo, options) => + { + if (options == null || options.EndpointBuilder == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 17)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func>)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); } + async Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "application/json"; + var result = await handler(); + await httpContext.Response.WriteAsync(result); + } async Task RequestHandlerFiltered(HttpContext httpContext) { var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); } + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 18)] = ( + (methodInfo, options) => + { + if (options == null || options.EndpointBuilder == null) + { + return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; + } + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 18)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func>)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + async Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "application/json"; + var result = await handler(); + await httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorIncrementalityTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorIncrementalityTests.cs new file mode 100644 index 000000000000..111442bea3a3 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorIncrementalityTests.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.CodeAnalysis; +namespace Microsoft.AspNetCore.Http.Generators.Tests; + +public class RequestDelegateGeneratorIncrementalityTests : RequestDelegateGeneratorTestBase +{ + [Fact] + public void MapAction_SameReturnType_DoesNotTriggerUpdate() + { + var source = @"app.MapGet(""/hello"", () => ""Hello world!"");"; + var updatedSource = @"app.MapGet(""/hello"", () => ""Bye world!"");"; + + var (result, compilation) = RunGenerator(source, updatedSource); + var outputSteps = GetRunStepOutputs(result); + + Assert.All(outputSteps, (value) => Assert.Equal(IncrementalStepRunReason.Cached, value.Reason)); + } + + [Fact] + public void MapAction_DifferentRoutePattern_DoesNotTriggerUpdate() + { + var source = @"app.MapGet(""/hello"", () => ""Hello world!"");"; + var updatedSource = @"app.MapGet(""/hello-2"", () => ""Hello world!"");"; + + var (result, compilation) = RunGenerator(source, updatedSource); + var outputSteps = GetRunStepOutputs(result); + + Assert.All(outputSteps, (value) => Assert.Equal(IncrementalStepRunReason.Cached, value.Reason)); + } + + [Fact] + public void MapAction_ChangeReturnType_TriggersUpdate() + { + var source = @"app.MapGet(""/hello"", () => ""Hello world!"");"; + var updatedSource = @"app.MapGet(""/hello"", () => Task.FromResult(""Hello world!""));"; + + var (result, compilation) = RunGenerator(source, updatedSource); + var outputSteps = GetRunStepOutputs(result); + + Assert.All(outputSteps, (value) => Assert.Equal(IncrementalStepRunReason.New, value.Reason)); + } + + private static IEnumerable<(object Value, IncrementalStepRunReason Reason)> GetRunStepOutputs(GeneratorRunResult result) => result.TrackedOutputSteps.SelectMany(step => step.Value).SelectMany(value => value.Outputs); +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 10c83cd038b1..0a192576a1e1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Immutable; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Text; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Generators; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.AspNetCore.Routing; @@ -17,13 +15,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.DependencyModel.Resolution; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Http.Generators.Tests; public class RequestDelegateGeneratorTestBase : LoggedTest { - internal static (ImmutableArray, Compilation) RunGenerator(string sources) + internal static (GeneratorRunResult, Compilation) RunGenerator(string sources, params string[] updatedSources) { var compilation = CreateCompilation(sources); var generator = new RequestDelegateGenerator().AsSourceGenerator(); @@ -38,18 +35,25 @@ internal static (ImmutableArray, Compilation) RunGenerator(s // Run the source generator driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var _); + foreach (var updatedSource in updatedSources) + { + var syntaxTree = CSharpSyntaxTree.ParseText(GetMapActionString(updatedSource), path: $"TestMapActions.cs"); + compilation = compilation + .ReplaceSyntaxTree(compilation.SyntaxTrees.First(), syntaxTree); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out updatedCompilation, + out var _); + } var diagnostics = updatedCompilation.GetDiagnostics(); Assert.Empty(diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning)); var runResult = driver.GetRunResult(); - return (runResult.Results, updatedCompilation); + return (Assert.Single(runResult.Results), updatedCompilation); } - internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArray results, string stepName) + internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(GeneratorRunResult result, string stepName) { // We only invoke the generator once in our test scenarios - var firstGeneratorPass = results[0]; - if (firstGeneratorPass.TrackedSteps.TryGetValue(stepName, out var staticEndpointSteps)) + if (result.TrackedSteps.TryGetValue(stepName, out var staticEndpointSteps)) { var staticEndpointStep = staticEndpointSteps.Single(); var staticEndpointOutput = staticEndpointStep.Outputs.Single(); @@ -135,9 +139,8 @@ internal HttpContext CreateHttpContext() return httpContext; } - private static Compilation CreateCompilation(string sources) - { - var source = $$""" + + private static string GetMapActionString(string sources) => $$""" #nullable enable using System; using System.Collections.Generic; @@ -171,6 +174,9 @@ public class Todo } } """; + private static Compilation CreateCompilation(string sources) + { + var source = GetMapActionString(sources); var syntaxTrees = new[] { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index 41b332b724f4..dca59e23a360 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -16,9 +16,9 @@ public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase [InlineData(@"app.MapPut(handler: () => ""Hello world!"", pattern: ""/hello"");", "MapPut", "Hello world!")] public async Task MapAction_NoParam_StringReturn(string source, string httpMethod, string expectedBody) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -51,11 +51,11 @@ public async Task MapGet_NoParam_StringReturn_WithFilter() }); """; var expectedBody = "Filtered: Hello world!"; - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); await VerifyAgainstBaselineUsingFile(compilation); - var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -82,9 +82,9 @@ public async Task MapGet_NoParam_StringReturn_WithFilter() [InlineData(@"app.MapGet(""/"", () => new DateTime(2023, 1, 1));", @"""2023-01-01T00:00:00""")] public async Task MapAction_NoParam_AnyReturn(string source, string expectedBody) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -116,9 +116,9 @@ public async Task MapAction_NoParam_AnyReturn(string source, string expectedBody public async Task MapAction_NoParam_ComplexReturn(string source) { var expectedBody = """{"id":0,"name":"Test Item","isComplete":false}"""; - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -145,9 +145,9 @@ public async Task MapAction_NoParam_ComplexReturn(string source) [InlineData(@"app.MapGet(""/"", () => ""Hello world!"");", "text/plain")] public void MapAction_ProducesCorrectContentType(string source, string expectedContentType) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); Assert.Equal("/", endpointModel.Route.RoutePattern); Assert.Equal("MapGet", endpointModel.HttpMethod); @@ -160,9 +160,9 @@ public void MapAction_ProducesCorrectContentType(string source, string expectedC [InlineData(@"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] public async Task MapAction_NoParam_TaskOfTReturn(string source, string expectedBody) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -188,9 +188,9 @@ public async Task MapAction_NoParam_TaskOfTReturn(string source, string expected [InlineData(@"app.MapGet(""/"", () => ValueTask.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] public async Task MapAction_NoParam_ValueTaskOfTReturn(string source, string expectedBody) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -219,9 +219,9 @@ public async Task MapAction_NoParam_ValueTaskOfTReturn(string source, string exp [InlineData(@"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] public async Task MapAction_NoParam_TaskLikeOfObjectReturn(string source, string expectedBody) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, GeneratorSteps.EndpointsStep); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -247,6 +247,8 @@ public async Task Multiple_MapAction_NoParam_StringReturn() var source = """ app.MapGet("/en", () => "Hello world!"); app.MapGet("/es", () => "Hola mundo!"); +app.MapGet("/en-task", () => Task.FromResult("Hello world!")); +app.MapGet("/es-task", () => new ValueTask("Hola mundo!")); """; var (_, compilation) = RunGenerator(source); @@ -261,10 +263,9 @@ public async Task MapAction_VariableRoutePattern_EmitsDiagnostic_NoSource() var route = "/en"; app.MapGet(route, () => "Hello world!"); """; - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); // Emits diagnostic but generates no source - var result = Assert.Single(results); var diagnostic = Assert.Single(result.Diagnostics); Assert.Equal(DiagnosticDescriptors.UnableToResolveRoutePattern.Id,diagnostic.Id); Assert.Empty(result.GeneratedSources); @@ -284,4 +285,18 @@ public async Task MapAction_VariableRoutePattern_EmitsDiagnostic_NoSource() Assert.Equal(200, httpContext.Response.StatusCode); Assert.Equal(expectedBody, body); } + + [Fact] + public void MapAction_RequestDelegateHandler_DoesNotEmit() + { + var source = """ +app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello world")); +"""; + var (result, _) = RunGenerator(source); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + + // Endpoint model is null because we don't pass transform + Assert.Null(endpointModel); + Assert.Empty(result.GeneratedSources); + } } diff --git a/src/Shared/RoslynUtils/CodeWriter.cs b/src/Shared/RoslynUtils/CodeWriter.cs deleted file mode 100644 index 4aec51ba3ce9..000000000000 --- a/src/Shared/RoslynUtils/CodeWriter.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text; - -namespace Microsoft.AspNetCore.Analyzers.Infrastructure; - -internal class CodeWriter -{ - private readonly StringBuilder _codeBuilder = new(); - private int _indent; - - public CodeWriter(StringBuilder stringBuilder) - { - _codeBuilder = stringBuilder; - } - - public void StartBlock() - { - WriteLine("{"); - Indent(); - } - - public void EndBlock() - { - Unindent(); - WriteLine("}"); - } - - public void Indent() - { - _indent++; - } - - public void Unindent() - { - _indent--; - } - - public void Indent(int tabs) - { - _indent += tabs; - } - - public void Unindent(int tabs) - { - _indent -= tabs; - } - - public void WriteLineNoIndent(string value) - { - _codeBuilder.AppendLine(value); - } - - public void WriteNoIndent(string value) - { - _codeBuilder.Append(value); - } - - public void Write(string value) - { - if (_indent > 0) - { - _codeBuilder.Append(new string(' ', _indent * 4)); - } - _codeBuilder.Append(value); - } - - public void WriteLine(string value) - { - if (_indent > 0) - { - _codeBuilder.Append(new string(' ', _indent * 4)); - } - _codeBuilder.AppendLine(value); - } - - public override string ToString() - { - return _codeBuilder.ToString(); - } -} From b83329e26ec1eafbc558f740750722a93662f685 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 2 Feb 2023 10:23:27 -0800 Subject: [PATCH 3/4] Route patterns don't factor into incrementality check --- src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs index 3f6520f1dede..b5a694770f80 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs @@ -58,7 +58,6 @@ public override bool Equals(object o) if (o is Endpoint endpoint) { return endpoint.HttpMethod.Equals(HttpMethod, StringComparison.OrdinalIgnoreCase) && - endpoint.Route.Equals(Route) && endpoint.Location.Item1.Equals(Location.Item1, StringComparison.OrdinalIgnoreCase) && endpoint.Location.Item2.Equals(Location.Item2) && endpoint.Response.Equals(Response) && From 6bdabaa22154aa04bb4a360efb0e4311370fd84e Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 2 Feb 2023 13:40:04 -0800 Subject: [PATCH 4/4] Address feedback from review --- ...icrosoft.AspNetCore.Http.Generators.csproj | 1 - .../gen/RequestDelegateGenerator.cs | 7 +--- ...omparer.cs => EndpointDelegateComparer.cs} | 4 +- ...aram_StringReturn_WithFilter.generated.txt | 5 +-- ...pAction_NoParam_StringReturn.generated.txt | 20 ++-------- src/Shared/RoslynUtils/LambdaComparer.cs | 37 ------------------- 6 files changed, 9 insertions(+), 65 deletions(-) rename src/Http/Http.Extensions/gen/StaticRouteHandlerModel/{EndpointShapeComparer.cs => EndpointDelegateComparer.cs} (83%) delete mode 100644 src/Shared/RoslynUtils/LambdaComparer.cs diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj index a29ac7cbb482..53ef4662cc6f 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj @@ -33,7 +33,6 @@ - diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 8f8bfbb0c8f7..8fa3a9116366 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -73,10 +73,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) [{{endpoint.EmitSourceKey()}}] = ( (methodInfo, options) => { - if (options == null || options.EndpointBuilder == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new SourceKey{{endpoint.EmitSourceKey()}}); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, @@ -112,7 +109,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Collect() .Select((endpoints, _) => { - var dedupedByDelegate = endpoints.Distinct(EndpointShapeComparer.Instance); + var dedupedByDelegate = endpoints.Distinct(EndpointDelegateComparer.Instance); var code = new StringBuilder(); foreach (var endpoint in dedupedByDelegate) { diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointDelegateComparer.cs similarity index 83% rename from src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs rename to src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointDelegateComparer.cs index c2a211bc1e13..7d159ebd7176 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointShapeComparer.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointDelegateComparer.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; -internal sealed class EndpointShapeComparer : IEqualityComparer, IComparer +internal sealed class EndpointDelegateComparer : IEqualityComparer, IComparer { - public static readonly EndpointShapeComparer Instance = new EndpointShapeComparer(); + public static readonly EndpointDelegateComparer Instance = new EndpointDelegateComparer(); public bool Equals(Endpoint a, Endpoint b) => Compare(a, b) == 0; diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt index 6e0354e6d028..7f9a7f9be477 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt @@ -87,10 +87,7 @@ namespace Microsoft.AspNetCore.Http.Generated [(@"TestMapActions.cs", 15)] = ( (methodInfo, options) => { - if (options == null || options.EndpointBuilder == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt index 4141c1cdd83e..35f739c6d74d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt @@ -117,10 +117,7 @@ namespace Microsoft.AspNetCore.Http.Generated [(@"TestMapActions.cs", 15)] = ( (methodInfo, options) => { - if (options == null || options.EndpointBuilder == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, @@ -162,10 +159,7 @@ namespace Microsoft.AspNetCore.Http.Generated [(@"TestMapActions.cs", 16)] = ( (methodInfo, options) => { - if (options == null || options.EndpointBuilder == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 16)); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, @@ -207,10 +201,7 @@ namespace Microsoft.AspNetCore.Http.Generated [(@"TestMapActions.cs", 17)] = ( (methodInfo, options) => { - if (options == null || options.EndpointBuilder == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 17)); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, @@ -252,10 +243,7 @@ namespace Microsoft.AspNetCore.Http.Generated [(@"TestMapActions.cs", 18)] = ( (methodInfo, options) => { - if (options == null || options.EndpointBuilder == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 18)); return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; }, diff --git a/src/Shared/RoslynUtils/LambdaComparer.cs b/src/Shared/RoslynUtils/LambdaComparer.cs deleted file mode 100644 index 214bf3412262..000000000000 --- a/src/Shared/RoslynUtils/LambdaComparer.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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; - -internal class LambdaComparer : IEqualityComparer, IComparer -{ - readonly Func lambdaComparer; - readonly Func? lambdaHash; - public LambdaComparer(Func lambdaComparer) : - this(lambdaComparer, null) - { - } - public LambdaComparer(Func lambdaComparer, Func? lambdaHash) - { - this.lambdaComparer = lambdaComparer; - this.lambdaHash = lambdaHash; - } - - public int Compare(T x, T y) - { - return lambdaComparer(x, y); - } - - public bool Equals(T x, T y) - { - return lambdaComparer(x, y) == 0; - } - - public int GetHashCode(T obj) - { - return lambdaHash != null - ? lambdaHash(obj) - : obj?.GetHashCode() ?? 0; - } -}