Using Sessions with ASP.NET Core

The web is a stateless platform.  To be more precise, HTTP – the protocol that powers the web – is stateless.  This can pose some issues for applications that need to maintain data across several page views.  But there are several workarounds for this shortcoming.  First is to store everything in a database.  This certainly works, but it can be overkill for simple key-value storage.  There is also the client-side solution of cookies.  But cookies are insecure and sensitive information stored potentially could be hijacked by malicious code.  There is a solution in between and that is the HTTP session.  A session stores an anonymous ID in a cookie.  It reveals no sensitive information and is useful only to the application that created it.  So even if it is hijacked, it is useless.  All of the interesting data associated with the ID is stored on the server.  By accessing the session data with the ID, applications can persist simple data across page views.  Session storage is meant for short term data.  For persistence, consider a database.  This post will discuss session handling from the ASP.NET Core platform.  But just about all modern web platforms in any language will support something similar.  This post also approaches ASP.NET Core from a cross-platform toolset of the .NET Core SDK (namely the dotnet tool) and Visual Studio Code (with the C# extension installed).  The concepts will be consistent across Windows, macOS and Linux and tools including Visual Studio.  The code will be almost identical.

I’m going to start off by creating a new .NET Core application using the empty ASP.NET Core web application template:

> dotnet new web -o Welcome

This will generate the simplest of ASP.NET Core web apps that merely displays ‘Hello World’ in the broswer.  Before proceeding, I’ll add MVC support to the app as the web template does not add it.  In Visual Studio Code, I’ll open the Startup.cs file and find the ConfigureServices method.  Then add the following statement:

Next, in the same file, find the Configure method.  Replace the call to app.Run with the following:

This code will be the same as defining a route with a default controller called Home, a default action called Index and an optional id.  Next I need to create those resources in the project.  In the Welcome folder (the root of the project) I’ll create a new folder named Controllers and a file inside called HomeController.cs with the follow code:

This defines an action method Index that returns a default view.  So I need to create that too.  In the project root, I’ll create a Views folder with a Home subfolder.  This all follows MVC convention.  In the Home folder I’ll add the Index.cshtml file.

Now to get the session code added.  Back in Startup.cs add this line, before the call to AddMvc in the ConfigureServices method:

And this code to the Configure method, before the call to UseMvcWithDefaultRoute:

And that’s all to add sessions to the application.  To actually use the session will need a bit more code.  First I’ll start by adding two new action methods to the HomeController class:

In the SeeGreeting method I’ll access the Session via the HttpContext.  Then I’ll get the value for the greeting key with the GetString method.  This will require adding the namespace Microsoft.AspNetCore.Http to the top of the file:

This will retrieve the value of the session key greeting, if it exists.  Otherwise it will return null.  So I’ll need to check to make sure it is not null:

Now I’ll need to create view files for each of these outcomes.  In the Views/Home folder, create new files for NoGreeting.cshtml and Get.cshtml.  Here is the code for NoGreeting.cshtml.  It’s just static text:

And the Get.cshtml file:

The SetGreeting action method will need a form in the view file:

And now for the action method.  I need two implementations of SetGreeting.  The first will handle the GET request that will show the form.  The second will handle the POST request that will process the form.  The message text from the form will be sent to the POST method as a parameter.  Then inside of the method, the SetString method is used to assign the message to the greeting key.  Notice the use of the [HttpGet] and [HttpPost] attributes to map HTTP verbs to method implementations.

With this, the application can be run.  Press F5 (in Visual Studio Code) to build and start the application and launch the default browser.  Then navigate to /Home/SeeGreeting.  The greeting key has not been set at this point so GetString will return null and a message explaining that will be displayed.

Click on the link to go to /Home/SetGreeting.  Put a greeting in the box and click the Set button to store the message in the session.

This will take you back to /Home/SeeGreeting.  This time, the greeting key is set so it is retrieved and displayed.

That proves it works.  But that is also the simplest case.  In real life, we have complex data types.  Not everything is stored as a string.  Or is it?  This is where JSON comes in useful.  If you’ve worked with the web at all, you’ve likely seen JSON.  Using the Newtonsoft.Json library for .NET Core, almost any object can be represented as JSON, which is a string, and then stored in the session.  First, I’ll install the required package at the command line:

> dotnet add package Newtonsoft.Json

Visual Studio Code will prompt to restore the packages.  Now I need an object to store.  I’ll create a new Models folder in the project root and give it a file Greeter.cs:

This will be the object I will convert to JSON and store in the session.  Next I need to modify the Set.cshtml form to accommodate the new properties:

And now I’ll modify the POST implementation of the SetGreeting method.  Notice that it now accepts a Greeter.  Since the form that POSTs to this method contains elements with the same names as the properties in the Greeter, ASP.NET Core will package the form values up into a Greeter and pass that to the SetGreeting method.  Then I can convert that Greeter to JSON and store the string in the session.  This also requires the namespace Newtonsoft.Json.

Now in the GetSetting method, if the greeting key is set, I’ll call the DeserializeObject method to convert the JSON string to a Greeter.  The DeserializeObject method takes a generic parameter which is the type of the target object, Greeter in this case.  After that, I do a simple test on the current DateTime to determine which message in the greeter to display.

It’s coming along!  But we can do better.  Rather than calling the JsonConvert methods all of the time, we can put these into extension methods on the ISession interface that the Session implements.  This is at the recommendation of the documentation on Microsoft’s site.  I’ll put these in a new file SessionExtensions.cs in a new folder Extensions.

This class merely abstracts the JSON converting code into two extension methods that work with any type.  The action methods in the controller have to be changed as well.  In SeeGreeting, the Greeter can be retrieved directly from the Session thus eliminating the step of explicitly calling JsonConvert.  Note if the greeting key is still not set, the result will still be null.  The Get extension method returns default(T) for keys that have not been set.  For Greeter, the default value is null.

In SetGreeting, the Greeter can be set directly on the Session object.

Now we’re cooking!  But there is one more enhancement to really make this code sparkle.  It’s likely that there will only be one Greeter in the Session.  In that case, we can use ASP.NET Core built-in dependency injection to automatically retrieve it for us.  This requires a bit of setup, but in the right place, it’s well worth the trouble.

To get the dependency injection set up, go to the ConfigureServices method in the Startup.cs class.  Here is the code to add:

A common way to register a type with the dependency injection container, is to specify an interface and an implementation of that interface to use when the interface is found in code.  In this case, I am only specifying a type, Greeter.  The lambda expression takes an IServiceProvider and uses it to get a Greeter instance from SessionVariables.GetGreeter.  The instance returned will be converted from the JSON stored in the Session.  The IServiceProvider is used to get an implementation of IHttpContextAccessor through the GetRequiredService method.  This IHttpContextAccessor is used to get at the HttpContext from outside of a controller.  From there I can get the Session and code as before.  Here is the source for the SessionVariables static class:

For the GetRequiredService method to succeed, I have to register the IHttpContextAccessor with the dependency injection container.  The implementation I will use is HttpContextAccessor provided by the .NET Core SDK.  At the top of ConfigureServices in Startup.cs:

Note that this is a singleton because the context will remain the same.  And there needs to be a small change to the HomeController.  I’ll add a private variable of type Greeter and use constructor injection to initialize it.  Then I can remove the call to the Get extension method in the SeeGreeting method and use the class variable instead.

The code to store the Greeter is still the same.

Now to tie this all together I’ll create a series of pages and access the Greeter from all of them.  First, I’ll create a Shared folder and a _Layout.cshtml file.

I’ve added a row of links to the top of the layout that will appear on every page.  To make the view files fit in this layout, I need to add a _ViewStart.cshtml file in the Views folder and set the Layout property:

Now I need the views, they are just like the Get.cshtml file except they have text that says ‘Contact’ or ‘About’ to prove they are separate pages.  And then in the controller, I’ll be reusing the code to get the message in three action methods so I’ll refactor it to a private method:

And the three action methods are reduced to:

Now when you run the application, you can set a new greeting and see that it shows up on all of the pages.  Of course, what I’ve shown is a simple example, but it can easily be extended to more complex scenarios.  I was aiming for clarity by sacrificing features.  The main thing to remember is that sessions do require cookies and that it should be used for simple values stored for short periods of time.  Longer storage requires a database server.

The code for this post can be found on Github.

Leave a Reply