Product Catalog with Shopping Cart

Build an ASP.NET Core MVC e-commerce app with EF Core and session cart.

Introduction

30 Second Summary

Every time you browse an online store, filter by category, or add something to your cart, there is a web application managing all of that behind the scenes. The product pages, the filtering logic, and the cart that remembers your items across pages are all built with patterns you can learn today.

In this project, you will build a product catalog web application with a shopping cart using ASP.NET Core MVC. You will create data models, seed a database with sample products, and implement a session-based cart that tracks items as users browse.

What You'll Build

A product catalog application called ShopMVC where users can browse products by category, view product details, and manage a shopping cart powered by Entity Framework Core and ASP.NET Core session state.

By the end of this project, you'll have:

  • Product and Category data models stored in a SQL Server LocalDB database managed by Entity Framework Core migrations.
  • A product catalog page with category filtering and a detail page with an Add to Cart button.
  • A session-based shopping cart that persists items across page navigations with add, remove, and price total functionality.
  • Secret Mission: Add product search and a live cart item count badge in the navigation bar.

Are there any prerequisites?

You should have basic C# knowledge and a basic understanding of how the MVC pattern works (models, views, and controllers). Familiarity with basic database concepts like tables and columns is helpful.

Before We Start

Every web application starts with a foundation. Before you can build a product catalog or a shopping cart, you need a project structure that wires together controllers, views, and models.

In this step, you will create a new ASP.NET Core MVC project in Visual Studio, install the database packages you need, and confirm the default template runs in your browser.

In this step, get ready to:
  • Create a new MVC project named ShopMVC in Visual Studio.
  • Install Entity Framework Core NuGet packages.
  • Verify the default template runs in your browser.
Create a new MVC project in Visual Studio

Visual Studio provides a project template that scaffolds an entire MVC application for you. This gives you a working app with controllers, views, and routing out of the box.

  • Open Visual Studio.
  • On the start window, click Create a new project.
  • In the Create a new project dialog, search for and select ASP.NET Core Web App (Model-View-Controller).
  • Click Next.
  • In the Configure your new project dialog, enter ShopMVC in the Project name field.
  • Click Next.
  • In the Additional information dialog, select .NET 10.0 (Standard Term Support) from the Framework dropdown.
  • Verify that Do not use top-level statements is unchecked.
  • Click Create.

What did Visual Studio just create?

The template generates a complete MVC project with folders for Controllers, Models, and Views. It also creates a Program.cs file that configures routing and dependency injection.

You also get a wwwroot folder with Bootstrap CSS and jQuery pre-installed, so your pages have styling from the start.

Install Entity Framework Core packages

Entity Framework Core is the database library you will use to store and retrieve products. It needs two NuGet packages: one for SQL Server connectivity, and one for migration tooling.

  • In Visual Studio, click Tools in the top menu bar.
  • Select NuGet Package Manager, then Package Manager Console.

A console panel opens at the bottom of Visual Studio. This is where you run package management commands.

  • Install the SQL Server provider by running this command in the Package Manager Console:
Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install the EF Core tooling package by running this command:
Install-Package Microsoft.EntityFrameworkCore.Tools

What do these packages do?

The Microsoft.EntityFrameworkCore.SqlServer package lets EF Core talk to SQL Server databases, including LocalDB on your machine.

The Microsoft.EntityFrameworkCore.Tools package adds PowerShell commands like Add-Migration and Update-Database to the Package Manager Console. You will use these in a later step.

  • Confirm both packages installed successfully by checking the console output shows "Successfully installed" for each package.

Seeing a package restore error?

Make sure you have an active internet connection. The Package Manager Console downloads packages from nuget.org.

If the console says the project is not found, check the Default project dropdown at the top of the Package Manager Console window. It should show ShopMVC.

Help me troubleshoot my NuGet package installation.

Run the app to verify the template works

Before writing any custom code, confirm the template runs. This ensures your environment is healthy.

  • Press Ctrl+F5 to run the app without debugging.

Why Ctrl+F5 instead of F5?

Ctrl+F5 launches the app without attaching the debugger. This starts the app faster. You can use F5 later when you need breakpoints, but for quick checks Ctrl+F5 is enough.

Your default browser opens to a page showing the ASP.NET Core welcome template. The URL will look something like https://localhost:7xxx with a port number specific to your project.

Browser showing a certificate warning?

Visual Studio uses a self-signed HTTPS certificate for local development. If your browser warns you about the connection, accept the certificate or click through the warning to proceed.

If nothing loads at all, check the Output window in Visual Studio (View > Output) for error messages about failed port binding.

Help me troubleshoot my ASP.NET Core app not loading in the browser.

Your project is scaffolded and running. Next up, you will build the data models and database that power the product catalog.

Build Data Models and Database

Your project is set up and running, but right now it has no data to display. Every e-commerce app needs a way to represent products and organize them into categories.

In this step, you will create the data models that define your store's products and categories, wire up Entity Framework Core to manage the database, and populate it with sample data using a migration.

In this step, get ready to:
  • Create Category and Product data models with a one-to-many relationship.
  • Set up the EF Core DbContext and seed data class.
  • Run a database migration to create and populate the SQL Server LocalDB database.
Create the Category and Product models

Data models define the shape of your data. You need two models: one for categories and one for products, linked together so each product belongs to a category.

  • In the Solution Explorer sidebar, right-click the Models folder.
  • Select Add, then Class....
  • Name the file Category.cs and click Add.
  • Replace the entire contents of the file with this code:
namespace ShopMVC.Models;

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public List<Product> Products { get; set; } = new();
}

What does this code do?

This class defines a Category with three properties.

  • Id is the primary key that EF Core uses to identify each category in the database.
  • Name stores the category's display name (like "Electronics" or "Books").
  • Products is a navigation property. It tells EF Core that one category can have many products.

You won't see a change yet. The next file uses this class.

  • Right-click the Models folder again and select Add > Class....
  • Name the file Product.cs and click Add.
  • Replace the entire contents of the file with this code:
namespace ShopMVC.Models;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public string ImageUrl { get; set; } = string.Empty;
    public int CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

What does this code do?

This class defines a Product with properties for its name, description, price, and image URL.

  • CategoryId is the foreign key that links this product to a specific category row in the database.
  • Category is the navigation property that lets EF Core load the related Category object when you query a product.
  • Together, CategoryId and the Products list in Category.cs form a one-to-many relationship: one category has many products.
  • Save both files with Ctrl+S.

Seeing red squiggles under Product or Category?

This is normal if you saved one file before creating the other. Both files reference each other, so Visual Studio shows errors until both exist. Once both Category.cs and Product.cs are saved, the errors disappear.

help me fix red squiggles in my model classes

Create the database context and seed data

EF Core needs a DbContext class to know which models map to database tables. You will also create a seed class that populates the database with sample products on first run.

  • In Solution Explorer, right-click the ShopMVC project (the top-level node with the green C# icon).
  • Select Add > New Folder and name it Data.
  • Right-click the new Data folder, select Add > Class..., and name it ShopContext.cs.
  • Replace the entire contents with this code:
using Microsoft.EntityFrameworkCore;
using ShopMVC.Models;

namespace ShopMVC.Data;

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

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();
}

What does this code do?

ShopContext is your database session. It inherits from DbContext, which is EF Core's main class for interacting with the database.

  • The constructor accepts DbContextOptions so ASP.NET Core's dependency injection can configure which database to connect to.
  • Products and Categories are DbSet properties. Each one maps to a table in the database that you can query with LINQ.

You won't see a change yet. Next, create the seed data class.

  • Right-click the Data folder, select Add > Class..., and name it SeedData.cs.
  • Replace the entire contents with this code:
using ShopMVC.Models;

namespace ShopMVC.Data;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<ShopContext>();

        // Skip seeding if products already exist
        if (context.Products.Any())
        {
            return;
        }

        // Create three categories
        var electronics = new Category { Name = "Electronics" };
        var clothing = new Category { Name = "Clothing" };
        var books = new Category { Name = "Books" };

        context.Categories.AddRange(electronics, clothing, books);

        // Create sample products linked to categories
        context.Products.AddRange(
            new Product
            {
                Name = "Wireless Headphones",
                Description = "High-quality wireless headphones with noise cancellation.",
                Price = 79.99m,
                ImageUrl = "https://placehold.co/300x200?text=Headphones",
                Category = electronics
            },
            new Product
            {
                Name = "Laptop Stand",
                Description = "Ergonomic aluminum laptop stand for better posture.",
                Price = 45.00m,
                ImageUrl = "https://placehold.co/300x200?text=Laptop+Stand",
                Category = electronics
            },
            new Product
            {
                Name = "Cotton T-Shirt",
                Description = "Comfortable 100% cotton t-shirt in various colors.",
                Price = 19.99m,
                ImageUrl = "https://placehold.co/300x200?text=T-Shirt",
                Category = clothing
            },
            new Product
            {
                Name = "Running Shoes",
                Description = "Lightweight running shoes for daily training.",
                Price = 89.99m,
                ImageUrl = "https://placehold.co/300x200?text=Running+Shoes",
                Category = clothing
            },
            new Product
            {
                Name = "C# in Depth",
                Description = "Comprehensive guide to the C# programming language.",
                Price = 39.99m,
                ImageUrl = "https://placehold.co/300x200?text=CSharp+Book",
                Category = books
            },
            new Product
            {
                Name = "Clean Code",
                Description = "A handbook of agile software craftsmanship.",
                Price = 34.99m,
                ImageUrl = "https://placehold.co/300x200?text=Clean+Code",
                Category = books
            }
        );

        context.SaveChanges();
    }
}

What does this code do?

SeedData is a static helper that runs once on startup to populate the database with sample data.

  • It creates a dependency injection scope and retrieves the ShopContext from the service provider.
  • The if (context.Products.Any()) check prevents duplicate data if the app restarts.
  • It creates 3 categories and 6 products, linking each product to its category by setting the Category navigation property.
  • SaveChanges() writes everything to the database in one transaction.

Now you need to tell the app how to connect to the database and register these services.

  • Open appsettings.json from the project root in Solution Explorer.
  • Replace the entire contents with this code to add the connection string:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ShopMVC;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

What is this connection string?

The DefaultConnection string tells EF Core where to find the database.

  • Server=(localdb)\mssqllocaldb points to SQL Server LocalDB, a lightweight version of SQL Server included with Visual Studio.
  • Database=ShopMVC is the name of the database EF Core will create.
  • Trusted_Connection=True uses your Windows credentials to authenticate.
  • Open Program.cs from the project root in Solution Explorer.
  • Replace the entire contents with this code:
using Microsoft.EntityFrameworkCore;
using ShopMVC.Data;

var builder = WebApplication.CreateBuilder(args);

// Add MVC services
builder.Services.AddControllersWithViews();

// Register the database context with the LocalDB connection string
builder.Services.AddDbContext<ShopContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapDefaultControllerRoute();

// Seed the database with sample data on first run
SeedData.Initialize(app.Services);

app.Run();

What changed in Program.cs?

You added three things to the default template:

  • using Microsoft.EntityFrameworkCore and using ShopMVC.Data at the top bring EF Core and your data classes into scope.
  • AddDbContext<ShopContext>(...) registers your database context with ASP.NET Core's dependency injection system. This lets controllers request a ShopContext instance automatically.
  • SeedData.Initialize(app.Services) runs after the app is built but before it starts listening for requests. It populates the database if it is empty.
  • Save all files with Ctrl+Shift+S.
  • Verify the project compiles by pressing Ctrl+Shift+B to build. You should see "Build succeeded" in the bottom status bar.

Build failed?

Check these common causes:

  • Make sure Category.cs and Product.cs are inside the Models folder. Their namespace must be ShopMVC.Models.
  • Make sure ShopContext.cs and SeedData.cs are inside the Data folder. Their namespace must be ShopMVC.Data.
  • Confirm that Microsoft.EntityFrameworkCore.SqlServer is installed. Check under Dependencies > Packages in Solution Explorer.

help me fix build errors in my ShopMVC project

✔️ Awesome, I've got everything!

Great. Make sure all files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete Program.cs at this point:

using Microsoft.EntityFrameworkCore;
using ShopMVC.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<ShopContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.MapDefaultControllerRoute();

SeedData.Initialize(app.Services);

app.Run();

Here is the complete Data/SeedData.cs:

using ShopMVC.Models;

namespace ShopMVC.Data;

public static class SeedData
{
    public static void Initialize(IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<ShopContext>();

        if (context.Products.Any())
        {
            return;
        }

        var electronics = new Category { Name = "Electronics" };
        var clothing = new Category { Name = "Clothing" };
        var books = new Category { Name = "Books" };

        context.Categories.AddRange(electronics, clothing, books);

        context.Products.AddRange(
            new Product
            {
                Name = "Wireless Headphones",
                Description = "High-quality wireless headphones with noise cancellation.",
                Price = 79.99m,
                ImageUrl = "https://placehold.co/300x200?text=Headphones",
                Category = electronics
            },
            new Product
            {
                Name = "Laptop Stand",
                Description = "Ergonomic aluminum laptop stand for better posture.",
                Price = 45.00m,
                ImageUrl = "https://placehold.co/300x200?text=Laptop+Stand",
                Category = electronics
            },
            new Product
            {
                Name = "Cotton T-Shirt",
                Description = "Comfortable 100% cotton t-shirt in various colors.",
                Price = 19.99m,
                ImageUrl = "https://placehold.co/300x200?text=T-Shirt",
                Category = clothing
            },
            new Product
            {
                Name = "Running Shoes",
                Description = "Lightweight running shoes for daily training.",
                Price = 89.99m,
                ImageUrl = "https://placehold.co/300x200?text=Running+Shoes",
                Category = clothing
            },
            new Product
            {
                Name = "C# in Depth",
                Description = "Comprehensive guide to the C# programming language.",
                Price = 39.99m,
                ImageUrl = "https://placehold.co/300x200?text=CSharp+Book",
                Category = books
            },
            new Product
            {
                Name = "Clean Code",
                Description = "A handbook of agile software craftsmanship.",
                Price = 34.99m,
                ImageUrl = "https://placehold.co/300x200?text=Clean+Code",
                Category = books
            }
        );

        context.SaveChanges();
    }
}

Here is the complete appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ShopMVC;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
Run the database migration

EF Core uses migrations to translate your C# models into database tables. You will create a migration and then apply it to generate the actual SQL Server LocalDB database.

  • Open the Package Manager Console by clicking Tools in the top menu bar.
  • Select NuGet Package Manager, then Package Manager Console.
  • A console panel opens at the bottom of Visual Studio. Create the migration by running this command:
Add-Migration InitialCreate

What does this command do?

Add-Migration compares your current model classes against the database (which does not exist yet) and generates a C# migration file describing the changes. The name InitialCreate is a label for this migration.

You will see a new Migrations folder appear in Solution Explorer with the generated migration file.

  • Apply the migration to create the database by running this command:
Update-Database

What does this command do?

Update-Database executes the migration file against the connection string in appsettings.json. It creates the ShopMVC database in LocalDB and builds the Products and Categories tables based on your model classes.

  • Verify the database was created by opening SQL Server Object Explorer.
  • Click the View menu at the top of Visual Studio, then select SQL Server Object Explorer.
  • Expand SQL Server > (localdb)\MSSQLLocalDB > Databases > ShopMVC > Tables.
  • You should see dbo.Categories and dbo.Products listed under Tables.

Migration command failed?

  • If you see a "Build failed" message, fix any compile errors first (Ctrl+Shift+B to build) and then retry the migration command.
  • If you see "Unable to create a 'DbContext'", make sure your Program.cs registers AddDbContext<ShopContext> and that the connection string exists in appsettings.json.
  • If Update-Database fails with a connection error, confirm LocalDB is running. In the Package Manager Console, run sqllocaldb start MSSQLLocalDB and try again.

help me fix my EF Core migration error

Your database is built and seeded with sample products. Next up, you will create the product catalog page to display these products in the browser.

Build the Product Catalog

Your database is seeded with products and categories. Now it is time to give users a way to actually see them.

In this step, you will build the product catalog page. This is where the MVC pattern comes together: a view model shapes the data, a controller queries the database and prepares that data, and a Razor view renders it as HTML the browser can display.

In this step, get ready to:
  • Create a view model to carry product and category data to the view.
  • Create a controller with a database query that supports category filtering.
  • Create a Razor view that displays products in a responsive card grid with a filter dropdown.
Create the product list view model

A view model is a class designed specifically to carry data from a controller to a view. Unlike your database models, a view model can combine data from multiple sources into one convenient package.

  • In the Solution Explorer, right-click the ShopMVC project.
  • Select Add, then New Folder.
  • Name the folder ViewModels.
  • Right-click the new ViewModels folder, select Add > Class, and name it ProductListViewModel.cs.
  • Replace the file contents with this code:
using ShopMVC.Models;

namespace ShopMVC.ViewModels;

public class ProductListViewModel
{
    public List<Product> Products { get; set; } = new();
    public List<Category> Categories { get; set; } = new();
    public int? SelectedCategoryId { get; set; }
}

What does this code do?

This view model bundles three pieces of data the catalog page needs.

  • Products holds the list of products to display (already filtered if a category is selected).
  • Categories holds every category so the filter dropdown can list them all.
  • SelectedCategoryId is nullable. When it has a value, the dropdown pre-selects the active filter.
  • Save ProductListViewModel.cs.
Create the products controller

The controller is responsible for handling incoming HTTP requests, querying the database, and passing the results to a view. Your ProductsController will use Entity Framework Core to fetch products and optionally filter them by category.

  • In Solution Explorer, right-click the Controllers folder.
  • Select Add > Controller.
  • Choose MVC Controller - Empty and name it ProductsController.cs.
  • Replace the file contents with this code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ShopMVC.Data;
using ShopMVC.ViewModels;

namespace ShopMVC.Controllers;

public class ProductsController : Controller
{
    private readonly ShopContext _context;

    public ProductsController(ShopContext context)
    {
        _context = context;
    }

    public async Task<IActionResult> Index(int? categoryId)
    {
        var productsQuery = _context.Products.Include(p => p.Category).AsQueryable();

        if (categoryId.HasValue)
        {
            productsQuery = productsQuery.Where(p => p.CategoryId == categoryId.Value);
        }

        var viewModel = new ProductListViewModel
        {
            Products = await productsQuery.ToListAsync(),
            Categories = await _context.Categories.ToListAsync(),
            SelectedCategoryId = categoryId
        };

        return View(viewModel);
    }
}

What does this code do?

The constructor receives a ShopContext instance through dependency injection. ASP.NET Core automatically provides it because you registered ShopContext in Program.cs during the previous step.

The Index action starts by building a query that includes each product's related category (using Include). If the user passes a categoryId query parameter, the query narrows down to products in that category only. Finally, it packages the results into a ProductListViewModel and hands it to the view.

  • Save ProductsController.cs.

What is Include and why do you need it?

By default, EF Core loads only the product's own columns. Without Include(p => p.Category), the Category property on each product would be null when the view tries to display category names.

This technique is called eager loading. It tells EF Core to join the Categories table in the same SQL query so the related data arrives in one trip to the database.

Create the product catalog view

The view renders the data your controller prepared. You will build a category filter dropdown and a responsive grid of product cards using Bootstrap classes that are already included in the MVC template.

  • In Solution Explorer, expand the Views folder.
  • Right-click Views, select Add > New Folder, and name it Products.
  • Right-click the new Products folder, select Add > View, choose Razor View - Empty, and name it Index.cshtml.
  • Replace the file contents with this code:
@model ShopMVC.ViewModels.ProductListViewModel

@{
    ViewData["Title"] = "Products";
}

<h1>Product Catalog</h1>

<div class="row mb-4">
    <div class="col-md-6">
        <form method="get" asp-action="Index">
            <div class="input-group">
                <select name="categoryId" class="form-select">
                    <option value="">All Categories</option>
                    @foreach (var category in Model.Categories)
                    {
                        if (Model.SelectedCategoryId == category.Id)
                        {
                            <option value="@category.Id" selected>@category.Name</option>
                        }
                        else
                        {
                            <option value="@category.Id">@category.Name</option>
                        }
                    }
                </select>
                <button type="submit" class="btn btn-primary">Filter</button>
            </div>
        </form>
    </div>
</div>

<div class="row">
    @foreach (var product in Model.Products)
    {
        <div class="col-md-4 mb-4">
            <div class="card h-100">
                <img src="@product.ImageUrl" class="card-img-top" alt="@product.Name">
                <div class="card-body">
                    <h5 class="card-title">@product.Name</h5>
                    <p class="card-text text-muted">@product.Category.Name</p>
                    <p class="card-text fw-bold">@product.Price.ToString("C")</p>
                    <a asp-action="Details" asp-route-id="@product.Id" class="btn btn-outline-primary">View Details</a>
                </div>
            </div>
        </div>
    }
</div>

What does this view do?

The @model directive at the top tells Razor which type of data to expect. This gives you IntelliSense and compile-time checking when you reference Model.Products or Model.Categories.

The filter form submits a GET request back to the same Index action with a categoryId query parameter. When SelectedCategoryId matches a category, that option gets the selected attribute so the dropdown remembers the user's choice.

The product grid uses Bootstrap's col-md-4 class to display three cards per row on medium screens and above. Each card shows the product image, name, category, price, and a link to view details.

  • Save Index.cshtml.
  • Press Ctrl+F5 to run the app without debugging.
  • In your browser's address bar, change the URL path to /Products and press Enter.

You should see six product cards arranged in a grid. Each card shows a placeholder image, product name, category label, and price.

  • Select Electronics from the category dropdown and click Filter.

The page reloads showing only Wireless Headphones and Laptop Stand. The dropdown stays on Electronics.

Not seeing any products?

  • Make sure the Views/Products folder name matches the controller name exactly. ASP.NET Core looks for views in a folder matching the controller's prefix.
  • Confirm the database was seeded in the previous step. In Visual Studio, open SQL Server Object Explorer and check that the Products table has rows.
  • Check the Output window for EF Core errors. A missing Include call causes null reference exceptions when the view accesses product.Category.Name.

Help me debug why my product catalog page is not loading.

✔️ Awesome, I've got everything!

Great. Make sure you have saved all three new files before moving on.

ⓧ I'd like to double check the full code

Here are the three files you created in this step:

using ShopMVC.Models;

namespace ShopMVC.ViewModels;

public class ProductListViewModel
{
    public List<Product> Products { get; set; } = new();
    public List<Category> Categories { get; set; } = new();
    public int? SelectedCategoryId { get; set; }
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ShopMVC.Data;
using ShopMVC.ViewModels;

namespace ShopMVC.Controllers;

public class ProductsController : Controller
{
    private readonly ShopContext _context;

    public ProductsController(ShopContext context)
    {
        _context = context;
    }

    public async Task<IActionResult> Index(int? categoryId)
    {
        var productsQuery = _context.Products.Include(p => p.Category).AsQueryable();

        if (categoryId.HasValue)
        {
            productsQuery = productsQuery.Where(p => p.CategoryId == categoryId.Value);
        }

        var viewModel = new ProductListViewModel
        {
            Products = await productsQuery.ToListAsync(),
            Categories = await _context.Categories.ToListAsync(),
            SelectedCategoryId = categoryId
        };

        return View(viewModel);
    }
}
@model ShopMVC.ViewModels.ProductListViewModel

@{
    ViewData["Title"] = "Products";
}

<h1>Product Catalog</h1>

<div class="row mb-4">
    <div class="col-md-6">
        <form method="get" asp-action="Index">
            <div class="input-group">
                <select name="categoryId" class="form-select">
                    <option value="">All Categories</option>
                    @foreach (var category in Model.Categories)
                    {
                        if (Model.SelectedCategoryId == category.Id)
                        {
                            <option value="@category.Id" selected>@category.Name</option>
                        }
                        else
                        {
                            <option value="@category.Id">@category.Name</option>
                        }
                    }
                </select>
                <button type="submit" class="btn btn-primary">Filter</button>
            </div>
        </form>
    </div>
</div>

<div class="row">
    @foreach (var product in Model.Products)
    {
        <div class="col-md-4 mb-4">
            <div class="card h-100">
                <img src="@product.ImageUrl" class="card-img-top" alt="@product.Name">
                <div class="card-body">
                    <h5 class="card-title">@product.Name</h5>
                    <p class="card-text text-muted">@product.Category.Name</p>
                    <p class="card-text fw-bold">@product.Price.ToString("C")</p>
                    <a asp-action="Details" asp-route-id="@product.Id" class="btn btn-outline-primary">View Details</a>
                </div>
            </div>
        </div>
    }
</div>

Your product catalog is live. Users can browse all products and filter by category. Next up, you will build a detail page so users can see more about each product and add it to their cart.

Build the Product Detail Page

Your product catalog page shows a grid of product cards, but clicking View Details doesn't go anywhere yet.

In this step, you'll build the product detail page that shows full information about a single product. You'll also wire up the site's navigation bar so users can easily jump between the catalog and the shopping cart.

In this step, get ready to:
  • Add a Details action to the ProductsController that loads a single product.
  • Create a detail view showing the product image, price, description, and an Add to Cart button.
  • Update the site layout with Products and Cart navigation links.
Add a Details action to ProductsController

The product catalog's View Details links already point to a Details action that doesn't exist yet. You need a controller action that takes a product ID from the URL, fetches that product from the database, and passes it to a view.

  • In Controllers/ProductsController.cs, find the closing brace of the Index method and add the following Details action below it:
    public async Task<IActionResult> Details(int id)
    {
        // Fetch a single product by ID, including its Category data
        var product = await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);

        // Return a 404 page if no product matches the ID
        if (product == null)
        {
            return NotFound();
        }

        // Pass the product directly to the view as the model
        return View(product);
    }

What does this code do?

The id parameter comes from the URL automatically through ASP.NET Core model binding. When a user visits /Products/Details/3, the framework passes 3 as the id argument.

The Include(p => p.Category) call uses eager loading to pull the related Category in the same database query. Without it, product.Category would be null when the view tries to display the category name.

  • Save Controllers/ProductsController.cs.

Seeing a red squiggly under FirstOrDefaultAsync?

Make sure you have using Microsoft.EntityFrameworkCore; at the top of the file. This namespace provides the async query extension methods like FirstOrDefaultAsync.

Help me debug this error.

Create the product details view

The Details action returns View(product), so ASP.NET Core will look for a Razor view at Views/Products/Details.cshtml. This view displays the full product information with a form that posts to the Cart controller.

  • Right-click the Views/Products folder in Solution Explorer.
  • Select Add, then New Item.
  • Choose Razor View - Empty and name it Details.cshtml.
  • Click Add.
  • Replace all the content in the new file with the following code:
@model ShopMVC.Models.Product

@{
    ViewData["Title"] = Model.Name;
}

<div class="row">
    <div class="col-md-6">
        <!-- Product image fills the left column -->
        <img src="@Model.ImageUrl" class="img-fluid rounded" alt="@Model.Name">
    </div>
    <div class="col-md-6">
        <h1>@Model.Name</h1>
        <p class="text-muted">@Model.Category.Name</p>
        <p class="fs-4 fw-bold text-success">@Model.Price.ToString("C")</p>
        <p>@Model.Description</p>

        <!-- Form posts the product ID to the Cart controller's Add action -->
        <form asp-controller="Cart" asp-action="Add" method="post">
            <input type="hidden" name="productId" value="@Model.Id" />
            <button type="submit" class="btn btn-primary btn-lg">Add to Cart</button>
        </form>

        <a asp-controller="Products" asp-action="Index" class="btn btn-link mt-3">Back to Catalog</a>
    </div>
</div>

What does this view do?

The @model directive tells Razor this view expects a Product object. This gives you IntelliSense and compile-time checking when accessing properties like Model.Name.

The <form> uses Tag Helpers (asp-controller and asp-action) to generate the correct URL for the Cart's Add action. The hidden input sends the productId so the cart knows which product to add.

  • Save Views/Products/Details.cshtml.
  • Press Ctrl+F5 to run the app without debugging.
  • Navigate to /Products and click View Details on any product card.
  • You should see the product image on the left and the name, category, price, description, and Add to Cart button on the right.

Seeing a 404 or blank page?

Check that your file is named exactly Details.cshtml (capital D) and lives inside Views/Products/. ASP.NET Core uses the controller name and action name to find the matching view file.

Also verify that the Details method in your controller is public and returns View(product).

Help me figure out why my detail page isn't loading.

Add navigation links to the layout

Right now, the only way to reach the product catalog is by typing the URL manually. The site's shared layout file controls the navigation bar that appears on every page.

  • Open Views/Shared/_Layout.cshtml from Solution Explorer.
  • Find the <ul class="navbar-nav flex-grow-1"> section that contains the existing navigation links.
  • Replace the contents of the <ul> with Home, Products, and Cart links so the full section looks like this:
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <!-- Link to the product catalog -->
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Products" asp-action="Index">Products</a>
                        </li>
                        <!-- Link to the shopping cart -->
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Cart" asp-action="Index">Cart</a>
                        </li>
                    </ul>

What are Tag Helpers doing here?

The asp-controller and asp-action attributes are Tag Helpers that generate the correct href at runtime. If your routing changes later, the links update automatically without you editing HTML.

  • Save Views/Shared/_Layout.cshtml.
  • Refresh your browser. You should see Products and Cart links in the top navigation bar alongside Home.
  • Click Products in the navbar to confirm it takes you to the catalog.

Nav links not appearing?

Make sure you replaced only the <ul> contents, not the surrounding <div> or <nav> elements. Accidentally deleting the navbar-collapse div will hide the links entirely.

Help me fix my navigation links.

✔️ Awesome, I've got everything!

Great. Double check you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Here is the full Controllers/ProductsController.cs at this point:

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ShopMVC.Data;
using ShopMVC.ViewModels;

namespace ShopMVC.Controllers;

public class ProductsController : Controller
{
    private readonly ShopContext _context;

    public ProductsController(ShopContext context)
    {
        _context = context;
    }

    public async Task<IActionResult> Index(int? categoryId)
    {
        var productsQuery = _context.Products.Include(p => p.Category).AsQueryable();

        if (categoryId.HasValue)
        {
            productsQuery = productsQuery.Where(p => p.CategoryId == categoryId.Value);
        }

        var viewModel = new ProductListViewModel
        {
            Products = await productsQuery.ToListAsync(),
            Categories = await _context.Categories.ToListAsync(),
            SelectedCategoryId = categoryId
        };

        return View(viewModel);
    }

    public async Task<IActionResult> Details(int id)
    {
        var product = await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);

        if (product == null)
        {
            return NotFound();
        }

        return View(product);
    }
}

Here is the full Views/Products/Details.cshtml:

@model ShopMVC.Models.Product

@{
    ViewData["Title"] = Model.Name;
}

<div class="row">
    <div class="col-md-6">
        <img src="@Model.ImageUrl" class="img-fluid rounded" alt="@Model.Name">
    </div>
    <div class="col-md-6">
        <h1>@Model.Name</h1>
        <p class="text-muted">@Model.Category.Name</p>
        <p class="fs-4 fw-bold text-success">@Model.Price.ToString("C")</p>
        <p>@Model.Description</p>

        <form asp-controller="Cart" asp-action="Add" method="post">
            <input type="hidden" name="productId" value="@Model.Id" />
            <button type="submit" class="btn btn-primary btn-lg">Add to Cart</button>
        </form>

        <a asp-controller="Products" asp-action="Index" class="btn btn-link mt-3">Back to Catalog</a>
    </div>
</div>

Here is the full Views/Shared/_Layout.cshtml:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - ShopMVC</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/ShopMVC.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-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">ShopMVC</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <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="Products" asp-action="Index">Products</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Cart" asp-action="Index">Cart</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; 2026 - ShopMVC - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Your product detail page is live and the navigation bar ties everything together. Next up, you'll build the shopping cart so that Add to Cart button actually does something.

Build the Shopping Cart

Your product catalog is live. Customers can browse, filter by category, and view detailed product pages. But there is no way for them to collect items as they shop.

In this step, you'll build a session-based shopping cart that remembers items across page navigations. ASP.NET Core session state stores data on the server and tracks each visitor with a cookie, solving the "stateless HTTP" problem without requiring a user account or database table.

In this step, get ready to:
  • Configure session state middleware in your application pipeline.
  • Create a Cart controller with add, remove, and display actions.
  • Build a cart view that shows items, quantities, and a running total.
Configure session state and create helper files

HTTP is stateless. Each request from a browser is independent, with no memory of previous requests. Session state solves this by storing data on the server and linking it to the visitor via a cookie.

Why session-based cart instead of a database table?

A session cart requires no user authentication and no extra database tables. It teaches you how ASP.NET Core middleware works while keeping the implementation simple.

In production, you would eventually move cart data to a database so it survives server restarts. For learning MVC fundamentals, session state is the right starting point.

  • In the Solution Explorer, open Program.cs.
  • Find the AddDbContext block (the builder.Services.AddDbContext<ShopContext>(...) lines). Add these lines directly below it:
builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});
  • Next, scroll down to find app.UseAuthorization(); in the middleware section. Add this line directly below it:
app.UseSession();

What does this code do?

  • AddDistributedMemoryCache() provides in-memory storage that the session middleware uses to hold session data.
  • AddSession() registers the session service. The options set a 30-minute timeout, mark the cookie as HTTP-only (not accessible to JavaScript), and flag it as essential (so it works even without cookie consent).
  • UseSession() adds the session middleware to the request pipeline. It must come after UseRouting and before MapDefaultControllerRoute.
  • Save Program.cs.

Seeing a build error?

Make sure app.UseSession(); is placed after app.UseAuthorization(); and before app.MapDefaultControllerRoute();. Middleware order matters in ASP.NET Core.

Help me fix my session middleware ordering in Program.cs

✔️ Awesome, I've got everything!

Great. Make sure you have saved Program.cs before moving on.

ⓧ I'd like to double check the full code

using Microsoft.EntityFrameworkCore;
using ShopMVC.Data;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddDbContext<ShopContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDistributedMemoryCache();

builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

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

app.UseRouting();

app.UseAuthorization();

app.UseSession();

app.MapDefaultControllerRoute();

SeedData.Initialize(app.Services);

app.Run();

ASP.NET Core session only stores strings and byte arrays natively. You need helper methods to serialize C# objects into JSON so you can store complex data like a list of cart items.

  • In Solution Explorer, right-click the ShopMVC project.
  • Select Add, then New Folder.
  • Name the folder Extensions.
  • Right-click the new Extensions folder, select Add > Class..., and name it SessionExtensions.cs.
  • Replace all the generated content with this code:
using System.Text.Json;

namespace ShopMVC.Extensions;

public static class SessionExtensions
{
    // Serialize any object to JSON and store it in session
    public static void SetObjectAsJson(this ISession session, string key, object value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    // Retrieve a JSON string from session and deserialize it back to an object
    public static T? GetObjectFromJson<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}

What does this code do?

  • SetObjectAsJson is an extension method on ISession. It converts any C# object to a JSON string and stores it under a key.
  • GetObjectFromJson<T> reverses the process. It reads the JSON string from session and deserializes it back into the specified type.
  • Save SessionExtensions.cs.

Red squiggly on ISession?

Make sure the namespace is ShopMVC.Extensions and the class is declared as public static. The ISession interface is part of the ASP.NET Core framework and should resolve automatically.

Help me fix the ISession error in my SessionExtensions class

✔️ Awesome, I've got everything!

Great. Confirm the file is saved and move on to the cart model.

ⓧ I'd like to double check the full code

using System.Text.Json;

namespace ShopMVC.Extensions;

public static class SessionExtensions
{
    public static void SetObjectAsJson(this ISession session, string key, object value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T? GetObjectFromJson<T>(this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}

Now you need a model to represent a single item in the cart. This is separate from the Product model because the cart only needs a subset of product data plus a quantity.

  • Right-click the Models folder, select Add > Class..., and name it CartItem.cs.
  • Replace all the generated content with this code:
namespace ShopMVC.Models;

public class CartItem
{
    // The product's database ID (used to find/remove items)
    public int ProductId { get; set; }

    // Display name for the cart table
    public string ProductName { get; set; } = string.Empty;

    // Price at the time of adding (snapshot, not a live lookup)
    public decimal Price { get; set; }

    // How many of this product the user wants
    public int Quantity { get; set; }
}

What does this code do?

CartItem is a lightweight class that stores just enough information to display a cart row and calculate totals. It snapshots the product name and price so the cart works even without querying the database on every page load.

  • Save CartItem.cs.
  • Confirm the project builds by pressing Ctrl+Shift+B. You should see Build succeeded in the Output window.

Build failed?

Check that CartItem.cs is inside the Models folder and uses the namespace ShopMVC.Models. Also verify SessionExtensions.cs is in the Extensions folder with namespace ShopMVC.Extensions.

Help me fix my build error after adding CartItem and SessionExtensions

✔️ Awesome, I've got everything!

The session infrastructure is ready. Time to build the controller.

ⓧ I'd like to double check the full code

namespace ShopMVC.Models;

public class CartItem
{
    public int ProductId { get; set; }
    public string ProductName { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}
Create the Cart controller

The Cart controller handles three user actions: viewing the cart, adding an item, and removing an item. Each action reads or writes the cart list from session using the extensions you just created.

  • Right-click the Controllers folder, select Add > Controller....
  • Choose MVC Controller - Empty and name it CartController.cs.
  • Replace all the generated content with this code:
using Microsoft.AspNetCore.Mvc;
using ShopMVC.Data;
using ShopMVC.Extensions;
using ShopMVC.Models;

namespace ShopMVC.Controllers;

public class CartController : Controller
{
    // Session key used to store/retrieve the cart list
    private const string CartSessionKey = "Cart";
    private readonly ShopContext _context;

    public CartController(ShopContext context)
    {
        _context = context;
    }

    // Display the current cart contents
    public IActionResult Index()
    {
        var cart = HttpContext.Session.GetObjectFromJson<List<CartItem>>(CartSessionKey)
                   ?? new List<CartItem>();
        return View(cart);
    }

    // Add a product to the cart (called from the product detail page form)
    [HttpPost]
    public IActionResult Add(int productId)
    {
        var product = _context.Products.Find(productId);
        if (product == null)
        {
            return NotFound();
        }

        var cart = HttpContext.Session.GetObjectFromJson<List<CartItem>>(CartSessionKey)
                   ?? new List<CartItem>();

        // Check if the product is already in the cart
        var existingItem = cart.FirstOrDefault(c => c.ProductId == productId);
        if (existingItem != null)
        {
            existingItem.Quantity++;
        }
        else
        {
            cart.Add(new CartItem
            {
                ProductId = product.Id,
                ProductName = product.Name,
                Price = product.Price,
                Quantity = 1
            });
        }

        HttpContext.Session.SetObjectAsJson(CartSessionKey, cart);
        return RedirectToAction("Index");
    }

    // Remove a product from the cart entirely
    [HttpPost]
    public IActionResult Remove(int productId)
    {
        var cart = HttpContext.Session.GetObjectFromJson<List<CartItem>>(CartSessionKey)
                   ?? new List<CartItem>();

        var item = cart.FirstOrDefault(c => c.ProductId == productId);
        if (item != null)
        {
            cart.Remove(item);
            HttpContext.Session.SetObjectAsJson(CartSessionKey, cart);
        }

        return RedirectToAction("Index");
    }
}

What does this code do?

  • Index() reads the cart from session. If no cart exists yet, it creates an empty list. The view receives this list as its model.
  • Add() is decorated with [HttpPost] because it modifies state. It looks up the product by ID, checks if it already exists in the cart (incrementing quantity if so), and saves the updated cart back to session.
  • Remove() finds the matching cart item and removes it entirely. Both Add and Remove redirect to Index so the user sees the updated cart immediately.

What is dependency injection?

The constructor accepts a ShopContext parameter. ASP.NET Core's dependency injection system automatically provides this when the controller is created. You registered ShopContext in Program.cs back in Step 2, and every controller that asks for it gets the same configured instance.

  • Save CartController.cs.

Red squiggly on GetObjectFromJson or SetObjectAsJson?

Check that the using ShopMVC.Extensions; line is at the top of CartController.cs. Without this import, the extension methods are not visible to the compiler.

Help me fix extension method errors in CartController

✔️ Awesome, I've got everything!

The controller is ready. Now you need a view to display the cart.

ⓧ I'd like to double check the full code

using Microsoft.AspNetCore.Mvc;
using ShopMVC.Data;
using ShopMVC.Extensions;
using ShopMVC.Models;

namespace ShopMVC.Controllers;

public class CartController : Controller
{
    private const string CartSessionKey = "Cart";
    private readonly ShopContext _context;

    public CartController(ShopContext context)
    {
        _context = context;
    }

    public IActionResult Index()
    {
        var cart = HttpContext.Session.GetObjectFromJson<List<CartItem>>(CartSessionKey)
                   ?? new List<CartItem>();
        return View(cart);
    }

    [HttpPost]
    public IActionResult Add(int productId)
    {
        var product = _context.Products.Find(productId);
        if (product == null)
        {
            return NotFound();
        }

        var cart = HttpContext.Session.GetObjectFromJson<List<CartItem>>(CartSessionKey)
                   ?? new List<CartItem>();

        var existingItem = cart.FirstOrDefault(c => c.ProductId == productId);
        if (existingItem != null)
        {
            existingItem.Quantity++;
        }
        else
        {
            cart.Add(new CartItem
            {
                ProductId = product.Id,
                ProductName = product.Name,
                Price = product.Price,
                Quantity = 1
            });
        }

        HttpContext.Session.SetObjectAsJson(CartSessionKey, cart);
        return RedirectToAction("Index");
    }

    [HttpPost]
    public IActionResult Remove(int productId)
    {
        var cart = HttpContext.Session.GetObjectFromJson<List<CartItem>>(CartSessionKey)
                   ?? new List<CartItem>();

        var item = cart.FirstOrDefault(c => c.ProductId == productId);
        if (item != null)
        {
            cart.Remove(item);
            HttpContext.Session.SetObjectAsJson(CartSessionKey, cart);
        }

        return RedirectToAction("Index");
    }
}
Create the Cart view

The cart view displays items in a table with columns for product name, price, quantity, subtotal, and a remove button. It also shows a grand total at the bottom.

  • In Solution Explorer, right-click the Views folder.
  • Select Add > New Folder and name it Cart.
  • Right-click the new Cart folder, select Add > View..., and name it Index.cshtml.
  • Choose Razor View - Empty and click Add.
  • Replace all the generated content with this code:
@model List<ShopMVC.Models.CartItem>

@{
    ViewData["Title"] = "Shopping Cart";
}

<h1>Shopping Cart</h1>

@if (Model.Count == 0)
{
    <div class="alert alert-info">
        Your cart is empty. <a asp-controller="Products" asp-action="Index">Browse products</a>
    </div>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Product</th>
                <th>Price</th>
                <th>Quantity</th>
                <th>Subtotal</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td>@item.ProductName</td>
                    <td>@item.Price.ToString("C")</td>
                    <td>@item.Quantity</td>
                    <td>@((item.Price * item.Quantity).ToString("C"))</td>
                    <td>
                        <form asp-action="Remove" method="post">
                            <input type="hidden" name="productId" value="@item.ProductId" />
                            <button type="submit" class="btn btn-danger btn-sm">Remove</button>
                        </form>
                    </td>
                </tr>
            }
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3" class="text-end fw-bold">Total:</td>
                <td class="fw-bold">@Model.Sum(i => i.Price * i.Quantity).ToString("C")</td>
                <td></td>
            </tr>
        </tfoot>
    </table>
}

What does this code do?

  • The @model directive tells Razor the view expects a List<CartItem>. This enables IntelliSense and compile-time checking.
  • The @if (Model.Count == 0) block shows a friendly empty-cart message with a link back to the catalog.
  • Each Remove button is wrapped in its own <form> that posts the productId to the Remove action. The footer row uses LINQ to sum all item subtotals into a grand total.
  • Save Index.cshtml.
  • Run the app with Ctrl+F5.
  • Navigate to /Products, click View Details on any product, then click Add to Cart.

You should be redirected to the shopping cart page showing the product you added in a table row with its price, quantity of 1, and a subtotal.

  • Add the same product again from the detail page. The quantity column should now show 2.
  • Click Remove on any item and confirm the total updates.
  • Navigate to /Products and back to /Cart to confirm your items persist across page navigations.

Cart page shows an error or is blank?

Make sure the Views/Cart/Index.cshtml file is inside a folder named exactly Cart (matching the controller name). ASP.NET Core looks for views in a folder that matches the controller name.

If you see a 500 error, check the Output window in Visual Studio for the full exception. A missing app.UseSession() call in Program.cs will cause session access to fail at runtime.

Help me fix my shopping cart page error

✔️ Awesome, I've got everything!

Your shopping cart is fully functional. Make sure all files are saved.

ⓧ I'd like to double check the full code

@model List<ShopMVC.Models.CartItem>

@{
    ViewData["Title"] = "Shopping Cart";
}

<h1>Shopping Cart</h1>

@if (Model.Count == 0)
{
    <div class="alert alert-info">
        Your cart is empty. <a asp-controller="Products" asp-action="Index">Browse products</a>
    </div>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Product</th>
                <th>Price</th>
                <th>Quantity</th>
                <th>Subtotal</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td>@item.ProductName</td>
                    <td>@item.Price.ToString("C")</td>
                    <td>@item.Quantity</td>
                    <td>@((item.Price * item.Quantity).ToString("C"))</td>
                    <td>
                        <form asp-action="Remove" method="post">
                            <input type="hidden" name="productId" value="@item.ProductId" />
                            <button type="submit" class="btn btn-danger btn-sm">Remove</button>
                        </form>
                    </td>
                </tr>
            }
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3" class="text-end fw-bold">Total:</td>
                <td class="fw-bold">@Model.Sum(i => i.Price * i.Quantity).ToString("C")</td>
                <td></td>
            </tr>
        </tfoot>
    </table>
}

Secret mission

Add Search and Cart Badge

Your product catalog works, but customers need a faster way to find what they want. Add a text search bar to the catalog and a live cart item count badge in the navigation bar so shoppers always know how many items they are carrying.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • SQL Server LocalDB database named ShopMVC (local to your machine, no cost).
  • ShopMVC project folder with all source files.

Keep everything running

No action needed. Choose this if you are still actively building or want to revisit the project later.

  • The LocalDB database persists on your machine with no ongoing cost.
  • Your project files remain in place and ready to open in Visual Studio.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your files and database stay intact for next time.

  • Stop the app if it is still running by pressing Ctrl+C in the terminal or closing the browser window.
  • Close the ShopMVC solution in Visual Studio.
  • The LocalDB database will remain available whenever you reopen the project.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete the LocalDB database:
  • Delete the project folder. On macOS or Linux, run:
rm -rf ~/source/repos/ShopMVC

On Windows, run:

Remove-Item -Recurse -Force ~\source\repos\ShopMVC

Default project location

Visual Studio typically creates projects in ~/source/repos/ (macOS/Linux) or ~\source\repos\ (Windows). If you chose a different location when creating the project, adjust the path accordingly.

Nice Work!

Nice Work!

You just built a fully functional e-commerce storefront using ASP.NET Core MVC and Entity Framework Core. From data models to database migrations to session-based cart management, you now understand how the MVC request lifecycle connects real user actions to server-side logic.

You've learned how to:

  • Build a complete ASP.NET Core MVC application from scratch with .NET 10, including controllers, views, and models that handle the full request lifecycle.
  • Create Entity Framework Core models with a one-to-many relationship, seed the database with sample data, and manage schema changes using migrations.
  • Build a session-based shopping cart using ASP.NET Core session state and JSON serialization to persist items across page navigations.
  • Secret Mission: Added text search to the product catalog and a live cart item count badge in the navigation bar.

Ready to quiz yourself?