Model Binding for multipart/form-data (File + JSON) post in ASP.NET Core 1.1

試著忘記壹切 提交于 2019-12-30 18:50:09

问题


I'm attempting to build an ASP.NET Core 1.1 Controller method to handle an HTTP Request that looks like the following:

POST https://localhost/api/data/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------625450203542273177701444
Host: localhost
Content-Length: 474

----------------------------625450203542273177701444
Content-Disposition: form-data; name="file"; filename="myfile.txt"
Content-Type: text/plain

<< Contents of my file >>

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

{"md5":"595f44fec1e92a71d3e9e77456ba80d0","sessionIds":["123","abc"]}
----------------------------625450203542273177701444--

It's a multipart/form-data request with one part being a (small) file and the other part a json blob that is based on a provided specification.

Ideally, I'd love my controller method to look like:

[HttpPost]
public async Task Post(UploadPayload payload)
{
   // TODO
}

public class UploadPayload
{
    public IFormFile File { get; set; }

    [Required]
    [StringLength(32)]
    public string Md5 { get; set; }

    public List<string> SessionIds { get; set; }
}

But alas, that doesn't Just Work {TM}. When I have it like this, the IFormFile does get populated, but the json string doesn't get deserialized to the other properties.

I've also tried adding a Text property to UploadPayload that has all the properties other than the IFormFile and that also doesn't receive the data. E.g.

public class UploadPayload
{
    public IFormFile File { get; set; }

    public UploadPayloadMetadata Text { get; set; }
}

public class UploadPayloadMetadata
{
    [Required]
    [StringLength(32)]
    public string Md5 { get; set; }

    public List<string> SessionIds { get; set; }
}

A workaround that I have is to avoid model binding and use MultipartReader along the lines of:

[HttpPost]
public async Task Post()
{
   ...

   var reader = new MultipartReader(Request.GetMultipartBoundary(), HttpContext.Request.Body);

   var section = await reader.ReadNextSectionAsync();
   var filePart = section.AsFileSection();

   // Do stuff & things with the file

   section = await reader.ReadNextSectionAsync();
   var jsonPart = section.AsFormDataSection();
   var jsonString = await jsonPart.GetValueAsync();

   // Use $JsonLibrary to manually deserailize into the model
   // Do stuff & things with the metadata

   ...
}

Doing the above bypasses model validation features, etc. Also, I thought maybe I could take that jsonString and then somehow get it into a state that I could then call await TryUpdateModelAsync(payloadModel, ...) but couldn't figure out how to get there either - and that didn't seem all that clean either.

Is it possible to get to my desired state of "transparent" model binding like my first attempt? If so, how would one get to that?


回答1:


The first problem here is that the data needs to be sent from the client in a slightly different format. Each property in your UploadPayload class needs to be sent in its own form part:

const formData = new FormData();
formData.append(`file`, file);
formData.append('md5', JSON.stringify(md5));
formData.append('sessionIds', JSON.stringify(sessionIds));

Once you do this, you can add the [FromForm] attribute to the MD5 property to bind it, since it is a simple string value. This will not work for the SessionIds property though since it is a complex object.

Binding complex JSON from the form data can be accomplished using a custom model binder:

public class FormDataJsonBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if(bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

        // Fetch the value of the argument by name and set it to the model state
        string fieldName = bindingContext.FieldName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(fieldName);
        if(valueProviderResult == ValueProviderResult.None) return Task.CompletedTask;
        else bindingContext.ModelState.SetModelValue(fieldName, valueProviderResult);

        // Do nothing if the value is null or empty
        string value = valueProviderResult.FirstValue;
        if(string.IsNullOrEmpty(value)) return Task.CompletedTask;

        try
        {
            // Deserialize the provided value and set the binding result
            object result = JsonConvert.DeserializeObject(value, bindingContext.ModelType);
            bindingContext.Result = ModelBindingResult.Success(result);
        }
        catch(JsonException)
        {
            bindingContext.Result = ModelBindingResult.Failed();
        }

        return Task.CompletedTask;
    }
}

You can then use the ModelBinder attribute in your DTO class to indicate that this binder should be used to bind the MyJson property:

public class UploadPayload
{
    public IFormFile File { get; set; }

    [Required]
    [StringLength(32)]
    [FromForm]
    public string Md5 { get; set; }

    [ModelBinder(BinderType = typeof(FormDataJsonBinder))]
    public List<string> SessionIds { get; set; }
}

You can read more about custom model binding in the ASP.NET Core documentation: https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding




回答2:


I'm not 100% clear on how this would work for ASP.NET Core but for Web API (so I assume a similar path exists here) you'd want to go down the road of a Media Formatter. Here's an example (fairly similar to your question) Github Sample with blog post

Custom formatters might be the ticket? https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-formatters



来源:https://stackoverflow.com/questions/44979737/model-binding-for-multipart-form-data-file-json-post-in-asp-net-core-1-1

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!