Story of File Uploading in ASP.NET Core (Part II - Angular/AJAX)

Uploading files from Angular end to an ASP.NET Core Web API can be done using the same IFormFile interface; introduced in the previous post. To keep things separated, a new API controller (UserController) has been created with the following POST action,

[HttpPost]
public async Task<IActionResult> PostUser([FromForm]UserVM vm)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    /* Use Automapper for mapping UserVM to User */

    User user = new User {
        Name = vm.Name
    };

    using (var memoryStream = new MemoryStream())
    {
        await vm.Avatar.CopyToAsync(memoryStream);
        user.Avatar = memoryStream.ToArray();
    }

    _context.Users.Add(user);
    await _context.SaveChangesAsync();

    return CreatedAtAction("GetUser", new { id = user.Id }, user);
}  

Only difference between the Create action introduced in the previous post and the newly created PostUser is: upon saving a user, the MVC action returns to the Index view, where as the Web API returns a 201 Created status containing a location link of the newly created resource in the response header.

The UserVm view model and User entity classes are also same as before.

A regular model driven (reactive) form for saving an user name with an avatar can be as following,

<form [formGroup]="userForm" (ngSubmit)="onSubmit()" novalidate>
    <div class="form-group">
        <label for="name">Name</label>
        <input type="text" id="name" class="form-control"
               formControlName="name" required>
    </div>
    <div class="form-group">
        <label for="avatar">Avatar</label>
        <input type="file" #uploader class="form-control" (change)="fileChange(uploader.files)" placeholder="Upload Avatar">
    </div>
    <button type="submit" class="btn btn-success">
        Save
    </button>
</form>

Note: Using the reactive form is not mandatory here. You can use template driven form if you prefer or you can invent a new form creation technique and use that. 😊

Notice that, we don't have a formControlName attribute on the file input element. Because we can't bind a file input element with a FormControl in our FormModel. If you try to do that, your application will run without problems and the from will be rendered but will give you the following error when you try to select a file,

ERROR DOMException: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.

However, we do have a FormControl for avatar in our FormModel,

this.userForm = new FormGroup({
    name: new FormControl(this.user.name),
    avatar: new FormControl(null)
}, { updateOn: 'submit' });

But the value of avatar is filled via a patch on the FormModel i.e. userForm. The patching is done when a file is selected hence the (change)="fileChange(uploader.files)". In the markup above, #uploader is a template reference variable and it's setting the file input as the event raiser i.e. target (this technique strips down other DOM events information and only dispatches the event information generated by the target element). Without the template reference variable you would have to write the change event handler like the following,

(change)="fileChange($event.target.files)"

The script for the fileChange method would be like the following,

fileChange(files: FileList) {
    if (files && files[0].size > 0) {
        this.userForm.patchValue({
            avatar: files[0]
        });
    }
}

The method takes the files array of the file input control, checks whether it is null and the file at first index has a file size greater than zero. If not, takes the file in the first index (we are uploading a single file here) and set the File object directly in avatar FormControl via a patch.

The form submit method onSubmit() looks like the following,

onSubmit() {
    if (this.userForm.valid) {
        this.userService.createUser(this.prepareSaveUser()).subscribe((user) => this.created.emit(user));
    }
}

prepareSaveUser(): FormData {
    const formModel = this.userForm.value;

    let formData = new FormData();
    formData.append("name", formModel.name);
    formData.append("avatar", formModel.avatar);

    return formData;
}

Notice that in the prepareSaveUser method, we have instantiate an instance of FormData. Later we have used the instance (formData) to append individual FromControl's value of the formModel. formData.append() accepts key-value pairs where each individual key represents a name of a server-side view model property and value is its raw values.

Why the FormData? Remember, when you put an Angular form directive on a native html form control, it no longer behaves like a real html form. For example, a native form control submission will reload the page and post the form control values on the server. But in SPA (Single Page Application) world, we use ajax to do the work without a cost of a page reload. That's why we don't have a enctype='multipart/form-data' attribute like we had for the MVC/Razor Pages forms. And we have to do the encoding pragmatically hence the FormData.

createUser method in the userService looks like the following,

createUser(user: FormData): Observable<User> {
    return this.http.post(this.baseUrl, user)
        .map(response => response.json())
} 

It's a simple http POST request that calls the PostUser action of the user API controller.

A Different Approach:

We already talked about why an Angular form doesn't act like a real native html form element. Also we are not encoding the form data on submit rather we are pragmatically using the FormData to do that.

So we can go with something like querySelector('#id') to get the file input control and get its value (file) if we want in the submit method. In Angular we can use @ViewChild for that. Following will get the file input element that has template reference variable #uploader attached to it and set its value in the uploader variable.

@ViewChild("uploader") uploader: any;

Now onn the submit method we can directly use the uploader variable to get the selected file available in the nativeElement property.

prepareSaveUser(): FormData {
    const formModel = this.userForm.value;

    let formData = new FormData();
    formData.append("name", formModel.name);

    formData.append("avatar", this.uploader.nativeElement.files[0]);

    return formData;
}

So, we do have multiple options. It's up to you which one you will go for. I tends to follow the first approach since it gives a impression that everything inside the form element is part of the FormModel(userForm)

In the next post we will upload the file in a folder rather than storing it as a byte[] array in the database.

https://github.com/fiyazbinhasan/AspNetCore-AngularSpa-Playground