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.
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:
Now if I add another different gadget to the cart..
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!