Aspnetcore: Issue with multipart/form-data post and json.net deserializtion (using interfaces)

Created on 8 Jan 2018  路  26Comments  路  Source: dotnet/aspnetcore

We use the aspnetcore 2.

We have configured JsonOptions in startup like this.

services.AddMvc().AddJsonOptions(options =>
            {
                options.SerializerSettings.ContractResolver = new DefaultContractResolver();
                options.SerializerSettings.Converters = new List<JsonConverter> {
                    new JsonDocumentConverter(),
                    new JsonDocumentMetaConverter(),
                    //new Bll.FileRecords.JsonDocumentMetaListConverter(),
                };
            });

public class JsonDocumentConverter : CustomCreationConverter<IDocument>
    {
        public override IDocument Create(Type objectType)
        {
            return new LocalDocument();
        }
    }
public class JsonDocumentMetaConverter : CustomCreationConverter<IDocumentMeta>
    {
        public override IDocumentMeta Create(Type objectType)
        {
            return new LocalDocumentMeta();
        }
    }

In our controller we have 2 methods

  1. The first one is called PostUrlencoded in which we make a http post request with content-type : application/x-www-form-urlencoded.
[HttpPost("PostUrlencoded")]
public async Task<IActionResult> PostUrlencoded([FromBody]IDocument document)
{ /*...*/ }

2.. The second one is called Post螠ultipart in which we make a http post request with content-type : multipart/form-data.

[HttpPost("Post螠ultipart")]
public async Task<IActionResult> Post螠ultipart([FromForm]IDocument document)
{ /*...*/ }

The first method works fine, and the deserialization works fine. In the second method deserialization fails.
The error is:
Model bound complex types must not be abstract or value types and must have a parameterless constructor.

That's because the JsonOptions isn't applied.

When we remove the jsonOptions from the startup.cs, we get the same error in both cases.

Design affected-medium area-mvc enhancement severity-blocking

Most helpful comment

Reopening this issue as there seems to be some customer interest in this being a first-class experience.
Will track this as part of the 3.0 milestone.

All 26 comments

If you use the Fromform attribute, the model's binding is not using JSON.NET.

Why is this happening?

The http request is sent like this

Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name=document
{...json..}

Shouldn't this be deserialized?

@mbonatsos , can you please share a sample project with us to reproduce the issue?

https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding#customize-model-binding-behavior-with-attributes
[FromHeader], [FromQuery], [FromRoute], [FromForm]: Use these to specify the exact binding source you want to apply.
[FromBody]: Use the configured formatters to bind data from the request body. The formatter is selected based on content type of the request.

I've created a sample project for you to see the issues and the workarounds.
In my opinion the modelbinder solution is not clean.

The mvc framework should deserialize a model from some data that is posted (http), regardless what the post request type is "multipart/form-data" or "application/x-www-form-urlencoded".

Sample.zip

@dougbu , can you please investigate this?

