How to File Upload in ASP.NET Core MVC Easy

Learn about how we can develop a simple form that posts text and images in ASP.NET Core by MVC and jQuery Ajax with example.

ASP.NET Core provides with an interface IFormFile to handle the Files submitted in a POST request form at the controller. IFormFile contains the file metadata of the uploaded File – such as ContentDisposition (the type of file), Name, Content in the form of a FileStream and so on.

Let’s look at how we can leverage the IFormFile type in developing a simple Form that submits both text and Files to a backend API and how the submitted data can be handled in AspNetCore. We’ll look at two distinct methods of submitting Form – the MVC submit way and the jQuery ajax way.

Getting Started –

Let’s say we want to create a Reader registration form where any given user shall submit his name, email address and his work (as a reader in some document) and we are to store this contents in our data store for a later use. This is complemented by a master Grid which shows all the readers and their work details which are submitted to the system.

To achieve this, we require to undergo the following steps:

  1. A Front end Form (HTML) which receives the input attributes Name, EmailAddress and Work (document)
  2. A backend controller which receives this form input and processes it for the data keeping
  3. A Model that is used to transport this data between the Front-End View and the backend Controller.

To keep things simple, we’ll try to upgrade our ReaderStore which we have developed to demonstrate jQuery Form POST to AspNetCore API in a previous article. Let’s begin by creating a Model which is used to bind between our View and the Controller. We design it similar to a ViewModel which represents both the Request and Response that is handled by the API and is consumed on the front-end Form.

// The Response Model
public class ReaderResponseModel
{
    public bool IsSuccess { get; set; }
    public string WorkPath { get; set; }
    public string ReaderId { get; set; }
    public string RedirectTo { get; set; }
}

// The Request Model which
// also holds the Response
public class ReaderRequestModel 
    : ReaderResponseModel
{
    [Required]
    [StringLength(200)]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    [StringLength(250)]
    public string EmailAddress { get; set; }
    public IFormFile Work { get; set; }
}

We shall structure a simple Form View using the built-in “asp-” Tag Helpers and Bootstrap 4 template that comes along with the AspNetCore MVC project type boilerplate code as below:

@model ReaderStore.WebApp.Models.ReaderRequestModel

<div class="container-fluid">
    <div class="row">
        <form method="post" asp-controller="Readers" 
            asp-action="New" enctype="multipart/form-data">
            
            <div class="form-group">
                <label asp-for="Name"></label>
                <input asp-for="Name" class="form-control" />
                <span class="text-danger" asp-validation-for="Name"></span>
            </div>
            <div class="form-group">
                <label asp-for="EmailAddress"></label>
                <input asp-for="EmailAddress" type="email" class="form-control" />
                <span class="text-danger" asp-validation-for="EmailAddress"></span>
            </div>
            <div class="form-group">
                <label asp-for="Work"></label>
                <input asp-for="Work" class="form-control-file" />
            </div>
            <button class="btn btn-primary" type="submit">Submit</button>
        </form>
    </div>
</div>

Note that we’re specifying the Form EncodingType as “multipart/form-data” since we are sending a mixed content containing both file and text. When the controller looks at this EncodingType, it understands that the submitted data contains both Files and Text and correctly scaffolds the data into the ReaderRequestModel which we’re using at the Controller end.

When we submit this form with relevant data, the Controller must receive and scaffold the data and return after processing it. The POST controller which handles this form is as below:

public class ReadersController : Controller
{
    // default constructor
    // where any instance level
    // assignments happen
    public ReadersController()
    {
    }

    // default GET Endpoint which
    // renders the View for us
    // from ~/Views/Readers/New.cshtml
    public IActionResult New()
    {
        return View();
    }

    // default POST Endpoint which
    // receives data from the Form submit
    // at ~/Views/Readers/New.cshtml
    // and returns the response to
    // the same View
    [HttpPost]
    public IActionResult New(ReaderRequestModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        // Logic happens here

        return View(model);
    }
}

We shall save the file part which is submitted along with the text data into a sub directory (say /Files under the filesystem) and then save the text data along with the generated FilePath of the saved File into our datastore.

Let’s assume our datastore Entity looks like below:

public class Reader
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string EmailAddress { get; set; }
    public string WorkPath { get; set; }
    public DateTime AddedOn { get; set; }
}

Now we shall fit the received model into this entity and store this object into our DB via some ORM (say EF Core), We’ll make use of a Repository class that encapsulates the Domain layer and makes our lives easy.

namespace ReaderStore.WebApp.Providers.Repositories
{
    public interface IReadersRepository
    {
        IQueryable<Reader> Readers { get; }
        Reader GetReader(Guid id);
        Reader AddReader(Reader reader);
    }
}

The logic for handling the multipart data received from the Form and responding back is as follows:

// default POST Endpoint which
// receives data from the Form submit
// at ~/Views/Readers/New.cshtml
// and returns the response to
// the same View
[HttpPost]
public async Task<IActionResult> New(ReaderRequestModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var res = await AddReader(model);

    // On successful submission
    // redirect to the Master Page
    if (res.IsSuccess)
    {
        return RedirectToActionPermanent("Index");
    }

    // On any error handle
    // within the same Page
    return View(model);
}

