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.
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; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddDbContext<BooksStoreDbContext>(opts => {
opts.UseSqlServer(
Configuration["ConnectionStrings:SportsStoreConnection"]);
});
services.AddScoped<IBooksStoreRepository, EFBooksStoreRepository>();
services.AddRazorPages();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
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:
@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:
@{
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:
<!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:
@page
<h4>Hello, I am the Cart Page</h4>
Run the app with url https://localhost:yourport/mycart (my url: https://localhost:44333/mycart):
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:
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:
@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:
@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:
...
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:
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:
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:
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:
@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:
Clicking a Add To Cart button:
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