Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to upload files to a Web API server and send parameters along to the action?

Good day!

I am working on a ASP.NET Web API 2 project. At a certain point there is a need to upload some files. The files need to be linked to a certain FileModel (our own class). So, the client needs to send IEnumerable as parameter and the files as content. Because it's a RESTful API, both have to be sent in the same request.

The best we could come up with is the follow Controller action:

public async Task<HttpResponseMessage> Add([FromUri] IEnumerable<FileModel> fileModels)
{
   // REQUEST INTEGRITY TESTING

   var streamProvider = new CustomMultipartFormDataStreamProvider(fileSavePath, fileModels);
   // Read the MIME multipart content using the stream provider we just created.
   var work = await Request.Content.ReadAsMultipartAsync(streamProvider).ContinueWith(async t =>
        {
            // SOME WORK DONE AFTER SAVING THE FILES TO THE HARD DRIVE
        }

}

The problem is following: the files are being uploaded with a 'multipart/form-data' Content-Type header. We need to know the contents of the FileModels before manipulating the files on the server side. If we use the MultipartFormDataStreamProvider, we can only access the non file parameters after the files have already been saved to the hard drive.

The only workaround for this we could find is to send the IEnumerable< FileModel > parameter in the URL. But given that the URL has a limited max length, this is not a reliable method.

The question is: Is there a way to submit both the IEnumerable< FileModel > fileModels parameter and the files in the body of the request and get access to the fileModels parameter before accessing the files? We also want to be able to use HttpContext.Current.Request.Files.Count;

Our current jQuery for file upload looks like this (for early testing purposes, it only supports one file upload):

$('#Upload').click(function(e) {
            e.preventDefault();

            var headers = new Array();
            headers["SessionId"] = sessionId;

            var files = $('#fileInput').get(0).files;
            var formData = new FormData();
            formData.append("files", files[0]);

            var fileModel = $('#fileSubmission').serialize();

            $.ajax({
                url: "api/Submissions/Add/?" + fileModel,
                headers: headers,
                type: 'POST',
                data: formData,
                cache: false,
                contentType: false,
                processData: false,
                dataType: 'json'
            });
        });

Thank you very much!

like image 854
tony.hegyes Avatar asked Jan 11 '15 07:01

tony.hegyes


People also ask

Can you send a file through API?

Transferring Files with APIs RESTful HTTP based APIs are the current 'go-to' approach for designing applications and file upload and download is a common business requirement for many applications. Files can be streamed attachments or links to the actual content.


1 Answers

I am sorry for the late answer, but we solved the problem (I forgot that I didn't upload the answer here). Basically what we did is that we called the ReadAsMultiPartAsync method on a temporary location and then we extracted the other parameters from the request. Afterwards, we validated the input and moved the files from the temporary to the permanent location.

If you want to see the code, this is what worked for our particular example and I believe it's pretty straight-forward to adapt to any work case scenario:

On the client side, we have the following form (yes, this implementation is for demo purposes and only supports sending one file ... also, the input type="file" field is indeed outside of the form; the fileId text input is completed manually in our case, just for testing purposes)

<input type="file" name="data" id="fileInput" multiple="multiple" />

<form id="fileSubmission">            
    <input type="text" width="10" onchange="getFileDetails()" autocomplete="off" placeholder="FileId" name="files[0].Id" id="fileId" /> 
    <input type="hidden" name="files[0].FileName" id="FileName"/>
    <input type="hidden" name="files[0].Extension" id="Extension"/>
    <input type="hidden" name="files[0].EntityId" id="EntityId"/>
    <br /><br />
    <input type="submit" id="Upload" value="Upload" />
</form>

where getFileDetails() populates the other input fields. Also, the form is being sent to the server by using the following jQuery/Javascript:

$('#Upload').click(function(e) {
            e.preventDefault();

            var courseId = $('#courseId').val();
            var fileId = $('#fileId').val();
            if (!courseId || !fileId) {
                return;
            }

            var headers = new Array();
            headers["SessionId"] = sessionId;
            headers["contentType"] = "application/json; charset=UTF-8";

            var formData = new FormData();
            var opmlFile = $('#fileInput').get(0).files;

            // this is like the model we're expecting on the server
            var files = [];
            files.push({ 'Id': $('#fileId').val(), 'OriginalFileName': opmlFile[0].name, 'FileName': $('#FileName').val(), 'Extension': $('#Extension').val(), 'EntityId': $('#EntityId').val() });

            formData.append("fileModels", JSON.stringify(files));
            formData.append("File_0", opmlFile[0]);


            $.ajax({
                url: "api/Courses/" + courseId + "/Submissions/Add/",
                headers: headers,
                type: 'POST',
                data: formData,
                cache: false,
                contentType: false,
                processData: false,
                dataType: 'json'
            });
        });

On the server side, we have the following :

// POST: api/Courses/{courseId}/Submissions/Add
[HttpPost]
[ValidateModelState]
[ValidateMimeMultipartContent]
[PermissionsAuthorize(CoursePermissions.CanCreateSubmissions)]
public async Task<HttpResponseMessage> Add(int courseId)
    {
        // the same as in the jQuery part
        const string paramName = "fileModels";

        // Put the files in a temporary location
        // this way we call ReadAsMultiPartAsync and we get access to the other data submitted
        var tempPath = HttpContext.Current.Server.MapPath("~/App_Data/Temp/" + Guid.NewGuid());
        Directory.CreateDirectory(tempPath);

        var streamProvider = new MultipartFormDataStreamProvider(tempPath);
        var readResult = await Request.Content.ReadAsMultipartAsync(streamProvider);

        if (readResult.FormData[paramName] == null)
        {
            // We don't have the FileModels ... delete the TempFiles and return BadRequest
            Directory.Delete(tempPath, true);
            return Request.CreateResponse(HttpStatusCode.BadRequest);
        }

        // The files have been successfully saved in a TempLocation and the FileModels are not null
        // Validate that everything else is fine with this command
        var fileModels = JsonConvert.DeserializeObject<IEnumerable<FileModelExtension>>(readResult.FormData[paramName]).ToList();

        // AT THIS POINT, ON THE SERVER, WE HAVE ALL THE FILE MODELS 
        // AND ALL THE FILES ARE SAVED IN A TEMPORARY LOCATION

        // NEXT STEPS ARE VALIDATION OF THE INPUT AND THEN 
        // MOVING THE FILE FROM THE TEMP TO THE PERMANENT LOCATION

        // YOU CAN ACCESS THE INFO ABOUT THE FILES LIKE THIS:
        foreach (var tempFile in readResult.FileData)
            {
                var originalFileName = tempFile.Headers.ContentDisposition.FileName.Replace("\"", string.Empty);

                var localTempPath = tempFile.LocalFileName;
            }

    }

I hope this will help anyone out there trying to submit files and other parameters at once to the server by using Post requests! :)

NOTE: Some of the attributes used on the server are custom. PermissionAuthorize, ValidateModelState and ValidateMimeMultiPartContent are custom Filters we have used. The implementation of the latter two has been inspired by http://benfoster.io/blog/automatic-modelstate-validation-in-aspnet-mvc

The multipartcontent attribute just does a check on actionContext.Request.Content.IsMimeMultipartContent(), like this:

public class ValidateMimeMultipartContent : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.Request.Content.IsMimeMultipartContent())
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.UnsupportedMediaType, Messages.UnsupportedMediaType);
        }
    }
}
like image 51
tony.hegyes Avatar answered Oct 07 '22 21:10

tony.hegyes