Skip to content

Commit d9ac5f4

Browse files
Add support for stating accepts/consumes metadata is required or not (dotnet#35875)
1 parent d631d7f commit d9ac5f4

File tree

10 files changed

+284
-133
lines changed

10 files changed

+284
-133
lines changed

src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,10 @@ public interface IAcceptsMetadata
2121
/// Gets the type being read from the request.
2222
/// </summary>
2323
Type? RequestType { get; }
24+
25+
/// <summary>
26+
/// Gets a value that determines if the request body is optional.
27+
/// </summary>
28+
bool IsOptional { get; }
2429
}
2530
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Microsoft.AspNetCore.Http.IResult
1010
Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
1111
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata
1212
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList<string!>!
13+
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.IsOptional.get -> bool
1314
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.RequestType.get -> System.Type?
1415
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata
1516
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ public static partial class RequestDelegateFactory
6262
private static ParameterExpression TempSourceStringExpr => TryParseMethodCache.TempSourceStringExpr;
6363
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
6464
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
65-
66-
private static readonly AcceptsMetadata DefaultAcceptsMetadata = new(new[] { "application/json" });
65+
private static readonly string[] DefaultAcceptsContentType = new[] { "application/json" };
6766

6867
/// <summary>
6968
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="handler"/>.
@@ -879,11 +878,11 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
879878
}
880879
}
881880

882-
factoryContext.Metadata.Add(DefaultAcceptsMetadata);
883881
var isOptional = IsOptionalParameter(parameter, factoryContext);
884882

885883
factoryContext.JsonRequestBodyType = parameter.ParameterType;
886884
factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional;
885+
factoryContext.Metadata.Add(new AcceptsMetadata(parameter.ParameterType, factoryContext.AllowEmptyRequestBody, DefaultAcceptsContentType));
887886

888887
if (!factoryContext.AllowEmptyRequestBody)
889888
{

src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,13 @@ public void MapPost_BuildsEndpointWithCorrectEndpointMetadata()
290290
// Trigger Endpoint build by calling getter.
291291
var endpoint = Assert.Single(dataSource.Endpoints);
292292

293-
var endpointMetadata = endpoint.Metadata.GetOrderedMetadata<IAcceptsMetadata>();
293+
var endpointMetadata = endpoint.Metadata.GetMetadata<IAcceptsMetadata>();
294+
294295
Assert.NotNull(endpointMetadata);
295-
Assert.Equal(2, endpointMetadata.Count);
296+
Assert.False(endpointMetadata!.IsOptional);
297+
Assert.Equal(typeof(Todo), endpointMetadata.RequestType);
298+
Assert.Equal(new[] { "application/xml" }, endpointMetadata.ContentTypes);
296299

297-
var lastAddedMetadata = endpointMetadata[^1];
298-
299-
Assert.Equal(typeof(Todo), lastAddedMetadata.RequestType);
300-
Assert.Equal(new[] { "application/xml" }, lastAddedMetadata.ContentTypes);
301300
}
302301

303302
[Fact]
@@ -567,13 +566,13 @@ public TestConsumesAttribute(Type requestType, string contentType, params string
567566
}
568567

569568
IReadOnlyList<string> IAcceptsMetadata.ContentTypes => _contentTypes;
570-
571569
Type? IAcceptsMetadata.RequestType => _requestType;
572570

571+
bool IAcceptsMetadata.IsOptional => false;
572+
573573
Type? _requestType;
574574

575575
List<string> _contentTypes = new();
576-
577576
}
578577

579578
class Todo

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,6 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
102102
},
103103
};
104104

105-
var hasJsonBody = false;
106-
107105
foreach (var parameter in methodInfo.GetParameters())
108106
{
109107
var parameterDescription = CreateApiParameterDescription(parameter, routeEndpoint.RoutePattern);
@@ -113,33 +111,37 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
113111
continue;
114112
}
115113

116-
if (parameterDescription.Source == BindingSource.Body)
117-
{
118-
hasJsonBody = true;
119-
}
120-
121114
apiDescription.ParameterDescriptions.Add(parameterDescription);
122115
}
123116

124-
// Get custom attributes for the handler. ConsumesAttribute is one of the examples.
125-
var acceptsRequestType = routeEndpoint.Metadata.GetMetadata<IAcceptsMetadata>()?.RequestType;
126-
if (acceptsRequestType is not null)
117+
// Get IAcceptsMetadata.
118+
var acceptsMetadata = routeEndpoint.Metadata.GetMetadata<IAcceptsMetadata>();
119+
if (acceptsMetadata is not null)
127120
{
121+
var acceptsRequestType = acceptsMetadata.RequestType;
122+
var isOptional = acceptsMetadata.IsOptional;
128123
var parameterDescription = new ApiParameterDescription
129124
{
130-
Name = acceptsRequestType.Name,
131-
ModelMetadata = CreateModelMetadata(acceptsRequestType),
125+
Name = acceptsRequestType is not null ? acceptsRequestType.Name : typeof(void).Name,
126+
ModelMetadata = CreateModelMetadata(acceptsRequestType ?? typeof(void)),
132127
Source = BindingSource.Body,
133-
Type = acceptsRequestType,
134-
IsRequired = true,
128+
Type = acceptsRequestType ?? typeof(void),
129+
IsRequired = !isOptional,
135130
};
136-
137131
apiDescription.ParameterDescriptions.Add(parameterDescription);
132+
133+
var supportedRequestFormats = apiDescription.SupportedRequestFormats;
134+
135+
foreach (var contentType in acceptsMetadata.ContentTypes)
136+
{
137+
supportedRequestFormats.Add(new ApiRequestFormat
138+
{
139+
MediaType = contentType
140+
});
141+
}
138142
}
139143

140-
AddSupportedRequestFormats(apiDescription.SupportedRequestFormats, hasJsonBody, routeEndpoint.Metadata);
141144
AddSupportedResponseTypes(apiDescription.SupportedResponseTypes, methodInfo.ReturnType, routeEndpoint.Metadata);
142-
143145
AddActionDescriptorEndpointMetadata(apiDescription.ActionDescriptor, routeEndpoint.Metadata);
144146

145147
return apiDescription;
@@ -150,7 +152,8 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
150152
var (source, name, allowEmpty) = GetBindingSourceAndName(parameter, pattern);
151153

152154
// Services are ignored because they are not request parameters.
153-
if (source == BindingSource.Services)
155+
// We ignore/skip body parameter because the value will be retrieved from the IAcceptsMetadata.
156+
if (source == BindingSource.Services || source == BindingSource.Body)
154157
{
155158
return null;
156159
}
@@ -222,33 +225,6 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
222225
}
223226
}
224227

225-
private static void AddSupportedRequestFormats(
226-
IList<ApiRequestFormat> supportedRequestFormats,
227-
bool hasJsonBody,
228-
EndpointMetadataCollection endpointMetadata)
229-
{
230-
var requestMetadata = endpointMetadata.GetOrderedMetadata<IApiRequestMetadataProvider>();
231-
var declaredContentTypes = DefaultApiDescriptionProvider.GetDeclaredContentTypes(requestMetadata);
232-
233-
if (declaredContentTypes.Count > 0)
234-
{
235-
foreach (var contentType in declaredContentTypes)
236-
{
237-
supportedRequestFormats.Add(new ApiRequestFormat
238-
{
239-
MediaType = contentType,
240-
});
241-
}
242-
}
243-
else if (hasJsonBody)
244-
{
245-
supportedRequestFormats.Add(new ApiRequestFormat
246-
{
247-
MediaType = "application/json",
248-
});
249-
}
250-
}
251-
252228
private static void AddSupportedResponseTypes(
253229
IList<ApiResponseType> supportedResponseTypes,
254230
Type returnType,

0 commit comments

Comments
 (0)