Session State In ASP.NET Core


Web applications work on HTTP protocol and HTTP is a stateless protocol. Every HTTP request is treated as an independent request. The Server does not have knowledge about the variable values, which are being used in the previous request.

Session is a feature in ASP.NET Core that enables us to save/store the user data. Session stores the data in the dictionary on the Server and SessionId is used as a key. The SessionId is stored on the client at cookie. The SessionId cookie is sent with every request. The SessionId cookie is per browser and it cannot be shared between the browsers. There is no timeout specified for SessionId cookie and they are deleted when the Browser session ends.

At the Server end, session is retained for a limited time. The default session timeout at the Server is 20 minutes but it is configurable.

Sessions are of two types, namely In-Proc or In-memory and Out-Proc or Distributed session. If our session is in-memory and our application is hosted on Web-Farm environment, we need to use sticky sessions to tie each session to a specific Server whereas an Out-Proc session does not require sticky sessions and they are the most preferred way to use sessions in our application.

Microsoft.AspNetCore.Session package provides middleware to manage the sessions in ASP.NET Core. To use session in our Application, we need to add this package as a dependency in project.json file.

The next step is to configure session in Startup class. We need to call “AddSession” method in ConfigureServices method of startup class. The “AddSession” method has one overload method, which accepts various session options such as Idle Timeout, Cookie Name and Cookie Domain etc. If we do not pass the session options, the system will take the default options. Now, we need to call “UseSession” method in Configure method of startup class. This method enables the session for the Application. Startup.cs

  1. using System;  
  2. using Microsoft.AspNetCore.Builder;  
  3. using Microsoft.AspNetCore.Hosting;  
  4. using Microsoft.AspNetCore.Http;  
  5. using Microsoft.Extensions.DependencyInjection;  
  6.   
  7. namespace WebApplication {  
  8.     public class Startup {  
  9.         public void Configure(IApplicationBuilder app)  
  10.         {  
  11.             app.UseSession();  
  12.             app.UseMvc();  
  13.             app.Run(context => {  
  14.                 return context.Response.WriteAsync(“Hello Readers!”);  
  15.             });  
  16.         }  
  17.   
  18.         public void ConfigureServices(IServiceCollection services)  
  19.         {  
  20.             services.AddMvc();  
  21.             services.AddSession(options => {   
  22.                 options.IdleTimeout = TimeSpan.FromMinutes(30);   
  23.             });  
  24.         }       
  25.     }  
  26. }  

It is important to call “UseSession” method before the “UseMvc” method in Configure method of startup class. If we call “UseMvc” method before “UseSession” method, the system will throw an exception. 

How to access the session

We can use session from HttpContext, once it is installed and configured. To use session in controller class, we need to reference “Microsoft.AspNet.Http” in controller. There are three methods that enables us to set the session value, which are Set, SetInt32 and SetString. The “Set” method accepts byte array as an argument. The SetInt32 and SetString method are the extension methods of Set and they internally cast byte array to int and string respectively. Same as there are three methods that  are used to retrieve the value from the session: Get, GetInt32 and GetString. The Get method returns byte of arrays. The main reason behind storing bytes array is to make sure that session values are serializable for the storage on remote Servers. Apart from int and string, we need to serialize to byte array to store it in session. Example

In the example given below, I have set my name into session in first request and retrieved the session value in another request.

  1. using Microsoft.AspNetCore.Http;  
  2. using Microsoft.AspNetCore.Mvc;  
  3.   
  4. public class HomeController : Controller  
  5. {  
  6.   
  7.     [Route(“home/index”)]  
  8.     public IActionResult Index()  
  9.     {  
  10.         HttpContext.Session.se.SetString(“name”,”Jignesh Trivedi”);  
  11.         return View();  
  12.     }  
  13.     [Route(“home/GetSessionData”)]  
  14.     public IActionResult GetSessionData()  
  15.     {  
  16.         ViewBag.data = HttpContext.Session.GetString(“name”);;  
  17.         return View();  
  18.     }  
  19. }  

Output

Custom Session Extension methods

As discussed earlier, there are two extension methods to get the data from session and set the data to session are available, namely GetInt32 and GetString, SetInt32 and SetString. Similarly, we can add our custom extension methods to get and set the value in session.

