Using Session State in ASP.NET Core
In this post we will discuss the session management in ASP.NET Core applications. If you already have knowledge of ASP.NET Web Forms or ASP.NET MVC, then you must be knowing the Session state as a concept. We always use a statement as "Web applications needs state management", what this really means? A simple answer here is that an HTTP protocol is stateless for performance reason. The reason behind this is that every HTTP request to the web server is separate and server does not link these requests to each other. There could be multiple HTTP requests as web server process them all. So for web applications it becomes necessary that we should use server-side state management to maintain the posted data to the web server.
The Session state plays an important role in web applications. Session state is maintained by the web server in its own memory. Session state uses store maintained by the application to persist data across requests by the client. The data maintained by the session is backed by the cache. The important point here is that the business critical data must be stored in the database and cached in the session so that to read the data request need to hit the database each time and hence an application's performance can be improved. In ASP.NET Core the session state is maintained by providing a cookie to the client. This cookie contains the Session ID, this is send to the server with each request.
Some important points with respect to the session
- The session cookie is specific to the browser so sessions cannot be shared across browsers. When the browser is closed the cookie is deleted.
- The default timeout for the session is 20 minutes. Once the session is expired the session data is deleted
- If the expired session cookie is received then a new session is created that uses the same session cookie.
- In ASP.NET Core, the session state is configured using IDistributedCache implementation. This provides backing store for the session.
Implementing the session state in ASP.NET Core application
The session state application is designed to target to .NET Core 3.0 and 3.1. The .NET Core 3.1 version is released and it can be downloaded from the following link
The Visual Studio 2019 with update 16.4.0 is used to implement the application.
I will be building a simple ShoppingCart application to demonstrate the session in ASP.NET Core. I am using SQL Server to create database and table. Run the following script to create database and tables
Create Database SuperMarket
Use SuperMarket
Create table Category
(
CategoryId int Primary Key,
CategoryName varchar(50) not null
)
insert into Category values(101,'Food')
insert into Category values(102,'IT')
insert into Category values(103,'Household')
create table Product
(
ProductId int Primary key,
ProductName varchar(50) not null,
UnitPrice int not null,
CategoryId int not null References Category (CategoryId)
)
insert into Product values(10001, 'Must Oil',100,101)
insert into Product values(10002, 'Soya Oil',120,101)
insert into Product values(10003, 'Sunf Oil',110,101)
insert into Product values(10004, 'Router',3000,102)
insert into Product values(10005, 'Ext. HDD',12000,102)
insert into Product values(10006, 'USB Store',600,102)
insert into Product values(10007, 'TV',30000,103)
insert into Product values(10008, 'Mixer',3000,103)
insert into Product values(10009, 'Iron',800,103)
create table BillMaster(
BillNo int identity primary key,
BillAmount int not null
)
create table BillDetails
(
BillItemId int identity primary key,
ProductId int not null references Product(ProductId),
ProductName varchar(50) not null,
Quantity int not null,
RowPrice int not null,
BillNo int not null references BillMaster(BIllNo)
)
Listing 1: The database and table schemas
Step 1: Open Visual Studio and create a new ASP.NET Core web application. Name the application as ASPNET_CoreSessionApps. Select MVC from the project template and select version as shown in the following image
Image 1: Selecting ASP.NET Core template
Selecting the Model-View-Controller (MVC) template
Image 2: Selecting the MVC Template
The project will be created with Models, Controllers and Views folders. Now lets scaffold models classes to map with tables for Product, Category, etc. We need to use the Entity Framework Core (EF Core) to scaffold model classes from database. To use scaffolding we need to add following packages in the project
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Relational
Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore.SqlServer
The above all packages are useful for connecting to SQL Server database and executing scaffolding command. We will also add Newtonsoft.Json
The following image shows all the installed packages
Image 3: List of added packages
Step 2: Open the Command and and navigate to the project folder. To scaffold model classes from the database we need to use the dotnet CLI command for EF core. First run the following command to install dotnet ef global tool as shown in the following listing
dotnet tool install --global dotnet-ef
Listing 2: Installing dotnet ef global tool
Lets modify the appsettings.json file for declaring connection string to connect to the SQL Server databases
dotnet ef dbcontext scaffold "Data Source=.;Initial Catalog=SuperMarket;Integrated Security=SSPI" Microsoft.EntityFrameworkCore.SqlServer -o Models
Listing 3: The Database Scaffolding command
The above command will add classes in the Model folder to map with tables from the SuperMarket database. If you see the Models folder you will see following classes are generated in it
SuperMarketContext.cs, Product.cs, Category.cs, BillMaster.cs, BillDetails.cs.
The SuperMarketContext.cs contains DbContext class. This class contains the Database connection string in the OnConfiguring() method. It is advisable that instead of having the connection string in the class file, we should better add it in appsettings.json. Copy the connection string from this file and comment the OnConfiguring() method. Paste the connection string in appsettings.json as shown in the following listing
......
"ConnectionStrings": {
"ApplicationDb": "Data Source=.;Initial Catalog=SuperMarket;Integrated Security=SSPI"
}
Listing 3: The connection string (Note I haven;t added the complete code of appsettings.json file)
The SuperMarketContext class define DbSet properties for the classes like Product, Category, BillMaster, BillDetails.
Step 3: In the project add a new folder and name it as Services. In this folder add the class file and name it as Repository.cs. In this class file we will add generic interface for declaring repository methods for performing CRUD operations for Category, Product, BillDetails. Note that I have added the complete code in the same code file. You can add separate code files for interface and classes. Add code in the Repository.cs as shown in the following listing
using ASPNET_CoreSessionApps.Models;
using System.Collections.Generic;
using System.Linq;
namespace ASPNET_CoreSessionApps.Services
{
public interface IRepository<T, U> where T : class
{
IEnumerable<T> Get();
T Get(U id);
bool Create(T data);
bool Update(U id, T data);
bool Delete(U id);
}
public class CategoryRepository : IRepository<Category, int>
{
SuperMarketContext ctx;
public CategoryRepository(SuperMarketContext c)
{
ctx = c;
}
public bool Create(Category data)
{
int res = 0;
bool isSuccess = false;
ctx.Category.Add(data);
res = ctx.SaveChanges();
if (res >= 0) {
isSuccess = true;
}
return isSuccess;
}
public bool Delete(int id)
{
bool isSuccess = false;
var cat = ctx.Category.Find(id);
if (cat != null) {
ctx.Category.Remove(cat);
ctx.SaveChanges();
isSuccess = true;
}
return isSuccess;
}
public IEnumerable<Category> Get()
{
var cats = ctx.Category.ToList();
return cats;
}
public Category Get(int id)
{
var cat = ctx.Category.Find(id);
return cat;
}
public bool Update(int id, Category data)
{
bool isSuccess = false;
var cat = ctx.Category.Find(id);
if (cat != null) {
cat.CategoryName = data.CategoryName;
ctx.SaveChanges();
}
return isSuccess;
}
}
public class ProductRepository : IRepository<Product, int>
{
SuperMarketContext ctx;
public ProductRepository(SuperMarketContext c)
{
ctx = c;
}
public bool Create(Product data)
{
bool isSuccess = false;
int res = 0;
ctx.Product.Add(data);
res = ctx.SaveChanges();
if (res > 0) {
isSuccess = true;
}
return isSuccess;
}
public bool Delete(int id)
{
bool isSuccess = false;
var prd = ctx.Product.Find(id);
if (ctx != null)
{
ctx.Product.Remove(prd);
ctx.SaveChanges();
isSuccess = true;
}
return isSuccess;
}
public IEnumerable<Product> Get()
{
var prds = ctx.Product.ToList();
return prds;
}
public Product Get(int id)
{
var prd = ctx.Product.Find(id);
return prd;
}
public bool Update(int id, Product data)
{
bool isSuccess = false;
var prd = ctx.Product.Find(id);
if (prd != null)
{
prd.ProductName = data.ProductName;
prd.UnitPrice = data.UnitPrice;
prd.CategoryId = data.CategoryId;
ctx.SaveChanges();
isSuccess = true;
}
return isSuccess;
}
}
public interface IBill
{
bool GenerateBill(BillMaster bill, BillDetails []details);
}
public class BillGenerator : IBill
{
SuperMarketContext ctx;
public BillGenerator(SuperMarketContext c)
{
ctx = c;
}
public bool GenerateBill(BillMaster bill, BillDetails []details)
{
int res = 0;
bool isSuccess = false;
ctx.BillMaster.Add(bill);
ctx.BillDetails.AddRange(details);
res = ctx.SaveChanges();
if (res > 0)
{
isSuccess = true;
}
return isSuccess;
}
}
}
Listing 4: The Repository interface and classes
The IRepository is multi-type generic interface that contains methods for performing CRUD operations. Classes CategoryRepository and ProductRepository implements IRepository interface for Category and Product model classes respectively. These classes contains code for performing CRUD operations using SuperMarketContect class. The interface IBill is implemented by BillGenerator class. This class is used to perform insert operations using BillMaster and BillDetails classes to generate the final bill of shopping.
Step 4: We need to register the SuperMarketContext classes and repository classes in the DI container provided by ASP.NET Core. Open the Startup.cs and locate the ConfigureServices method. Add the following code in this method
// register the DbContext here
services.AddDbContext<SuperMarketContext>(options => {
options.UseSqlServer(Configuration.GetConnectionString("ApplicationDb"));
});
// register all repository classes
services.AddScoped<IRepository<Category, int>, CategoryRepository>();
services.AddScoped<IRepository<Product, int>, ProductRepository>();
services.AddScoped<IBill, BillGenerator>();
Listing 5: Registration of SuperMarketContext and repository classes in DI
Step 5: In ASP.NET Core, Session state is accessed in PageModel or in MVC controllers using HttpContext.Session. This property implements an ISession interface. This interface provides various extension methods to set and retrieve values as shown below
Get(ISession, String)
GetInt32(ISession, String)
GetString(ISession, String)
SetInt32(ISession, String, Int32)
SetString(ISession, String, String)
These methods are defined in SessionExtensions class in the Microsoft.AspNetCore.Http namespace. The following listing shows the SessionExtensions class
namespace Microsoft.AspNetCore.Http
{
public static class SessionExtensions
{
public static byte[] Get(this ISession session, string key);
public static int? GetInt32(this ISession session, string key);
public static string GetString(this ISession session, string key);
public static void SetInt32(this ISession session, string key, int value);
public static void SetString(this ISession session, string key, string value);
}
}
Listing 6: The SessionExtensions class
All these methods are used for setting and retrieving integer and string values. But if we want to set and retrieve a complete CLR object then we need to write the custom logic. To ad the custom session extension, add a new folder in the project and name it as SessionExtensions. In this folder add a new class file and name it as SessionObjectExtension.cs. Add the code in this file ad shown in the following listing
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace ASPNET_CoreSessionApps.SessionExtensions
{
public static class SessionExtensions
{
public static void SetSessionData<T>(this ISession session, string sessionKey, T sessionValue)
{
session.SetString(sessionKey, JsonConvert.SerializeObject(sessionValue));
}
public static T GetSessionData<T>(this ISession session, string sessionKey)
{
var data = session.GetString(sessionKey);
if (data == null) {
return default(T);
} else {
return JsonConvert.DeserializeObject<T>(data);
}
}
}
}
Listing 7: The Custom Session Extension class
The static class SessionExtensions contains two generic methods. The SetSesionData is a generic method. This method accepts key and value parameters where key is the session key of the type string and the value is of the type generic. The value parameter is the CLR object that will be stored as JSON in the session. (Note: Here we are using Newtonsoft.Json package and its classes for JSON operations). The generic method GetSessionData will accept the sessionKey as input parameter and based on the key the JSON data stored in the session state will deserialized into the CLR object and the CLR object will be returned from the method.
Step 6: To configure the session state for ASP.NET Core application, add the following code in the ConfigureServices method of the startup class
// some code is removed
services.AddDistributedMemoryCache();
services.AddSession(session => {
session.IdleTimeout = TimeSpan.FromMinutes(20);
});
// ... some code is removed
Listing 8: Configure Session State
Since the Distributed memory cache provides backing store for the session we are adding the AddDietribuitedMemotyCache() method in the service. The AddSession() method configure session state with the session timeout.
Step 7: In the Controllers folder add an empty MVC Controller and name it as CategoryController. In this controller add the code as shown in the following listing
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
namespace ASPNET_CoreSessionApps.Controllers
{
public class CategoryController : Controller
{
IRepository<Category,int> _catRepo;
public CategoryController(IRepository<Category, int> cat)
{
_catRepo = cat;
}
// GET: /<controller>/
public IActionResult Index()
{
var cats = _catRepo.Get();
return View(cats);
}
public IActionResult LoadProducts(int id)
{
HttpContext.Session.SetInt32("CategoryId", id);
return RedirectToAction("Index", "Product");
}
}
}
Listing 9: The CategoryController
The LoadProducts method will create a new Session Key "CategoryId" and it will store the CategoryId in it. This method will redirect to the Index method of the Product Controller.
Scaffold the Index view from the Index() action method of the CategoryController. The Model class for the View should be Category class. Replace the code of Index.cshtml as shown in the code of following listing
@model IEnumerable<ASPNET_CoreSessionApps.Models.Category>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<td>Category Id</td>
<td>Category Name</td>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td><a asp-controller="Category" asp-action="LoadProducts" asp-route-id="@item.CategoryId">@item.CategoryId</a></td>
<td>@item.CategoryName</td>
</tr>
}
</tbody>
</table>
Listing 10: The Index.cshtml for Category Controller
The LoadProduct() action method from the CategoryController will be executed on selecting the Category Row from the table of the Index View. The CategoryId will be stored in session state.
Step 8: In the Controllers folder add a new empty MVC controller and add the code in it as shown in the following listing
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.Services;
using ASPNET_CoreSessionApps.SessionExtensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
namespace ASPNET_CoreSessionApps.Controllers
{
public class ProductController : Controller
{
IRepository<Product, int> _prdRepo;
public ProductController(IRepository<Product, int> prd)
{
_prdRepo = prd;
}
// GET: /<controller>/
// The CategoryId will be retrieved from the session and Products will
//be displayed based on the selected CategoryId
public IActionResult Index()
{
var catId = HttpContext.Session.GetInt32("CategoryId");
List<Product> Products = null;
if (catId != 0)
{
Products = _prdRepo.Get().Where(p => p.CategoryId == catId).ToList();
}
return View(Products);
}
// This method will redirect to the Index view of the BillGenerator
// The method will add UnitPrice and BillDetails object in the Session
public IActionResult SelectForPurchase(int id)
{
var prd = _prdRepo.Get(id);
var billDetails = new BillDetails()
{
ProductId = id,
ProductName = prd.ProductName,
Quantity = 0,
RowPrice = 0
};
HttpContext.Session.SetInt32("UnitPrice", prd.UnitPrice);
HttpContext.Session.SetSessionData<BillDetails>("SelProduct", billDetails);
return RedirectToAction("Index","BillGenerator");
}
}
}
Listing 11: The ProductController
In the above code, the Index() action method retrieves CategoryId from the session and loads the Product based in the CategoryId. The SelectForPurchase() action method will store the UnitPrice of the selected Product in session and the BillDetails object will also be stored in the Session object. The BillDetails object contains the purchased Product along with the ProductId. The method redirects to Index action from the BillGenerator controller. Scaffold the Index view from the Index action method of the ProductController class with model class as Product. In the Index View add the code shown in the following listing
@model IEnumerable<ASPNET_CoreSessionApps.Models.Product>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<td>ProductId</td>
<td>ProductName</td>
<td>Unit Price</td>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@item.ProductId
</td>
<td>
@item.ProductName
</td>
<td>
@item.UnitPrice
</td>
<td>
<a asp-controller="Product" asp-action="SelectForPurchase" asp-route-id="@item.ProductId">Select Product</a>
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="5">
<a asp-controller="Category" asp-action="Index">Select Different Category</a>
</td>
</tr>
</tfoot>
</table>
Listing 12: The Index view
The anchor tag executes the SelectForPurchase() action method. This will select the Product for purchase. The anchor tag for Select Different category redirect to the Index method from the CategoryController. The end-user can select the Category and again load products for the category and can purchase it.
Step 9: In the Controllers folder add a new empty MVC controller, name this controller as BillGeneratorController. The Index() action method of this controller will read UnitPrice and BillDetails from the session keys UnitPrice and SelProduct respectively. The HttpPost Index() action method will be posted from two submit button of the Index view. The Index view have Save and Continue Purchase and Save and CheckOut submit buttons. If Save and Continue submit button is used then the PurchasedProduct session object is created and the purchased product is stored in it. This will redirect to an Index action method of the ProductController to purchase new product. If the Save and CheckOut submit is clicked then the PurchasedProduct session will store all purchased products in session and the Index view from the FinalBillController will be displayed. Add the code in the BillGeneratorController as shown in the following listing
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.SessionExtensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace ASPNET_CoreSessionApps.Controllers
{
public class BillGeneratorController : Controller
{
List<BillDetails> selProducts;
public BillGeneratorController()
{
selProducts = new List<BillDetails>();
}
// GET: /<controller>/
public IActionResult Index()
{
ViewBag.UnitPrice = HttpContext.Session.GetInt32("UnitPrice");
var billDetails = HttpContext.Session.GetSessionData<BillDetails>("SelProduct");
if (billDetails != null)
{
selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
}
return View(billDetails);
}
[HttpPost]
public IActionResult Index(BillDetails b, string purchase)
{
if (purchase == "Save and Continue Purchase")
{
//Check if the session contains List of purchased prducts
selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
if (selProducts == null)
{
selProducts = new List<Models.BillDetails>();
}
//Save the selected product in Session
selProducts.Add(b);
HttpContext.Session.SetSessionData<List<BillDetails>>("PurchasedProduct", selProducts);
//Go to the ProductMVC
return RedirectToAction("Index", "Product");
}
else if(purchase == "Save and CheckOut")
{
selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
if (selProducts == null)
{
selProducts = new List<Models.BillDetails>();
}
var selBill = HttpContext.Session.GetSessionData<BillDetails>("SelProduct");
ViewBag.UnitPrice = HttpContext.Session.GetInt32("UnitPrice");
selBill.Quantity = b.Quantity;
selBill.RowPrice = b.RowPrice;
selProducts.Add(selBill);
HttpContext.Session.SetSessionData<List<BillDetails>>("PurchasedProduct", selProducts);
return RedirectToAction("Index", "FinalBill");
}
return View();
}
}
}
Listing 13: The BillGenerator controller
Scaffold the Index view from the BillGeneratorController with the model class as BillDetails. In the Index view add the code as shown in the following listing
@model ASPNET_CoreSessionApps.Models.BillDetails
<form asp-controller="BillGenerator" role="form" id="frmbill">
<table class="table table-striped table-bordered table-condensed">
<tr>
<td>
Product Id:
</td>
<td>
<input type="text" asp-for="ProductId" readonly class="form-control"/>
</td>
</tr>
<tr>
<td>
Product Name:
</td>
<td>
<input type="text" asp-for="ProductName" readonly class="form-control"/>
<span>Unit Price : @ViewBag.UnitPrice</span>
</td>
</tr>
<tr>
<td>
Quantity:
</td>
<td>
<input type="text" asp-for="Quantity" class="form-control"
/>
</td>
</tr>
<tr>
<td>
Price:
</td>
<td>
<input type="text" asp-for="RowPrice" class="form-control"/>
</td>
</tr>
<tr>
<td>
<input type="submit" name="purchase"
value="Save and Continue Purchase"
class="btn btn-link"/>
</td>
<td>
<input type="submit" name="purchase"
value="Save and CheckOut"
class="btn btn-link" />
</td>
</tr>
</table>
</form>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#Quantity").on('change', function () {
$("#RowPrice").val($(this).val() * @ViewBag.UnitPrice);
});
});
</script>
Listing 14: The Index.cshtml
In the above listing we are using jQuery to calculate the product price in the bill based on the quantity entered of the product.
Step 10: In the Controllers folder add a new empty MVC controller and name it as FinalBillController. This controller will have an Index() action method that contains logic for reading all purchased products from the session key PurchasedProduct session key. Add the code in this controller as shown in the following listing
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using ASPNET_CoreSessionApps.Services;
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.SessionExtensions;
namespace ASPNET_CoreSessionApps.Controllers
{
public class FinalBillController : Controller
{
IBill finalBill;
public FinalBillController(IBill fb)
{
finalBill = fb;
}
// GET: /<controller>/
public IActionResult Index()
{
var selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
var billMaster = new BillMaster();
//Calculate the Total Bill Amount
foreach (var item in selProducts)
{
billMaster.BillAmount += item.RowPrice;
}
billMaster.BillDetails = selProducts;
finalBill.GenerateBill(billMaster, billMaster.BillDetails.ToArray());
return View(billMaster);
}
}
}
Listing 15: The FinalBillGenerator Controller
Scaffold an Index view from the Index() action method with model class as BillMaster. Add the code in the view as shown in the following listing
@model ASPNET_CoreSessionApps.Models.BillMaster
<h1> The Final Bill</h1>
<form id="frmFinal" asp-controller="FinalBill" role="form">
<table class="table table-bordered table-condensed table-striped">
<thead>
<tr>
<td colspan="2">
Bill No
</td>
<td>
<input type="text" asp-for="@Model.BillNo" class="form-control"/>
</td>
</tr>
<tr>
<td>Product Name</td>
<td>Quantity</td>
<td>Row Price</td>
</tr>
</thead>
<tbody>
@foreach (var item in Model.BillDetails)
{
<tr>
<td>@item.ProductName</td>
<td>@item.Quantity</td>
<td>@item.RowPrice</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="2">
Total Bill
</td>
<td>
<input type="text" asp-for="@Model.BillAmount" class="form-control" readonly/>
</td>
</tr>
</tfoot>
</table>
</form>
Listing 16: The Index.cshtml view
The index view will show the final generated bill.
Finally modify the _Layout.cshtml to add link for Category Controller as shown in the following listing
.....
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Category" asp-action="Index">Category</a>
</li>
......
Note: Some code is removed from here
Listing 17: The _Layout.cshtml
Run the application The browser will show the application as shown in the following image
Image 4: The application loaded in the browser
Click on Category link. This will show the Category Index view as shown in the following image
Image 5: The Category Index
Click on the Category Id link, this will show the Product list for the selected Category as shown in the following image
Image 6: The Product List
Click on the Select Product link, this will show the Bill Generator view with the selected product and its Unit Price as shown in the following image
Image 7: The Select Product Bill
Enter Quantity in Quantity text box and press table, it will show the Price for the product as Unit Price * Quantity as shown in the following image
Image 8: The Product Price based on Quantity
Click on Save and CheckOut, link it will show the final generated bill as shown in the following image
Image 8: The Final Bill
That's it.
The Complete code of the article can be downloaded from here.
Conclusion: The Session State is the most important feature of the of the Web Applications. The Session State backed by the Distributed Cache will maintain the session data on the server and this data will be available across the controllers in Http request.
Step 7: In the Controllers folder add an empty MVC Controller and name it as CategoryController. In this controller add the code as shown in the following listing
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
namespace ASPNET_CoreSessionApps.Controllers
{
public class CategoryController : Controller
{
IRepository<Category,int> _catRepo;
public CategoryController(IRepository<Category, int> cat)
{
_catRepo = cat;
}
// GET: /<controller>/
public IActionResult Index()
{
var cats = _catRepo.Get();
return View(cats);
}
public IActionResult LoadProducts(int id)
{
HttpContext.Session.SetInt32("CategoryId", id);
return RedirectToAction("Index", "Product");
}
}
}
The LoadProducts method will create a new Session Key "CategoryId" and it will store the CategoryId in it. This method will redirect to the Index method of the Product Controller.
Scaffold the Index view from the Index() action method of the CategoryController. The Model class for the View should be Category class. Replace the code of Index.cshtml as shown in the code of following listing
@model IEnumerable<ASPNET_CoreSessionApps.Models.Category>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<td>Category Id</td>
<td>Category Name</td>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td><a asp-controller="Category" asp-action="LoadProducts" asp-route-id="@item.CategoryId">@item.CategoryId</a></td>
<td>@item.CategoryName</td>
</tr>
}
</tbody>
</table>
Listing 10: The Index.cshtml for Category Controller
The LoadProduct() action method from the CategoryController will be executed on selecting the Category Row from the table of the Index View. The CategoryId will be stored in session state.
Step 8: In the Controllers folder add a new empty MVC controller and add the code in it as shown in the following listing
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.Services;
using ASPNET_CoreSessionApps.SessionExtensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
namespace ASPNET_CoreSessionApps.Controllers
{
public class ProductController : Controller
{
IRepository<Product, int> _prdRepo;
public ProductController(IRepository<Product, int> prd)
{
_prdRepo = prd;
}
// GET: /<controller>/
// The CategoryId will be retrieved from the session and Products will
//be displayed based on the selected CategoryId
public IActionResult Index()
{
var catId = HttpContext.Session.GetInt32("CategoryId");
List<Product> Products = null;
if (catId != 0)
{
Products = _prdRepo.Get().Where(p => p.CategoryId == catId).ToList();
}
return View(Products);
}
// This method will redirect to the Index view of the BillGenerator
// The method will add UnitPrice and BillDetails object in the Session
public IActionResult SelectForPurchase(int id)
{
var prd = _prdRepo.Get(id);
var billDetails = new BillDetails()
{
ProductId = id,
ProductName = prd.ProductName,
Quantity = 0,
RowPrice = 0
};
HttpContext.Session.SetInt32("UnitPrice", prd.UnitPrice);
HttpContext.Session.SetSessionData<BillDetails>("SelProduct", billDetails);
return RedirectToAction("Index","BillGenerator");
}
}
}
In the above code, the Index() action method retrieves CategoryId from the session and loads the Product based in the CategoryId. The SelectForPurchase() action method will store the UnitPrice of the selected Product in session and the BillDetails object will also be stored in the Session object. The BillDetails object contains the purchased Product along with the ProductId. The method redirects to Index action from the BillGenerator controller. Scaffold the Index view from the Index action method of the ProductController class with model class as Product. In the Index View add the code shown in the following listing
@model IEnumerable<ASPNET_CoreSessionApps.Models.Product>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<td>ProductId</td>
<td>ProductName</td>
<td>Unit Price</td>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@item.ProductId
</td>
<td>
@item.ProductName
</td>
<td>
@item.UnitPrice
</td>
<td>
<a asp-controller="Product" asp-action="SelectForPurchase" asp-route-id="@item.ProductId">Select Product</a>
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="5">
<a asp-controller="Category" asp-action="Index">Select Different Category</a>
</td>
</tr>
</tfoot>
</table>
The anchor tag executes the SelectForPurchase() action method. This will select the Product for purchase. The anchor tag for Select Different category redirect to the Index method from the CategoryController. The end-user can select the Category and again load products for the category and can purchase it.
Step 9: In the Controllers folder add a new empty MVC controller, name this controller as BillGeneratorController. The Index() action method of this controller will read UnitPrice and BillDetails from the session keys UnitPrice and SelProduct respectively. The HttpPost Index() action method will be posted from two submit button of the Index view. The Index view have Save and Continue Purchase and Save and CheckOut submit buttons. If Save and Continue submit button is used then the PurchasedProduct session object is created and the purchased product is stored in it. This will redirect to an Index action method of the ProductController to purchase new product. If the Save and CheckOut submit is clicked then the PurchasedProduct session will store all purchased products in session and the Index view from the FinalBillController will be displayed. Add the code in the BillGeneratorController as shown in the following listing
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.SessionExtensions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace ASPNET_CoreSessionApps.Controllers
{
public class BillGeneratorController : Controller
{
List<BillDetails> selProducts;
public BillGeneratorController()
{
selProducts = new List<BillDetails>();
}
// GET: /<controller>/
public IActionResult Index()
{
ViewBag.UnitPrice = HttpContext.Session.GetInt32("UnitPrice");
var billDetails = HttpContext.Session.GetSessionData<BillDetails>("SelProduct");
if (billDetails != null)
{
selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
}
return View(billDetails);
}
[HttpPost]
public IActionResult Index(BillDetails b, string purchase)
{
if (purchase == "Save and Continue Purchase")
{
//Check if the session contains List of purchased prducts
selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
if (selProducts == null)
{
selProducts = new List<Models.BillDetails>();
}
//Save the selected product in Session
selProducts.Add(b);
HttpContext.Session.SetSessionData<List<BillDetails>>("PurchasedProduct", selProducts);
//Go to the ProductMVC
return RedirectToAction("Index", "Product");
}
else if(purchase == "Save and CheckOut")
{
selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
if (selProducts == null)
{
selProducts = new List<Models.BillDetails>();
}
var selBill = HttpContext.Session.GetSessionData<BillDetails>("SelProduct");
ViewBag.UnitPrice = HttpContext.Session.GetInt32("UnitPrice");
selBill.Quantity = b.Quantity;
selBill.RowPrice = b.RowPrice;
selProducts.Add(selBill);
HttpContext.Session.SetSessionData<List<BillDetails>>("PurchasedProduct", selProducts);
return RedirectToAction("Index", "FinalBill");
}
return View();
}
}
}
Listing 13: The BillGenerator controller
Scaffold the Index view from the BillGeneratorController with the model class as BillDetails. In the Index view add the code as shown in the following listing
@model ASPNET_CoreSessionApps.Models.BillDetails
<form asp-controller="BillGenerator" role="form" id="frmbill">
<table class="table table-striped table-bordered table-condensed">
<tr>
<td>
Product Id:
</td>
<td>
<input type="text" asp-for="ProductId" readonly class="form-control"/>
</td>
</tr>
<tr>
<td>
Product Name:
</td>
<td>
<input type="text" asp-for="ProductName" readonly class="form-control"/>
<span>Unit Price : @ViewBag.UnitPrice</span>
</td>
</tr>
<tr>
<td>
Quantity:
</td>
<td>
<input type="text" asp-for="Quantity" class="form-control"
/>
</td>
</tr>
<tr>
<td>
Price:
</td>
<td>
<input type="text" asp-for="RowPrice" class="form-control"/>
</td>
</tr>
<tr>
<td>
<input type="submit" name="purchase"
value="Save and Continue Purchase"
class="btn btn-link"/>
</td>
<td>
<input type="submit" name="purchase"
value="Save and CheckOut"
class="btn btn-link" />
</td>
</tr>
</table>
</form>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$("#Quantity").on('change', function () {
$("#RowPrice").val($(this).val() * @ViewBag.UnitPrice);
});
});
</script>
Listing 14: The Index.cshtml
In the above listing we are using jQuery to calculate the product price in the bill based on the quantity entered of the product.
Step 10: In the Controllers folder add a new empty MVC controller and name it as FinalBillController. This controller will have an Index() action method that contains logic for reading all purchased products from the session key PurchasedProduct session key. Add the code in this controller as shown in the following listing
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using ASPNET_CoreSessionApps.Services;
using ASPNET_CoreSessionApps.Models;
using ASPNET_CoreSessionApps.SessionExtensions;
namespace ASPNET_CoreSessionApps.Controllers
{
public class FinalBillController : Controller
{
IBill finalBill;
public FinalBillController(IBill fb)
{
finalBill = fb;
}
// GET: /<controller>/
public IActionResult Index()
{
var selProducts = HttpContext.Session.GetSessionData<List<BillDetails>>("PurchasedProduct");
var billMaster = new BillMaster();
//Calculate the Total Bill Amount
foreach (var item in selProducts)
{
billMaster.BillAmount += item.RowPrice;
}
billMaster.BillDetails = selProducts;
finalBill.GenerateBill(billMaster, billMaster.BillDetails.ToArray());
return View(billMaster);
}
}
}
Scaffold an Index view from the Index() action method with model class as BillMaster. Add the code in the view as shown in the following listing
@model ASPNET_CoreSessionApps.Models.BillMaster
<h1> The Final Bill</h1>
<form id="frmFinal" asp-controller="FinalBill" role="form">
<table class="table table-bordered table-condensed table-striped">
<thead>
<tr>
<td colspan="2">
Bill No
</td>
<td>
<input type="text" asp-for="@Model.BillNo" class="form-control"/>
</td>
</tr>
<tr>
<td>Product Name</td>
<td>Quantity</td>
<td>Row Price</td>
</tr>
</thead>
<tbody>
@foreach (var item in Model.BillDetails)
{
<tr>
<td>@item.ProductName</td>
<td>@item.Quantity</td>
<td>@item.RowPrice</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="2">
Total Bill
</td>
<td>
<input type="text" asp-for="@Model.BillAmount" class="form-control" readonly/>
</td>
</tr>
</tfoot>
</table>
</form>
Listing 16: The Index.cshtml view
The index view will show the final generated bill.
Finally modify the _Layout.cshtml to add link for Category Controller as shown in the following listing
.....
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Category" asp-action="Index">Category</a>
</li>
......
Note: Some code is removed from here
Listing 17: The _Layout.cshtml
Run the application The browser will show the application as shown in the following image
Image 4: The application loaded in the browser
Click on Category link. This will show the Category Index view as shown in the following image
Image 5: The Category Index
Click on the Category Id link, this will show the Product list for the selected Category as shown in the following image
Image 6: The Product List
Click on the Select Product link, this will show the Bill Generator view with the selected product and its Unit Price as shown in the following image
Image 7: The Select Product Bill
Enter Quantity in Quantity text box and press table, it will show the Price for the product as Unit Price * Quantity as shown in the following image
Image 8: The Product Price based on Quantity
Click on Save and CheckOut, link it will show the final generated bill as shown in the following image
Image 8: The Final Bill
That's it.
The Complete code of the article can be downloaded from here.
Conclusion: The Session State is the most important feature of the of the Web Applications. The Session State backed by the Distributed Cache will maintain the session data on the server and this data will be available across the controllers in Http request.