Description
Follow on from #35421
Right now, the implicit request body parameter for an endpoint routing delegate (e.g. the todo
in (Todo? todo) => { }
) will flow whether it's required or not (based on the nullability declaration) to ApiExplorer
(and thus OpenAPI libraries like Swashbuckle/nSwag).
However when manually describing the request body parameter via the .Accepts<TRequest>()
extension methods or the Consumes
attribute, there is no way to describe optionality. For the methods, using Accepts<TRequest?>()
compiles but doesn't flow the nullability declaration as it isn't preserved by the compiler in a way that the method implementation can see that the generic type arg was declared as nullable. For the attribute (and the related metadata interface) there is no member to set that indicates whether the request body parameter is required or not. Right now when ApiExplorer
is populated via this metadata, it's always assumed the body is required.
We should add the ability to describe whether the request body is required or not via the accepts/consumes metadata.
In addition, we should constrain the TRequest
generic type argument on Accept<TRequest>
to be non-nullable, so that users don't attempt to use the nullable reference type syntax there to describe optionality. We can work with the compiler team in an upcoming release to enable that if possible and in that case we can remove the constraint which is a non-breaking change.
Proposed API changes
namespace Microsoft.AspNetCore.Http
{
public static class OpenApiEndpointConventionBuilderExtensions
{
- public static DelegateEndpointConventionBuilder Accepts<TRequest>(this DelegateEndpointConventionBuilder builder, string contentType, params string[] additionalContentTypes);
+ public static DelegateEndpointConventionBuilder Accepts<TRequest>(this DelegateEndpointConventionBuilder builder, string contentType, params string[] additionalContentTypes)
+ where TRequest : notnull;
+ public static DelegateEndpointConventionBuilder Accepts<TRequest>(this DelegateEndpointConventionBuilder builder, bool isRequired, string contentType, params string[] additionalContentTypes)
+ where TRequest : notnull;
public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder, Type requestType, string contentType, params string[] additionalContentTypes);
+ public static DelegateEndpointConventionBuilder Accepts(this DelegateEndpointConventionBuilder builder, Type requestType, bool isRequired, string contentType, params string[] additionalContentTypes);
}
}
namespace Microsoft.AspNetCore.Mvc
{
public class ConsumesAttribute : Attribute, ...
{
public ConsumesAttribute(string contentType, params string[] otherContentTypes);
public ConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes);
+ public bool IsRequired { get; set; } = true;
}
}
namespace Microsoft.AspNetCore.Http.Metadata
{
public interface IAcceptsMetadata
{
IReadOnlyList<string> ContentTypes { get; }
Type? RequestType { get; }
+ bool IsRequired { get; }
}
}
Example usage
app.MapPost("/todo", async (HttpRequest request) =>
{
var todo = await request.Body.ReadAsJsonAsync<Todo>();
return todo is Todo ? Results.Ok(todo) : Results.Ok();
})
.Accepts<Todo>(isRequired: false, contentType: "application/json");
app.MapPost("/todo", HandlePostTodo);
[Consumes(typeof(Todo), "application/json", IsRequired = false)]
IResult HandlePostTodo(HttpRequest request)
{
var todo = await request.Body.ReadAsJsonAsync<Todo>();
return todo is Todo ? Results.Ok(todo) : Results.Ok();
}