ASP.NET MVC Session state Performance Issue
I cannot recall any real Web Application that doesn’t make use of the Session State
feature, the one that is capable to store data that are available across multiple requests from the same browser. More over, in this very modern times, Web Applications tends to make extensive use of Ajax
requests to the server, in order to avoid page refreshes. So the question here is, have you ever noticed performance issues while making multiple ajax requests to an ASP.NET MVC action when using Session data?
Weeks ago I read a nice ASP.NET MVC related post which I liked but when I reached the comments area I found viewers complaining about the performance of the post’s solution project after a certain point in application’s cycle. I was currius to find out what was happening cause on the first view everything seemed to be good. So I downloaded the solution and after searching a little bit.. boom! There it was, hidding behind the Session State feature.
Put Session into the Context
Before demonstrating the real problem with coding, let’s find out the basics on how Session works. When a new request is arrived to the server for the first time, which means no session cookie
is contained, the server will create a new session identifier accessible through the:
System.Web.HttpContext.Current.Session.SessionID
Though, this does not mean that from now on all requests back to server will contain a session cookie. Instead, this will happend only if a specific request store some data in the Session. In other words, ASP.NET Framework
adds the session cookie to the response at the first time some data is stored in the session. So how is this related to performance issues? Well, normally ASP.NET can process multiple requests from the same browser concurrently which means for example, multiple Ajax requests can be processed simultaneously.
The above schema works only when no session cookie is contained in browser’s request or in other words, server hasn’t yet stored any data into the Session. If server does store some data into the session and hence adds a session cookie in the response, then all subsequent requests using the same session cookie are queued and proccessed sequentially.
This is actually a normal behavior, cause consider for example that multiple requests may modify or read the same Session key value simultaneously. That would certainly result in inconsistent data.
Code time
Create an ASP.NET Empty Web Application checking the MVC option and add the following HomeController.
HomeController.cs
[OutputCache(NoStore = true, Duration = 0)]
public class HomeController : Controller
{
public List<string> boxes = new List<string>() { "red", "green", "blue", "black", "gray", "yellow", "orange" };
// GET: Home
public ActionResult Index()
{
return View();
}
public string GetBox()
{
Random rnd = new Random();
int index = rnd.Next(0, boxes.Count);
return boxes[index];
}
public ActionResult StartSession()
{
System.Web.HttpContext.Current.Session["Name"] = "Chris";
return RedirectToAction("Index");
}
}
The controller has a GetBox which returns a random color from a list of colors defined in the class. We will use it’s value to add a div element in the view having the background-color property set accoarding the returned value. More over, has a StartSession action which simply stores a session value and hence it’s the point that the server adds a session cookie into the response. You will need to add the Index view so right click in the index method, add it and paste the following code.
Views/Home/Index.cshtml
<!DOCTYPE html>
<html lang="en" ng-app="asyncApp">
<head>
<title>Troubleshooting ASP.NET MVC Performance Issues</title>
<link rel="stylesheet" href="https://www.dotnetcodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9tYXhjZG4uYm9vdHN0cmFwY2RuLmNvbS8=bootstrap/3.3.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://www.dotnetcodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9tYXhjZG4uYm9vdHN0cmFwY2RuLmNvbS8=bootstrap/3.3.2/css/bootstrap-theme.min.css">
<script src="https://www.dotnetcodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9hamF4Lmdvb2dsZWFwaXMuY29tL2FqYXgvlibs/jquery/1.11.2/jquery.min.js"></script>
<script src="https://www.dotnetcodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9tYXhjZG4uYm9vdHN0cmFwY2RuLmNvbS8=bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script src="https://www.dotnetcodegeeks.com/wp-content/litespeed/localres/aHR0cHM6Ly9hamF4Lmdvb2dsZWFwaXMuY29tL2FqYXgvlibs/angularjs/1.3.14/angular.js"></script>
<link href="~/Content/Site.css" rel="stylesheet" />
</head>
<body ng-controller="asyncCtrl" ng-init="getBoxes()">
<nav role="navigation" class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" data-target="#navbarCollapse" data-toggle="collapse" class="navbar-toggle">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<!-- Collection of nav links and other content for toggling -->
<div id="navbarCollapse" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">Performace testing</li>
<li>
@Html.ActionLink("Start Session", "StartSession")
</li>
<li>
<a class="links" ng-click="getBoxes()">Not resolved</a>
</li>
<li>
<a class="links" ng-click="getBoxes(true)">Resolved</a>
</li>
<li>
<form class="navbar-form">
<label class="checkbox" style="margin-top:5px">
@Html.CheckBox("isSessionNewChk", Session.IsNewSession, new { @disabled = "disabled" })
Is Session New
</label>
</form>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li>{{boxes.length}} Boxes</li>
</ul>
</div>
</div>
</nav>
<br /><br /><br />
<div class="container">
<div class="row">
<div id="boxesContainer" ng-repeat="color in boxes track by $index">
<div class="box" ng-class="color" />
</div>
</div>
<br />
<div class="row">
<div id="timeOccured" ng-show="showResults" class="alert" ng-class="isResolved()" ng-bind="timeElapsed"></div>
</div>
</div>
<script src="~/Scripts/app.js"></script>
</body>
</html>
Add a Content folder in the root of your application and create the following css stylesheet.
Content/Site.css
.box {
height: 15px;
width: 15px;
margin: 2px;
float:left;
}
#timeOccured {
clear:both;
}
.links {
cursor:pointer;
}
.red {
background-color: red;
}
.green {
background-color: green;
}
.blue {
background-color: blue;
}
.black {
background-color: black;
}
.gray {
background-color: gray;
}
.yellow {
background-color: yellow;
}
.orange {
background-color: orange;
}
You will have noticed that I make use of simple AngularJS code but that’s OK if you aren’t familiar with it. Add a Scripts folder and create the following app.js javascript file.
Scripts/app.js
angular.module('asyncApp', [])
.value('mvcuri', 'http://localhost:49588/home/getbox')
.value('mvcurisessionresolved', 'http://localhost:49588/SessionResolved/getbox')
.controller('asyncCtrl', function ($http, $scope, mvcuri, mvcurisessionresolved) {
$scope.boxes = [];
$scope.showResults = false;
var uri;
$scope.getBoxes = function (resolved) {
var start = new Date();
var counter = 300;
if (resolved)
uri = mvcurisessionresolved;
else
uri = mvcuri;
// Init variables
$scope.boxes = [];
$scope.showResults = false;
$scope.timeElapsed = '';
for (var i = 0; i < 300; i++) {
$http.get(uri)
.success(function (data, status, headers, config) {
$scope.boxes.push(data);
counter--;
if (counter == 0) {
var time = new Date().getTime() - start.getTime();
$scope.timeElapsed = 'Time elapsed (ms): ' + time;
$scope.showResults = true;
}
})
.error(function (error) {
$scope.timeElapsed = error.Message;
}).finally(function () {
});
}
};
$scope.isResolved = function () {
return uri == mvcuri ? 'alert-danger' : 'alert-success';
}
});
Make sure you replace the localhost:port with yours. Before explain all application’s functionality let’s add a new MVC controller named SessionResolvedController.
SessionResolvedController.cs
[OutputCache(NoStore = true, Duration = 0)]
public class SessionResolvedController : Controller
{
public List<string> boxes = new List<string>() { "red", "green", "blue", "black", "gray", "yellow", "orange" };
public string GetBox()
{
Random rnd = new Random();
int index = rnd.Next(0, boxes.Count);
return boxes[index];
}
}
At this point you should be able to fire your application.
So what is happening here? When the application starts, 300 Ajax calls are sent to the HomeController’s
GetBox action and foreach color returned a respective box is added to the view.
Sample code from Index.cshtml
<body ng-controller="asyncCtrl" ng-init="getBoxes()">
<div id="boxesContainer" ng-repeat="color in boxes track by $index">
<div class="box" ng-class="color" />
</div>
Every time you refresh the Page you will notice that the Is Session New checkbox is checked, which means that there isn’t yet a session cookie sent to the browser. All Ajax requests are processed concurrently as expected and the time elapsed to complete all the ajax calls is displayed in a red box under all boxes. The Start Session button will simply store a session value as follow.
System.Web.HttpContext.Current.Session["Name"] = "Chris";
Press the button and notice the difference in the elapsed time for displaying all the 300 boxes. Also notice that the checkbox isn’t checked anymore.
Now that there is a Session cookie in each request to the server, all requests are proccessed sequentially as described previously. That’s why we need twice the time we needed before storing a session variable. You can repeat the same ajax requests without refreshing the page by clicking the Not Resolved button.
Solution
At the moment, if you press the Resolved button you will get the same results calling the SessionResolvedController’s GetBox() action.
<a class="links" ng-click="getBoxes(true)">Resolved</a>
Notice that we don’t read or modify any Session state data when calling the GetBox() actions, so the question is, is there anything that we can do to prevent our requests being processed sequentially despite the fact that they contain a cookie session? The answer is YES and it’s hidding in the SessionState attribute that can be applied in the controller’s class as follow:
SessionResolvedController.cs
[SessionState(SessionStateBehavior.Disabled)]
public class SessionResolvedController : Controller
{
public List<string> boxes = new List<string>() { "red", "green", "blue", "black", "gray", "yellow", "orange" };
public string GetBox()
{
Random rnd = new Random();
int index = rnd.Next(0, boxes.Count);
return boxes[index];
}
}
Applying this attribute to the controller will make all requests targeting this controller processed concurrently. Build, run your application and repeat what we did before. Notice that the Resolved button now finishes at the half time which was the time needed when no session cookie existed in the request.
I run the application without debugging in order to get better results. If you run with debugging you will get higher delays but that should not be a problem, the differences will be still noticeable.
And following is a complete demonstration of what we have done till now. When I fired the application for the first time, it tooked 928 milliseconds to complete all 300 Ajax requests. Then I clicked to create a Session cookie and that caused an important performance issue increasing the delay to 2201 milliseconds! When I requested the data through the SessionResolvedController the delay was almost the same when there wasn’t any Session cookie.
I have also used the:
[OutputCache(NoStore = true, Duration = 0)]
to the MVC Controllers which will result to ask the browser (client) not to store the content retrieved from the server. This way the test is even more reliable.
Food for thought
So is this feature solves all our problems? No. If you apply this attribute you won’t be able to access any Session data in the respective controller, which means if you try so you will get a big fat Null reference exception.
I would recommend to use a Web API layer for all Ajax requests and leave MVC Controllers as are. The problem here has architectural design nature, not technical. ASP.NET MVC framework is smart enough to understand if requests must be processed concurrently or sequentially. Use Session state data when rendering a View or posting something to an MVC Controller’s action and make your ajax requests to a Web API layer.
That’s it, we are done describing a common perfomance issue using the Session state feature. I hope you enjoyed the post.
- You can download the project from here.
Reference: | ASP.NET MVC Session state Performance Issue from our NCG partner Christos Sakellarios at the chsakell’s Blog blog. |