Chris
Chris

Reputation: 365

Upload File(s) with Additional Information (Angular 8 to C# Core 3)

I finally figured out how to upload files from Angular 8 frontend to C# .Net Core 3.0 backend API Controller. But to get what I fully need, I also have a class that defines the file contents (e.g. # of header lines, column values, and so on...) set by the user that needs to be passed in as well.

On the client side I simply create a FormData object and push the files into it and then send that to the backend. This works great if I JUST send in this class and then receive it on the backend as a "IFormFileCollection". But if I put the FormData and my additional class in a Wrapper class, then I get an error (posted at bottom). Here I post the code that is not working.

Here is my client code that send:

export interface FileProvider 
{
  formData: FormData;
  testString: string;
}

then my TypeScript code send to the back end:

 async UploadFiles() {

    try {

      let files: File[] = this.files;
      let myFormData: FormData = new FormData();

      for (let i = 0; i < files.length; i++) {
        let file: File = files[i];
        myFormData.append('file', file, file.name);
      }

      /** Wrap this in a class. */
      let fileProvider: FileProvider = {
        formData: myFormData,
        testString: "This is just a test string... but will be a class"
      }

      let promise = new Promise((resolve, reject) => {
        this.dataService.UploadData(fileProvider).subscribe(data => resolve(data), error => reject(error))
      });

      let result = await promise;
      alert("Successfully loaded Data");
    }
    catch (error) {
      alert(error.message + ", Status: " + error.status + ",  OK: " + error.ok + ",  " + error.error);
    }
  }

my small DataService post method:

  UploadData(fileProvider: FileProvider) {
    let path: string = this.api + 'wells/formdata';
    return this.http.post(path, fileProvider);
  }

And my C# .Net Core 3 backend Controller code:

        [Serializable]
        public class FileProvider
        {
            public IFormCollection FormData { get; set; }
            public string TestString { get; set; }
        }


     [HttpPost("formdata"), DisableRequestSizeLimit]
        public IActionResult Upload(FileProvider fileProvider)
        {
            try
            {

                IFormFileCollection fileCollection = fileProvider.FormData.Files;
                string testString = fileProvider.TestString;


                foreach (IFormFile file in fileCollection)
                {

                    /// Read at a line a time.
                    StringBuilder lineAtATime = new StringBuilder();
                    using (var reader = new StreamReader(file.OpenReadStream()))
                    {
                        while (reader.Peek() >= 0)
                        {
                            string line = reader.ReadLine();
                            lineAtATime.Append(line);
                        }
                    }
                    string textByLines = lineAtATime.ToString();


                }
                return Ok();
            }
            catch (Exception ex)
            {
                return StatusCode(500, "Internal server error: " + ex.Message);
            }
        }

The final error that gets thrown is the following:

"Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'Microsoft.AspNetCore.Http.IFormCollection' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly. To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object. Path 'formData', line 1, position 14."

What can I do to pass in both parameters classes?

Thanks!!

Upvotes: 4

Views: 4214

Answers (1)

itminus
itminus

Reputation: 25360

The reason is you're sending a json-like object instead of a payload in multipart/form-data format. Since JSON can not represent binary bytes (files), you can never make it by that way.

How to fix:

Instead of uploading a custom FileProvider (json), simply append the testString field to FormData, and upload the FormData directly.

Server Side

replace the FormData property with IList<IFormFile> Files:

[Serializable]
public class FileProvider
{
    public string TestString { get; set; }
    public IFormCollection FormData { get; set; }
    public IList<IFormFile> Files { get; set; }
}

public IActionResult Upload([FromForm]FileProvider fileProvider)
{
    var files = fileProvider.Files;
    var testString = fileProvider.TestString;
    ...
}

Client Side

Change your Service to receive a parameter of FormData type:

  UploadData(fileProvider: FileProvider) {
  UploadData(formdata: FormData) {
    let path: string = this.api+ 'wells/formdata';
    return this.http.post(path, formdata);
  }

Finally, append extra fields to FormData directly :

async UploadFiles() {
    try {

      let files: File[] = this.files;
      let myFormData: FormData = new FormData();

      for (let i = 0; i < files.length; i++) {
        let file: File = files[i];
        myFormData.append('file', file, file.name);
        myFormData.append('files', file, file.name);    // the filed name is `files` because the server side declares a `Files` property
      }
      myFormData.append("testString", "This is just a test string... but will be a class");       // add extra fields 
      // ... add more fields as you like

      let fileProvider: FileProvider = {
        formData: myFormData,
        testString: "This is just a test string... but will be a class"
      }

      let promise = new Promise((resolve, reject) => {
        this.dataService.UploadData(fileProvider).subscribe(data => resolve(data), error => reject(error))
      });

      let resp = await this.dataService.UploadData(myFormData).toPromise();
      alert("Successfully loaded Data");
    }
    catch (error) {
      alert(error.message + ", Status: " + error.status + ",  OK: " + error.ok + ",  " + error.error);
    }
}

Upvotes: 10

Related Questions