Story of File Uploading in ASP.NET Core (Part I - MVC/Razor Pages)

The built-in IFormFile interface can be used to represent a file sent via some http request on the ASP.NET Core server side. Following is the skeleton of IFormFile:

public interface IFormFile
{
    // Gets the raw Content-Type header of the uploaded file.
    string ContentType { get; }

    // Gets the raw Content-Disposition header of the uploaded file.
    string ContentDisposition { get; }

    // Gets the header dictionary of the uploaded file.
    IHeaderDictionary Headers { get; }

    // Gets the file length in bytes.
    long Length { get; }

    // Gets the name from the Content-Disposition header.
    string Name { get; }

    // Gets the file name from the Content-Disposition header.
    string FileName { get; }

    // Copies the contents of the uploaded file to the target stream.
    void CopyTo(Stream target);

    // Asynchronously copies the contents of the uploaded file to the target stream.
    Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken));

    // Opens the request stream for reading the uploaded file.
    Stream OpenReadStream();
}

To understand better how the IFormFile extract information from an uploaded file, consider the following MVC form (TagHelper):

<form method="post" enctype="multipart/form-data" asp-action="Create">
    <div class="form-group">
        <label asp-for="Name" class="control-label"></label>
        <input asp-for="Name" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Avatar" class="control-label"></label>
        <input asp-for="Avatar" type="file" class="form-control" />
    </div>
    <div class="form-group">
        <input type="submit" value="Create" class="btn btn-default" />
    </div>
</form>

If you are using razor pages and your page is bind to an instance of UserVm view model i.e. [BindProperty] public UserVm Vm { get; set; }. The form TagHelper should have the following markup,

<form method="post" enctype="multipart/form-data">
    <div class="form-group">
        <label asp-for="Vm.Name" class="control-label"></label>
        <input asp-for="Vm.Name" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Vm.Avatar" class="control-label"></label>
        <input asp-for="Vm.Avatar" type="file" class="form-control" />
    </div>
    <div class="form-group">
        <input type="submit" value="Create" class="btn btn-default" />
    </div>
</form>

Notice the enctype="multipart/form-data" attribute. Submitted form data are sent to the server side once they are encoded first. According to W3C specs, there are three encoding types available to use with form submission. They are the followings:

  1. application/x-www-form-urlencoded
  2. multipart/form-data
  3. text/plain

For a form control, application/x-www-form-urlencoded is always the default encoding technique; used to encode form data. But if you have a file input control that resides in your form, you have to use the multipart/form-data encoding. It will let you submit forms that contain files, non-ASCII data, and binary data.

The form above can be used to create an user with his/her name and a selected avatar using the file input. Following is the view model (DTO) class representing properties that the individual form controls are attached to:

public class UserVM
{
    public string Name { get; set; }
    public IFormFile Avatar { get; set; }
}

Note: MVC/Razor Pages forms (TagHelper, HtmlHelper) are not real native html form elements. Instead they are forms on steroids. They give you the impression of a real form control but you can do something special with them. For example, if you want to fire a server side action (method) on form submission, use the asp-action attribute. Similarly to explicitly define the controller, the action method belongs to; use the asp-controller attribute.

In the form example above, action (method) fired on form submission is Create, hence the asp-action="Create". Following is the method body,

[HttpPost]
public async Task<IActionResult> Create([FromForm] UserVM vm)
{
    if (ModelState.IsValid)
    {
        var user = new User
        {
            Name = vm.Name
        };

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

        _context.Add(user);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    return View(vm);
}

Here, User is nothing but a entity class that has the following structure,

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public byte[] Avatar { get; set; }
}

Notice that we are copying the content of the uploaded file into a memory stream and next we are storing it in the Avatar field of the entity class. Since, field Avatar is a byte[] type, we had to cast the memory stream to array, hence the memoryStream.ToArray().

Note: Don't get mixed up between view model (DTO) and entity class. Entity class are solely used to represent a database table in ORM (Object Relational Mapping) languages. Where as the view models are used as data transfer objects between the View and the Controller.

We are using entity framework as an ORM here. The _context is an instance of a class (ApplicationDbContext) which is inheriting the DbContext of entity framework.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
    {

    }
    public DbSet<User> Users { get; set; }
}

DBSet<User> Users represents a database table named Users.

The form submission will create a request payload that pretty much looks like the following:

Notice how the multipart/form-data encodes a form element name and its value. These are simple key-value pairs delimited by boundary value available in the Content-Type header. (We will talk about the boundary value later in the series)

Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryj6v39iycf8i7NeBD

Ignore the _RequestVerificationToken for now. It's a hidden form element that automatically gets injected by the framework. We will talk about it later on the blog series.

We are storing the the file as a byte array in our database. But in real life that can cause a huge performance problem in the application. That's why it's better to save the uploaded file in a separate folder and save a reference (url, filename) of that file in the database. We will see how to do it in the next post. Before finishing, for those of you who are using the MVC's HtmlHelper element of the form control, here is how your form should look like,

@using (Html.BeginForm("Create", "UsersMvc", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    <div class="form-group">
        <label class="control-label">Name</label>
        @Html.TextBoxFor(model => model.Name, new { @class = "form-control" })
    </div>
    <div class="form-group">
        <label class="control-label">Avatar</label>
        @Html.TextBoxFor(model => model.Avatar, new { type = "file", @class="form-control" })
    </div>
    <div class="form-group">
        <input type="submit" value="Create" class="btn btn-default" />
    </div>
}

Last but not least, if you are working with Razor Pages. Code behind .cs file should have the following OnPostAsync() action method.

[BindProperty]
public UserVm Vm { get; set; }

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var user = new User
    {
        Name = Vm.Name
    };

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

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

    return RedirectToPage("./Index");
}

Notice that, in this case we have used the [BindProperty] attribute to bind an instance of UserVm to the razor page. That's why we don't need any binding parameter as the argument of the OnPostAsync() method.

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