diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs
index a3325158a7ac..ce20bb9fdce5 100644
--- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs
+++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs
@@ -21,5 +21,10 @@ public interface IAcceptsMetadata
/// Gets the type being read from the request.
///
Type? RequestType { get; }
+
+ ///
+ /// Gets a value that determines if the request body is optional.
+ ///
+ bool IsOptional { get; }
}
}
diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
index 86257d4625ca..b0d505496ade 100644
--- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
@@ -10,6 +10,7 @@ Microsoft.AspNetCore.Http.IResult
Microsoft.AspNetCore.Http.IResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList!
+Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.IsOptional.get -> bool
Microsoft.AspNetCore.Http.Metadata.IAcceptsMetadata.RequestType.get -> System.Type?
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata
Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata.AllowEmpty.get -> bool
diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
index ff3bde84039a..8d6c4f3389c1 100644
--- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
+++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
@@ -62,8 +62,7 @@ public static partial class RequestDelegateFactory
private static ParameterExpression TempSourceStringExpr => TryParseMethodCache.TempSourceStringExpr;
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
-
- private static readonly AcceptsMetadata DefaultAcceptsMetadata = new(new[] { "application/json" });
+ private static readonly string[] DefaultAcceptsContentType = new[] { "application/json" };
///
/// Creates a implementation for .
@@ -879,11 +878,11 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
}
}
- factoryContext.Metadata.Add(DefaultAcceptsMetadata);
var isOptional = IsOptionalParameter(parameter, factoryContext);
factoryContext.JsonRequestBodyType = parameter.ParameterType;
factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional;
+ factoryContext.Metadata.Add(new AcceptsMetadata(parameter.ParameterType, factoryContext.AllowEmptyRequestBody, DefaultAcceptsContentType));
if (!factoryContext.AllowEmptyRequestBody)
{
diff --git a/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs
index 1d222785a013..49e92ad1f0e0 100644
--- a/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs
+++ b/src/Http/Routing/test/UnitTests/Builder/DelegateEndpointRouteBuilderExtensionsTest.cs
@@ -290,14 +290,13 @@ public void MapPost_BuildsEndpointWithCorrectEndpointMetadata()
// Trigger Endpoint build by calling getter.
var endpoint = Assert.Single(dataSource.Endpoints);
- var endpointMetadata = endpoint.Metadata.GetOrderedMetadata();
+ var endpointMetadata = endpoint.Metadata.GetMetadata();
+
Assert.NotNull(endpointMetadata);
- Assert.Equal(2, endpointMetadata.Count);
+ Assert.False(endpointMetadata!.IsOptional);
+ Assert.Equal(typeof(Todo), endpointMetadata.RequestType);
+ Assert.Equal(new[] { "application/xml" }, endpointMetadata.ContentTypes);
- var lastAddedMetadata = endpointMetadata[^1];
-
- Assert.Equal(typeof(Todo), lastAddedMetadata.RequestType);
- Assert.Equal(new[] { "application/xml" }, lastAddedMetadata.ContentTypes);
}
[Fact]
@@ -567,13 +566,13 @@ public TestConsumesAttribute(Type requestType, string contentType, params string
}
IReadOnlyList IAcceptsMetadata.ContentTypes => _contentTypes;
-
Type? IAcceptsMetadata.RequestType => _requestType;
+ bool IAcceptsMetadata.IsOptional => false;
+
Type? _requestType;
List _contentTypes = new();
-
}
class Todo
diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
index 03c86fe3aacf..b3cf26a7d728 100644
--- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
+++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
@@ -102,8 +102,6 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
},
};
- var hasJsonBody = false;
-
foreach (var parameter in methodInfo.GetParameters())
{
var parameterDescription = CreateApiParameterDescription(parameter, routeEndpoint.RoutePattern);
@@ -113,33 +111,37 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
continue;
}
- if (parameterDescription.Source == BindingSource.Body)
- {
- hasJsonBody = true;
- }
-
apiDescription.ParameterDescriptions.Add(parameterDescription);
}
- // Get custom attributes for the handler. ConsumesAttribute is one of the examples.
- var acceptsRequestType = routeEndpoint.Metadata.GetMetadata()?.RequestType;
- if (acceptsRequestType is not null)
+ // Get IAcceptsMetadata.
+ var acceptsMetadata = routeEndpoint.Metadata.GetMetadata();
+ if (acceptsMetadata is not null)
{
+ var acceptsRequestType = acceptsMetadata.RequestType;
+ var isOptional = acceptsMetadata.IsOptional;
var parameterDescription = new ApiParameterDescription
{
- Name = acceptsRequestType.Name,
- ModelMetadata = CreateModelMetadata(acceptsRequestType),
+ Name = acceptsRequestType is not null ? acceptsRequestType.Name : typeof(void).Name,
+ ModelMetadata = CreateModelMetadata(acceptsRequestType ?? typeof(void)),
Source = BindingSource.Body,
- Type = acceptsRequestType,
- IsRequired = true,
+ Type = acceptsRequestType ?? typeof(void),
+ IsRequired = !isOptional,
};
-
apiDescription.ParameterDescriptions.Add(parameterDescription);
+
+ var supportedRequestFormats = apiDescription.SupportedRequestFormats;
+
+ foreach (var contentType in acceptsMetadata.ContentTypes)
+ {
+ supportedRequestFormats.Add(new ApiRequestFormat
+ {
+ MediaType = contentType
+ });
+ }
}
- AddSupportedRequestFormats(apiDescription.SupportedRequestFormats, hasJsonBody, routeEndpoint.Metadata);
AddSupportedResponseTypes(apiDescription.SupportedResponseTypes, methodInfo.ReturnType, routeEndpoint.Metadata);
-
AddActionDescriptorEndpointMetadata(apiDescription.ActionDescriptor, routeEndpoint.Metadata);
return apiDescription;
@@ -150,7 +152,8 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
var (source, name, allowEmpty) = GetBindingSourceAndName(parameter, pattern);
// Services are ignored because they are not request parameters.
- if (source == BindingSource.Services)
+ // We ignore/skip body parameter because the value will be retrieved from the IAcceptsMetadata.
+ if (source == BindingSource.Services || source == BindingSource.Body)
{
return null;
}
@@ -222,33 +225,6 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
}
}
- private static void AddSupportedRequestFormats(
- IList supportedRequestFormats,
- bool hasJsonBody,
- EndpointMetadataCollection endpointMetadata)
- {
- var requestMetadata = endpointMetadata.GetOrderedMetadata();
- var declaredContentTypes = DefaultApiDescriptionProvider.GetDeclaredContentTypes(requestMetadata);
-
- if (declaredContentTypes.Count > 0)
- {
- foreach (var contentType in declaredContentTypes)
- {
- supportedRequestFormats.Add(new ApiRequestFormat
- {
- MediaType = contentType,
- });
- }
- }
- else if (hasJsonBody)
- {
- supportedRequestFormats.Add(new ApiRequestFormat
- {
- MediaType = "application/json",
- });
- }
- }
-
private static void AddSupportedResponseTypes(
IList supportedResponseTypes,
Type returnType,
diff --git a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
index ea6a51351f72..12179ee8bfd8 100644
--- a/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
+++ b/src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
@@ -51,23 +51,6 @@ public void UsesApplicationNameAsControllerNameIfNoDeclaringType()
Assert.Equal(nameof(EndpointMetadataApiDescriptionProviderTest), apiDescription.ActionDescriptor.RouteValues["controller"]);
}
- [Fact]
- public void AddsJsonRequestFormatWhenFromBodyInferred()
- {
- static void AssertJsonRequestFormat(ApiDescription apiDescription)
- {
- var requestFormat = Assert.Single(apiDescription.SupportedRequestFormats);
- Assert.Equal("application/json", requestFormat.MediaType);
- Assert.Null(requestFormat.Formatter);
- }
-
- AssertJsonRequestFormat(GetApiDescription(
- (InferredJsonClass fromBody) => { }));
-
- AssertJsonRequestFormat(GetApiDescription(
- ([FromBody] int fromBody) => { }));
- }
-
[Fact]
public void AddsRequestFormatFromMetadata()
{
@@ -109,11 +92,27 @@ public void AddsMultipleRequestFormatsFromMetadata()
}
[Fact]
- public void AddsMultipleRequestFormatsFromMetadataWithRequestType()
+ public void AddsMultipleRequestFormatsFromMetadataWithRequestTypeAndOptionalBodyParameter()
+ {
+ var apiDescription = GetApiDescription(
+ [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = true)]
+ () =>
+ { });
+
+ Assert.Equal(2, apiDescription.SupportedRequestFormats.Count);
+
+ var apiParameterDescription = apiDescription.ParameterDescriptions[0];
+ Assert.Equal("InferredJsonClass", apiParameterDescription.Type.Name);
+ Assert.False(apiParameterDescription.IsRequired);
+ }
+
+ [Fact]
+ public void AddsMultipleRequestFormatsFromMetadataWithRequiredBodyParameter()
{
var apiDescription = GetApiDescription(
- [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1")]
- () => { });
+ [Consumes(typeof(InferredJsonClass), "application/custom0", "application/custom1", IsOptional = false)]
+ (InferredJsonClass fromBody) =>
+ { });
Assert.Equal(2, apiDescription.SupportedRequestFormats.Count);
@@ -318,14 +317,11 @@ public void DoesNotAddFromServiceParameterAsService()
}
[Fact]
- public void AddsFromBodyParameterAsBody()
+ public void DoesNotAddFromBodyParameterInTheParameterDescription()
{
static void AssertBodyParameter(ApiDescription apiDescription, Type expectedType)
{
- var param = Assert.Single(apiDescription.ParameterDescriptions);
- Assert.Equal(expectedType, param.Type);
- Assert.Equal(expectedType, param.ModelMetadata.ModelType);
- Assert.Equal(BindingSource.Body, param.Source);
+ Assert.Empty(apiDescription.ParameterDescriptions);
}
AssertBodyParameter(GetApiDescription((InferredJsonClass foo) => { }), typeof(InferredJsonClass));
@@ -345,7 +341,7 @@ public void AddsDefaultValueFromParameters()
public void AddsMultipleParameters()
{
var apiDescription = GetApiDescription(([FromRoute] int foo, int bar, InferredJsonClass fromBody) => { });
- Assert.Equal(3, apiDescription.ParameterDescriptions.Count);
+ Assert.Equal(2, apiDescription.ParameterDescriptions.Count);
var fooParam = apiDescription.ParameterDescriptions[0];
Assert.Equal(typeof(int), fooParam.Type);
@@ -358,12 +354,6 @@ public void AddsMultipleParameters()
Assert.Equal(typeof(int), barParam.ModelMetadata.ModelType);
Assert.Equal(BindingSource.Query, barParam.Source);
Assert.True(barParam.IsRequired);
-
- var fromBodyParam = apiDescription.ParameterDescriptions[2];
- Assert.Equal(typeof(InferredJsonClass), fromBodyParam.Type);
- Assert.Equal(typeof(InferredJsonClass), fromBodyParam.ModelMetadata.ModelType);
- Assert.Equal(BindingSource.Body, fromBodyParam.Source);
- Assert.False(fromBodyParam.IsRequired); // Reference type in oblivious nullability context
}
[Fact]
@@ -385,34 +375,6 @@ public void TestParameterIsRequired()
Assert.False(barParam.IsRequired);
}
-#nullable enable
-
- [Fact]
- public void TestIsRequiredFromBody()
- {
- var apiDescription0 = GetApiDescription(([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] InferredJsonClass fromBody) => { });
- var apiDescription1 = GetApiDescription((InferredJsonClass? fromBody) => { });
- Assert.Equal(1, apiDescription0.ParameterDescriptions.Count);
- Assert.Equal(1, apiDescription1.ParameterDescriptions.Count);
-
- var fromBodyParam0 = apiDescription0.ParameterDescriptions[0];
- Assert.Equal(typeof(InferredJsonClass), fromBodyParam0.Type);
- Assert.Equal(typeof(InferredJsonClass), fromBodyParam0.ModelMetadata.ModelType);
- Assert.Equal(BindingSource.Body, fromBodyParam0.Source);
- Assert.False(fromBodyParam0.IsRequired);
-
- var fromBodyParam1 = apiDescription1.ParameterDescriptions[0];
- Assert.Equal(typeof(InferredJsonClass), fromBodyParam1.Type);
- Assert.Equal(typeof(InferredJsonClass), fromBodyParam1.ModelMetadata.ModelType);
- Assert.Equal(BindingSource.Body, fromBodyParam1.Source);
- Assert.False(fromBodyParam1.IsRequired);
- }
-
- // This is necessary for TestIsRequiredFromBody to pass until https://github.com/dotnet/roslyn/issues/55254 is resolved.
- private object RandomMethod() => throw new NotImplementedException();
-
-#nullable disable
-
[Fact]
public void AddsDisplayNameFromRouteEndpoint()
{
@@ -654,6 +616,148 @@ public void HandleAcceptsMetadata()
});
}
+ [Fact]
+ public void HandleAcceptsMetadataWithTypeParameter()
+ {
+ // Arrange
+ var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(null));
+ builder.MapPost("/api/todos", (InferredJsonClass inferredJsonClass) => "")
+ .Accepts(typeof(InferredJsonClass), "application/json");
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
+
+ // Act
+ provider.OnProvidersExecuting(context);
+ provider.OnProvidersExecuted(context);
+
+ // Assert
+ var parameterDescriptions = context.Results.SelectMany(r => r.ParameterDescriptions);
+ var bodyParameterDescription = parameterDescriptions.Single();
+ Assert.Equal(typeof(InferredJsonClass), bodyParameterDescription.Type);
+ Assert.Equal(typeof(InferredJsonClass).Name, bodyParameterDescription.Name);
+ Assert.True(bodyParameterDescription.IsRequired);
+ }
+
+#nullable enable
+
+ [Fact]
+ public void HandleDefaultIAcceptsMetadataForRequiredBodyParameter()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
+ builder.MapPost("/api/todos", (InferredJsonClass inferredJsonClass) => "");
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
+
+ // Act
+ provider.OnProvidersExecuting(context);
+ provider.OnProvidersExecuted(context);
+
+ // Assert
+ var parameterDescriptions = context.Results.SelectMany(r => r.ParameterDescriptions);
+ var bodyParameterDescription = parameterDescriptions.Single();
+ Assert.Equal(typeof(InferredJsonClass), bodyParameterDescription.Type);
+ Assert.Equal(typeof(InferredJsonClass).Name, bodyParameterDescription.Name);
+ Assert.True(bodyParameterDescription.IsRequired);
+
+ // Assert
+ var requestFormats = context.Results.SelectMany(r => r.SupportedRequestFormats);
+ var defaultRequestFormat = requestFormats.Single();
+ Assert.Equal("application/json", defaultRequestFormat.MediaType);
+ }
+
+#nullable restore
+
+#nullable enable
+
+ [Fact]
+ public void HandleDefaultIAcceptsMetadataForOptionalBodyParameter()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
+ builder.MapPost("/api/todos", (InferredJsonClass? inferredJsonClass) => "");
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
+
+ // Act
+ provider.OnProvidersExecuting(context);
+ provider.OnProvidersExecuted(context);
+
+ // Assert
+ var parameterDescriptions = context.Results.SelectMany(r => r.ParameterDescriptions);
+ var bodyParameterDescription = parameterDescriptions.Single();
+ Assert.Equal(typeof(InferredJsonClass), bodyParameterDescription.Type);
+ Assert.Equal(typeof(InferredJsonClass).Name, bodyParameterDescription.Name);
+ Assert.False(bodyParameterDescription.IsRequired);
+
+ // Assert
+ var requestFormats = context.Results.SelectMany(r => r.SupportedRequestFormats);
+ var defaultRequestFormat = requestFormats.Single();
+ Assert.Equal("application/json", defaultRequestFormat.MediaType);
+ }
+
+#nullable restore
+
+#nullable enable
+
+ [Fact]
+ public void HandleIAcceptsMetadataWithConsumesAttributeAndInferredOptionalFromBodyType()
+ {
+ // Arrange
+ var services = new ServiceCollection();
+ var serviceProvider = services.BuildServiceProvider();
+ var builder = new TestEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
+ builder.MapPost("/api/todos", [Consumes("application/xml")] (InferredJsonClass? inferredJsonClass) => "");
+ var context = new ApiDescriptionProviderContext(Array.Empty());
+
+ var endpointDataSource = builder.DataSources.OfType().Single();
+ var hostEnvironment = new HostEnvironment
+ {
+ ApplicationName = nameof(EndpointMetadataApiDescriptionProviderTest)
+ };
+ var provider = new EndpointMetadataApiDescriptionProvider(endpointDataSource, hostEnvironment, new ServiceProviderIsService());
+
+ // Act
+ provider.OnProvidersExecuting(context);
+ provider.OnProvidersExecuted(context);
+
+ // Assert
+ var parameterDescriptions = context.Results.SelectMany(r => r.ParameterDescriptions);
+ var bodyParameterDescription = parameterDescriptions.Single();
+ Assert.Equal(typeof(void), bodyParameterDescription.Type);
+ Assert.Equal(typeof(void).Name, bodyParameterDescription.Name);
+ Assert.True(bodyParameterDescription.IsRequired);
+
+ // Assert
+ var requestFormats = context.Results.SelectMany(r => r.SupportedRequestFormats);
+ var defaultRequestFormat = requestFormats.Single();
+ Assert.Equal("application/xml", defaultRequestFormat.MediaType);
+ }
+
+#nullable restore
+
private static IEnumerable GetSortedMediaTypes(ApiResponseType apiResponseType)
{
return apiResponseType.ApiResponseFormats
diff --git a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs
index 08607ce5bd22..a233aa8c8a51 100644
--- a/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs
+++ b/src/Mvc/Mvc.Core/src/Builder/OpenApiEndpointConventionBuilderExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
@@ -122,16 +123,16 @@ public static DelegateEndpointConventionBuilder ProducesValidationProblem(this D
}
///
- /// Adds the to for all builders
+ /// Adds to for all builders
/// produced by .
///
- /// The type of the request.
+ /// The type of the request body.
/// The .
- /// The request content type. Defaults to "application/json" if empty.
- /// Additional response content types the endpoint produces for the supplied status code.
+ /// The request content type that the endpoint accepts.
+ /// The list of additional request content types that the endpoint accepts.
/// A that can be used to further customize the endpoint.
public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder,
- string contentType, params string[] additionalContentTypes)
+ string contentType, params string[] additionalContentTypes) where TRequest : notnull
{
Accepts(builder, typeof(TRequest), contentType, additionalContentTypes);
@@ -139,19 +140,68 @@ public static DelegateEndpointConventionBuilder Accepts(this DelegateE
}
///
- /// Adds the to for all builders
+ /// Adds to for all builders
/// produced by .
///
+ /// The type of the request body.
/// The .
- /// The type of the request. Defaults to null.
- /// The response content type that the endpoint accepts.
- /// Additional response content types the endpoint accepts
+ /// Sets a value that determines if the request body is optional.
+ /// The request content type that the endpoint accepts.
+ /// The list of additional request content types that the endpoint accepts.
+ /// A that can be used to further customize the endpoint.
+ public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder,
+ bool isOptional, string contentType, params string[] additionalContentTypes) where TRequest : notnull
+ {
+ Accepts(builder, typeof(TRequest), isOptional, contentType, additionalContentTypes);
+
+ return builder;
+ }
+
+ ///
+ /// Adds to for all builders
+ /// produced by .
+ ///
+ /// The .
+ /// The type of the request body.
+ /// The request content type that the endpoint accepts.
+ /// The list of additional request content types that the endpoint accepts.
/// A that can be used to further customize the endpoint.
public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder,
Type requestType, string contentType, params string[] additionalContentTypes)
{
- builder.WithMetadata(new ConsumesAttribute(requestType, contentType, additionalContentTypes));
+ builder.WithMetadata(new AcceptsMetadata(requestType, false, GetAllContentTypes(contentType, additionalContentTypes)));
+ return builder;
+ }
+
+
+ ///
+ /// Adds to for all builders
+ /// produced by .
+ ///
+ /// The .
+ /// The type of the request body.
+ /// Sets a value that determines if the request body is optional.
+ /// The request content type that the endpoint accepts.
+ /// The list of additional request content types that the endpoint accepts.
+ /// A that can be used to further customize the endpoint.
+ public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder,
+ Type requestType, bool isOptional, string contentType, params string[] additionalContentTypes)
+ {
+ builder.WithMetadata(new AcceptsMetadata(requestType, isOptional, GetAllContentTypes(contentType, additionalContentTypes)));
return builder;
}
+
+ private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes)
+ {
+ var allContentTypes = new string[additionalContentTypes.Length + 1];
+ allContentTypes[0] = contentType;
+
+ for (var i = 0; i < additionalContentTypes.Length; i++)
+ {
+ allContentTypes[i + 1] = additionalContentTypes[i];
+ }
+
+ return allContentTypes;
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs b/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs
index 817e54a724d5..f7ba8e772279 100644
--- a/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs
+++ b/src/Mvc/Mvc.Core/src/ConsumesAttribute.cs
@@ -35,8 +35,8 @@ public class ConsumesAttribute :
///
/// Creates a new instance of .
- /// The request content type
- /// The additional list of allowed request content types
+ /// The request content type.
+ /// The additional list of allowed request content types.
///
public ConsumesAttribute(string contentType, params string[] otherContentTypes)
{
@@ -55,13 +55,14 @@ public ConsumesAttribute(string contentType, params string[] otherContentTypes)
}
ContentTypes = GetContentTypes(contentType, otherContentTypes);
+ _contentTypes = GetAllContentTypes(contentType, otherContentTypes);
}
///
/// Creates a new instance of .
- /// The type being read from the request
- /// The request content type
- /// The additional list of allowed request content types
+ /// The type being read from the request.
+ /// The request content type.
+ /// The additional list of allowed request content types.
///
public ConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes)
{
@@ -96,6 +97,12 @@ public ConsumesAttribute(Type requestType, string contentType, params string[] o
///
public MediaTypeCollection ContentTypes { get; set; }
+ ///
+ /// Gets or sets a value that determines if the request body is optional.
+ /// This value is only used to specify if the request body is required in API explorer.
+ ///
+ public bool IsOptional { get; set; }
+
readonly Type? _requestType;
readonly List _contentTypes = new();
diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt
index 80e9d26d26a6..bd730b121109 100644
--- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt
+++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt
@@ -529,6 +529,8 @@
*REMOVED*~virtual Microsoft.AspNetCore.Mvc.Routing.UrlHelperBase.IsLocalUrl(string url) -> bool
*REMOVED*~virtual Microsoft.AspNetCore.Mvc.Routing.UrlHelperBase.Link(string routeName, object values) -> string
Microsoft.AspNetCore.Mvc.ConsumesAttribute.ConsumesAttribute(System.Type! requestType, string! contentType, params string![]! otherContentTypes) -> void
+Microsoft.AspNetCore.Mvc.ConsumesAttribute.IsOptional.get -> bool
+Microsoft.AspNetCore.Mvc.ConsumesAttribute.IsOptional.set -> void
Microsoft.AspNetCore.Mvc.JsonOptions.AllowInputFormatterExceptionMessages.get -> bool
Microsoft.AspNetCore.Mvc.JsonOptions.AllowInputFormatterExceptionMessages.set -> void
Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.CreateAsyncReleaser(Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor! descriptor) -> System.Func?
@@ -545,7 +547,9 @@ Microsoft.AspNetCore.Mvc.Infrastructure.ActionDescriptorCollection.Items.get ->
Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(System.Runtime.Serialization.SerializationInfo! info, System.Runtime.Serialization.StreamingContext context) -> void
Microsoft.AspNetCore.Mvc.Infrastructure.AmbiguousActionException.AmbiguousActionException(string? message) -> void
Microsoft.AspNetCore.Mvc.Infrastructure.ContentResultExecutor.ContentResultExecutor(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory! httpResponseStreamWriterFactory) -> void
+static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, System.Type! requestType, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder!
static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, System.Type! requestType, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder!
+static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, bool isOptional, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder!
static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Accepts(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, string! contentType, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder!
static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.ExcludeFromDescription(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder!
static Microsoft.AspNetCore.Http.OpenApiEndpointConventionBuilderExtensions.Produces(this Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder! builder, int statusCode, System.Type? responseType = null, string? contentType = null, params string![]! additionalContentTypes) -> Microsoft.AspNetCore.Builder.DelegateEndpointConventionBuilder!
diff --git a/src/Shared/RoutingMetadata/AcceptsMetadata.cs b/src/Shared/RoutingMetadata/AcceptsMetadata.cs
index eadfd5deb565..3763978a7de7 100644
--- a/src/Shared/RoutingMetadata/AcceptsMetadata.cs
+++ b/src/Shared/RoutingMetadata/AcceptsMetadata.cs
@@ -29,7 +29,7 @@ public AcceptsMetadata(string[] contentTypes)
///
/// Creates a new instance of with a type.
///
- public AcceptsMetadata(Type? type, string[] contentTypes)
+ public AcceptsMetadata(Type? type, bool isOptional, string[] contentTypes)
{
RequestType = type ?? throw new ArgumentNullException(nameof(type));
@@ -39,6 +39,7 @@ public AcceptsMetadata(Type? type, string[] contentTypes)
}
ContentTypes = contentTypes;
+ IsOptional = isOptional;
}
///
@@ -47,8 +48,13 @@ public AcceptsMetadata(Type? type, string[] contentTypes)
public IReadOnlyList ContentTypes { get; }
///
- /// Accepts request content types of any shape.
+ /// Gets the type being read from the request.
///
public Type? RequestType { get; }
+
+ ///
+ /// Gets a value that determines if the request body is optional.
+ ///
+ public bool IsOptional { get; }
}
}