File Handling in ASP.NET Core MVC

File Handling in ASP.NET Core MVC

In this article, I will discuss How to Implement File Handling, i.e., how to upload files in an ASP.NET Core MVC Application using Buffering and Streaming Approaches with one Real-time Product Management Application. 

What is a File, and What Are the Different Types of Files?

A file is a collection of data or information stored with a specific name and path on a storage medium (e.g., a hard disk, SSD, or cloud storage). In web applications, files commonly refer to images, documents (PDF, Word), videos, or any other binary/textual data a user may upload or download.

Types of Files (common categories relevant to web applications):

  • Image files (JPEG, PNG, GIF, etc.).
  • Document files (PDF, DOCX, XLSX, etc.).
  • Audio/Video files (MP3, MP4, WAV, etc.).
  • Compressed/Archive files (ZIP, RAR, 7z, etc.).
  • Text/Configuration files (.txt, .xml, .json, etc.).
Why Do We Need File Handling in Web Applications?

In web applications, file handling is crucial for scenarios such as:

  • File Upload: Users need to upload files such as profile pictures, product images, or documents.
  • File Download: Users or administrators may need to download or retrieve these files for further use.
  • Data Management: Files may be stored as part of user or product data in e-commerce, CRM, or other large-scale applications.
Different Ways to Implement File Handling in ASP.NET Core MVC

There are two ways to implement File Handling in ASP.NET Core MVC: Buffering and Streaming.

Buffering Approach to Implement File Handling in ASP.NET Core MVC:

The entire file is read into memory by copying its content into a Memory Stream. Once the file is fully loaded into memory, we convert it into a byte array. Finally, we write the byte array to the disk. This is straightforward but not ideal for large files because it may consume too much memory. The syntax is given below to use Buffering Approach in ASP.NET Core MVC for File Uploading:

Buffering Approach to Upload File in ASP.NET Core MVC

Advantages of Buffering:
  • It is simpler to implement.
  • Suitable for smaller files (e.g., images, PDFs that are not too large).
  • Straightforward validations (e.g., file size, extension) before saving.
Disadvantages of Buffering:
  • Large files can consume significant memory, impacting server performance.
Streaming Approach to Implement File Handling in ASP.NET Core MVC:

In the streaming approach, the file is copied directly from the request stream to a file stream without buffering the entire file in memory. Instead of loading the whole file into memory, we open a FileStream that writes directly to the disk. This approach is more memory-efficient for large files since it does not require buffering the whole file, which minimizes memory usage. The syntax is given below to use the  Streaming Approach in ASP.NET Core MVC for File Uploading:

Streaming Approach to Upload File in ASP.NET Core MVC

In the FileStream Constructor:

  • filePath tells the system where to save the file.
  • FileMode.Create ensures a new file is created (or an existing one is overwritten).
  • FileAccess.Write allows data to be written to the file.
  • FileShare.None ensures that no other process can access the file during this write operation.
Advantages of Streaming:
  • It is ideal for large file uploads and reduces memory pressure on the server.
  • More scalable for large-scale or concurrent uploads.
Disadvantages of Streaming:
  • More complex to implement.
  • Validations (like file size and extension) might be trickier when data arrives in chunks.
Which is Good from the Performance Point of View?
  • Streaming is typically more performant and resource-friendly for large files because it prevents loading the entire file into memory. So, if your application needs to handle large files (hundreds of MBs or GBs) or you anticipate a large volume of concurrent uploads, go with Streaming.
  • Buffering is fine for smaller files and much easier to implement and maintain. So, if your application deals with small to medium file sizes (such as images and standard PDFs), go with Buffering.

Building a Real-time Product Management Application

We will build a Product Management application that lets users upload a main product image and multiple related images. The photos will be saved with unique names on the file system and in the database, and the image metadata will be stored in a SQL Server database using EF Core’s Code First approach. The application will also include functionality for updating and deleting files, displaying product lists with thumbnails, and showing detailed product views with all related images. The application supports the following Features:

  • Upload one main image.
  • Upload several related images.
  • Store images on the server’s file system and (for demonstration) also in the database.
  • Update product details (including replacing the main image or adding more related photos).
  • Delete products along with their associated images.
  • View a paginated and filterable list of products.
  • See product details in a rich view that includes a clickable image gallery.

Let us first understand the pictorial flow of our Ecommerce Application.

Product List or Index Page:

Our Product List view displays a list of all available products with filtering and pagination. Each product includes options to view details, edit, or delete. It also provides a Create New Product button to add a new product.

File Handling in ASP.NET Core MVC

Create New Product Page:

When the admin clicks the Create New Product button, it will open the following Create New Product Page. This page allows admins to add new products by providing basic details, a main image, and multiple related images. The new product is added to the database upon submission, and the page redirects to the Index Page.

File Handling in ASP.NET Core MVC Application

Product Details Page:

When you click on the Details button from the Index Page of a Particular Product, it will open the following Product Details Page. This Page shows detailed information about a specific product, including all associated images. Clicking on any image highlights it in a larger view. Displays product brand, price, discount, and features. It also includes buttons for adding the product to the cart and purchasing it.

File Handling in ASP.NET Core MVC Application with Examples

Product Edit Page:

When you click on the Edit button from the Index Page of a Particular Product, it will open the Product Edit Page. It has two sections. In the first section, it will allow us to Update the Product details, and in the second section, it will allow us to delete any related images. I am not providing the screenshot here because the page size is bigger. You will see this page once you run the application.

Product Delete Page:

When you click the Delete button from the Index Page of a Particular Product, it will open the following Product Delete Page. From this page, once you click the Delete button, the Products will be removed from the database, and the associated images will be deleted.

How to upload files in an ASP.NET Core MVC Application using Buffering and Streaming Approaches

Let us proceed and Implement this Product Management application step by step using ASP.NET Core MVC, Entity Framework Core Code First Approach with SQL Server Database.

Create a New ASP.NET Core MVC Project and Install EF Core Packages

Open Visual Studio, create a new ASP.NET Core Web App (Model-View-Controller) project using the latest .NET version and give the project name ProductManagementApp. Once you create the Project, Install the necessary EF Core Packages targeting the SQL Server database. Please execute the following commands in the Visual Studio Package Manager Console:

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Create the Models

Now, we will create two Models for our application.

Product

Create a class file named Product.cs within the Models folder and copy and paste the following code. This Model represents the core product information. It holds basic details such as the product name, description, brand, price, and discount. It also contains fields for the main image file name and image data.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ProductManagementApp.Models
{
    public class Product
    {
        [Key]
        public int ProductId { get; set; }

        [Required]
        [StringLength(100)]
        public string Name { get; set; }

        [StringLength(500)]
        public string Description { get; set; }

        // Additional fields
        [StringLength(100)]
        public string Brand { get; set; }

        [Range(0, double.MaxValue)]
        [Column(TypeName ="decimal(18,2)")]
        public decimal Price { get; set; }

        [Range(0, 100)]
        [Column(TypeName = "decimal(18,2)")]
        public decimal Discount { get; set; } // e.g., store as 0-100 for % discount

        // Image handling
        public string? MainImageFileName { get; set; }
        public byte[]? MainImageData { get; set; }

        // Navigation: Related images
        public ICollection<ProductImage> RelatedImages { get; set; }
    }
}

Note: I will show you how to store the Image in both the server file system and the database. However, you should not store the image file data in the database in real-time applications. The recommended approach is to store only the file names in the database and the actual file in a folder, which will give better performance and reduce the database size.

ProductImage

Create a class file named ProductImage.cs within the Models folder and copy and paste the following code. This Model Manages information about additional images related to a product. Each related image we upload gets stored as an instance of this model. It is used to link multiple images to a single product.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ProductManagementApp.Models
{
    public class ProductImage
    {
        [Key]
        public int ImageId { get; set; } //Primary Key

        [Required]
        public int ProductId { get; set; }

        public string ImageFileName { get; set; }
        public byte[] ImageData { get; set; }

        [ForeignKey("ProductId")]
        public Product Product { get; set; }
    }
}
Create the ApplicationDbContext

First, create a folder named Data in the project root directory and then create a new class file named ApplicationDbContext.cs in the Data folder and copy and paste the following code. The ApplicationDbContext manages the application’s database connection and provides access to the Products and ProductImages tables. Seed data has been provided to facilitate testing.

using Microsoft.EntityFrameworkCore;
using ProductManagementApp.Models;

namespace ProductManagementApp.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Seed products for three different brands with three product types each:
            // Brand: AlphaTech, BetaWorks, GammaCorp
            // Product Types: Watch, Mobile, Laptop

            modelBuilder.Entity<Product>().HasData(
                // AlphaTech products
                new Product
                {
                    ProductId = 1,
                    Name = "Alpha Watch",
                    Description = "A smart and stylish watch from AlphaTech.",
                    Brand = "AlphaTech",
                    Price = 199.99m,
                    Discount = 5
                },
                new Product
                {
                    ProductId = 2,
                    Name = "Alpha Mobile",
                    Description = "A high-performance mobile device by AlphaTech.",
                    Brand = "AlphaTech",
                    Price = 499.99m,
                    Discount = 10
                },
                new Product
                {
                    ProductId = 3,
                    Name = "Alpha Laptop",
                    Description = "A lightweight and powerful laptop from AlphaTech.",
                    Brand = "AlphaTech",
                    Price = 999.99m,
                    Discount = 15
                },

                // BetaWorks products
                new Product
                {
                    ProductId = 4,
                    Name = "Beta Watch",
                    Description = "An elegant watch featuring modern functionalities by BetaWorks.",
                    Brand = "BetaWorks",
                    Price = 149.99m,
                    Discount = 5
                },
                new Product
                {
                    ProductId = 5,
                    Name = "Beta Mobile",
                    Description = "An advanced mobile device with cutting-edge technology from BetaWorks.",
                    Brand = "BetaWorks",
                    Price = 599.99m,
                    Discount = 10
                },
                new Product
                {
                    ProductId = 6,
                    Name = "Beta Laptop",
                    Description = "A powerful laptop built for both gaming and professional use by BetaWorks.",
                    Brand = "BetaWorks",
                    Price = 1099.99m,
                    Discount = 20
                },

                // GammaCorp products
                new Product
                {
                    ProductId = 7,
                    Name = "Gamma Watch",
                    Description = "A sporty watch with fitness tracking features from GammaCorp.",
                    Brand = "GammaCorp",
                    Price = 129.99m,
                    Discount = 5
                },
                new Product
                {
                    ProductId = 8,
                    Name = "Gamma Mobile",
                    Description = "A compact mobile device with excellent battery life by GammaCorp.",
                    Brand = "GammaCorp",
                    Price = 399.99m,
                    Discount = 5
                },
                new Product
                {
                    ProductId = 9,
                    Name = "Gamma Laptop",
                    Description = "A versatile laptop with long-lasting battery performance from GammaCorp.",
                    Brand = "GammaCorp",
                    Price = 899.99m,
                    Discount = 10
                }
            );
        }

        // DbSets for our entities
        public DbSet<Product> Products { get; set; }
        public DbSet<ProductImage> ProductImages { get; set; }
    }
}
Configure Database Connection String

In your appsettings.json, add a connection string pointing to SQL Server Database, which will be used by our DbContext class. So, please modify the appsettings.json file as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ProductManagementDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Set Up DbContext, Dependency Injection and Middleware:

Please modify the Program class as follows.

using Microsoft.EntityFrameworkCore;
using ProductManagementApp.Data;

namespace ProductManagementApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            // Register the ApplicationDbContext with dependency injection
            builder.Services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Products}/{action=Index}/{id?}");

            app.Run();
        }
    }
}
Migrate and Create the Database

Open Visual Studio Package Manager Console and execute the Add-Migration Mig1 and Update-Database commands as shown in the below image:

How to Upload File in ASP.NET Core MVC

This will create our ProductManagementDB database with Products and ProductImages tables, as shown in the image below:

How to Upload File in ASP.NET Core MVC with Examples

Create ViewModels for Product Management

Although we can use the Product entity directly with the View, it’s often better to separate concerns with ViewModels. Using ViewModels separates concerns and ensures only necessary data is sent to views. So, create a folder named ViewModels in the project root directory where we will create all our View Models to manage the Product data.

CreateProductViewModel:

Create a class file named CreateProductViewModel.cs within the ViewModels folder and copy and paste the following code. This View Model is used to capture data when creating a new product. This ViewModel is bound to the Create view so that when an admin enters product data and selects files, the form data (including uploaded images) is easily managed.

using System.ComponentModel.DataAnnotations;
namespace ProductManagementApp.ViewModels
{
    public class CreateProductViewModel
    {
        [Required, StringLength(100)]
        public string Name { get; set; }

        [StringLength(500)]
        public string Description { get; set; }

        [StringLength(100)]
        public string Brand { get; set; }

        [Range(0, double.MaxValue)]
        public decimal Price { get; set; }

        [Range(0, 100)]
        public decimal Discount { get; set; }

        // File upload fields
        public IFormFile MainImageFile { get; set; }
        public List<IFormFile> RelatedImageFiles { get; set; } = new();
    }
}

Here,

  • MainImageFile: An IFormFile to upload the main image.
  • RelatedImageFiles: A list of IFormFile for uploading multiple additional images.
UpdateProductViewModel:

Create a class file named UpdateProductViewModel.cs within the ViewModels folder and copy and paste the following code. This View Model is used to support the editing of an existing product. The Edit view uses this ViewModel. It allows the administrator to update textual details and manage images (either by replacing the main image or appending new related images).

using Microsoft.AspNetCore.Http;
using ProductManagementApp.Models;
using System.ComponentModel.DataAnnotations;

namespace ProductManagementApp.ViewModels
{
    public class UpdateProductViewModel
    {
        public int ProductId { get; set; }

        [Required, StringLength(100)]
        public string Name { get; set; }

        [StringLength(500)]
        public string Description { get; set; }

        [StringLength(100)]
        public string Brand { get; set; }

        [Range(0, double.MaxValue)]
        public decimal Price { get; set; }

        [Range(0, 100)]
        public decimal Discount { get; set; }

        // Do NOT mark this as required; it should be optional.
        public IFormFile? MainImageFile { get; set; }

        // For uploading additional related images (optional)
        public List<IFormFile>? RelatedImageFiles { get; set; } = new List<IFormFile>();

        // For displaying the currently saved main image.
        public string? ExistingMainImageFileName { get; set; }

        // For displaying existing related images.
        public List<ProductImage>? ExistingRelatedImages { get; set; } = new List<ProductImage>();
    }
}

Here,

  • The product details that can be updated.
  • Optional new file upload fields: MainImageFile (to replace the main image) and RelatedImageFiles (to add more related images).
  • ExistingMainImageFileName and ExistingRelatedImages: Hold current image information so that the UI can display what is already stored.
DeleteProductViewModel:

Create a class file named DeleteProductViewModel.cs within the ViewModels folder and copy and paste the following code. This View Model is used to confirm the deletion of a product. It is used in the Delete view so that the administrator can see key product information before confirming deletion.

namespace ProductManagementApp.ViewModels
{
    public class DeleteProductViewModel
    {
        public int ProductId { get; set; }

        // Display the product name for confirmation
        public string Name { get; set; }

        // Optionally show additional details
        public string Brand { get; set; }
        public decimal Price { get; set; }
        public string? MainImageFileName { get; set; }

        // You can include a short description or key features if desired
        public string Description { get; set; }
    }
}
ProductDisplayViewModel:

Create a class file named ProductDisplayViewModel.cs within the ViewModels folder and copy and paste the following code. The ProductDisplayViewModel and ProductImageDisplayViewModel are used to map the product and its related images into a format optimized for display. When displaying product details (for example, in the Details view or product listings), these ViewModels format and present the data cleanly, including creating thumbnail views for images.

namespace ProductManagementApp.ViewModels
{
    public class ProductDisplayViewModel
    {
        public int ProductId { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public string Brand { get; set; }
        public decimal Price { get; set; }
        public decimal Discount { get; set; }
        public string? MainImageFileName { get; set; }
        public List<ProductRelatedImageDisplayViewModel> RelatedImages { get; set; }
            = new List<ProductRelatedImageDisplayViewModel>();
    }

    public class ProductRelatedImageDisplayViewModel
    {
        public int ImageId { get; set; }
        public string ImageFileName { get; set; }
    }
}
ProductListViewModel:

Create a class file named ProductListViewModel.cs within the ViewModels folder and copy and paste the following code. This View Model supports the Index view that lists multiple products with filter and pagination functionality. The Index view binds to this ViewModel to display a filtered, paginated list of products and search options.

namespace ProductManagementApp.ViewModels
{
    public class ProductListViewModel
    {
        public IEnumerable<ProductDisplayViewModel> Products { get; set; }

        // Filter parameters
        public string SearchName { get; set; }
        public string SearchBrand { get; set; }
        public decimal? MinPrice { get; set; }
        public decimal? MaxPrice { get; set; }

        // Pagination parameters
        public int CurrentPage { get; set; }
        public int TotalPages { get; set; }
    }
}
Creating Products Controller

Create an Empty MVC Controller named ProductsController within the Controllers folder and then copy and paste the following code:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ProductManagementApp.Data;
using ProductManagementApp.Models;
using ProductManagementApp.ViewModels;
namespace ProductManagementApp.Controllers
{
public class ProductsController : Controller
{
private readonly ApplicationDbContext _context;
private readonly IWebHostEnvironment _hostEnvironment;
private readonly string _imagesFolderPath;
private const int PageSize = 3; // Adjust page size as needed
public ProductsController(ApplicationDbContext context, IWebHostEnvironment hostEnvironment)
{
_context = context;
_hostEnvironment = hostEnvironment;
// Set the folder path where images will be stored
_imagesFolderPath = Path.Combine(_hostEnvironment.WebRootPath, "Product-Images");
// Ensure the images folder exists
if (!Directory.Exists(_imagesFolderPath))
{
Directory.CreateDirectory(_imagesFolderPath);
}
}
// GET: Products/Index
public async Task<IActionResult> Index(string? searchName, string? searchBrand, decimal? minPrice, decimal? maxPrice, int page = 1)
{
// Use AsNoTracking() for read-only queries to improve performance
var query = _context.Products.AsNoTracking().AsQueryable();
// Apply filters if provided.
if (!string.IsNullOrEmpty(searchName))
{
query = query.Where(p => p.Name.Contains(searchName));
}
if (!string.IsNullOrEmpty(searchBrand))
{
query = query.Where(p => p.Brand.Contains(searchBrand));
}
if (minPrice.HasValue)
{
query = query.Where(p => p.Price >= minPrice.Value);
}
if (maxPrice.HasValue)
{
query = query.Where(p => p.Price <= maxPrice.Value);
}
// Count the total records for pagination.
var totalRecords = await query.CountAsync();
// Calculate total pages.
var totalPages = (int)Math.Ceiling(totalRecords / (double)PageSize);
// Get the current page records.
var products = await query
.OrderBy(p => p.Name) // Adjust sorting as needed
.Skip((page - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
// Map products to the display view model.
var productDisplayList = products.Select(p => new ProductDisplayViewModel
{
ProductId = p.ProductId,
Name = p.Name,
Description = p.Description,
Brand = p.Brand,
Price = p.Price,
Discount = p.Discount,
MainImageFileName = p.MainImageFileName
}).ToList();
// Create our view model for the list.
var vm = new ProductListViewModel
{
Products = productDisplayList,
SearchName = searchName,
SearchBrand = searchBrand,
MinPrice = minPrice,
MaxPrice = maxPrice,
CurrentPage = page,
TotalPages = totalPages
};
return View(vm);
}
// GET: Products/Details/5
public async Task<IActionResult> Details(int id)
{
// Use AsNoTracking() for a read-only query including related images.
var product = await _context.Products.AsNoTracking()
.Include(p => p.RelatedImages)
.FirstOrDefaultAsync(p => p.ProductId == id);
if (product == null)
{
return NotFound();
}
// Map to the display view model.
var vm = new ProductDisplayViewModel
{
ProductId = product.ProductId,
Name = product.Name,
Description = product.Description,
Brand = product.Brand,
Price = product.Price,
Discount = product.Discount,
MainImageFileName = product.MainImageFileName,
RelatedImages = product.RelatedImages
.Select(img => new ProductRelatedImageDisplayViewModel
{
ImageId = img.ImageId,
ImageFileName = img.ImageFileName
})
.ToList()
};
return View(vm);
}
// GET: Products/Create
public IActionResult Create()
{
// Simply display the create view.
return View();
}
// POST: Products/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreateProductViewModel vm)
{
// If the submitted model is invalid, re-display the form.
if (!ModelState.IsValid)
{
return View(vm);
}
// Create a new Product entity from the view model data.
var product = new Product
{
Name = vm.Name,
Description = vm.Description,
Brand = vm.Brand,
Price = vm.Price,
Discount = vm.Discount
};
// Process Main Image Upload using Buffering Approach
if (vm.MainImageFile != null && vm.MainImageFile.Length > 0)
{
// Buffering Approach Explanation:
// The entire uploaded file is read into memory using a MemoryStream.
// Once fully buffered, the file content is converted into a byte array,
// which is then used to store the file on disk and, optionally, in the database.
// This approach is simple but may consume significant memory for large files.
using var memoryStream = new MemoryStream();
await vm.MainImageFile.CopyToAsync(memoryStream); // Read file into memory
var fileBytes = memoryStream.ToArray(); // Convert the buffered stream to a byte array
// Optionally store the file bytes in the database (not recommended in production)
product.MainImageData = fileBytes;
// Generate a unique file name to avoid collisions and assign it to the product.
var uniqueFileName = GenerateUniqueFileName(vm.MainImageFile.FileName);
product.MainImageFileName = uniqueFileName;
// Write the buffered file bytes to the specified location on disk.
var filePath = Path.Combine(_imagesFolderPath, uniqueFileName);
await System.IO.File.WriteAllBytesAsync(filePath, fileBytes);
}
// Save the product to the database to obtain its ProductId.
_context.Products.Add(product);
await _context.SaveChangesAsync();
// Process Related Images Upload using Buffering Approach
if (vm.RelatedImageFiles != null && vm.RelatedImageFiles.Any())
{
foreach (var file in vm.RelatedImageFiles)
{
if (file.Length > 0)
{
// Buffer the file into memory.
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream); // Load file into memory
var fileBytes = memoryStream.ToArray(); // Convert buffered data to byte array
// Create a new ProductImage entity and assign the binary data.
var productImage = new ProductImage
{
ProductId = product.ProductId,
ImageData = fileBytes
};
// Generate a unique file name for the related image.
var uniqueFileName = GenerateUniqueFileName(file.FileName);
productImage.ImageFileName = uniqueFileName;
// Write the buffered image bytes to disk.
var relatedFilePath = Path.Combine(_imagesFolderPath, uniqueFileName);
await System.IO.File.WriteAllBytesAsync(relatedFilePath, fileBytes);
// Add the related image entity to the context.
_context.ProductImages.Add(productImage);
}
}
// Save all the related images to the database.
await _context.SaveChangesAsync();
}
// Redirect to the Index view after successfully creating the product.
return RedirectToAction(nameof(Index));
}
// GET: Products/Edit/5
[HttpGet]
public async Task<IActionResult> Edit(int id)
{
// Use AsNoTracking() for the GET request to simply fetch data.
var product = await _context.Products.AsNoTracking()
.Include(p => p.RelatedImages)
.FirstOrDefaultAsync(p => p.ProductId == id);
if (product == null)
{
return NotFound();
}
// Map the product to the update view model.
var vm = new UpdateProductViewModel
{
ProductId = product.ProductId,
Name = product.Name,
Description = product.Description,
Brand = product.Brand,
Price = product.Price,
Discount = product.Discount,
ExistingMainImageFileName = product.MainImageFileName,
ExistingRelatedImages = product.RelatedImages.ToList()
};
return View(vm);
}
// POST: Products/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(UpdateProductViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
// Retrieve the product with related images from the database (tracking enabled for update).
var product = await _context.Products
.Include(p => p.RelatedImages)
.FirstOrDefaultAsync(p => p.ProductId == vm.ProductId);
if (product == null)
{
return NotFound();
}
// Update product details.
product.Name = vm.Name;
product.Description = vm.Description;
product.Brand = vm.Brand;
product.Price = vm.Price;
product.Discount = vm.Discount;
// Update Main Image using Streaming Approach
if (vm.MainImageFile != null && vm.MainImageFile.Length > 0)
{
// Delete the old main image file from the file system, if it exists.
if (!string.IsNullOrEmpty(product.MainImageFileName))
{
var oldPath = Path.Combine(_imagesFolderPath, product.MainImageFileName);
if (System.IO.File.Exists(oldPath))
{
System.IO.File.Delete(oldPath);
}
}
// Generate a unique file name for the new main image.
var uniqueFileName = GenerateUniqueFileName(vm.MainImageFile.FileName);
product.MainImageFileName = uniqueFileName;
var filePath = Path.Combine(_imagesFolderPath, uniqueFileName);
// Streaming Approach Explanation:
// Instead of reading the entire file into memory (buffering) before saving,
// we open a file stream to the target file and copy the uploaded file's stream directly to it.
// This minimizes memory usage, especially for large files.
// Create a FileStream to write to the file.
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
// - filePath: Tells the system where to save the file
// - FileMode.Create: Ensures a new file is created (or an existing one is overwritten)
// - FileAccess.Write: Opens the file for writing only. Allows data to be written to the file
// - FileShare.None: Ensures exclusive access to the file while open. That means no other process can access the file during this write operation
await vm.MainImageFile.CopyToAsync(fileStream);
}
// Optionally, if you need to store the file bytes in the database (not recommended for production),
// you could read the file from disk here. For example:
product.MainImageData = System.IO.File.ReadAllBytes(filePath);
}
// Process Additional Related Images using Streaming Approach
if (vm.RelatedImageFiles != null && vm.RelatedImageFiles.Any())
{
foreach (var file in vm.RelatedImageFiles)
{
if (file.Length > 0)
{
// Generate a unique file name for each related image.
var uniqueFileName = GenerateUniqueFileName(file.FileName);
// Create a new ProductImage entity.
var productImage = new ProductImage
{
ProductId = product.ProductId,
ImageFileName = uniqueFileName
};
var filePath = Path.Combine(_imagesFolderPath, uniqueFileName);
// Streaming Approach Explanation:
// We open a file stream for the destination file and copy the uploaded file's stream
// directly into it without first loading the entire file into memory.
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await file.CopyToAsync(fileStream);
}
// Optionally, you can store binary data if necessary:
productImage.ImageData = System.IO.File.ReadAllBytes(filePath);
// Add the new related image entity to the context.
_context.ProductImages.Add(productImage);
}
}
}
// Update the product in the database and save changes.
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Products/Delete/5
[HttpGet]
public async Task<IActionResult> Delete(int id)
{
// Use AsNoTracking() for the deletion confirmation view.
var product = await _context.Products.AsNoTracking()
.FirstOrDefaultAsync(p => p.ProductId == id);
if (product == null)
{
return NotFound();
}
// Map product details to the delete view model.
var vm = new DeleteProductViewModel
{
ProductId = product.ProductId,
Name = product.Name,
Brand = product.Brand,
Price = product.Price,
MainImageFileName = product.MainImageFileName,
Description = product.Description
};
return View(vm);
}
// POST: Products/Delete/5
[HttpPost, ActionName("Delete")]
public async Task<IActionResult> DeleteConfirmed(int id)
{
// Retrieve the product with its related images (tracking is required for deletion).
var product = await _context.Products
.Include(p => p.RelatedImages)
.FirstOrDefaultAsync(p => p.ProductId == id);
if (product == null)
{
return NotFound();
}
// Delete the main image file from the file system if it exists.
if (!string.IsNullOrEmpty(product.MainImageFileName))
{
var filePath = Path.Combine(_imagesFolderPath, product.MainImageFileName);
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
}
}
// Delete all related image files from the file system.
foreach (var img in product.RelatedImages)
{
var path = Path.Combine(_imagesFolderPath, img.ImageFileName);
if (System.IO.File.Exists(path))
{
System.IO.File.Delete(path);
}
}
// Remove the product images and the product record from the database.
_context.ProductImages.RemoveRange(product.RelatedImages);
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
// GET: Products/DeleteImage/5
public async Task<IActionResult> DeleteImage(int imageId)
{
// Retrieve the related image.
var image = await _context.ProductImages.FindAsync(imageId);
if (image == null)
{
return NotFound();
}
// Remove the image file from the file system.
var imagePath = Path.Combine(_imagesFolderPath, image.ImageFileName);
if (System.IO.File.Exists(imagePath))
{
System.IO.File.Delete(imagePath);
}
// Remove the image record from the database.
_context.ProductImages.Remove(image);
await _context.SaveChangesAsync();
// Redirect back to the edit view for the product.
return RedirectToAction(nameof(Edit), new { id = image.ProductId });
}
// Helper method to generate a unique file name based on the original file name.
private string GenerateUniqueFileName(string originalFileName)
{
var extension = Path.GetExtension(originalFileName);
return Guid.NewGuid().ToString() + extension;
}
}
}
CRUD Operations on Products:
  • Create: Provides GET and POST actions to display a form for creating a new product and then processing the submitted data, including handling image uploads, to save the product in the database.
  • Index Action: Retrieves a paginated and filterable list of products. It applies search and filtering criteria (such as product name, brand, and price range) and then maps the results to a view model for display.
  • Details Action: Retrieves a single product along with its related images and maps the data to a display view model for a detailed view.
  • Update (Edit): Offers GET and POST actions to display an editable form pre-populated with an existing product’s data and then process updates. This action also supports updating product images using a streaming approach (to reduce memory usage for large files).
  • Delete: Provides a confirmation view and then handles the deletion of a product along with all its associated images from the database and the file system.

Note: For large/production scenarios, consider using cloud storage (like Azure Blob Storage or AWS S3) for your images and store references in the database.

Create the Views

We will have five main views that we need to create within the Views/Products folder: Index.cshtml, Create.cshtml, Edit.cshtml, Details.cshtml, and Delete.cshtml.

Index.cshtml

Create a view named Index.cshtml within the Views/Products folder and then copy and paste the following code. The Index View displays a paginated, filterable list of products. The controller action builds a ProductListViewModel and passes it to this view, then iterates over the list of products to render each row.

@model ProductManagementApp.ViewModels.ProductListViewModel
@{
ViewData["Title"] = "Products List";
}
<div class="container-sm w-100 mx-auto mt-4">
<!-- Page Header -->
<div class="row align-items-center mb-3">
<div class="col">
</div>
<div class="col-auto">
<a asp-action="Create" class="btn btn-primary">+ Create New Product</a>
</div>
</div>
<!-- Filter Section -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<form method="get" asp-action="Index">
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">
🔍 Product Name
</label>
<input type="text" name="searchName" class="form-control" placeholder="Search by name" value="@Model.SearchName" />
</div>
<div class="col-md-3">
<label class="form-label">
🏷️ Brand
</label>
<input type="text" name="searchBrand" class="form-control" placeholder="Search by brand" value="@Model.SearchBrand" />
</div>
<div class="col-md-2">
<label class="form-label">
💲 Min Price
</label>
<input type="number" name="minPrice" class="form-control" placeholder="Min" step="0.01" value="@(Model.MinPrice.HasValue ? Model.MinPrice.Value.ToString("F2") : "")" />
</div>
<div class="col-md-2">
<label class="form-label">
💲 Max Price
</label>
<input type="number" name="maxPrice" class="form-control" placeholder="Max" step="0.01" value="@(Model.MaxPrice.HasValue ? Model.MaxPrice.Value.ToString("F2") : "")" />
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-outline-primary w-100">
Filter ✨
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Responsive Table -->
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-light">
<tr>
<th class="text-center" style="min-width:100px;">Image</th>
<th style="min-width:200px;">Name</th>
<th style="min-width:150px;">Brand</th>
<th class="text-end" style="min-width:120px;">Price</th>
<th class="text-center" style="min-width:180px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model.Products)
{
// Resolve main image URL; if missing, fallback to a placeholder.
var mainImgUrl = !string.IsNullOrEmpty(product.MainImageFileName)
? Url.Content("~/product-images/" + product.MainImageFileName)
: Url.Content("~/product-images/NoImage.png");
// Calculate discounted price if applicable.
decimal discountedPrice = product.Price;
if (product.Discount > 0 && product.Discount <= 100)
{
discountedPrice = product.Price * (1 - product.Discount / 100);
}
<tr>
<!-- Image Column -->
<td class="text-center">
<img src="@mainImgUrl" alt="@product.Name" class="img-fluid rounded" style="max-width:90px; max-height:90px;" />
@* @if (!string.IsNullOrEmpty(product.MainImageFileName))
{
<img src="@mainImgUrl" alt="@product.Name" class="img-fluid rounded" style="max-width:90px; max-height:90px;" />
}
else
{
<span class="text-muted">No image</span>
} *@
</td>
<!-- Name Column with text truncation -->
<td>
<span class="d-inline-block text-truncate" style="max-width:190px;" title="@product.Name">
@product.Name
</span>
</td>
<!-- Brand Column with text truncation -->
<td>
<span class="d-inline-block text-truncate" style="max-width:130px;" title="@product.Brand">
@product.Brand
</span>
</td>
<!-- Price Column -->
<td class="text-end">
@if (product.Discount > 0)
{
<div>
<span class="text-danger fw-bold">$@discountedPrice.ToString("F2")</span>
</div>
<div>
<small class="text-muted text-decoration-line-through">$@product.Price.ToString("F2")</small>
<span class="badge bg-success ms-1">Save @product.Discount.ToString("F0")%</span>
</div>
}
else
{
<span class="fw-bold">$@product.Price.ToString("F2")</span>
}
</td>
<!-- Actions Column -->
<td class="text-center">
<a asp-action="Details" asp-route-id="@product.ProductId" class="btn btn-info btn-sm me-1">Details</a>
<a asp-action="Edit" asp-route-id="@product.ProductId" class="btn btn-warning btn-sm me-1">Edit</a>
<a asp-action="Delete" asp-route-id="@product.ProductId" class="btn btn-danger btn-sm">Delete</a>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Pagination Section -->
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
@* First Page Button *@
@if (Model.CurrentPage > 1)
{
<li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="1"
asp-route-searchName="@Model.SearchName"
asp-route-searchBrand="@Model.SearchBrand"
asp-route-minPrice="@Model.MinPrice"
asp-route-maxPrice="@Model.MaxPrice">
« First
</a>
</li>
}
else
{
<li class="page-item disabled">
<span class="page-link">« First</span>
</li>
}
@* Previous Page Button *@
@if (Model.CurrentPage > 1)
{
<li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="@(Model.CurrentPage - 1)"
asp-route-searchName="@Model.SearchName"
asp-route-searchBrand="@Model.SearchBrand"
asp-route-minPrice="@Model.MinPrice"
asp-route-maxPrice="@Model.MaxPrice">
‹ Prev
</a>
</li>
}
else
{
<li class="page-item disabled">
<span class="page-link">‹ Prev</span>
</li>
}
@* Current Page Display *@
<li class="page-item active">
<span class="page-link">@Model.CurrentPage</span>
</li>
@* Next Page Button *@
@if (Model.CurrentPage < Model.TotalPages)
{
<li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="@(Model.CurrentPage + 1)"
asp-route-searchName="@Model.SearchName"
asp-route-searchBrand="@Model.SearchBrand"
asp-route-minPrice="@Model.MinPrice"
asp-route-maxPrice="@Model.MaxPrice">
Next ›
</a>
</li>
}
else
{
<li class="page-item disabled">
<span class="page-link">Next ›</span>
</li>
}
@* Last Page Button *@
@if (Model.CurrentPage < Model.TotalPages)
{
<li class="page-item">
<a class="page-link" asp-action="Index" asp-route-page="@(Model.TotalPages)"
asp-route-searchName="@Model.SearchName"
asp-route-searchBrand="@Model.SearchBrand"
asp-route-minPrice="@Model.MinPrice"
asp-route-maxPrice="@Model.MaxPrice">
Last »
</a>
</li>
}
else
{
<li class="page-item disabled">
<span class="page-link">Last »</span>
</li>
}
</ul>
</nav>
</div>
Key Features of Index View:
  • A header section with a Create New Product button.
  • A filter section that allows searching by product name, brand, and price range.
  • A responsive table that lists each product with its main image thumbnail, name, brand, price (with discount details if applicable), and action buttons (Details, Edit, Delete).
  • Pagination controls at the bottom that let users navigate through pages.
Create.cshtml

Create a view named Create.cshtml within the Views/Products folder, and then copy and paste the following code. The Create View provides a form for administrators to create a new product. This view binds to the CreateProductViewModel so that when the form is submitted, product details and file uploads are passed to the controller’s POST action.

@model ProductManagementApp.ViewModels.CreateProductViewModel
@{
ViewData["Title"] = "Create Product";
}
<div class="container my-5">
<!-- Centered Row -->
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<!-- Card Header -->
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Create New Product</h3>
</div>
<!-- Card Body: Form -->
<div class="card-body">
<form asp-action="Create" method="post" enctype="multipart/form-data">
<!-- Product Name & Brand -->
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" placeholder="Enter product name" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Brand" class="form-label"></label>
<input asp-for="Brand" class="form-control" placeholder="Enter brand name" />
<span asp-validation-for="Brand" class="text-danger"></span>
</div>
</div>
<!-- Price & Discount -->
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Price" class="form-label"></label>
<input asp-for="Price" class="form-control" placeholder="Enter price" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Discount" class="form-label">Discount (%)</label>
<input asp-for="Discount" class="form-control" placeholder="Enter discount percentage" />
<span asp-validation-for="Discount" class="text-danger"></span>
</div>
</div>
<!-- Short Description / Features -->
<div class="mb-3">
<label asp-for="Description" class="form-label">Short Description / Features</label>
<textarea asp-for="Description" class="form-control" rows="3" placeholder="Enter features or bullet points (separate by semicolons)"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<hr />
<!-- Main Image Upload -->
<div class="mb-3">
<label asp-for="MainImageFile" class="form-label">Main Image</label>
<input asp-for="MainImageFile" type="file" class="form-control" />
<span asp-validation-for="MainImageFile" class="text-danger"></span>
</div>
<!-- Related Images Upload -->
<div class="mb-3">
<label asp-for="RelatedImageFiles" class="form-label">Related Images</label>
<input asp-for="RelatedImageFiles" type="file" multiple class="form-control" />
<span asp-validation-for="RelatedImageFiles" class="text-danger"></span>
</div>
<!-- Form Buttons -->
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-success me-2">Create Product</button>
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
</form>
</div> <!-- End card-body -->
</div> <!-- End card -->
</div> <!-- End col -->
</div> <!-- End row -->
</div> <!-- End container -->
Key Features of Create View:
  • Input fields are used to enter product details (name, brand, price, discount, description).
  • File input fields are used to upload the main product image and multiple related images.
  • Validation messages to ensure proper input.
  • A Create Product button is needed to submit the form, and a Back to List button is needed to cancel the operation and return to the product list page.
Edit.cshtml

Create a view named Edit.cshtml within the Views/Products folder and then copy and paste the following code. This Edit View allows administrators to update existing product details and manage images. The form uses the UpdateProductViewModel. When submitted, only changed fields or new images are processed. Existing images are maintained unless explicitly deleted via a separate action.

@model ProductManagementApp.ViewModels.UpdateProductViewModel
@{
ViewData["Title"] = "Edit Product";
}
<div class="container my-5">
<!-- Edit Form Card -->
<div class="row justify-content-center mb-4">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Product Information</h4>
</div>
<div class="card-body">
<form asp-action="Edit" method="post" enctype="multipart/form-data">
<input type="hidden" asp-for="ProductId" />
<!-- Basic Info: Name & Brand -->
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" placeholder="Enter product name" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Brand" class="form-label"></label>
<input asp-for="Brand" class="form-control" placeholder="Enter brand name" />
<span asp-validation-for="Brand" class="text-danger"></span>
</div>
</div>
<!-- Pricing: Price & Discount -->
<div class="row mb-3">
<div class="col-md-6">
<label asp-for="Price" class="form-label"></label>
<input asp-for="Price" class="form-control" placeholder="Enter price" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="col-md-6">
<label asp-for="Discount" class="form-label">Discount (%)</label>
<input asp-for="Discount" class="form-control" placeholder="Enter discount percentage" />
<span asp-validation-for="Discount" class="text-danger"></span>
</div>
</div>
<!-- Description / Features -->
<div class="mb-3">
<label asp-for="Description" class="form-label">Short Description / Features</label>
<textarea asp-for="Description" class="form-control" rows="3" placeholder="Separate bullet points with semicolons"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<hr />
<!-- Main Image Section -->
<div class="mb-3">
<label class="form-label">Existing Main Image</label><br />
@if (!string.IsNullOrEmpty(Model.ExistingMainImageFileName))
{
var mainImgUrl = Url.Content("~/product-images/" + Model.ExistingMainImageFileName);
<img src="@mainImgUrl" alt="Main Image" class="img-thumbnail mb-2" style="max-width:150px; object-fit:cover;" />
}
else
{
<p class="text-muted">No main image available.</p>
}
</div>
<div class="mb-3">
<label asp-for="MainImageFile" class="form-label">Replace Main Image (Optional)</label>
<input asp-for="MainImageFile" type="file" class="form-control" />
<span asp-validation-for="MainImageFile" class="text-danger"></span>
</div>
<!-- Related Images Section -->
<div class="mb-3">
<label asp-for="RelatedImageFiles" class="form-label">Add More Related Images (Optional)</label>
<input asp-for="RelatedImageFiles" type="file" multiple class="form-control" />
<span asp-validation-for="RelatedImageFiles" class="text-danger"></span>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-success me-2">Update Product</button>
<a asp-action="Index" class="btn btn-info">Back to List</a>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Existing Related Images Card -->
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">Existing Related Images</h5>
</div>
<div class="card-body">
@if (Model.ExistingRelatedImages != null && Model.ExistingRelatedImages.Any())
{
<div class="row">
@foreach (var img in Model.ExistingRelatedImages)
{
var imgUrl = Url.Content("~/product-images/" + img.ImageFileName);
<div class="col-4 col-md-3 mb-3 text-center">
<img src="@imgUrl" alt="Related Image" class="img-thumbnail" style="max-width:100px; object-fit:cover;" />
<div class="mt-2">
<!-- Instead of a simple alert, we now call a function to open a modal -->
<a href="javascript:void(0);" class="btn btn-sm btn-danger"
onclick="confirmDelete('@img.ImageId')">
Delete
</a>
</div>
</div>
}
</div>
}
else
{
<p class="text-muted">No related images found.</p>
}
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteConfirmModalLabel">Confirm Deletion</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Are you sure you want to delete this image?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<!-- The delete button will have its href set dynamically -->
<a id="deleteConfirmButton" class="btn btn-danger" href="#">Delete</a>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
// Function to open the delete confirmation modal
function confirmDelete(imageId) {
// Construct the URL for deletion. Adjust the URL if necessary.
var url = '@Url.Action("DeleteImage", "Products")' + '?imageId=' + imageId;
document.getElementById("deleteConfirmButton").setAttribute("href", url);
// Open the modal using Bootstrap's modal method
var deleteModal = new bootstrap.Modal(document.getElementById("deleteConfirmModal"));
deleteModal.show();
}
</script>
}
Key Features of Edit View:
  • A form with pre-populated fields (from the current product data) allowing changes to the name, brand, price, discount, and description.
  • A section displaying the existing main image with an option to replace it.
  • Another section for uploading additional related images.
  • A card below the form that displays the existing related images with Delete buttons next to each image (which opens a confirmation modal for deleting an image).
  • Buttons to update the product or go back to the list.
Details.cshtml

Create a view named Details.cshtml within the Views/Products folder and then copy and paste the following code. This Details View shows the detailed view of a single product, including a gallery of images. The controller action maps the Product data into a ProductDisplayViewModel bound to this view. JavaScript functions (e.g., setMainImage()) allow dynamic switching of the main image based on thumbnail interactions.

@model ProductManagementApp.ViewModels.ProductDisplayViewModel
@{
ViewData["Title"] = "Product Details";
}
<div class="container my-5">
<!-- Optional: A header section can be used here if desired -->
<div class="row g-4">
<!-- Left Column: Thumbnails -->
<div class="col-md-2">
<div class="card shadow-sm">
<div class="card-body p-2">
<div class="d-flex flex-column align-items-center">
<!-- Display main image as a thumbnail (if available) -->
@if (!string.IsNullOrEmpty(Model.MainImageFileName))
{
var mainThumbUrl = Url.Content("~/product-images/" + Model.MainImageFileName);
<img src="@mainThumbUrl"
alt="Main Thumbnail"
class="img-thumbnail mb-3"
style="width:70px; cursor:pointer;"
onmouseover="setMainImage('@mainThumbUrl')"
onclick="setMainImage('@mainThumbUrl')" />
}
<!-- Display related images as thumbnails -->
@if (Model.RelatedImages != null && Model.RelatedImages.Any())
{
foreach (var rel in Model.RelatedImages)
{
var thumbUrl = Url.Content("~/product-images/" + rel.ImageFileName);
<img src="@thumbUrl"
alt="Related Thumbnail"
class="img-thumbnail mb-3"
style="width:70px; cursor:pointer;"
onmouseover="setMainImage('@thumbUrl')"
onclick="setMainImage('@thumbUrl')" />
}
}
</div>
</div>
</div>
</div>
<!-- Center Column: Main Image Display -->
<div class="col-md-5">
<div class="card shadow-lg">
<div class="card-body p-3 text-center">
@if (!string.IsNullOrEmpty(Model.MainImageFileName))
{
var bigImgUrl = Url.Content("~/product-images/" + Model.MainImageFileName);
<img id="bigImage"
src="@bigImgUrl"
alt="@Model.Name"
class="img-fluid rounded"
style="max-width: 400px; max-height: 400px; object-fit: contain;" />
}
else
{
<p class="text-muted">No main image available.</p>
}
</div>
</div>
</div>
<!-- Right Column: Product Details & Actions -->
<div class="col-md-5">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<!-- Product Title & Brand (displayed here instead of top) -->
<h2 class="fw-bold">@Model.Name</h2>
@if (!string.IsNullOrEmpty(Model.Brand))
{
<p class="text-muted">by @Model.Brand</p>
}
<!-- Pricing Section -->
@{
decimal discountedPrice = Model.Price;
if (Model.Discount > 0 && Model.Discount <= 100)
{
discountedPrice = Model.Price * (1 - Model.Discount / 100);
}
}
<div class="mb-4">
<h2 class="text-danger fw-bold mb-1">$@discountedPrice.ToString("F2")</h2>
@if (Model.Discount > 0)
{
<p class="mb-0">
<small class="text-muted text-decoration-line-through me-2">$@Model.Price.ToString("F2")</small>
<span class="badge bg-success">Save @Model.Discount.ToString("F0")%</span>
</p>
}
</div>
<!-- Key Features -->
@if (!string.IsNullOrEmpty(Model.Description))
{
<div class="mb-4">
<h5 class="fw-semibold">Product Description</h5>
<ul class="list-unstyled">
@foreach (var bullet in Model.Description.Split(';'))
{
if (!string.IsNullOrWhiteSpace(bullet))
{
<li class="mb-1">
<i class="bi bi-check2-circle text-success me-1"></i>
@bullet.Trim()
</li>
}
}
</ul>
</div>
}
<!-- Action Buttons -->
<div class="d-grid gap-2 mt-4">
<button class="btn btn-warning btn-lg fw-bold">
<i class="bi bi-cart-fill me-2"></i> Add to Cart
</button>
<button class="btn btn-danger btn-lg fw-bold">
<i class="bi bi-lightning-fill me-2"></i> Buy Now
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Back to List Button -->
<div class="row mt-5">
<div class="col text-center">
<a asp-action="Index" class="btn btn-success btn-lg">Back to List</a>
</div>
</div>
</div>
@section Scripts {
<script>
// Function to update the main image source on mouse over or click
function setMainImage(newSrc) {
var bigImg = document.getElementById('bigImage');
if (bigImg) {
bigImg.src = newSrc;
}
}
</script>
}
Key Features of Details View:
  • A left-side panel displaying thumbnails for the main and related images. Hovering or clicking on a thumbnail updates the large image display.
  • A central panel that shows the main image in a larger format.
  • A right-side panel that details the product’s name, brand, pricing (with discount calculations), and a bulleted list of product features (split from the description).
  • Action buttons for Add to Cart and Buy Now to simulate a purchasing workflow.
  • A Back to List button to return to the main product listing.
Delete.cshtml

Create a view named Delete.cshtml within the Views/Products folder, then copy and paste the following code. This Delete View Displays a confirmation page before permanently deleting a product. This view uses the DeleteProductViewModel to present key details of the product being deleted. When the form is submitted, the controller action deletes the product and removes its images from the database and the file system.

@model ProductManagementApp.ViewModels.DeleteProductViewModel
@{
ViewData["Title"] = "Delete Product";
}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-danger text-white">
<h3 class="mb-0">Delete Product</h3>
</div>
<div class="card-body">
<p class="lead">Are you sure you want to delete the following product?</p>
<hr />
<div class="row">
<div class="col-md-4 text-center">
@if (!string.IsNullOrEmpty(Model.MainImageFileName))
{
var mainImgUrl = Url.Content("~/product-images/" + Model.MainImageFileName);
<img src="@mainImgUrl" alt="@Model.Name" class="img-fluid rounded mb-2" style="max-width: 150px;" />
}
else
{
<p class="text-muted">No image available</p>
}
</div>
<div class="col-md-8">
<h4>@Model.Name</h4>
@if (!string.IsNullOrEmpty(Model.Brand))
{
<p><strong>Brand:</strong> @Model.Brand</p>
}
<p><strong>Price:</strong> $@Model.Price.ToString("F2")</p>
@if (!string.IsNullOrEmpty(Model.Description))
{
<p><strong>Description:</strong> @Model.Description</p>
}
</div>
</div>
<hr />
<form asp-action="Delete" method="post">
<input type="hidden" asp-for="ProductId" />
<div class="d-flex justify-content-end">
<a asp-action="Index" class="btn btn-success me-2">Cancel</a>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
Modifying the _Layout.cshtml File:

Now, we need to enable client-side validation, and also, we need to add Bootstrap references. So, please modify the _Layout.cshtml file as follows:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- Ensures proper rendering on mobile devices -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - ProductManagementApp</title>
<!-- Bootstrap 5 CSS -->
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<!-- Site CSS -->
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/ProductManagementApp.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">ProductManagementApp</a>
<!-- Navbar Toggler for Bootstrap 5 -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Collapsible Navbar Section -->
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2025 - ProductManagementApp - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<!-- jQuery (for validation & older scripts, if needed) -->
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<!-- jQuery Validation Plugin -->
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<!-- jQuery Unobtrusive Validation -->
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<!-- Bootstrap 5 Bundle (includes Popper) -->
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- Your Site JS -->
<script src="~/js/site.js" asp-append-version="true"></script>
<!-- Scripts Section for Razor Views -->
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Creating a folder to store the Uploaded Images:

Create a folder named Product-Images within the wwwroot directory, where we will store all our uploaded images. Once you create the folder, please add the following Image and give the Image name NoImage.PNG. This will be the Fall-back Image. It means if the Main Image is not available, then this Image will be displayed on the Product Listing Page.

ASP.NET Core MVC Product Management Application

Now, run the application and test the functionalities, and it should work as expected.

This ASP.NET Core MVC Product Management Application demonstrates a structured approach to Implement File Handling, which includes efficiently handling file uploads, database operations, and image management. The application ensures scalability and maintainability by using EF Core, View Models, and file system storage. This approach can be extended to cloud storage solutions like Azure Blob Storage or AWS S3 for enhanced performance and reliability.

In the next article, I will discuss How to Restrict Uploaded File Size in the ASP.NET Core MVC Application with Examples. In this article, I explain how to Implement File Handling in ASP.NET Core MVC Application with examples using Buffering and Streaming Approaches. I hope you enjoy this article on File Uploading in the ASP.NET Core MVC Applications.

Registration Open For New Online Training

Enhance Your Professional Journey with Our Upcoming Live Session. For complete information on Registration, Course Details, Syllabus, and to get the Zoom Credentials to attend the free live Demo Sessions, please click on the below links.

1 thought on “File Handling in ASP.NET Core MVC”

  1. blank
    Collins Aigbekhai

    I am new to asp.net core, I have been trying to upload image using the knowledge I have with mvc with ado.net but it’s not working. I would like to learn more about using sql server ado.net in asp.net core, I am familiar with ado.net using mvc but asp.net core is quite different, I am also not very comfortable with ef programming, I would love if I can get some support.

Leave a Reply

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