Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

An Introduction to ASP.NET Core MVC through an Example (Part 4)

5.00/5 (3 votes)
5 Apr 2022CPOL7 min read 14.3K  
An introduction to ASP.NET Core MVC
In this article, we will add a shopping cart that is an important component of an ecommerce application.

Introduction

In this article, we will create the shopping cart experience that will be familiar to anyone who has ever made a purchase online. A shopping cart experience can look like this:

  • An Add To Cart button will be displayed alongside each of the books in the catalog. Clicking this button will show a summary of the books the customer has selected so far, including the total cost.
  • At this point, the user can click the Continue Shopping button to return to the book catalog or click the Checkout Now button to complete the order and finish the shopping session.

The Checkout Now button and some other advanced experiences will be covered in the next article. By the way, so far, we've completed 3 parts of our journey to build the BooksStore application:

Using the Code

Configuring Cart Pages

In this section, we are going to use Razor Pages to implement the shopping cart. To do this, first step, we are going to configure the Startup class to enable Razor Pages in the BooksStore application.

C#
using System;
using BooksStore.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace BooksStore
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. 
        // Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddDbContext<BooksStoreDbContext>(opts => {
                opts.UseSqlServer(
                    Configuration["ConnectionStrings:SportsStoreConnection"]);
            });
            services.AddScoped<IBooksStoreRepository, EFBooksStoreRepository>();
            services.AddRazorPages();
        }
        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                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.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute("genpage",
                         "{genre}/{bookPage:int}",
                    new { Controller = "Home", action = "Index" });
                endpoints.MapControllerRoute("page", "{bookPage:int}",
                    new { Controller = "Home", action = "Index", bookPage = 1 });
                endpoints.MapControllerRoute("genre", "{genre}",
                    new { Controller = "Home", action = "Index", bookPage = 1 });
                endpoints.MapControllerRoute("pagination", "Books/Page{bookPage}",
                    new { Controller = "Home", action = "Index", bookPage = 1 });
                endpoints.MapDefaultControllerRoute();
                endpoints.MapRazorPages();
            });
            SeedData.EnsurePopulated(app);
        }
    }
}

The AddRazorPages method sets up the services used by Razor Pages, and the MapRazorPages method registers Razor Pages as endpoints that the URL routing system can use to handle requests.

Next step, we add a folder named Pages, which is the conventional location for Razor Pages, to the BooksStore project. We are going to add a file named _ViewImports.cshtml to the Pages folder with the following code:

Razor
@namespace BooksStore.Pages
@using Microsoft.AspNetCore.Mvc.RazorPages
@using BooksStore.Models
@using BooksStore.MyTagHelper
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

These expressions set the namespace that the Razor Pages will belong to and allow the BooksStore classes to be used in Razor Pages without needing to specify their namespace.

Next, we are going to add a file named _ViewStart.cshtml to the Pages folder, with the following code:

Razor
@{
    Layout = "_MyCartLayout";
}

Razor Pages have their own configuration files, and this one specifies that the Razor Pages in the BooksStore project will use a layout file named _MyCartLayout by default.

Finally, to provide the layout the Razor Pages will use, add a file named _MyCartLayout.cshtml to the Pages folder with the following markup:

Razor
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Books Store</title>
    <link href="~/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div class="bg-dark text-white p-2">
        <span class="navbar-brand ml-2">BOOKS STORE</span>
    </div>
    <div class="m-1 p-1">
        @RenderBody()
    </div>
</body>
</html>

Creating a Cart Page

 

In Visual Studio, we are going to add the Razor Page template item and set the item name to MyCart.cshtml to the Pages folder. This will create a MyCart.cshtml file and a MyCart.cshtml.cs class file. Replace the contents of the MyCart.cshtml file with the following markup:

Razor
@page
<h4>Hello, I am the Cart Page</h4>

Run the app with url https://localhost:yourport/mycart (my url: https://localhost:44333/mycart):

Image 1

Creating the Add To Cart Buttons

Before implementing the cart feature, we need to create the buttons that will add books to the cart. To prepare for this, we are going to add a class file called UrlExtensions.cs to the MyTagHelper folder and define the extension method with the following code:

C#
using Microsoft.AspNetCore.Http;
namespace BooksStore.MyTagHelper
{
    public static class UrlExtensions
    {
        public static string PathAndQuery(this HttpRequest request) =>
           request.QueryString.HasValue
                ? $"{request.Path}{request.QueryString}"
                : request.Path.ToString();    
    }
}

The PathAndQuery extension method operates on the HttpRequest class, which ASP.NET Core uses to describe an HTTP request. The extension method generates a URL that the browser will be returned to after the cart has been updated, taking into account the query string, if there is one.

Next, we also add the namespace that contains the extension method to the view imports file so that we can use it in the partial view by adding the BooksStore.MyTagHelper namespace to the _ViewImports.cshtml file in the BooksStore/Views folder:

Razor
@using BooksStore
@using BooksStore.Models
@using BooksStore.MyTagHelper
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BooksStore

Finally, we are going to update the partial view that describes each book so that it contains an Add to Cart button by adding the buttons to the BookTemplate.cshtml file view in the BooksStore/Views/Shared folder with the following markup:

Razor
@model Book
<div class="card" style="width: 100%;">
    <div class="card-body">
        <h5 class="card-title">
            @Model.Title
            <span class="badge badge-pill badge-primary">
                <small>@Model.Price.ToString("c")</small>
            </span>
        </h5>
        <form id="@Model.BookID" asp-page="/MyCart" method="post">
            <input type="hidden" asp-for="BookID" />
            <input type="hidden" name="returnUrl"
                   value="@ViewContext.HttpContext.Request.PathAndQuery()" />
            <div class="card-text p-1">
                <strong>@Model.Genre</strong>
                <p>@Model.Description</p>
                <button type="submit"
                    class="btn btn-warning btn-sm pull-right">
                    Add To Cart
                </button>
            </div>
        </form>
    </div>
</div>

In the preceding markup:

  • We have added a form element that contains hidden input elements specifying the BookID value from the view model and the URL that the browser should be returned to after the cart has been updated.
  • The form element and one of the input elements are configured using built-in tag helpers, which are a useful way of generating forms that contain model values and that target controllers or Razor Pages.
  • The other input element uses the extension method we created to set the return URL. We also added a button element that will submit the form to the application.

Enabling Sessions

We am going to store details of a user’s cart using session state, which is data associated with a series of requests made by a user. There are different ways to store session state, but in this project we are going to store it in memory. This has the advantage of simplicity, but it means that the session data is lost when the application is stopped or restarted. In the Startup.cs file in the BooksStore project, we are going to add services and middleware to enable sessions:

C#
...
public void ConfigureServices(IServiceCollection services)
   {
       ...
      services.AddRazorPages();
      services.AddDistributedMemoryCache();
      services.AddSession();
    }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 {
       ...
      app.UseStaticFiles();
      app.UseSession();
      app.UseRouting();
       ...
 }

The AddDistributedMemoryCache method call sets up the in-memory data store. The AddSession method registers the services used to access session data, and the UseSession method allows the session system to automatically associate requests with sessions when they arrive from the client.

Using the Cart Features

Now to use the cart features, we are going to adding a class file called MyCart.cs to the Models folder in the BooksStore project and use it to define the classes with the following code:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BooksStore.Models
{
    public class MyCart
    {
        public List<CartLine> Lines { get; set; } = new List<CartLine>();

        public void AddItem(Book book, int quantity)
        {
            CartLine line = Lines
                .Where(b => b.Book.BookID == book.BookID)
                .FirstOrDefault();
            if (line == null)
            {
                Lines.Add(new CartLine
                {
                    Book = book,
                    Quantity = quantity
                });
            }
            else
            {
                line.Quantity += quantity;
            }
        }

        public void RemoveLine(Book book) =>
            Lines.RemoveAll(l => l.Book.BookID == book.BookID);

        public decimal ComputeTotalValue() =>
            Lines.Sum(e => e.Book.Price * e.Quantity);

        public void Clear() => Lines.Clear();
    }

    public class CartLine
    {
        public int CartLineID { get; set; }
        public Book Book { get; set; }
        public int Quantity { get; set; }
    }
}

The MyCart class uses the CartLine class, defined in the same file, to represent a book selected by the customer and the quantity the user wants to buy. We defined methods to add an item to the cart, remove a previously added item from the cart, calculate the total cost of the items in the cart, and reset the cart by removing all the items.

The session state feature in ASP.NET Core stores only int, string, and byte[] values. Since we want to store a MyCart object, we need to define extension methods to the ISession interface, which provides access to the session state data to serialize MyCart objects into JSON and convert them back. We are going to add a class file called SessionExtensions.cs to the MyTagHelper folder and define the extension methods with the following code:

C#
using Microsoft.AspNetCore.Http;
using System.Text.Json;

namespace BooksStore.MyTagHelper
{
    public static class SessionExtensions
    {
        public static void SetJson(this ISession session, string key, object value)
        {
            session.SetString(key, JsonSerializer.Serialize(value));
        }
        public static T GetJson<T>(this ISession session, string key)
        {
            var sessionData = session.GetString(key);
            return sessionData == null
                ? default(T) : JsonSerializer.Deserialize<T>(sessionData);
        }
    }
}

These methods serialize objects into the JavaScript Object Notation format, making it easy to store and retrieve MyCart objects.

In the class file named MyCart.cshtml.cs in the Pages folder, which was created by the Razor Page template item in Visual Studio, define the class with the following code:

C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using BooksStore.MyTagHelper;
using BooksStore.Models;
using System.Linq;

namespace BooksStore.Pages
{
    public class MyCartModel : PageModel
    {
        private IBooksStoreRepository repository;

        public MyCartModel(IBooksStoreRepository repo)
        {
            repository = repo;
        }

        public MyCart myCart { get; set; }

        public string ReturnUrl { get; set; }

        public void OnGet(string returnUrl)
        {
            ReturnUrl = returnUrl ?? "/";
            myCart = HttpContext.Session.GetJson<MyCart>("mycart") ?? new MyCart();
        }

        public IActionResult OnPost(long bookId, string returnUrl)
        {
            Book book = repository.Books
                .FirstOrDefault(b => b.BookID == bookId);
            myCart = HttpContext.Session.GetJson<MyCart>("mycart") ?? new MyCart();
            myCart.AddItem(book, 1);
            HttpContext.Session.SetJson("mycart", myCart);
            return RedirectToPage(new { returnUrl = returnUrl });
        }
    }
}

In the preceding code:

  • The page model class, which is named MyCartModel, defines an OnPost handler method, which is invoked to handle HTTP POST requests. It does this by retrieving a Book from the database, retrieving the user’s cart from the session data, and updating its content using the Book.
  • The GET request is handled by the OnGet handler method, which sets the values of the ReturnUrl and myCart properties, after which the Razor content section of the page is rendered.
  • The handler methods use parameter names that match the input elements in the HTML forms produced by the BookTemplate.cshtml view.

The MyCart Razor Page will receive the HTTP POST request that the browser sends when the user clicks an Add To Cart button. It will use the request form data to get the Book object from the database and use it to update the user’s cart, which will be stored as session data for use by future requests. Updating the contents of the MyCart.cshtml file in the BooksStore/Pages folder with the following markup:

Razor
@page
@model MyCartModel

<h2>Your cart</h2>

<table class="table table-bordered">
    <thead class="thead-light">
        <tr>
            <th>Quantity</th>
            <th>Item</th>
            <th class="text-right">Price</th>
            <th class="text-right">Subtotal</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var line in Model.myCart.Lines)
        {
            <tr>
                <td class="text-center">@line.Quantity</td>
                <td class="text-left">@line.Book.Title</td>
                <td class="text-right">@line.Book.Price.ToString("c")</td>
                <td class="text-right">
                    @((line.Quantity * line.Book.Price).ToString("c"))
                </td>
            </tr>
        }
    </tbody>
    <tfoot>
        <tr>
            <td colspan="3" class="text-right">Total:</td>
            <td class="text-right">
                @Model.myCart.ComputeTotalValue().ToString("c")
            </td>
        </tr>
    </tfoot>
</table>
<div class="text-center">
    <a class="btn btn-info" href="@Model.ReturnUrl">Continue shopping</a>
</div>

Run the application:

Image 2

Clicking a Add To Cart button:

Image 3

Clicking the Continue Shopping button returns the user to the page they came from.

Points of Interest

In this article, we will create the shopping cart experience with the Add To Cart and the Continue shopping buttons. The Checkout Now button and some other advanced experiences will be covered in the next article.

History

  • 6th April, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)