Skip to content

Improve Model Binding for HTTP Request Body as Stream #41426

Closed
@commonsensesoftware

Description

@commonsensesoftware

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

When building a web API, authors occasionally need to support file uploads. There are two possible ways this can be achieved today:

  1. Process HttpContext.Request.Body directly
  2. Use model binding with IFormFile or IFormFileCollection

This issue exists for traditional controller actions as well as Minimal APIs

Option 1 - Process Request Body Directly

This implementation will correctly upload a file; however, HttpRequest has special binding semantics and will not modeled by the API Explorer, making it impossible to use with OpenAPI without additional work on the developer's part. Even if HttpRequest was explored, it would be incorrect because the intent is to document the HTTP body and not the entire HTTP request.

app.MapPost(
    "order/import",
    async (
        HttpRequest request,
        [FromHeader( Name = "Content-Disposition" )] string contentDisposition,
        CancellationToken cancellationToken) =>
    {
    	if (!ContentDispositionHeaderValue.TryParse(contentDisposition, out var header) ||
    		!header.FileName.HasValue)
        {
            return Results.BadRequest();
        }

        var source = request.Body;
        var path = Path.Combine( Path.GetTempPath(), "Quarantine", header.FileName.Value );
        using var destination = new FileStream( path, FileMode.Create );

        await source.CopyToAsync( destination, cancellationToken );
        await destination.FlushAsync( cancellationToken );
        destination.Seek(0L, SeekOrigin.Begin);

        var id = await GetOrderId(id, cancellationToken);
        var scheme = request.Scheme;
        var host = request.Host;
        var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/order/{id}" );

        return Results.Created( location, default );
    })
    .Accepts<Stream>( "application/pdf" )
    .Produces( 201 );

Option 2 - Use IFormFile or IFormFileCollection

This is a common approach which uses multipart/form-data as the media type to upload one or more files. As specified in RFC 7578, this approach is intended for HTML forms. When a HTML <form> sends a POST back to the server with its key/value pairs, it needs a way to include other content, such as files, in a distinctly separate way - e.g. multipart.

While this approach can work for a web API, it is unnecessary. A web API should not have to use HTML semantics to upload a file. Moreover, this approach requires clients to format request bodies in a particular way thus changing the wire protocol of the API.

The following is a completely valid file upload:

POST /message HTTP/2
Host: my.microblog.com
Content-Type: text/plain
Content-Length: 42
Content-Disposition: inline; filename="infomericial.txt"

I'm a Pepper! Wouldn't you like to be a Pepper too?

The following is also a valid file upload:

POST /message HTTP/2
Host: my.microblog.com
Content-Type: application/json
Content-Length: 42
Content-Disposition: inline; name="infomericial"

{"message": "I'm a Pepper! Wouldn't you like to be a Pepper too?"}

Describe the solution you'd like

Any HTTP request with a body can potentially be considered a file upload. "There can be only one" parameter bound to the HTTP request body.

For traditional, controller actions, an argument defined as [FromBody] Stream body, where the name body is irrelevant, should be handled by BodyModelBinderProvider and BodyModelBinder. The current implementation does not allow zero InputFormatter instances, but it should. Binding to Stream should be considered a special case and should be bound when the following are true:

  1. ModelBinderProviderContext.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body)
  2. context.Metadata.ModelType.Equals(typeof(Stream))

InputFormatter instances need not be considered. The onus of understanding and processing the content is on the developer who makes a conscience decision to use this setup. A developer can specify which file types are allowed by declaratively using [Consumes] or imperatively asserting the content.

For Minimal APIs, a method parameter defined as Stream body or [FromBody] Stream body should be sufficient. A developer can specify which file types are allowed by declaratively using Accepts or imperatively asserting the content.

Additional context

Related to #4868

There should also be better support multipart/*; specifically for file uploads. This feature request has general applicability beyond file uploads so I'll track that as a separate issue.

Metadata

Metadata

Assignees

Labels

EpicGroups multiple user stories. Can be grouped under a theme.old-area-web-frameworks-do-not-use*DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions