Skip to content

WithOpenApi generates annotations for endpoints without MethodInfo #47346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,20 @@ static bool ShouldDisableInferredBody(string method)
foreach (var endpoint in _endpointDataSource.Endpoints)
{
if (endpoint is RouteEndpoint routeEndpoint &&
routeEndpoint.Metadata.GetMetadata<MethodInfo>() is { } methodInfo &&
routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>() is { } httpMethodMetadata &&
routeEndpoint.Metadata.GetMetadata<IExcludeFromDescriptionMetadata>() is null or { ExcludeFromDescription: false })
{
var httpMethods = routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>() is { } httpMethodMetadata
? httpMethodMetadata.HttpMethods
: new[] { HttpMethods.Get };
var methodInfo = routeEndpoint.Metadata.GetMetadata<MethodInfo>();

// 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));
}
Expand All @@ -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;
}
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActionDescriptor>());

var endpointDataSource = builder.DataSources.OfType<EndpointDataSource>().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<string> GetSortedMediaTypes(ApiResponseType apiResponseType)
{
return apiResponseType.ApiResponseFormats
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,6 @@ private static void AddAndConfigureOperationForEndpoint(EndpointBuilder endpoint
var metadata = new EndpointMetadataCollection(routeEndpointBuilder.Metadata);
var methodInfo = metadata.OfType<MethodInfo>().SingleOrDefault();

if (methodInfo is null)
{
return;
}

var applicationServices = routeEndpointBuilder.ApplicationServices;
var hostEnvironment = applicationServices.GetService<IHostEnvironment>();
var serviceProviderIsService = applicationServices.GetService<IServiceProviderIsService>();
Expand Down
31 changes: 19 additions & 12 deletions src/OpenApi/src/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,22 @@ internal OpenApiGenerator(
/// <param name="pattern">The route pattern.</param>
/// <returns>An <see cref="OpenApiPathItem"/> annotation derived from the given inputs.</returns>
internal OpenApiOperation? GetOpenApiOperation(
MethodInfo methodInfo,
MethodInfo? methodInfo,
EndpointMetadataCollection metadata,
RoutePattern pattern)
{
if (metadata.GetMetadata<IHttpMethodMetadata>() is { } httpMethodMetadata &&
httpMethodMetadata.HttpMethods.SingleOrDefault() is { } method &&
metadata.GetMetadata<IExcludeFromDescriptionMetadata>() is null or { ExcludeFromDescription: false })
if (metadata.GetMetadata<IExcludeFromDescriptionMetadata>() is null or { ExcludeFromDescription: false })
{
var method = metadata.GetMetadata<IHttpMethodMetadata>() 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
Expand All @@ -77,9 +78,15 @@ private OpenApiOperation GetOperation(string httpMethod, MethodInfo methodInfo,
Summary = metadata.GetMetadata<IEndpointSummaryMetadata>()?.Summary,
Description = metadata.GetMetadata<IEndpointDescriptionMetadata>()?.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<OpenApiParameter>(),
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)
Expand Down Expand Up @@ -321,7 +328,7 @@ private static void GenerateDefaultResponses(Dictionary<int, (Type?, MediaTypeCo
return null;
}

private List<OpenApiTag> GetOperationTags(MethodInfo methodInfo, EndpointMetadataCollection metadata)
private List<OpenApiTag> GetOperationTags(MethodInfo? methodInfo, EndpointMetadataCollection metadata)
{
var metadataList = metadata.GetOrderedMetadata<ITagsMetadata>();

Expand All @@ -333,7 +340,7 @@ private List<OpenApiTag> GetOperationTags(MethodInfo methodInfo, EndpointMetadat
{
foreach (var tag in metadataItem.Tags)
{
tags.Add(new OpenApiTag() { Name = tag });
tags.Add(new OpenApiTag { Name = tag });
}
}

Expand All @@ -342,7 +349,7 @@ private List<OpenApiTag> 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;
}
Expand All @@ -353,7 +360,7 @@ private List<OpenApiTag> GetOperationTags(MethodInfo methodInfo, EndpointMetadat
controllerName = _environment?.ApplicationName ?? string.Empty;
}

return new List<OpenApiTag>() { new OpenApiTag() { Name = controllerName } };
return new List<OpenApiTag> { new() { Name = controllerName } };
}

private List<OpenApiParameter> GetOpenApiParameters(MethodInfo methodInfo, RoutePattern pattern, bool disableInferredBody)
Expand Down
21 changes: 17 additions & 4 deletions src/OpenApi/test/OpenApiGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ namespace Microsoft.AspNetCore.OpenApi.Tests;
public class OpenApiOperationGeneratorTests
{
[Fact]
public void OperationNotCreatedIfNoHttpMethods()
public void OperationDefaultsToGetIfNoHttpMethods()
{
var operation = GetOpenApiOperation(() => { }, "/", Array.Empty<string>());

Assert.Null(operation);
Assert.NotNull(operation);
}

[Fact]
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -973,7 +985,8 @@ private static OpenApiOperation GetOpenApiOperation(
string pattern = null,
IEnumerable<string> httpMethods = null,
string displayName = null,
object[] additionalMetadata = null)
object[] additionalMetadata = null,
bool hasMethodInfo = true)
{
var methodInfo = action.Method;
var attributes = methodInfo.GetCustomAttributes();
Expand All @@ -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()
Expand Down