The JSON input formatter accepts application/json, text/json and application/*+json. It does not support any multipart content types by default.

To support multipart and input formatters together, need to position the Body stream after the boundary for the appropriate section. See https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.FileUpload/Controllers/FileController.cs for example. (This isn't simple. MVC helps most when combining files and application/x-www-form-urlencoded sections into a multipart/form-data submission. That is the normal use of multipart/form-data.)

As a smaller detail, also need to update the JsonInputFormatter because it will check the overall content type, not that of the "current" section. E.g. in your ConfigureServices(IServiceCollection services):
c# services.AddMvc(options => { var jsonInputFormatter = options.InputFormatters.OfType<JsonInputFormatter>().First(); jsonInputFormatter.SupportedMediaTypes.Add("multipart/form-data"); };

Clearing the investigate label.

I probably misunderstood the concern, focusing on using input formatters with multipart requests.

The sample contains a number of issues:

  • [FromBody] and [FromForm] are completely separate. E.g. nothing in MvcJsonOptions is used when binding [FromForm] parameters.
  • Requests matching actions with [FromForm] parameters must contain an application/form-url-encoded body or a multipart/form-data body with an application/x-www-form-urlencoded sectoin. The tests sending application/json data to actions with [FromForm] parameters led to my earlier confusion.
  • Binding [FromForm] Document document parameters should work fine given a valid request (see above) and the default configuration.
  • Binding [FromForm] IDocument document parameters requires an IModelBindingProvider implementation that gets the metadata for Document and returns the associated binder. Should not need a specific binder (just use ComplexTypeModelBinder) e.g.
    c# if (context.Metadata.ModelType == typeof(IDocument)) { var metadata = context.MetadataProvider.GetMetadataForType(typeof(Document)); return context.CreateBinder(metadata); }
  • Add the above IModelBindingProvider implementation last, certainly after the BodyModelBindingProvider. Otherwise, the ComplexTypeModelBinder will attempt to bind when the request contains application/json data -- and fail.
  • IFormFile action parameters should not have an associated [FromForm] attribute.

I believe and would expect the mvc framework to deserialize this http request, with out any other configuration.
I would also expect [FromForm] working in the same way with [FromBody] for the deserialization of posted data. I mean that if i post this

Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name=document
{...json..}

MvcJsonOptions should be used/respected. Is confusing not to work there.

I also think that the implementation with IModelBindingProvider is not so clean.

I would like to see some more opinions from other team members.

Thank you for your feedback.
We're closing this issue as the behavior discussed seems to be by design.

I am not conviced that the behavior should be this.
I coundm't understand the arguments behind this decision.

I always hear from the members of the asp.net team in confernces and videos that the framework is openly developed, not only open source.

I ask for more info about this and you close the issue not fair.

I was hoping this would have worked with either [FromForm] or automatically.
Is there any reason why it doesn't?
I can send multiple IFormFile in a similar way and they get picked up without problems, why doesn't this?

Content-Disposition: form-data; name="news"
Content-Type: application/json

{"title":"title1","text":"text1"}
public async Task<IActionResult> PostNews([FromForm]News news, IFormFile attachment = null, IFormFile image = null)
{
...
}

I guess I can just do something like this, but this feels REALLY clunky.

public async Task<IActionResult> PostNews(IFormFile news, IFormFile attachment = null, IFormFile image = null)
{
    if (news != null && news.ContentType == "application/json")
    {
        News newsObject = null;

        var js = new JsonSerializer();
        using (var ms = new MemoryStream())
        using (var sr = new StreamReader(ms))
        using (var jtr = new JsonTextReader(sr))
        {
            await news.CopyToAsync(ms);
            ms.Seek(0, SeekOrigin.Begin);
            newsObject = js.Deserialize<News>(jtr);
        }
    }
}

I knew someone else would say something about this. I expect more reactions.
It was pretty authoritarian the way this was closed.

Reopening this issue as there seems to be some customer interest in this being a first-class experience.
Will track this as part of the 3.0 milestone.

I guess what we are looking for is something similar to this https://stackoverflow.com/questions/41367602/upload-files-and-json-in-asp-net-core-web-api

Where we want the parameter in [FromBody] to be parsed using a jsonbinding/serialization.

Was refactoring some code, and found my old JsonModelBinder class. Scratched my head on why I would need that, after all it's {currentYear}. Figured I could delete it...

I was wrong.

We could really use this.

For everyone else still waiting, I've switched to Bruno's Nuget: https://github.com/BrunoZell/JsonModelBinder

@pranavkm Did you have a look at this? I'm wondering what it would take to get this scenario supported out-of-the-box and whether it would be a good PR candidate?

I haven't, but we wanted to look at improvements in the multipart area. I'll bring this up in our planning, we should at the very least get a design out for this that we'd accept a PR for.

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

Whelp, I just wasted an afternoon by assuming this was possible.

Requests matching actions with [FromForm] parameters must contain an application/form-url-encoded body or a multipart/form-data body with an application/x-www-form-urlencoded sectoin.

I have an application/x-www-form-urlencoded section in my request, but the model binder still does not work for me. This is with standard form content (no json).

Whelp, I just wasted an afternoon by assuming this was possible.

Requests matching actions with [FromForm] parameters must contain an application/form-url-encoded body or a multipart/form-data body with an application/x-www-form-urlencoded sectoin.

I have an application/x-www-form-urlencoded section in my request, but the model binder still does not work for me. This is with standard form content (no json).

Ditto - would be great to finally make json + IFormFile multipart requests a first class citizen

Yup it's on our radar. We'd hoped to address it in 5.0 but unfortunately weren't able to fit it.

Thanks for contacting us.
We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

Was this page helpful?
0 / 5 - 0 ratings