Skip to content
3 changes: 2 additions & 1 deletion src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal static class ApiDescriptionExtensions
public static HttpMethod? GetHttpMethod(this ApiDescription apiDescription) =>
apiDescription.HttpMethod?.ToUpperInvariant() switch
{
// Only add methods documented in the OpenAPI spec: https://spec.openapis.org/oas/v3.1.1.html#path-item-object
// Only add methods documented in the OpenAPI spec: https://spec.openapis.org/oas/v3.2.0.html#path-item-object
"GET" => HttpMethod.Get,
"POST" => HttpMethod.Post,
"PUT" => HttpMethod.Put,
Expand All @@ -29,6 +29,7 @@ internal static class ApiDescriptionExtensions
"HEAD" => HttpMethod.Head,
"OPTIONS" => HttpMethod.Options,
"TRACE" => HttpMethod.Trace,
"QUERY" => HttpMethod.Query,
_ => null,
};

Expand Down
3 changes: 2 additions & 1 deletion src/OpenApi/src/Services/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ internal static class OpenApiConstants
HttpMethod.Options,
HttpMethod.Head,
HttpMethod.Patch,
HttpMethod.Trace
HttpMethod.Trace,
HttpMethod.Query
];
// Represents primitive types that should never be represented as
// a schema reference and always inlined.
Expand Down
25 changes: 13 additions & 12 deletions src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public async Task<OpenApiDocument> GetOpenApiDocumentAsync(IServiceProvider scop
// Sort schemas by key name for better readability and consistency
// This works around an API change in OpenAPI.NET
document.Components.Schemas = new Dictionary<string, IOpenApiSchema>(

document.Components.Schemas.OrderBy(kvp => kvp.Key),
StringComparer.Ordinal);
}
Expand Down Expand Up @@ -418,7 +419,7 @@ private async Task<OpenApiResponse> GetResponseAsync(
var response = new OpenApiResponse
{
Description = apiResponseType.Description ?? ReasonPhrases.GetReasonPhrase(statusCode),
Content = new Dictionary<string, IOpenApiMediaType>()
Content = new Dictionary<string, IOpenApiMediaType> ()
};

// ApiResponseFormats aggregates information about the supported response content types
Expand All @@ -444,7 +445,7 @@ private async Task<OpenApiResponse> GetResponseAsync(
// looks for when generating ApiResponseFormats above so we need to pull the content
// types defined there separately.
var explicitContentTypes = apiDescription.ActionDescriptor.EndpointMetadata
.OfType<ProducesAttribute>()
.OfType<ProducesAttribute> ()
.SelectMany(attr => attr.ContentTypes);
foreach (var contentType in explicitContentTypes)
{
Expand Down Expand Up @@ -567,7 +568,7 @@ private async Task<OpenApiRequestBody> GetFormRequestBody(
{
// Assume "application/x-www-form-urlencoded" as the default media type
// to match the default assumed in IFormFeature.
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/x-www-form-urlencoded" }];
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/x-www-form-urlencoded" } ];
}

var requestBody = new OpenApiRequestBody
Expand All @@ -576,7 +577,7 @@ private async Task<OpenApiRequestBody> GetFormRequestBody(
// serializing a form collection from an empty body. Instead, requiredness
// must be set on a per-parameter basis. See below.
Required = true,
Content = new Dictionary<string, IOpenApiMediaType>()
Content = new Dictionary<string, IOpenApiMediaType> ()
};

var schema = new OpenApiSchema { Type = JsonSchemaType.Object, Properties = new Dictionary<string, IOpenApiSchema>() };
Expand Down Expand Up @@ -633,9 +634,9 @@ private async Task<OpenApiRequestBody> GetFormRequestBody(
// Resolve complex type state from endpoint metadata when checking for
// minimal API types to use trim friendly code paths.
var isComplexType = endpointMetadata
.OfType<IParameterBindingMetadata>()
.SingleOrDefault(parameter => parameter.Name == description.Name)?
.HasTryParse == false;
.OfType<IParameterBindingMetadata> ()
.SingleOrDefault(parameter => parameter.Name == description.Name)?.
HasTryParse == false;
if (hasMultipleFormParameters)
{
// Here and below: POCOs do not need to be need under their parameter name in the grouping.
Expand Down Expand Up @@ -731,26 +732,26 @@ private async Task<OpenApiRequestBody> GetJsonRequestBody(
{
// Assume "application/octet-stream" as the default media type
// for stream-based parameter types.
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/octet-stream" }];
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/octet-stream" } ];
}
else if (bodyParameter.Type.IsJsonPatchDocument())
{
// Assume "application/json-patch+json" as the default media type
// for JSON Patch documents.
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/json-patch+json" }];
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/json-patch+json" } ];
}
else
{
// Assume "application/json" as the default media type
// for everything else.
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/json" }];
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/json" } ];
}
}

var requestBody = new OpenApiRequestBody
{
Required = IsRequired(bodyParameter),
Content = new Dictionary<string, IOpenApiMediaType>(),
Content = new Dictionary<string, IOpenApiMediaType> (),
Description = GetParameterDescriptionFromAttribute(bodyParameter)
};

Expand Down Expand Up @@ -782,7 +783,7 @@ private async Task<OpenApiRequestBody> GetJsonRequestBody(
private static Type GetTargetType(ApiDescription description, ApiParameterDescription parameter)
{
var bindingMetadata = description.ActionDescriptor.EndpointMetadata
.OfType<IParameterBindingMetadata>()
.OfType<IParameterBindingMetadata> ()
.SingleOrDefault(metadata => metadata.Name == parameter.Name);
var parameterType = parameter.Type is not null
? Nullable.GetUnderlyingType(parameter.Type) ?? parameter.Type
Expand Down
3 changes: 2 additions & 1 deletion src/OpenApi/src/Services/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,13 @@ private OpenApiOperation GetOperation(string httpMethod, MethodInfo methodInfo,

static bool ShouldDisableInferredBody(string method)
{
// GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies
// GET, DELETE, HEAD, CONNECT, TRACE, QUERY and OPTIONS normally do not contain bodies
return method.Equals(HttpMethods.Get, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Delete, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Head, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Options, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Trace, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Query, StringComparison.Ordinal) ||
method.Equals(HttpMethods.Connect, StringComparison.Ordinal);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ public static class HttpMethodTestData
new object[] { "HEAD", HttpMethod.Head },
new object[] { "OPTIONS", HttpMethod.Options },
new object[] { "TRACE", HttpMethod.Trace },
new object[] { "gEt", HttpMethod.Get }, // Test case-insensitivity
new object[] { "QUERY", HttpMethod.Query },
new object[] { "gEt", HttpMethod.Get }, // Test case-insensitivity
};
}

Expand All @@ -88,4 +89,20 @@ public void GetHttpMethod_ReturnsHttpMethodForApiDescription(string httpMethod,
// Assert
Assert.Equal(expectedHttpMethod, result);
}

[Fact]
public void GetHttpMethod_ReturnsNullForUnsupportedMethod()
{
// Arrange - Test that unsupported HTTP methods return null
var apiDescription = new ApiDescription
{
HttpMethod = "UNSUPPORTED"
};

// Act
var result = apiDescription.GetHttpMethod();

// Assert
Assert.Null(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,37 @@
}
}
}
},
"/query": {
"x-oai-additionalOperations": {
"QUERY": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
}
},
"components": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,37 @@
}
}
}
},
"/query": {
"x-oai-additionalOperations": {
"QUERY": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
}
},
"components": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,35 @@
}
}
}
},
"/query": {
"query": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
},
"components": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,37 @@
}
}
}
},
"/query": {
"x-oai-additionalOperations": {
"QUERY": {
"tags": [
"Test"
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CurrentWeather"
}
}
}
}
}
}
}
}
},
"components": {
Expand Down
Loading