Saturday, July 1, 2023

ASP.NET Core - File Upload in Server Drive


In this tutorial we will learn about how to upload any kind of file into the web server. For this purpose, we will use ASP.NET Core technology and Visual Studio 2022 IDE.

Pre Requisites
  • We need Visual Studio 2022 IDE.
  • We need ASP.NET Core SDK installed in the system.
  • We need internet connection to download NuGet and other packages during project development. 
  • We need SQL Server Management Studio 2018 to deal with SQLServer database.
Project Creation Steps
First of all, we will create an ASP.NET Core project with the following features.
  • Template will be ASP.NET Core Empty
  • Framework version will be ASP.NET Core 5.0
  • Project name will be FileUploadToWeb
  • Solution name will be FileUploadToWebSolution
STEP1. Open the Visual Studio 2022. In the Get Started, click the "Create a new project".


STEP2. Search the ASP.NET CORE EMPTY template in the search box. Select the ASP.NET Core Empty template. Click the Next button.


STEP3. Fill the details of the application in the "Configure your new project" as shown below.


STEP4. Fill the "Additional Information" as shown below. The .NET 5.0 version is used in application. Click the Create button.


STEP5. When we create the project, following files and folders are created in the "Solution Explorer" as shown below.


Click the "ShowAll Files" to expand the folders to get their contets. These are the default files and folders which are created when Empty template is selected to create the project. These contents are minimum files required to provide web host environment. But we need additional files and folder to convert this project into MVC Web application.

If we run the application now, we get Hello World message in the browser. This message is coming because of MapGet method given in the Startup class.

Install Packages using NuGet Package Manager Console

The NuGet Package Manager Console is used to download packages from NuGet. To open the Package Manager Console in Visual Studio, do the following steps.
Tools > NuGet Package Manager > Package Manager Console
The Package Manager Console will appear as shown below.

We download the packages using commands in the  Package Manager Console. Run the following commands one by one to download and install the EntityFrameworkCore packages needed in the project.

PM> Install-Package Microsoft.EntityFrameworkCore -Version 5.0
PM> Install-Package Microsoft.EntityFrameworkCore.Design -Version 5.0
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Version 5.0
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 5.0

Rebuild the project. Note that Packages node is created in the Solution Explorer. All the installed packages are visible in the Packages node.


Note that for the CRUD operations, we will use SQL Server database. This is why we are using EntityFrameworkCore package related to SQL Server.

Create Folders Required for the MVC Application

Create the following three folders to convert this project into MVC project.  The details of these folders are given ahead.
  • Models
  • Views
  • Controllers
Create the following additional folders in this project. These are needed for files upload. The details of these folders are given one by one.
  • Files
  • wwwroot
  • Data

Files: We create this folder to save the uploaded files in it.
wwwroot: This folder contains all the static file such as Bootstrap, CSS, JavaScript etc. The wwwroot is the WebRootFolder of the application. We add client side libraries in this folder. This folder is needed in almost all the applications in which views are used to provide UI to the application. We will discuss about it later.
Data: In this folder, we create the class files needed for CRUD operations using Entity Framework. The DbContext class file will be used in this. We will discuss about it later.

After creating the folders in the project, we get the following folders in the Solution Explorer.


Add Client Side Library for UI

To add "Client Side Library", right click the "FileUploadToWeb" project in the Solution Explorer. Click Add and then "Client Side Library" as shown below.

The Visual Studio will open the Add Client Side Library dialog as shown below. 

Search twitter-bootstrap in the Library textbox. Use @ symbol to get a particular version of the library. For example, twitter-bootstrap@4.6.2. Expand the dialog box. Choose specific files radio button as shown below.

We have selected specific files (for example bootstrap.min.css and bootstrap.min.js). Click the Install button. These libraries will be installed inside the wwwroot folder. Also, libman.json will be created in the content root folder as shown below.

Double click the libman.json file. We get the schema of the libraries installed in the project.

Right click the libman.json file. All the possible operations are given in the menu items as shown below.

It is obvious that installing and uninstalling the libraries and restoring them or changing the library versions is very easy using the libman.json file.

Add MVC Service

Adding the folders is not enough. We need to modify the Startup class file to include the MVC service in the project. We can use either AddMvc extension method or AddControllersWithViews extension method to enable the MVC service in the project. Inside the ConfigureServices method we include the service as shown below. 

