19

I am working on Web APIs - Web API 2. My basic need is to create an API to update the profile of the user. In this, the ios and android will send me the request in multipart/form-data. They will send me a few parameters with an image. But whenever I try to create the API, my model comes to be null every time.

I have also added this line in WebApiConfig:

config.Formatters.JsonFormatter.SupportedMediaTypes
                    .Add(new MediaTypeHeaderValue("multipart/form-data"));

This is my class:

public class UpdateProfileModel
{
   public HttpPostedFileBase ProfileImage { get; set; }
   public string Name { get; set; }
}

This is my controller:

[Route("api/Account/UpdateProfile")]
[HttpPost]
public HttpResponseMessage UpdateProfile(UpdateProfileModel model)
{
}

I am even not getting parameter values in my Model. Am I doing something wrong?

None of the answers related to this were helpful for me. It's about 3rd day and I have tried almost everything and every method. but I am unable to achieve it.

Although I can use this but this as shown below but this doesn't seem to be a good approach. so I am avoiding it.

var httpRequest = HttpContext.Current.Request;
if (httpRequest.Form["ParameterName"] != null)
{
    var parameterName = httpRequest.Form["ParameterName"];
}

and for files I can do this:

if (httpRequest.Files.Count > 0)
{
     //I can access my files here and save them
}

Please help if you have any good approach for it Or Please explain to me why I am unable to get these values in the Model.

Thanks a lot in Advance

spaleet
  • 838
  • 2
  • 10
  • 23
Rohit Arora
  • 2,246
  • 2
  • 24
  • 40
  • 1
    Looking at this link http://www.asp.net/web-api/overview/advanced/sending-html-form-data-part-2 I don't thik multipart/form-data is a bad option. If you are still insecure about the security than add some bearer token or some predefined authentication. – Just code Dec 05 '15 at 04:30
  • 1
    Thanks for replying @justcode. I never said that its bad option. I just dont want to get parameters everytime by requesting and checking nulls in it. I want them to be directly bind to my model as we normally do in other apis... – Rohit Arora Dec 05 '15 at 04:33
  • 1
    Than, in my opinion the base64 is the alternate option. I don't know if there is any another way. – Just code Dec 05 '15 at 04:36
  • 1
    ohk.. actually one of my known said that it is also possible with multipart/form-data also but he dont remember the steps. So i just posted it here if anyone could help and guide me in the right direction. Other wise i have to go with Base64... – Rohit Arora Dec 05 '15 at 04:39
  • 2
    You could try adding [FormData] to the parameter. IIRC, marking it as [FormData] is the same as using Form["propertyName"] – Camilo Terevinto Dec 05 '15 at 04:41
  • 1
    How do i add this to parameter? could you please give me an example. – Rohit Arora Dec 05 '15 at 04:44
  • 1
    Please comment nd let me know why did you downvote and then downvote... – Rohit Arora Dec 05 '15 at 07:19
  • 1
    @cFrozenDeath Please tell me how do i add this [FormData] to the parameters. In which namespace iw ill get this [FormData]. I am unable to find it too..thanks in advance.. – Rohit Arora Dec 07 '15 at 04:04
  • 1
    @RohitArora sorry I didn't get your previous notifications. You do that like this: `public HttpResponseMessage UpdateProfile([FormData]UpdateProfileModel model)` – Camilo Terevinto Dec 07 '15 at 13:35
  • 1
    @cFrozenDeath no problem .... but could you please tell me [FormData] belongs to which namespace ? I am unable to find it. I have already tried this. I am only getting [FromBody] and [FromUri]. – Rohit Arora Dec 08 '15 at 03:50
  • 1
    possible duplicate - http://stackoverflow.com/questions/28369529/how-to-setup-a-webapi-controller-for-multipart-form-data – Amit Kumar Ghosh Dec 08 '15 at 07:41
  • 1
    @AmitKumarGhosh no this isn't duplicate because i do not want to use it like the OP and other persons in that question. I can do it like that but that approach doesn't seem good. I have many parameters. I dont want to get every parameter alone. I have also mentioned in my question. Thanks. – Rohit Arora Dec 08 '15 at 07:45
  • 1
    Are you getting an exception? – Amit Kumar Ghosh Dec 08 '15 at 07:48
  • 1
    @AmitKumarGhosh No...i am not getting any exception. I just want to bind it with my parameter model automatically or in such a way that i do not have to request every parameter ... – Rohit Arora Dec 08 '15 at 07:51
  • Just found this answer to be related with it. http://stackoverflow.com/questions/21689925/posting-multiple-content-types-to-web-api – Uday Vaswani Dec 15 '15 at 07:34

5 Answers5

8

The answer provided by JPgrassi is what you would be doing to have MultiPart data. I think there are few more things that needs to be added, so I thought of writing my own answer.

MultiPart form data, as the name suggest, is not single type of data, but specifies that the form will be sent as a MultiPart MIME message, so you cannot have predefined formatter to read all the contents. You need to use ReadAsync function to read byte stream and get your different types of data, identify them and de-serialize them.

There are two ways to read the contents. First one is to read and keep everything in memory and the second way is to use a provider that will stream all the file contents into some randomly name files(with GUID) and providing handle in form of local path to access file (The example provided by jpgrassi is doing the second).

First Method: Keeping everything in-memory

//Async because this is asynchronous process and would read stream data in a buffer. 
//If you don't make this async, you would be only reading a few KBs (buffer size) 
//and you wont be able to know why it is not working
public async Task<HttpResponseMessage> Post()
{

if (!request.Content.IsMimeMultipartContent()) return null;

        Dictionary<string, object> extractedMediaContents = new Dictionary<string, object>();

        //Here I am going with assumption that I am sending data in two parts, 
        //JSON object, which will come to me as string and a file. You need to customize this in the way you want it to.           
        extractedMediaContents.Add(BASE64_FILE_CONTENTS, null);
        extractedMediaContents.Add(SERIALIZED_JSON_CONTENTS, null);

        request.Content.ReadAsMultipartAsync()
                .ContinueWith(multiPart =>
                {
                    if (multiPart.IsFaulted || multiPart.IsCanceled)
                    {
                        Request.CreateErrorResponse(HttpStatusCode.InternalServerError, multiPart.Exception);
                    }

                    foreach (var part in multiPart.Result.Contents)
                    {
                        using (var stream = part.ReadAsStreamAsync())
                        {
                            stream.Wait();
                            Stream requestStream = stream.Result;

                            using (var memoryStream = new MemoryStream())
                            {
                                requestStream.CopyTo(memoryStream);
                                //filename attribute is identifier for file vs other contents.
                                if (part.Headers.ToString().IndexOf("filename") > -1)
                                {                                        
                                    extractedMediaContents[BASE64_FILE_CONTENTS] = memoryStream.ToArray();
                                }
                                else
                                {
                                    string jsonString = System.Text.Encoding.ASCII.GetString(memoryStream.ToArray());
                                   //If you need just string, this is enough, otherwise you need to de-serialize based on the content type. 
                                   //Each content is identified by name in content headers.
                                   extractedMediaContents[SERIALIZED_JSON_CONTENTS] = jsonString;
                                }
                            }
                        }
                    }
                }).Wait();

        //extractedMediaContents; This now has the contents of Request in-memory.
}

Second Method: Using a provider (as given by jpgrassi)

Point to note, this is only filename. If you want process file or store at different location, you need to stream read the file again.

 public async Task<HttpResponseMessage> Post()
{
HttpResponseMessage response;

    //Check if request is MultiPart
    if (!Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }
    //This specifies local path on server where file will be created
    string root = HttpContext.Current.Server.MapPath("~/App_Data");
    var provider = new MultipartFormDataStreamProvider(root);

    //This write the file in your App_Data with a random name
    await Request.Content.ReadAsMultipartAsync(provider);

    foreach (MultipartFileData file in provider.FileData)
    {
        //Here you can get the full file path on the server
        //and other data regarding the file
        //Point to note, this is only filename. If you want to keep / process file, you need to stream read the file again.
        tempFileName = file.LocalFileName;
    }

    // You values are inside FormData. You can access them in this way
    foreach (var key in provider.FormData.AllKeys)
    {
        foreach (var val in provider.FormData.GetValues(key))
        {
            Trace.WriteLine(string.Format("{0}: {1}", key, val));
        }
    }

    //Or directly (not safe)    
    string name = provider.FormData.GetValues("name").FirstOrDefault();


    response = Request.CreateResponse(HttpStatusCode.Ok);              

return response;
}
Guanxi
  • 3,103
  • 21
  • 38
  • 1
    Thanks very much for your answer. I am still looking for a way so that i can have predifned formatter or i will try to create one so that my model values are binded to the parameter. – Rohit Arora Dec 12 '15 at 06:13
7

