Distributed sessions in ASP.NET Core

November 30, 2022
Written by
Daniel Lawson
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

In this article, you will learn what sessions are and how to use them in your ASP.NET Core applications. Then, you will see the limitations of the default session and how to get beyond them by distributing your sessions.

Prerequisites

You will need the following to realize the project in this article:

What are sessions?

The HTTP protocol is, by default, stateless. But sometimes it's essential to hold onto user data while users are browsing your website. This data can help to remember their recent actions. For example, on an e-commerce website, this data can help to store products in a shopping cart.

Sessions are one of the several ways to manage state in an ASP.NET Core application. A session state is a mechanism for the storage of user data across the application. It creates and stores an identifier in a cookie, which is then used to retrieve the session data on the server. A session state keeps the data as long as the cookie is configured to last. The default session state version in ASP.NET Core stores the session data in the memory (RAM) of the web server.

Now, let’s see how to implement a session state in an ASP.NET Core app.

How to add Session State to ASP.NET Core

Create the ASP.NET Core MVC app

The first step is to create the ASP.NET Core MVC app. You can use the terminal and run these commands:

dotnet new mvc -o DistributedSessions
cd DistributedSessions

When your app is created, you can run it with this command:

dotnet run

You should have a similar output in your terminal window:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7106
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5050
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

You should be able to see a basic ASP.NET Core MVC template at the link mentioned in your terminal window.

Add the Session middleware to the pipeline

You are all set. You can open the solution in your IDE in order to add the session middleware to the pipeline. Open the program.cs file and add the two lines of code highlighted below:

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddSession();

var app = builder.Build();

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

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

app.UseRouting();

app.UseAuthorization();

app.UseSession();

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

app.Run();

These two lines of code are sufficient to add a basic session state management to your app. However, it’s possible to override the default behavior by updating the session options.

You can learn more about the session state options in the official Microsoft documentation.

It is crucial to add the UseSession middleware after UseRouting and before MapControllerRoute. Please refer to the official Microsoft documentation if you want to know more about the middleware pipeline’s order.

Add data to the session

The sample project in this article will simulate a shopping cart of a bookseller's website. You will use session state to keep the selected books in the cart. The source code is available on GitHub.

First, create the Book.cs file to define your Book class:

namespace DistributedSessions;

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public double Price { get; set; }

    public Book(int id, string title, double price)
    {
        Id = id;
        Title = title;
        Price = price;
    }
}

Also, add another class called Data in a separate Data.cs file:

namespace DistributedSessions;

public class Data
{
    public static List<Book> Books = new()
    {
        new Book(1, "Case of the Laughing Goose", 18.90),
        new Book(2, "Call of Lords", 25),
        new Book(3, "Strike the Future",23.16),
        new Book(4, "Wild and Wicked", 14.45)
    };
}

The Data class has one property which is a list of books. In a real-world app, the data will come from a data source (a database, a file, etc.). In this example, we’ll assume that the list of books in the Data class plays that role.

You will display the books on the home page. In the Controllers folder, Go to the HomeController.cs file. Update the Index() method as below:

    public IActionResult Index()
    {
        var books = Data.Books;
        return View(books);
    }

The code above is doing 2 things: Getting all the books and passing them to the home page view. The home page view is strongly typed as it is receiving a list of books as the model. As a result, you need to update the view code by giving it a model. Go to the Views/Home/Index.cshtml file and update its content as follows:

@model List<Book>

@{
    ViewData["Title"] = "Home Page";
}

<div class="container">
    <div class="row">
        <div class="col-10 text-center">
            <h1 class="display-4">Welcome</h1>
            <p>Here are our books:</p>
        </div>
        <div class="col-2 pt-5">
             <a class="btn btn-outline-success btn-lg" 
                asp-controller="Cart" 
                asp-action="Index">
                Cart
            </a>
        </div>
    </div>
</div>

<div class="text-center">
    <table class="table table-hover">
        <thead>
            <tr class="table-secondary">
                <th scope="col">Id</th>
                <th scope="col">Title</th>
                <th scope="col">Price</th>
                <th scope="col">Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in Model)
            {
                <tr>
                    <td>@book.Id</td>
                    <td>@book.Title</td>
                    <td>@book.Price</td>
                    <td>
                        <a class="btn btn-success btn-sm p-lg-1" 
                            asp-controller="Cart" 
                            asp-action="AddToCart" 
                            asp-route-id="@book.Id">
                            Add to cart
                        </a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
</div>

In the command line, navigate to your project folder and run your app with the dotnet run command. At this point, you should be able to see the list of books on the home page of your app like this:

App home page, listing books in a table with an ID column, a title column, a price column, and actions column. There&#x27;s an add to cart button in the actions column for each book.

A click on the Add to cart button will add the corresponding book to the cart. You will use the session to accomplish that behavior. In the Controllers folder, create a controller named CartController, with the content below:

using Microsoft.AspNetCore.Mvc;
using System.Text;
using System.Text.Json;

namespace DistributedSessions.Controllers;

public class CartController : Controller
{
    public async Task<IActionResult> Index()
    {
        // Get the value of the session
        var data = await GetBooksFromSession();

        //Pass the list to the view to render
        return View(data);
    }

    private async Task<List<Book>> GetBooksFromSession()
    {
        await HttpContext.Session.LoadAsync();
        var sessionString = HttpContext.Session.GetString("cart");
        if (sessionString is not null)
        {
            return JsonSerializer.Deserialize<List<Book>>(sessionString);
        }

        return Enumerable.Empty<Book>().ToList();
    }
}

The HttpContext, as the name implies, gets the context of the current HTTP request. That is where the session object resides. The session state is saved as a key-value pair. You store and retrieve the data by a key of your choice. A new session cookie will automatically be created if it doesn't exist already. Since the session cookie is stored in the browser, the data in that session is tied to the browser and that browser only. This means two users each using their own browser, do not share the session data.

The Index() action result will get the data from the session and display it in the view. If there is no data in the session, an empty list will be displayed.

If there is data in the session, it will be a JSON formatted string. You will need to convert the JSON into .NET objects, in this case, a list of books. This conversion is called deserialization. 

Now, you will develop the functionality to add a book to the cart. For that, create another method in the CartController, called AddToCart.

    public async Task<IActionResult> AddToCart(int id)
    {
        var data = await GetBooksFromSession();

        var book = Data.Books.FirstOrDefault(b => b.Id == id);

        if (book is not null)
        {
            data.Add(book);

            HttpContext.Session.SetString("cart", JsonSerializer.Serialize(data));

            TempData["Success"] = "The book is added successfully";

            return RedirectToAction("Index");
        }

        return NotFound();
    }

This code gets the session value string and deserializes it back to a list of books. Then, it adds the selected book to the list, serializes the list to JSON, and saves the JSON back in the session.

After that, you will create the cart page. In the Views folder, create a folder called Cart. In that folder, create an Index.cshtml file, with the content below:

@model List<Book>

<div class="container">

    @if (TempData["Success"] is not null)
    {
        <div class="alert alert-success col-12 mt-4"><strong> @TempData["Success"]</strong></div>
    }

    <div class="row">
        <div class="col-10 text-center">
            <h1>Cart from session</h1>
            <p>Here's your cart (retrieved from session)</p>
        </div>
        <div class="col-2 pt-3">
            <a class="btn btn-outline-success btn-lg" asp-controller="Home" asp-action="Index">Home</a>
        </div>
    </div>
</div>

<div class="text-center">
    <table class="table table-hover">
        <thead>
            <tr class="table-secondary">
                <th scope="col">Id</th>
                <th scope="col">Title</th>
                <th scope="col">Price</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var book in Model)
            {
                <tr>
                    <td>@book.Id</td>
                    <td>@book.Title</td>
                    <td>@book.Price</td>
                </tr>
            }
        </tbody>
    </table>
</div>

This view is similar to the home page view. It will also take a list of books as a model and will render the list on the page.

Rerun your app. When you click on the Add to cart button in the book list on the home page, you should see the selected book on the Cart page. Now, the data is coming from the session.
Congrats, you’ve successfully implemented basic session management in ASP.NET Core MVC!

Limitations of the default in-memory session

The default in-memory session is great to get started quickly. It stores the data in the memory of the server the app is running on, which is very fast. But there is a drawback. When your web application gets a lot of traffic, you may need to deploy your application to multiple servers and use a load balancer to route traffic to the servers. As users use the application, their HTTP requests will bounce around multiple application servers. As a result, a user could have books in their cart on one server, but when they request another page, their cart is suddenly empty because it was served by another server. This happens because each server has its own session data stored in memory, and the data gets out of sync. In the next section of this article, you’ll learn how to solve this problem with distributed sessions.

How to distribute Session State in ASP.NET Core

A distributed session is a way to persist session data by storing it in a single data store that all instances of your applications use. For your fictional online bookstore, it can help to keep the client’s shopping cart across all instances of your applications.

Now, let’s set up a distributed session in the ASP.NET Core app. To configure your session to be distributed, you have to configure distributed caching in ASP.NET Core, and the session will automatically use the IDistributedCache. There are multiple distributed cache providers that you can use. In this article, you'll configure a Redis database as your distributed caching to store the session data.

Redis is a popular open-source data store that is built for various use cases, including session data management and caching. If you want to learn more about Redis, go to the official documentation page.

Test the connection to Redis

First, test the connection to your Redis server using the redis-cli. If you are running your Redis server locally without requiring authentication, you can simply execute redis-cli to connect to the server. Otherwise, refer to the redis-cli documentation to specify the server location and credentials. Once you are connected to the server, execute the ping command.

If Redis is correctly installed and started, you should have a message “PONG” in your command prompt.

Configure distributed caching with Redis

Now that you have verified your Redis server is fully functional, it’s time to interact with the Redis server. There is a NuGet package designed for that purpose: Microsoft.Extensions.Caching.StackExchangeRedis. To install the package, go to your terminal window and run inside your project folder:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Next, configure the connection string to your Redis database. If you're running the server locally without authentication, you can update the appsettings.Development.json file like this:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "RedisSession": "localhost:6379"
  }
}

6379 is the default port Redis will run on. Change this if you configured your Redis server to use a different port.

If you're using a username and password, you should store the connection string using user-secrets, environment variables, or a vault service.

Next, put the highlighted code in front of AddSession in Program.cs:

builder.Services.AddStackExchangeRedisCache(options =>
    options.Configuration = builder.Configuration.GetConnectionString("RedisSession"));

builder.Services.AddSession();

var app = builder.Build();

This will configure the IDistributedCache to use the Redis server with the RedisSession connection string.

Rerun your app and add some books to the cart. At this point, the session state data is stored in the Redis database. You can query the data from the redis-cli. First list the keys using the KEYS * command, then copy one of the keys (they are GUIDs). To get the session data, run the following HGET command:

HGET 8c4ab198-6b5f-b096-6467-faa42fda2fc4 data

8c4ab198-6b5f-b096-6467-faa42fda2fc4 is the key I copied, so replace that with the key you copied. The result will be hash because multiple values are stored as a hash under this key.

The output will be an encoded version of the JSON string holding the books in your cart, looking like this:

"\x02\x00\x00\x01\x82Q\xce\x1bM\x1a\xc04\xca\x95\xef\x1c<\xbf&w\x00\x04cart\x00\x00\x00m[{\"Id\":1,\"Title\":\"Case of the Laughing Goose\",\"Price\":18.9},{\"Id\":4,\"Title\":\"Wild and Wicked\",\"Price\":14.45}]"

Conclusion

You learned how to store data into session state and how to retrieve it in ASP.NET Core. You also learned how by default the data is stored in the memory of the server and how that can cause issues when running multiple instances of your application behind a load balancer. Finally, you learned how to overcome these issues by distributing your session, in this case using a Redis database to store your session data.

As a parting thought, cloud providers such as AWS, Azure, etc. have competing caching services including hosted Redis servers that make it easy to get a Redis server up and running for your caching and session needs. Happy coding!

Daniel Lawson is a Software Developer and Cloud Enthusiast. He is passionate about C# and AWS. He can be reached on Twitter or LinkedIn.