public void ConfigureServices(IServiceCollection services)
  {
  services.AddControllersWithViews();
  }

NOTE: You can learn about the differences among AddMvc, AddControllersWithViews, AddControllers and AddRazorPages extension methods in separate tutorial. Right now, it is enough to know that if we are using razor views in the project, we should use either AddMvc or AddControllersWithViews.

Add Middleware

Middleware are used in the ASP.NET Core to handle the HTTP requests and HTTP responses. By default following middleware are included in the Configure method in the Startup class file in this application. These are extension methods of IApplicationBuilder. 
  • UseDeveloperExceptionPage
  • UseRouting
  • UseEndpoints

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }

NOTE: The IApplicationBuilder parameter of Configure method provides different middleware to handle the HTTP requests and responses. These middleware are included in the Configure method in a definite sequence as per the need of the application.

Middleware Examples

UseStaticFiles: As we have to deal with the static files such as BootStrap, CSS, JavaScript, add the UseStaticFiles middleware extension method in the application.

UseRouting and UseEndpoints: The UseRouting middleware is used to enable routing in the application. But enabling routing is not enough. We need different endpoints in the application to handle different HTTP requests. The UseEndpoints middleware is used for this purpose. In MVC, the HTTP requests are handled by action methods of controllers in ASP.NET Core MVC. We use UseEndpoints middleware to add endpoints in the application.
  • The default MapGet method will handle the HTTP GET requests only. 
  • To handle any kind of HTTP requests such as GET, POST, PUT etc., we use Map method. 
  • The MapControllerRoute method will map HTTP request to an action method of a controller.

MapControllerRoute:
This method takes name and pattern named parameters. The method will map the routes of all action methods of the controllers. 

We remove the MapGet method and add the MapControllerRoute method. Its name parameter is the default. Its pattern parameter has a string pattern. In this string pattern, the controller and action are keywords and the value assigned to them are their default values. It means that if controller and action names are not given in the URL, then controller will be File and action will be Index(for the below example). The default endpoint will be Index method of the File controller in this application(It can be any controller but we have chosen FileController. This controller will be created inside Controllers folder later)

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(name: "default", pattern: "{controller=File}/{action=Index}/{Id?}");
            });
        }

Add FileController

Now include the FileController controller inside the Controllers folder. Select the Controllers folder and right click it. Click Add and then Controllers as shown below.

The "Add New Scaffolded Item" opens as shown below. Select the MVC Controller - Empty template and click Add button.

We get the "Add New Item" dialog box. Rename the HomeController to FileController and click Add button. 

Visual Studio 2022 automatically creates index action method in the controller file. The FileController class file will be added in the Controllers folder.

The code in the FileController is as follows.

using Microsoft.AspNetCore.Mvc;

namespace FileUploadToWeb.Controllers
{
    public class FileController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}
The above Index method returns a view using View() method. As the View method is parameter less, it will return a view which name will be the name of the method in which the method is. So, View method will return "Index" named view. Next, we create the Index razor view.

Add View

To add view, we can use manual technique or can take the help of Visual Studio. Inside the Views folder, create File folder and inside this folder, create a razor view named as Index. Or, Right click inside the Index method keeping cursor anywhere in the method. Click "Add View..." menu item. The "Add New Scaffolded Item" windows appears. Select "Razor View - Empty". Click Add button. The "Add New Item" window appears. Type "Index.cshtml" in the Name text box. Click Add button.

The Index view is created along with File folder in the Views folder as shown below.
Open the Index.cshtml file and clear its contents and write the following code in it.
<h1>I am Index razor view in File Controller</h1>

Build the project and press F5 to run the application. As routing is enabled and action method of a controller is mapped in the Startup class file. We get no issue and the following output appears.

Add Models and Connection String

Now the application is good enough to include the file upload features in the application. We outline the points to be discussed to go ahead.
  • Connection String in appsettings json file
  • Models and ViewModel in Models folder
  • First Approach with EF Core, AppDbContext class file in Data folder
  • Common layout for views, tag helpers and Bootstrap 

Connection String: Connection string keeps information about database server, it's database and password etc. Include the connection string in the appsettings.json file. The connection string provides valuable information to the Entity Framework to create tables mapped with the models in the database mentioned in the connection string. The connection string value is read by the entity frame work.

How to Add Connection String in appsettings.json file: Click View > SQL Server Object Explorer


Open the SQL Server Object Explorer pane as shown below.

Right click the localdb and click the Properties. In the Properties pane, we get the Connection String in the General section as shown below. Copy the value of Connection String. 


Now, open the appsettings.json file. Paste the copied value to a connection string named as DefaultConnection.
So, we get the following connection string in the appsettings.json file.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=master;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False"
  },
  "AllowedHosts": "*"
}
Models and ViewModel in Models folder: Add the following model classes in the Models folder.

Models/FileModel.cs


using System;

namespace FileUploadToWeb.Models
{
    public class FileModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string FileType { get; set; }
        public string Extension { get; set; }
        public string CreatedBy { get; set; }
        public DateTime? CreatedOn { get; set; }
        public string Filepath { get; set; }
    }
}
File/ AppDbContext.cs
Go in the File/AppDbContext class file and update the code as shown below


using FileUploadToWeb.Models;
using Microsoft.EntityFrameworkCore;

namespace FileUploadToWeb.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options): base(options)
        {              
        }

        public DbSet files { get; set; } 
    }
}
First, we create models needed for the file upload and saving them in the drive of web server. We use Code First Approach with EF Core. 

Add the IConfiguration  using Framework Constructor DI in Startup class

private readonly IConfiguration config;

        public Startup(IConfiguration config)
        {
            this.config = config;
        }
The config object is used to read configuration value from appsettings.json file. For example, we read the connection string from the appsettings.json file. The config object has GetConnectionString method especially to read the value of a connection string. This is shown in the below code.

Add the DbContext Service  in Startup class


public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddDbContext(options=>options.UseSqlServer(config.GetConnectionString("DefaultConnection")));
        }

Add Migration

The add-migration command is run to build the classes corresponding to the tables being created in the database. The tables are generated after running the update-database command. The add-migration command will create Migrations folder in Solution Explorer. This folder contains the details of the classes generated after running the command.

PM> Add-Migration Initial
Build started...
Build succeeded.
To undo this action, use Remove-Migration.

Migrations folder is created as shown below.

The update-database command is run to build tables in the database.
PM> update-database
Build started...
Build succeeded.
Done.

The tables are created in the AppliedK database as shown below after execution of update-database command.


Layout etc. for UI

Rebuild the project and run the application. We are getting no issue. Now, we convert the Index view into a UI which allows selecting multiple files and uploading them into the web server folder named as Files. Before going any further, we first create the following files
  • Views/Shared/_Layout.cshtml 
  • Views/ViewStart.cshtml
  • Views/ViewImports.cshtml
Inside the Views folder, create Shared folder which will hold layout razor files. The Views folder contains other subfolders for each controller.


Inside the Views folder, we create ViewsImports and ViewStart files.

Add Layout Razor file:
Select the Shared folder. Right click and click Add > Views... The "Add New Scaffolded Item" windows appears. Select "Razor View - Empty". Click Add button. The "Add New Item" window appears. Select Razor Layout template. The default name _Layout.cshtml is okay. Click Add button. The _Layout.cshtml file is added in the Shared folder.


The content of layout view is as follows.

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>

Add ViewStart Razor file:
Select the Views folder. Right click and click Add > Views... The "Add New Scaffolded Item" windows appears. Select "Razor View - Empty". Click Add button. The "Add New Item" window appears. Select Razor View Start template. The default name _ViewStart.cshtml is okay. Click Add button. The _ViewStart.cshtml file is added in the Views folder.

The content of _ViewStart view is as follows.
@{
    Layout = "_Layout";
}


Add ViewImports Razor file:
Select the Views folder. Right click and click Add > Views... The "Add New Scaffolded Item" windows appears. Select "Razor View - Empty". Click Add button. The "Add New Item" window appears. Select Razor View Imports template. The default name _ViewImports.cshtml is okay. Click Add button. The _ViewImports.cshtml file is added in the Views folder.


The content of _ViewImports view is nothing. We add the following code in it.
@using FileUploadToWeb
@using FileUploadToWeb.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Tips: To avoid the spelling error for tag helper, In the Solution Explorer/Frameworks node, search the Microsoft.AspNetCore.Mvc.TagHelpers. Right click it. We get the following. Copy the text and paste it in the _ViewImports file.