In the example given below, I have created an extension method to set double value to session and get double value from session.

  1. using System;  
  2. using Microsoft.AspNetCore.Http;  
  3. public static class SessionExtensions  
  4. {  
  5.     public static double? GetDouble(this ISession session, string key)  
  6.     {  
  7.         var data = session.Get(key);  
  8.         if (data == null)  
  9.         {  
  10.             return null;  
  11.         }  
  12.         return BitConverter.ToDouble(data, 0);  
  13.     }   
  14.   
  15.     public static void SetDouble(this ISession session, string key, double value)  
  16.     {  
  17.         session.Set(key, BitConverter.GetBytes(value));  
  18.     }  
  19. }  

Usage of the extension method

  1. using Microsoft.AspNetCore.Http;  
  2. using Microsoft.AspNetCore.Mvc;  
  3. public class HomeController : Controller  
  4. {  
  5.   
  6.     [Route(“home/index”)]  
  7.     public IActionResult Index()  
  8.     {  
  9.         HttpContext.Session.SetString(“name”,”Jignesh Trivedi”);  
  10.         HttpContext.Session.SetDouble(“Percentage”,75.56);  
  11.         return View();  
  12.     }  
  13.     [Route(“home/GetSessionData”)]  
  14.     public IActionResult GetSessionData()  
  15.     {  
  16.         ViewBag.data = HttpContext.Session.GetString(“name”);  
  17.         ViewBag.Percent = HttpContext.Session.GetDouble(“Percentage”);  
  18.         return View();  
  19.     }  
  20. }  

Output

Store Complex Data in to Session

As we are aware, session is able to store only byte of an array. Compared to the previous version, ASP.NET Core does not perform any operation such as serialization/ de-serialization on the values stored in session. Here, I am converting the complex object into JSON and store it as a string. Later, I am retrieving it as a string and de-serialize to original object.

Thus, I have written the extension method for set and get complex object to session.

  1. using System;  
  2. using Microsoft.AspNetCore.Http;  
  3. using Newtonsoft.Json;  
  4.   
  5. public static class SessionExtensions  
  6. {  
  7.     public static T GetComplexData<T>(this ISession session, string key)  
  8.     {  
  9.         var data = session.GetString(key);  
  10.         if (data == null)  
  11.         {  
  12.             return default(T);  
  13.         }  
  14.         return JsonConvert.DeserializeObject<T>(data);  
  15.     }   
  16.   
  17.     public static void SetComplexData(this ISession session, string key, object value)  
  18.     {  
  19.         session.SetString(key, JsonConvert.SerializeObject(value));  
  20.     }  
  21. }  

Usage of the extension method

In the following example, I have created one public class and within controller action method, created the instance of the class, stored some data and sent it for storing in session. Similarly, I created one action method to retrieve the complex data from session.

  1. public class User   
  2. {  
  3.     public string Name { getset; }  
  4.     public double Percentage { getset; }  
  5. }  

Action method

  1. [Route(“home/SetComplexData”)]  
  2. public IActionResult SetComplexData()  
  3. {  
  4.     User user = new User();  
  5.     user.Name = “Jignesh Trivedi”;  
  6.     user.Percentage = 75.45;             
  7.       
  8.     HttpContext.Session.SetComplexData(“UserData”, user);  
  9.     return View(“index”);  
  10. }  
  11. [Route(“home/GetComplexData”)]  
  12. public IActionResult GetComplexData()  
  13. {  
  14.     ViewBag.data = HttpContext.Session.GetComplexData<User>(“UserData”);  
  15.     return View();  
  16. }  

Summary

The main advantages of ASP.NET Core is that it supports modularity. It means that we need to add the module, as we wish to use it in our application. The usage of session is slightly different than the classic ASP.NET Application. This article helps us to understand how to install, configure and use session with ASP.NET Core.


State management

State can be stored using several approaches. Each approach is described later in this topic.

Storage approachStorage mechanism
CookiesHTTP cookies. May include data stored using server-side app code.
Session stateHTTP cookies and server-side app code
TempDataHTTP cookies or session state
Query stringsHTTP query strings
Hidden fieldsHTTP form fields
HttpContext.ItemsServer-side app code
CacheServer-side app code

Cookies store data across requests. Because cookies are sent with every request, their size should be kept to a minimum. Ideally, only an identifier should be stored in a cookie with the data stored by the app. Most browsers restrict cookie size to 4096 bytes. Only a limited number of cookies are available for each domain.

Cookies are often used for personalization, where content is customized for a known user. The user is only identified and not authenticated in most cases. The cookie can store the user’s name, account name, or unique user ID such as a GUID. The cookie can be used to access the user’s personalized settings, such as their preferred website background color.

The session data is backed by a cache and considered ephemeral data. 

ASP.NET Core maintains session state by providing a cookie to the client that contains a session ID. The cookie session ID:

  • Is sent to the app with each request.
  • Is used by the app to fetch the session data.

Session state exhibits the following behaviors:

  • The session cookie is specific to the browser. Sessions aren’t shared across browsers.
  • Session cookies are deleted when the browser session ends.
  • If a cookie is received for an expired session, a new session is created that uses the same session cookie.
  • Empty sessions aren’t retained. The session must have at least one value set to persist the session across requests. When a session isn’t retained, a new session ID is generated for each new request.
  • The app retains a session for a limited time after the last request. The app either sets the session timeout or uses the default value of 20 minutes. Session state is ideal for storing user data:
    • That’s specific to a particular session.
    • Where the data doesn’t require permanent storage across sessions.
  • Session data is deleted either when the ISession.Clear implementation is called or when the session expires.
  • There’s no default mechanism to inform app code that a client browser has been closed or when the session cookie is deleted or expired on the client.
  • Session state cookies aren’t marked essential by default. Session state isn’t functional unless tracking is permitted by the site visitor. For more information, see General Data Protection Regulation (GDPR) support in ASP.NET Core.

 Warning

Don’t store sensitive data in session state. The user might not close the browser and clear the session cookie. Some browsers maintain valid session cookies across browser windows. A session might not be restricted to a single user. The next user might continue to browse the app with the same session cookie.

The in-memory cache provider stores session data in the memory of the server where the app resides. In a server farm scenario:

Configure session state

The Microsoft.AspNetCore.Session package:

  • Is included implicitly by the framework.
  • Provides middleware for managing session state.

To enable the session middleware, Startup must contain:

Distributed caching in ASP.NET Core

A distributed cache is a cache shared by multiple app servers, typically maintained as an external service to the app servers that access it. A distributed cache can improve the performance and scalability of an ASP.NET Core app, especially when the app is hosted by a cloud service or a server farm.

A distributed cache has several advantages over other caching scenarios where cached data is stored on individual app servers.

When cached data is distributed, the data:

  • Is coherent (consistent) across requests to multiple servers.
  • Survives server restarts and app deployments.
  • Doesn’t use local memory.

Distributed cache configuration is implementation specific. This article describes how to configure SQL Server and Redis distributed caches. Third party implementations are also available, such as NCache (NCache on GitHub). Regardless of which implementation is selected, the app interacts with the cache using the IDistributedCache interface.

Prerequisites

To use a SQL Server distributed cache, add a package reference to the Microsoft.Extensions.Caching.SqlServer package.

To use a Redis distributed cache, add a package reference to the Microsoft.Extensions.Caching.StackExchangeRedis package.

To use NCache distributed cache, add a package reference to the NCache.Microsoft.Extensions.Caching.OpenSource package.

IDistributedCache interface

The IDistributedCache interface provides the following methods to manipulate items in the distributed cache implementation:

  • GetGetAsync: Accepts a string key and retrieves a cached item as a byte[] array if found in the cache.
  • SetSetAsync: Adds an item (as byte[] array) to the cache using a string key.
  • RefreshRefreshAsync: Refreshes an item in the cache based on its key, resetting its sliding expiration timeout (if any).
  • RemoveRemoveAsync: Removes a cache item based on its string key.

Establish distributed caching services

Register an implementation of IDistributedCache in Startup.ConfigureServices. Framework-provided implementations described in this topic include:

Distributed Memory Cache

The Distributed Memory Cache (AddDistributedMemoryCache) is a framework-provided implementation of IDistributedCache that stores items in memory. The Distributed Memory Cache isn’t an actual distributed cache. Cached items are stored by the app instance on the server where the app is running.

The Distributed Memory Cache is a useful implementation:

  • In development and testing scenarios.
  • When a single server is used in production and memory consumption isn’t an issue. Implementing the Distributed Memory Cache abstracts cached data storage. It allows for implementing a true distributed caching solution in the future if multiple nodes or fault tolerance become necessary.

The sample app makes use of the Distributed Memory Cache when the app is run in the Development environment in Startup.ConfigureServices:C#Copy

services.AddDistributedMemoryCache();

Distributed SQL Server Cache

The Distributed SQL Server Cache implementation (AddDistributedSqlServerCache) allows the distributed cache to use a SQL Server database as its backing store. To create a SQL Server cached item table in a SQL Server instance, you can use the sql-cache tool. The tool creates a table with the name and schema that you specify.

Create a table in SQL Server by running the sql-cache create command. Provide the SQL Server instance (Data Source), database (Initial Catalog), schema (for example, dbo), and table name (for example, TestCache):.NET Core CLICopy

dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache

A message is logged to indicate that the tool was successful:ConsoleCopy

Table and index were created successfully.

The table created by the sql-cache tool has the following schema:

SqlServer Cache Table

 Note

An app should manipulate cache values using an instance of IDistributedCache, not a SqlServerCache.

The sample app implements SqlServerCache in a non-Development environment in Startup.ConfigureServices:C#Copy

services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = 
        _config["DistCache_ConnectionString"];
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});

 Note

ConnectionString (and optionally, SchemaName and TableName) are typically stored outside of source control (for example, stored by the Secret Manager or in appsettings.json/appsettings.{ENVIRONMENT}.json files). The connection string may contain credentials that should be kept out of source control systems.

Distributed Redis Cache

Redis is an open source in-memory data store, which is often used as a distributed cache. You can configure an Azure Redis Cache for an Azure-hosted ASP.NET Core app, and use an Azure Redis Cache for local development.

An app configures the cache implementation using a RedisCache instance (AddStackExchangeRedisCache).

For more information, see Azure Cache for Redis.

See this GitHub issue for a discussion on alternative approaches to a local Redis cache.

Distributed NCache Cache

NCache is an open source in-memory distributed cache developed natively in .NET and .NET Core. NCache works both locally and configured as a distributed cache cluster for an ASP.NET Core app running in Azure or on other hosting platforms.

To install and configure NCache on your local machine, see NCache Getting Started Guide for Windows.

To configure NCache:

  1. Install NCache open source NuGet.
  2. Configure the cache cluster in client.ncconf.
  3. Add the following code to Startup.ConfigureServices:C#Copyservices.AddNCacheDistributedCache(configuration => { configuration.CacheName = "demoClusteredCache"; configuration.EnableLogs = true; configuration.ExceptionsEnabled = true; });

Use the distributed cache

To use the IDistributedCache interface, request an instance of IDistributedCache from any constructor in the app. The instance is provided by dependency injection (DI).

When the sample app starts, IDistributedCache is injected into Startup.Configure. The current time is cached using IHostApplicationLifetime (for more information, see Generic Host: IHostApplicationLifetime):C#Copy

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
    IHostApplicationLifetime lifetime, IDistributedCache cache)
{
    lifetime.ApplicationStarted.Register(() =>
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        cache.Set("cachedTimeUTC", encodedCurrentTimeUTC, options);
    });

The sample app injects IDistributedCache into the IndexModel for use by the Index page.

Each time the Index page is loaded, the cache is checked for the cached time in OnGetAsync. If the cached time hasn’t expired, the time is displayed. If 20 seconds have elapsed since the last time the cached time was accessed (the last time this page was loaded), the page displays Cached Time Expired.

Immediately update the cached time to the current time by selecting the Reset Cached Time button. The button triggers the OnPostResetCachedTime handler method.C#Copy

public class IndexModel : PageModel
{
    private readonly IDistributedCache _cache;

    public IndexModel(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string CachedTimeUTC { get; set; }

    public async Task OnGetAsync()
    {
        CachedTimeUTC = "Cached Time Expired";
        var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC");

        if (encodedCachedTimeUTC != null)
        {
            CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC);
        }
    }

    public async Task<IActionResult> OnPostResetCachedTime()
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));
        await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options);

        return RedirectToPage();
    }
}

 Note

There’s no need to use a Singleton or Scoped lifetime for IDistributedCache instances (at least for the built-in implementations).

You can also create an IDistributedCache instance wherever you might need one instead of using DI, but creating an instance in code can make your code harder to test and violates the Explicit Dependencies Principle.

Recommendations

When deciding which implementation of IDistributedCache is best for your app, consider the following:

  • Existing infrastructure
  • Performance requirements
  • Cost
  • Team experience

Caching solutions usually rely on in-memory storage to provide fast retrieval of cached data, but memory is a limited resource and costly to expand. Only store commonly used data in a cache.

Generally, a Redis cache provides higher throughput and lower latency than a SQL Server cache. However, benchmarking is usually required to determine the performance characteristics of caching strategies.

When SQL Server is used as a distributed cache backing store, use of the same database for the cache and the app’s ordinary data storage and retrieval can negatively impact the performance of both. We recommend using a dedicated SQL Server instance for the distributed cache backing store.