private async Task<ReaderResponseModel> AddReader(ReaderRequestModel model)
{
    var res = new ReaderResponseModel();

    // magic happens here
    // check if model is not empty
    if (model != null)
    {
        // create new entity
        var reader = new Reader();

        // add non-file attributes
        reader.Name = model.Name;
        reader.EmailAddress = model.EmailAddress;

        // check if any file is uploaded
        var work = model.Work;
        
        if (work != null)
        {
            // get the file extension and 
            // create a new File Name using Guid
            var fileName = $"{Guid.NewGuid()}{Path.GetExtension(work.FileName)}"; 
            // create full file path using 
            // the IHostEnvironment.ContentRootPath 
            // which is basically the execution directory 
            // and append a sub directory workFiles 
            // [Should be present before hand!!!] 
            // and lastly append the file name 
            var filePath = Path.Combine(_env.ContentRootPath, "Files", fileName); 
            
            // open-create the file in a stream and 
            // copy the uploaded file content into 
            // the new file (IFormFile contains a stream) 
            using (var fileSteam = new FileStream(filePath, FileMode.Create)) 
            { 
                await work.CopyToAsync(fileSteam); 
            } 
            
            // assign the generated filePath to the 
            // workPath property in the entity 
            
            reader.WorkPath = $"{Request.Scheme}://{Request.Host}/Files/{fileName}";     
        } 
        
        // add the created entity to the datastore 
        // using a Repository class IReadersRepository 
        // which is registered as a Scoped Service 
        // in Startup.cs 
        var created = _repo.AddReader(reader); 
        
        // Set the Success flag and generated details 
        // to show in the View 
        res.IsSuccess = true; 
        res.ReaderId = created.Id.ToString(); 
        res.WorkPath = created.WorkPath; 
        res.RedirectTo = Url.Action("Index"); 
    } 
    
    // return the model back to view 
    // with added changes and flags 
    return res; 
} 

In a success scenario, when we submit the Form with a File and the Reader information to the Controller, it adds the Reader to the ReaderStore and saves the File to /Files subdirectory and redirects back to the Main Grid View. Back in the Grid, we bind all the Readers which are currently available in the ReaderStore as a Table with the information along with the FilePath for the Work as below:

@model IEnumerable<ReaderStore.WebApp.Models.Entities.Reader>
<div class="container-fluid">
    <div class="row">
        <div class="col-12">
            <a class="float-right btn btn-primary" href="@Url.Action("New", "Readers")">Add Reader</a>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <table class="table">
                <tr>
                    <th>Id</th>
                    <th>Name</th>
                    <th>EmailAddress</th>
                    <th>Work</th>
                    <th>Added On</th>
                </tr>
                @foreach (var r in Model)
                {
                    <tr>
                        <td>
                            <a href="@Url.Action("Index", "Readers", new { id = r.Id })">
                                @r.Id</a>
                        </td>
                        <td>@r.Name</td>
                        <td>@r.EmailAddress</td>
                        <td><a href="@r.WorkPath">@r.WorkPath</a></td>
                        <td>@r.AddedOn</td>
                    </tr>
                }
                @if(Model.Count() == 0) {
                    <tr>
                        <td colspan="5">No Records Found</td>
                    </tr>
                }
            </table>
        </div>
    </div>
</div>

If we click on the workFiles link we won’t be able to access it, because we haven’t added a static file handler for this folder. We’ll add it in the ConfigureServices() method in the Startup class as below:

app.UseStaticFiles(new StaticFileOptions
{
    RequestPath = "/Files",
    FileProvider = new PhysicalFileProvider(
        Path.Combine(Directory.GetCurrentDirectory(), "Files")),
    ServeUnknownFileTypes = true,
    OnPrepareResponse = ctx =>
    {
        const int durationInSeconds = 60 * 60 * 24 * 365;
        ctx.Context.Response.Headers[HeaderNames.CacheControl] 
		= "public,max-age=" + durationInSeconds;
    }
});

When we try accessing the file using the generated link under /Files we can be able to view the file (for supported MIME types by the browser) or download (if the type is not supported to view).

POST multipart Form with jQuery Ajax –

To override the current MVC form submit behavior and POST the form with files via jQuery as we did before with a simple Form with text data, we would need to modify our jQuery submit function a bit to POST data in multipart/form-data encType.

$(".form").submit(function (event) {
    debugger;
    event.preventDefault();

    // fetch the form object
    $f = $(event.currentTarget);

    // check if form is valid
    if ($f.valid()) {
        $("div.loader").show();

        // fetch the action and method
        var url = $f.attr("action");
        var method = $f.attr("method");

        if (method.toUpperCase() === "POST") {
            // prepare the FORM data to POST
            var data = new FormData(this);
            
            // ajax POST
            $.ajax({
                url: url,
                method: "POST",
                data: data,
                processData: false,
                contentType: false,
                success: handleResponse,
                error: handleError,
                complete: function (jqXHR, status) {
                    console.log(jqXHR);
                    console.log(status);
                    $f.trigger('reset');
                }
            });
        }
    }
});

function handleResponse(res) {
    debugger;
    $("div.loader").hide();
    // check if isSuccess from Response
    // is False or Not set
    if (res.isSuccess) {
        debugger;
        // handle successful scenario
        // redirect to the specified page
        window.location = res.redirectTo;
    }
    else {
        // error handling
    }
}

The application experience looks like this:

wp-content/uploads/2022/05/file-upload-form.gif

Buy Me A Coffee

Found this article helpful? Please consider supporting!

In this way, we can develop a multi content form with fields and file using the IFormFile type which greatly helps in efficiently handling form file uploads for all types. The working application is available under the GitHub repo: https://github.com/referbruv/aspnetcore-file-upload-sample


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *