diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index 8695097020fd..99edc9c9a378 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -60,17 +60,20 @@ static bool ShouldDisableInferredBody(string method) foreach (var endpoint in _endpointDataSource.Endpoints) { if (endpoint is RouteEndpoint routeEndpoint && - routeEndpoint.Metadata.GetMetadata() is { } methodInfo && - routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata && routeEndpoint.Metadata.GetMetadata() is null or { ExcludeFromDescription: false }) { + var httpMethods = routeEndpoint.Metadata.GetMetadata() is { } httpMethodMetadata + ? httpMethodMetadata.HttpMethods + : new[] { HttpMethods.Get }; + var methodInfo = routeEndpoint.Metadata.GetMetadata(); + // We need to detect if any of the methods allow inferred body - var disableInferredBody = httpMethodMetadata.HttpMethods.Any(ShouldDisableInferredBody); + var disableInferredBody = httpMethods.Any(ShouldDisableInferredBody); // REVIEW: Should we add an ApiDescription for endpoints without IHttpMethodMetadata? Swagger doesn't handle // a null HttpMethod even though it's nullable on ApiDescription, so we'd need to define "default" HTTP methods. // In practice, the Delegate will be called for any HTTP method if there is no IHttpMethodMetadata. - foreach (var httpMethod in httpMethodMetadata.HttpMethods) + foreach (var httpMethod in httpMethods) { context.Results.Add(CreateApiDescription(routeEndpoint, httpMethod, methodInfo, disableInferredBody)); } @@ -82,13 +85,13 @@ public void OnProvidersExecuted(ApiDescriptionProviderContext context) { } - private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string httpMethod, MethodInfo methodInfo, bool disableInferredBody) + private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string httpMethod, MethodInfo? methodInfo, bool disableInferredBody) { // Swashbuckle uses the "controller" name to group endpoints together. // For now, put all methods defined the same declaring type together. string controllerName; - if (methodInfo.DeclaringType is not null && !TypeHelper.IsCompilerGeneratedType(methodInfo.DeclaringType)) + if (methodInfo?.DeclaringType is not null && !TypeHelper.IsCompilerGeneratedType(methodInfo.DeclaringType)) { controllerName = methodInfo.DeclaringType.Name; } @@ -114,6 +117,11 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string }, }; + if (methodInfo == null) + { + return apiDescription; + } + var hasBodyOrFormFileParameter = false; foreach (var parameter in PropertyAsParameterInfo.Flatten(methodInfo.GetParameters(), ParameterBindingMethodCache)) diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs index b6aefb7330f5..a0e124b46580 100644 --- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs +++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs @@ -1344,6 +1344,29 @@ public void FavorsParameterCasingInRoutePattern(string pattern, string expectedN Assert.Equal(expectedName, parameter.ParameterDescriptor.Name); } + [Fact] + public void HandlesEndpointWithoutMethodInfo() + { + var builder = CreateBuilder(); + builder.MapGet("/", (HttpContext context) => Task.CompletedTask); + + var context = new ApiDescriptionProviderContext(Array.Empty()); + + var endpointDataSource = builder.DataSources.OfType().Single(); + var hostEnvironment = new HostEnvironment + { + ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest) + }; + var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + var apiDescription = Assert.Single(context.Results); + Assert.Equal("GET", apiDescription.HttpMethod); + } + private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType) { return apiResponseType.ApiResponseFormats diff --git a/src/OpenApi/src/OpenApiEndpointConventionBuilderExtensions.cs b/src/OpenApi/src/OpenApiEndpointConventionBuilderExtensions.cs index 72f1dc8fe1f2..acc62c9db8f2 100644 --- a/src/OpenApi/src/OpenApiEndpointConventionBuilderExtensions.cs +++ b/src/OpenApi/src/OpenApiEndpointConventionBuilderExtensions.cs @@ -80,11 +80,6 @@ private static void AddAndConfigureOperationForEndpoint(EndpointBuilder endpoint var metadata = new EndpointMetadataCollection(routeEndpointBuilder.Metadata); var methodInfo = metadata.OfType().SingleOrDefault(); - if (methodInfo is null) - { - return; - } - var applicationServices = routeEndpointBuilder.ApplicationServices; var hostEnvironment = applicationServices.GetService(); var serviceProviderIsService = applicationServices.GetService(); diff --git a/src/OpenApi/src/OpenApiGenerator.cs b/src/OpenApi/src/OpenApiGenerator.cs index 9a874fd2663d..8b8c8b88c91c 100644 --- a/src/OpenApi/src/OpenApiGenerator.cs +++ b/src/OpenApi/src/OpenApiGenerator.cs @@ -54,21 +54,22 @@ internal OpenApiGenerator( /// The route pattern. /// An annotation derived from the given inputs. internal OpenApiOperation? GetOpenApiOperation( - MethodInfo methodInfo, + MethodInfo? methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern) { - if (metadata.GetMetadata() is { } httpMethodMetadata && - httpMethodMetadata.HttpMethods.SingleOrDefault() is { } method && - metadata.GetMetadata() is null or { ExcludeFromDescription: false }) + if (metadata.GetMetadata() is null or { ExcludeFromDescription: false }) { + var method = metadata.GetMetadata() is { HttpMethods: { Count: 1 } httpMethods } + ? httpMethods.Single() + : HttpMethods.Get; return GetOperation(method, methodInfo, metadata, pattern); } return null; } - private OpenApiOperation GetOperation(string httpMethod, MethodInfo methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern) + private OpenApiOperation GetOperation(string httpMethod, MethodInfo? methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern) { var disableInferredBody = ShouldDisableInferredBody(httpMethod); return new OpenApiOperation @@ -77,9 +78,15 @@ private OpenApiOperation GetOperation(string httpMethod, MethodInfo methodInfo, Summary = metadata.GetMetadata()?.Summary, Description = metadata.GetMetadata()?.Description, Tags = GetOperationTags(methodInfo, metadata), - Parameters = GetOpenApiParameters(methodInfo, pattern, disableInferredBody), - RequestBody = GetOpenApiRequestBody(methodInfo, metadata, pattern), - Responses = GetOpenApiResponses(methodInfo, metadata) + Parameters = methodInfo is not null + ? GetOpenApiParameters(methodInfo, pattern, disableInferredBody) + : new List(), + RequestBody = methodInfo is not null + ? GetOpenApiRequestBody(methodInfo, metadata, pattern) + : null, + Responses = methodInfo is not null + ? GetOpenApiResponses(methodInfo, metadata) + : new OpenApiResponses() }; static bool ShouldDisableInferredBody(string method) @@ -321,7 +328,7 @@ private static void GenerateDefaultResponses(Dictionary GetOperationTags(MethodInfo methodInfo, EndpointMetadataCollection metadata) + private List GetOperationTags(MethodInfo? methodInfo, EndpointMetadataCollection metadata) { var metadataList = metadata.GetOrderedMetadata(); @@ -333,7 +340,7 @@ private List GetOperationTags(MethodInfo methodInfo, EndpointMetadat { foreach (var tag in metadataItem.Tags) { - tags.Add(new OpenApiTag() { Name = tag }); + tags.Add(new OpenApiTag { Name = tag }); } } @@ -342,7 +349,7 @@ private List GetOperationTags(MethodInfo methodInfo, EndpointMetadat string controllerName; - if (methodInfo.DeclaringType is not null && !TypeHelper.IsCompilerGeneratedType(methodInfo.DeclaringType)) + if (methodInfo?.DeclaringType is not null && !TypeHelper.IsCompilerGeneratedType(methodInfo.DeclaringType)) { controllerName = methodInfo.DeclaringType.Name; } @@ -353,7 +360,7 @@ private List GetOperationTags(MethodInfo methodInfo, EndpointMetadat controllerName = _environment?.ApplicationName ?? string.Empty; } - return new List() { new OpenApiTag() { Name = controllerName } }; + return new List { new() { Name = controllerName } }; } private List GetOpenApiParameters(MethodInfo methodInfo, RoutePattern pattern, bool disableInferredBody) diff --git a/src/OpenApi/test/OpenApiGeneratorTests.cs b/src/OpenApi/test/OpenApiGeneratorTests.cs index d93e86ea7d2e..6f42e6035fff 100644 --- a/src/OpenApi/test/OpenApiGeneratorTests.cs +++ b/src/OpenApi/test/OpenApiGeneratorTests.cs @@ -22,11 +22,11 @@ namespace Microsoft.AspNetCore.OpenApi.Tests; public class OpenApiOperationGeneratorTests { [Fact] - public void OperationNotCreatedIfNoHttpMethods() + public void OperationDefaultsToGetIfNoHttpMethods() { var operation = GetOpenApiOperation(() => { }, "/", Array.Empty()); - Assert.Null(operation); + Assert.NotNull(operation); } [Fact] @@ -913,6 +913,18 @@ public void HandlesEndpointWithNoRequestBody() Assert.Null(operationWithNoBodyParams.RequestBody); } + [Fact] + public void HandlesEndpointWithNoMethodInfo() + { + var operationWithNoMethodInfo = GetOpenApiOperation((HttpContext context) => Task.CompletedTask, "/", httpMethods: new[] { "PUT" }, hasMethodInfo: false); + + Assert.Empty(operationWithNoMethodInfo.Parameters); + Assert.Empty(operationWithNoMethodInfo.Responses); + Assert.Null(operationWithNoMethodInfo.RequestBody); + var tag = Assert.Single(operationWithNoMethodInfo.Tags); + Assert.Equal(nameof(OpenApiOperationGeneratorTests), tag.Name); + } + [Fact] public void HandlesParameterWithNameInAttribute() { @@ -973,7 +985,8 @@ private static OpenApiOperation GetOpenApiOperation( string pattern = null, IEnumerable httpMethods = null, string displayName = null, - object[] additionalMetadata = null) + object[] additionalMetadata = null, + bool hasMethodInfo = true) { var methodInfo = action.Method; var attributes = methodInfo.GetCustomAttributes(); @@ -989,7 +1002,7 @@ private static OpenApiOperation GetOpenApiOperation( hostEnvironment, new ServiceProviderIsService()); - return generator.GetOpenApiOperation(methodInfo, endpointMetadata, routePattern); + return generator.GetOpenApiOperation(hasMethodInfo ? methodInfo : null, endpointMetadata, routePattern); } private static void TestAction()