By default there is not a media type formatter built into the api that can handle multipart/form-data and perform model binding. The built in media type formatters are :

 JsonMediaTypeFormatter: application/json, text/json
 XmlMediaTypeFormatter: application/xml, text/xml
 FormUrlEncodedMediaTypeFormatter: application/x-www-form-urlencoded
 JQueryMvcFormUrlEncodedFormatter: application/x-www-form-urlencoded

This is the reason why most answers involve taking over responsibility to read the data directly from the request inside the controller. However, the Web API 2 formatter collection is meant to be a starting point for developers and not meant to be the solution for all implementations. There are other solutions that have been created to create a MediaFormatter that will handle multipart form data. Once a MediaTypeFormatter class has been created it can be re-used across multiple implementations of Web API.

How create a MultipartFormFormatter for ASP.NET 4.5 Web API

You can download and build the full implementation of the web api 2 source code and see that the default implementations of media formatters do not natively process multi part data. https://aspnetwebstack.codeplex.com/

Community
  • 1
  • 1
Bill
  • 1,241
  • 17
  • 25
3

You can't have parameters like that in your controller because there's no built-in media type formatter that handles Multipart/Formdata. Unless you create your own formatter, you can access the file and optional fields accessing via a MultipartFormDataStreamProvider :

Post Method

 public async Task<HttpResponseMessage> Post()
{
    HttpResponseMessage response;

        //Check if request is MultiPart
        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        string root = HttpContext.Current.Server.MapPath("~/App_Data");
        var provider = new MultipartFormDataStreamProvider(root);

        //This write the file in your App_Data with a random name
        await Request.Content.ReadAsMultipartAsync(provider);

        foreach (MultipartFileData file in provider.FileData)
        {
            //Here you can get the full file path on the server
            //and other data regarding the file
            tempFileName = file.LocalFileName;
        }

        // You values are inside FormData. You can access them in this way
        foreach (var key in provider.FormData.AllKeys)
        {
            foreach (var val in provider.FormData.GetValues(key))
            {
                Trace.WriteLine(string.Format("{0}: {1}", key, val));
            }
        }

        //Or directly (not safe)    
        string name = provider.FormData.GetValues("name").FirstOrDefault();


        response = Request.CreateResponse(HttpStatusCode.Ok);              

    return response;
}

Here's a more detailed list of examples: Sending HTML Form Data in ASP.NET Web API: File Upload and Multipart MIME

jpgrassi
  • 5,482
  • 2
  • 36
  • 55
  • 1
    Thanks very much for your answer. I am still looking for a way so that i can have predifned formatter or i will try to create one so that my model values are binded to the parameter. – Rohit Arora Dec 12 '15 at 06:13
2

Not so sure this would be helpful in your case , have a look

mvc upload file with model - second parameter posted file is null

and

ASP.Net MVC - Read File from HttpPostedFileBase without save

Community
  • 1
  • 1
alj
  • 170
  • 10
  • 1
    @ali this links are for MVC not Web APi. I have already done this for MVC but i am unable to achieve it for Web API(when i use multipart/form-data). – Rohit Arora Dec 07 '15 at 04:02
2

So, what worked for me is -

[Route("api/Account/UpdateProfile")]
[HttpPost]
public Task<HttpResponseMessage> UpdateProfile(/* UpdateProfileModel model */)
{
     string root = HttpContext.Current.Server.MapPath("~/App_Data");
        var provider = new MultipartFormDataStreamProvider(root);
        await Request.Content.ReadAsMultipartAsync(provider);
        foreach (MultipartFileData file in provider.FileData)
        {

        }
}

Also -

config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));

isn't required.

I guess the multipart/form-data is internally handled somewhere after the form is submitted.

Very clearly described here -

http://www.asp.net/web-api/overview/advanced/sending-html-form-data-part-2

Amit Kumar Ghosh
  • 3,618
  • 1
  • 20
  • 24
  • 1
    Thanks for your answer, But you have removed the parameter model, how will i get values in it then ? – Rohit Arora Dec 08 '15 at 10:12
  • 1
    You can read the content using a `MultipartFormDataStreamProvider` class. The URL describes it all. – Amit Kumar Ghosh Dec 08 '15 at 10:14
  • 1
    yes, but i will still have to read all the values and files from it (which i am trying to avoid). Also the Model validations will not work in it. – Rohit Arora Dec 08 '15 at 10:19
  • 1
    In that case, I would suggest you end up writing your own mediatypeformatter and handle your own model binding i.e. the serialization/deserialization logic. – Amit Kumar Ghosh Dec 08 '15 at 11:58