AngularJS feat. Web API – Enable Session State

Have you ever tried to use the Session object while using the Web API? If you did so, you must be dissapointed seeing that

System.Web.HttpContext.Current.Session

was always null and hence, you were unable to use it. This post is the second part of the 3 part Series about Web API feat. AngularJS. You can read the first post here, where we saw how to setup an ASP.NET MVC project to work with AngularJS. We built a GadgetStore application where user could add gadgets to a cart and then proceed with the checkout and order process. We also set that all server data used by AngularJS come from Web API Controllers and only. What we left though undone, is two basic things:

  • On a Page refresh, user looses all cart items since they are currently stored in javascript objects
  • No Authentication logic exists in GadgetStore application

This post will solve the first of the above problems, that is make sure that when the user refreshes the page doesn’t loose his cart items. In order to achieve this, we need to make our Web API Controllers support Session State. Let’s start.

Theory

Session State is not enabled by default in Web API and that’s fine cause doing so would break it’s HTTP Stateless nature. However, there are times that you need to enable Session State and there are two ways to achieve it. The first one is to enable it globally for all API Controllers which is something I wouldn’t recommend since it breaks the HTTP stateless concept for the entire Web API layer. The second one is to enable Session State for specific routes only, by using a simple trick that is move the Web API routes definitions in the same place where you define your MVC routes. There, using the MapHttpRoutemethod to define your API routes, you can set a custom HttpControllerRouteHandler to each of your routes. For those routes you want to enable Session State, the overrided GetHttpHandler method of this custom HttpControllerRouteHandler implementation, must return an instance of an HttpControllerHandler that implements the IRequiresSessionState interface.
web-api-session-01

Back to code

Download and open the starting solution from here. This is the GadgetStore application we built in the first part of these series. Create a folder name Infrastructure in the Store inside the MVC Project and add the following two classes:

public class SessionEnabledControllerHandler : HttpControllerHandler, IRequiresSessionState
    {
        public SessionEnabledControllerHandler(RouteData routeData)
            : base(routeData)
        { }
    }

 

public class SessionEnabledHttpControllerRouteHandler : HttpControllerRouteHandler
    {
        protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            return new SessionEnabledControllerHandler(requestContext.RouteData);
        }
    }

Now switch to the WebApiConfig.cs file and comment out the Web API route definition as follow:

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();

            // Moved to RouteConfig.cs to enable Session
            /*
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
             */
        }
    }

Now switch to RouteConfig where the MVC routes are defined and paste the following code:

public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            #region Web API Routes

            // Web API Session Enabled Route Configurations
            routes.MapHttpRoute(
                name: "SessionsRoute",
                routeTemplate: "api/sessions/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            ).RouteHandler = new SessionEnabledHttpControllerRouteHandler(); ;

            // Web API Stateless Route Configurations
            routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
            #endregion

            #region MVC Routes
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
            #endregion
        }
    }

You also need to add reference to System.Web.Http namespace for this to work. I made up a trick here, that allows you to use all of your controllers both as Session State enabled or not. The first Web API route (highlighted) makes the controller Session State enabled while the second one doesn’t. So if you have a controller named ItemsController and you want to use Session inside it’s actions, the all requests to this controller must be in the following form:

http://localhost:61691/api/sessions/items

On the other hand, if you want to use the default Stateless behavior, then you can send requests matching the default route:

http://localhost:61691/api/items

Now let’s see what changes we need to make in order not to loose the cart items of our AngularJS application. What we have to do, is every time the user adds or removes an item to/from the cart, save it’s state in the Session. If you remember, the javascript’s cart holds an array of items where each item declares gadget’s ammount added and it’s properties as well.

addProduct: function (id, name, price, category) {
            var addedToExistingItem = false;
            for (var i = 0; i < cartData.length; i++) {
                if (cartData[i].GadgetID == id) {
                    cartData[i].count++;
                    addedToExistingItem = true;
                    break;
                }
            }
            if (!addedToExistingItem) {
                cartData.push({
                    count: 1, GadgetID: id, Price: price, Name: name, CategoryID: category
                });
            }
        }

We need the same model at server side so for start, add the following class inside the Models folder.