Build the application.

Add the Bootstrap CSS in Layout Razor View
Open the Views/Shared/_Layout.cshtml file and update the code as shown below.

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link href="~/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div>
        @RenderBody()
    </div>
</body>
</html>
REMARK: The tilde(~) in the <link href="~/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" /> refers to the Web root folder i.e. wwwroot. In case of static files like JavaScript and CSS, ~ represents the wwwroot folder.

Updated Views/Files/Index.cshtml file is as given below.

<div class="container">
    <h3>Upload files here</h3>
    <hr />
    <form method="post">
        <input type="file" name="files" required multiple />
        <input type="text" name="description" required />
        <button type="submit" class="btn btn-primary" asp-action="UploadFiles" asp-controller="File">Upload file</button>
    </form>
    <br/>
    <br/>
    <h5>Uploaded Files Details:</h5>
    <hr/>
    <table>
        <thead>
            <tr>
                <th>Name</th>
                <th>Description</th>
                <th>File Type</th>
                <th>Extension</th>
                <th>File Path</th>
                <th>Created By</th>
                <th>Created On</th>
            </tr>
        </thead>
      
    </table>
</div>
Run the Application. 



Create UploadFiles Action method in FileController
Look at the following line of code in Index.cshtml file.
<button type="submit" class="btn btn-primary" asp-action="UploadFiles">Upload file</button>
It implies that when submit button is clicked, uploaded files will be submitted to UploadFiles action method in FileController.

We create the action method as given below.

[HttpPost]
        public IActionResult UploadFiles(List files, string description)
        {
            foreach (var file in files)
            {
                string filePath = Path.Combine(Directory.GetCurrentDirectory(), "Files", file.FileName);
                //copy each file to the web server Files folder
                using (FileStream fs = new FileStream(filePath,FileMode.Create))
                {
                    file.CopyTo(fs);
                }
                // create an instance of FileModel and initialize it with form data for each file
                FileModel fileModel = new FileModel()
                {
                    Name = file.FileName,
                    Description = description,
                    FileType = file.ContentType,
                    Extension = Path.GetExtension(file.FileName),
                    CreatedBy = Environment.UserName,
                    CreatedOn = DateTime.UtcNow,
                    Filepath = filePath
                };
                // pass each file to the context object so that Entity framework will add it into the 'files' table given in AppDbContext file
            }
            return RedirectToAction("Index");
        }
REMARKS
  • Look at the parameters of UploadFiles action method. The parameters files and description must exactly match with the form parameters. The spelling must be same. Behind the scene, data binding happens based on parameter matching. 
  • Also, note that still we have not used @model statement at the top of the Index view.
  • We have saved the uploaded files in Files folder but the DbContext object is not used till this stage. We will save the details of the uploaded files in database tables named as dbo.files in the next step.We modify the code as follows.
Add the AppDbContext using Constructor Dependency Injection in FileController

using FileUploadToWeb.Data;
using FileUploadToWeb.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.IO;

namespace FileUploadToWeb.Controllers
{
    public class FileController : Controller
    {
        private readonly AppDbContext context;

        public FileController(AppDbContext context)
        {
            this.context = context;
        }
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult UploadFiles(List files, string description)
        {
            foreach (var file in files)
            {
                string filePath = Path.Combine(Directory.GetCurrentDirectory(), "Files", file.FileName);
                //copy each file to the web server Files folder
                using (FileStream fs = new FileStream(filePath,FileMode.Create))
                {
                    file.CopyTo(fs);
                }
                // create an instance of FileModel and initialize it with form data for each file
                FileModel fileModel = new FileModel()
                {
                    Name = file.FileName,
                    Description = description,
                    FileType = file.ContentType,
                    Extension = Path.GetExtension(file.FileName),
                    CreatedBy = Environment.UserName,
                    CreatedOn = DateTime.UtcNow,
                    Filepath = filePath
                };
                // pass each file to the context object so that Entity framework will add it into the 'files' table given in AppDbContext file
                context.Add(fileModel);
                context.SaveChanges();
            }
            return RedirectToAction("Index");
        }
    }
}
Uploaded Files Details in files Table:
When we run the application and upload multiple files, these files are saved in Files folder and the details of these uploaded files are saved in files table as well. This is shown below.

Uploaded files saved in 'Files' folder:


What if we want to show the details of all uploaded files in Index Razor view?

To display the information of all uploaded files, we create a private method in the FileController to list all the uploaded files. We then call this private method in the Index method so that when Index method is called, the list of files object is returned by the method. This object can be passed as model object to the Index Razor View.


public IActionResult Index()
        {
           var files =  ListUploadedFiles();
            return View(files);
        }

private List<FileModel> ListUploadedFiles()
        {
            var files = context.files.ToList();
            return files;
        }
Updated Index.cshtml file

@model IEnumerable<FileModel>

<div class="container">
    <h3>Upload files here</h3>
    <hr />
    <form method="post" enctype="multipart/form-data">
        <input type="file" name="files" required multiple />
        <input type="text" name="description" required />
        <button type="submit" class="btn btn-primary" asp-action="UploadFiles" asp-controller="File">Upload file</button>
    </form>

    <h5>Uploaded Files Details:</h5>

    <table class="table table-striped">
        <thead>
            <tr>
                <th>Name</th>
                <th>Description</th>
                <th>File Type</th>
                <th>Extension</th>
                <th>Created By</th>
                <th>Created On</th>
            </tr>
        </thead>
        <tbody>

            @foreach (var file in Model)
            {
                <tr>
                    <td>@file.Name</td>
                    <td>@file.Description</td>
                    <td>@file.FileType</td>
                    <td>@file.Extension</td>
                    <td>@file.CreatedBy</td>
                    <td>@file.CreatedOn</td>
                </tr>
            }

        </tbody>
    </table>
</div>
Run the application.

Add the Delete and Download Buttons

@model IEnumerable<FileModel>

<div class="container">
    <h3>Upload files here</h3>
    <hr />
    <form method="post" enctype="multipart/form-data">
        <input type="file" name="files" required multiple />
        <input type="text" name="description" required />
        <button type="submit" class="btn btn-primary" asp-action="UploadFiles" asp-controller="File">Upload file</button>
    </form>

    <h5>Uploaded Files Details:</h5>

    <table class="table table-striped">
        <thead>
            <tr>
                <th>Name</th>
                <th>Description</th>
                <th>File Type</th>
                <th>Extension</th>
                <th>Created By</th>
                <th>Created On</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>

            @foreach (var file in Model)
            {
                <tr>
                    <td>@file.Name</td>
                    <td>@file.Description</td>
                    <td>@file.FileType</td>
                    <td>@file.Extension</td>
                    <td>@file.CreatedBy</td>
                    <td>@file.CreatedOn</td>
                    <td> <a asp-action="DeleteFile" asp-controller="File" class="btn btn-danger" asp-route-id="@file.Id">Delete </a></td>
                    <td><a asp-action="DownloadFile" asp-controller="File" class="btn btn-primary" asp-route-id="@file.Id">Download </a></td>

                </tr>
            }

        </tbody>
    </table>
</div>

Business Logic to Delete or Download file

 public IActionResult DeleteFile(int id)
        {
            //delete from database table
           FileModel fileToDelete=  ListUploadedFiles().Where(x => x.Id == id).FirstOrDefault();
            if (fileToDelete!=null)
            {
                context.Remove(fileToDelete);
                context.SaveChanges();
                TempData["Message"] = "File with Id =" + fileToDelete.Id + " is deleted successfully";
                //delete from Files folder
                System.IO.File.Delete(fileToDelete.Filepath);
            }
            return RedirectToAction("Index");
        }

        public IActionResult DownloadFile(int id)
        {
            FileModel fileToDownload = ListUploadedFiles().Where(x => x.Id == id).FirstOrDefault();
            if (fileToDownload != null)
            {
                TempData["Message"] = "File with Id =" + fileToDownload.Id + " is downloaded successfully";
                string filePath = fileToDownload.Filepath;
                return PhysicalFile(filePath, MimeTypes.GetMimeType(filePath), Path.GetFileName(filePath));

                
            }
            return RedirectToAction("Index");
        }
Add the following code in Index.cshtml below <h5> tag


<h5>Uploaded Files Details:</h5>
        @if (TempData["Message"]!=null)
        {
            
@TempData["Message"]
}

REMARKS:

No comments:

Post a Comment

Hot Topics