public class CartItem
    {
        public int Count { get; set; }
        public int GadgetID { get; set; }
        public decimal Price { get; set; }
        public string Name { get; set; }
        public int CategoryID { get; set; }
    }

Let’s create now the API Controller that will hold the Session cart items. Add an Empty Web API Controller named TempOrdersController and paste the following code:

public class TempOrdersController : ApiController
    {
        // GET: api/TempOrders
        public List<CartItem> GetTempOrders()
        {
            List<CartItem> cartItems = null;

            if (System.Web.HttpContext.Current.Session["Cart"] != null)
            {
                cartItems = (List<CartItem>)System.Web.HttpContext.Current.Session["Cart"];
            }

            return cartItems;
        }

        // POST: api/TempOrders
        [HttpPost]
        public HttpResponseMessage SaveOrder(List<CartItem> cartItems)
        {
            if (!ModelState.IsValid)
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest);
            }

            System.Web.HttpContext.Current.Session["Cart"] = cartItems;

            return new HttpResponseMessage(HttpStatusCode.OK);
        }
    }

Now let’s see all javascript changes we need to do inside the AngularJS application. First off all, locate the cartCmp.js file and add a new function to the cart factory:

storeCart.factory('cart', function () {
    var cartData = [];

    return {
        addProduct: function (id, name, price, category) {
            var addedToExistingItem = false;
            for (var i = 0; i < cartData.length; i++) {
                if (cartData[i].GadgetID == id) {
                    cartData[i].count++;
                    addedToExistingItem = true;
                    break;
                }
            }
            if (!addedToExistingItem) {
                cartData.push({
                    count: 1, GadgetID: id, Price: price, Name: name, CategoryID: category
                });
            }
        },
        removeProduct: function (id) {
            for (var i = 0; i < cartData.length; i++) {
                if (cartData[i].GadgetID == id) {
                    cartData.splice(i, 1);
                    break;
                }
            }
        },
        getProducts: function () {
            return cartData;
        },
        pushItem: function (item) {
            cartData.push({
                count: item.Count, GadgetID: item.GadgetID, Price: item.Price, Name: item.Name, CategoryID: item.CategoryID
            });
        }
    };
});

// Code omitted

Open gadgetStore.js and for start add a new constant value which defines the TempController’s Url.

angular.module('gadgetsStore')
	.constant('gadgetsUrl', 'http://localhost:61691/api/gadgets')
	.constant('ordersUrl', 'http://localhost:61691/api/orders')
	.constant('categoriesUrl', 'http://localhost:61691/api/categories')
    .constant('tempOrdersUrl', 'http://localhost:61691/api/sessions/temporders')
	.controller('gadgetStoreCtrl', function ($scope, $http, $location, gadgetsUrl, categoriesUrl, ordersUrl, tempOrdersUrl, cart) {
// Code omitted

Notice that we chose the Session Enabled route for the TempOrdersController, so that we can use System.Web.HttpContext.Current.Session. While at the same file, add the following two scope functions:

$scope.saveOrder = function () {
	        var currentProducts = cart.getProducts();

	        $http.post(tempOrdersUrl, currentProducts)
			    .success(function (data, status, headers, config) {
			    }).error(function (error) {
			    }).finally(function () {
			    });
	    }

	    $scope.checkSessionGadgets = function () {
	        $http.get(tempOrdersUrl)
            .success(function (data) {
                if (data) {
                    for (var i = 0; i < data.length; i++) {
                        var item = data[i];
                        cart.pushItem(item);
                    }
                }
            })
            .error(function (error) {
                console.log('error checking session: ' + error);
            });
	    }

The $scope.SaveOrder will be called each time the user adds or removes an item to cart and the $scope.checkSessionGadgets is the one that called when the user reaches our application’s page. This function is the one that actually solves the page refresh problem since it initiates the user’s cart. Switch to the MVC View /Home/Views/Index.cshtml and add the ng-init directive to the body element:

<body ng-controller='gadgetStoreCtrl' class="container" ng-init="checkSessionGadgets()">

The only thing remained to do is to save cart’s state each time the user changes it. Change the $scope.addProductToCart function as follow:

$scope.addProductToCart = function (product) {
        cart.addProduct(product.GadgetID, product.Name, product.Price, product.CategoryID);
        $scope.saveOrder();
    }

Same as above for removing an item from cart:

$scope.remove = function (id) {
        cart.removeProduct(id);
        $scope.saveOrder();
    }

We also need to make sure that when the user completes his order, also removes Session’s items. Switch again to the gadgetStore.js file and change the $scope.SendOrder function as follow:

$scope.sendOrder = function (shippingDetails) {
	        var order = angular.copy(shippingDetails);
	        order.gadgets = cart.getProducts();
	        $http.post(ordersUrl, order)
			.success(function (data, status, headers, config) {
			    $scope.data.OrderLocation = headers('Location');
			    $scope.data.OrderID = data.OrderID;
			    cart.getProducts().length = 0;
			    $scope.saveOrder();
			})
			.error(function (error) {
			    $scope.data.orderError = error;
			}).finally(function () {
			    $location.path("/complete");
			});
	    }

Now you are ready to build and run your application. What I want to show you is what AngularJS sends when the user adds 3 times the same item to cart:
web-api-session-02
Now if I add another different gadget to the cart..
web-api-session-03
Try to refresh the page and ensure that your cart items arent’ lost. We are done here with configuring our AngularJS – Web API Application support page refreshes. I hope you enjoyed the post as much as I did. Of course you can download the updated version of the GadgetStore application from here. The next post of these series will show you how to secure this application, so make sure you get subscribed and get notified!

Advertisements


Categories: ASP.NET

Tags:

14 replies

  1. Works like a charm, thanks! Can’t wait for the authentication part

  2. Very nice series! Thanks

  3. This is awesome! Really awesome!

  4. Hi Chris,
    I am eagerly waiting for your authentication part.Please guide me to complete the authentication part.

  5. Chris ,
    Please show the security part..

  6. Hi, This is a great article but what if I want to disable session in my Web API. I am asking this because I am using Web APi devloped in legacy asmx service. I just added the controller and implemented my old logic there. Previously there were 5 methods for one transaction now i have one. This resulted in server CPU choking and some reads are pointing to Disable session. Please guide me in this regard. Thanks

  7. you are a king Chris! Thanks for everything

  8. Hi,

    I can’t find “RouteConfig.cs” file in my solution.

    Could you please share the project solution? I Get 404 from dropbox.

Trackbacks

  1. AngularJS feat. Web API – Security | chsakell's Blog
  2. AngularJS feat. Web API | chsakell's Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

mohitgoyal.co

Automating infrastructure one line at a time

Diary Of A Programmer

Because every day is worth noting

Chara Plessa

The purpose of this blog is to broaden my education, promote experimentation and enhance my professional development. Albert Einstein once said that “If you can’t explain it simply, you don’t understand it well enough” and I strongly believe him!

chsakell's Blog

Anything around ASP.NET MVC,WEB API, WCF, Entity Framework & AngularJS

Kumikoro

A Front End Developer's Blog

Muhammad Hassan

Full Stack Developer | ASP.NET | MVC | WebAPI | Advanced Javascript | AngularJS | Angular2 | C# | ES6 | SQL | TypeScript | HTML5 | NodeJS, MS candidate @LUMS, Grad & EX-Adjunct Faculty @NUCES-FAST, seasonal blogger & open-source contributor. Seattle, WA.

Software Engineering

Web development

IEvangelist

.NET, ASP.NET, C#, MVC, TypeScript, AngularJS

leastprivilege.com

Dominick Baier on Identity & Access Control

Happy DotNetting

In Love with Technology

Knoldus

Knols of experience to your advantage

knowshnet

Search - Read - Request - Share

Rahul's space

Learn, Share and Grow with me !

Dhananjay Kumar

Developer Evangelist @Infragistics | MVP @Microsoft

SQL Authority with Pinal Dave

SQL Server Performance Tuning Expert

Conficient Blog

Random bits of tech from @conficient

Code! Code! Code!

SOLID & KISS

Code Wala

Designing and coding

Microsoft Mentalist

A way to start with Microsoft Technologies

%d bloggers like this: