Introduction
ASP.NET Web API and SignalR are something I was interested in since they were introduced but I never had a chance to play with. I made up one use case that these technologies can help and wrote a simple web app with popular AngularJS framework that manages customer complaints. Using this web app, you can search complaints from a customer, then add a new complaint, edit or delete one. Moreover, in case people are seeing complaints from the same customer, their browsers will be in sync while anyone is adding or deleting complaints.
Background
Couple of years ago, I wrote a web app called “self-service” for a telemetry unit that shows all information in categorized tabs. One of its tabs was “Schedules” that shows scheduled tasks for the unit and as there was no clue about when each schedule will be completed and disappear from the database, I had to reluctantly do periodic Ajax polling, say every 30 second. One guy on Stackoverflow assured me that SignalR could be a solution. Fast forward to now.
Using the Code
Assume that a database has a table CUSTOMER_COMPLAINTS
that will hold complaints from customers and the web app will be used to manage contents of this table.
Before starting the project, used environment is:
- Visual Studio 2013 Premium
- Web API 2
- SignalR 2.1
- EntityFramework 6
- AngularJS 1.3
First, create a new project WebApiAngularWithPushNoti
with empty template and Web API ticked. ASP.NET Web API can be used independently without MVC framework to provide RESTful services to wide range of clients based on HTTP.
Right-click the project and add a new data entity for CUSTOMER_COMPLAINTS
table as below:
This step will install EntityFramework 6 package into the project and Visual Studio will ask for connection to the database and model name to create, ModelComplaints
for this project. Confirm that EntityFramework generated a class CUSTOMER_COMPLAINTS
under ModelComplaints.tt. This is your model class that will be used to create an ApiController
.
public partial class CUSTOMER_COMPLAINTS
{
public int COMPLAINT_ID { get; set; }
public string CUSTOMER_ID { get; set; }
public string DESCRIPTION { get; set; }
}
Right click Controllers folder, Add | New Scaffolded Item as below.
Now ComplaintsController.cs is in place under Controllers folder and confirm that ASP.NET scaffolding automatically generated C# codes for CRUD operation with CUSTOMER_COMPLAINTS
model as below.
namespace WebApiAungularWithPushNoti.Controllers
{
public class ComplaintsController : ApiController
{
private MyEntities db = new MyEntities();
public IQueryable<CUSTOMER_COMPLAINTS> GetCUSTOMER_COMPLAINTS()
{
return db.CUSTOMER_COMPLAINTS;
}
[ResponseType(typeof(CUSTOMER_COMPLAINTS))]
public IHttpActionResult GetCUSTOMER_COMPLAINTS(int id)
{
CUSTOMER_COMPLAINTS cUSTOMER_COMPLAINTS = db.CUSTOMER_COMPLAINTS.Find(id);
if (cUSTOMER_COMPLAINTS == null)
{
return NotFound();
}
return Ok(cUSTOMER_COMPLAINTS);
}
To get ready for SignalR, create a new folder Hubs and right-click it, Add | SignalR Hub Class (v2). If you don’t see SignalR Hub Class (v2) on pop up menu, it can be found in Add New Item screen under Visual C#, Web, SignalR category. This step will install SignalR package into the project and add several JavaScript files under Scripts folder in addition to MyHub.cs under Hubs folder.
Open MyHub.cs and replace contents with the following codes. Note that Subscribe()
method is to be called from JavaScript on client browser when user searches a certain customer id so that user starts to get real time notifications about the customer. Similarly Unsubscribe()
method is to stop getting notifications from the given customer.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;
namespace WebApiAungularWithPushNoti.Hubs
{
public class MyHub : Hub
{
public void Subscribe(string customerId)
{
Groups.Add(Context.ConnectionId, customerId);
}
public void Unsubscribe(string customerId)
{
Groups.Remove(Context.ConnectionId, customerId);
}
}
}
Right-click the project, Add | OWIN Startup Class (or can be found in Add New Item screen under Visual C#, Web category), name it Startup.cs, replace contents with the following codes.
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(WebApiAungularWithPushNoti.Startup))]
namespace WebApiAungularWithPushNoti
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
Right-click the project, add a new HTML page index.html. Right-click it, Set as Start Page. Open index.html and place the following codes. Note that we are using a pure HTML page with some Angular directives and there is no @Html.xxx if you are from MVC. Also, script file versions should be matched with actual files that you have got when you added SignalR.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Customer Complaints</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
</head>
<body ng-app="myApp" ng-controller="myCtrl" ng-cloak>
<div>
<h2>Search customer complaints</h2>
<input type="text" ng-model="customerId"
size="10" placeholder="Customer ID" />
<input type="button" value="Search"
ng-click="getAllFromCustomer();" />
<p ng-show="errorToSearch">{{errorToSearch}}</p>
</div>
<div ng-show="toShow()">
<table>
<thead>
<th>Complaint id</th>
<th>Description</th>
</thead>
<tbody>
<tr ng-repeat="complaint in complaints | orderBy:orderProp">
<td>{{complaint.COMPLAINT_ID}}</td>
<td>{{complaint.DESCRIPTION}}</td>
<td><button ng-click="editIt
(complaint)">Edit</button></td>
<td><button ng-click="deleteOne
(complaint)">Delete</button></td>
</tr>
</tbody>
</table>
</div>
<div>
<h2>Add complaint</h2>
<input type="text" ng-model="descToAdd"
size="40" placeholder="Description" />
<input type="button" value="Add" ng-click="postOne();" />
<p ng-show="errorToAdd">{{errorToAdd}}</p>
</div>
<div>
<h2>Edit complaint</h2>
<p>Complaint id: {{idToUpdate}}</p>
<input type="text" ng-model="descToUpdate"
size="40" placeholder="Description" />
<input type="button" value="Save" ng-click="putOne();" />
<p ng-show="errorToUpdate">{{errorToAdd}}</p>
</div>
<script src="Scripts/jquery-1.10.2.min.js"></script>
<script src="Scripts/jquery.signalR-2.1.2.min.js"></script>
<script src="signalr/hubs"></script>
<script src="Scripts/complaints.js"></script>
</body>
</html>
And when finished, the page will look like:
Under Scripts folder, create a new JavaScript file complaints.js, put the following code:
(function () { var app = angular.module('myApp', []),
uri = 'api/complaints',
errorMessage = function (data, status) {
return 'Error: ' + status +
(data.Message !== undefined ? (' ' + data.Message) : '');
},
hub = $.connection.myHub;
app.controller('myCtrl', ['$http', '$scope', function ($http, $scope) {
$scope.complaints = [];
$scope.customerIdSubscribed;
$scope.getAllFromCustomer = function () {
if ($scope.customerId.length == 0) return;
$http.get(uri + '/' + $scope.customerId)
.success(function (data, status) {
$scope.complaints = data; if ($scope.customerIdSubscribed &&
$scope.customerIdSubscribed.length > 0 &&
$scope.customerIdSubscribed !== $scope.customerId) {
hub.server.unsubscribe($scope.customerIdSubscribed);
}
hub.server.subscribe($scope.customerId);
$scope.customerIdSubscribed = $scope.customerId;
})
.error(function (data, status) {
$scope.complaints = [];
$scope.errorToSearch = errorMessage(data, status);
})
};
$scope.postOne = function () {
$http.post(uri, {
COMPLAINT_ID: 0,
CUSTOMER_ID: $scope.customerId,
DESCRIPTION: $scope.descToAdd
})
.success(function (data, status) {
$scope.errorToAdd = null;
$scope.descToAdd = null;
})
.error(function (data, status) {
$scope.errorToAdd = errorMessage(data, status);
})
};
$scope.putOne = function () {
$http.put(uri + '/' + $scope.idToUpdate, {
COMPLAINT_ID: $scope.idToUpdate,
CUSTOMER_ID: $scope.customerId,
DESCRIPTION: $scope.descToUpdate
})
.success(function (data, status) {
$scope.errorToUpdate = null;
$scope.idToUpdate = null;
$scope.descToUpdate = null;
})
.error(function (data, status) {
$scope.errorToUpdate = errorMessage(data, status);
})
};
$scope.deleteOne = function (item) {
$http.delete(uri + '/' + item.COMPLAINT_ID)
.success(function (data, status) {
$scope.errorToDelete = null;
})
.error(function (data, status) {
$scope.errorToDelete = errorMessage(data, status);
})
};
$scope.editIt = function (item) {
$scope.idToUpdate = item.COMPLAINT_ID;
$scope.descToUpdate = item.DESCRIPTION;
};
$scope.toShow = function () {
return $scope.complaints && $scope.complaints.length > 0;
};
$scope.orderProp = 'COMPLAINT_ID';
hub.client.addItem = function (item) {
$scope.complaints.push(item);
$scope.$apply(); }
hub.client.deleteItem = function (item) {
var array = $scope.complaints;
for (var i = array.length - 1; i >= 0; i--) {
if (array[i].COMPLAINT_ID === item.COMPLAINT_ID) {
array.splice(i, 1);
$scope.$apply();
}
}
}
hub.client.updateItem = function (item) {
var array = $scope.complaints;
for (var i = array.length - 1; i >= 0; i--) {
if (array[i].COMPLAINT_ID === item.COMPLAINT_ID) {
array[i].DESCRIPTION = item.DESCRIPTION;
$scope.$apply();
}
}
}
$.connection.hub.start(); }]);
})();
Note that at initial page load, it creates a proxy to SignalR hub on the web server and connects to it. When user searches a certain customer, it subscribes to a group named after its customer id by calling Subscribe()
method on the server. Also it creates client functions – addItem
, updateItem
, deleteItem
– to be called by the server on CRUD operation.
var hub = $.connection.myHub; hub.server.subscribe($scope.customerId); hub.client.addItem = function (item) { $.connection.hub.start();
Back to the Controllers folder, add one more class ApiControllerWithHub.cs which is borrowed from Brad Wilson’s WebstackOfLove, replace content with the following code:
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
namespace WebApiAungularWithPushNoti.Controllers
{
public abstract class ApiControllerWithHub<THub> : ApiController
where THub : IHub
{
Lazy<IHubContext> hub = new Lazy<IHubContext>(
() => GlobalHost.ConnectionManager.GetHubContext<THub>()
);
protected IHubContext Hub
{
get { return hub.Value; }
}
}
}
Open ComplaintsController.cs, on top of auto-generated C# codes by ASP.NET scaffolding, make the class inherited from ApiControllerWithHub
instead of default ApiController
so that action method can access a hub instance to push notification by calling client functions.
using WebApiAungularWithPushNoti.Hubs;
namespace WebApiAungularWithPushNoti.Controllers
{
public class ComplaintsController : ApiControllerWithHub<MyHub>
For example, PostCUSTOMER_COMPLAINTS()
, after it successfully added a new complaint to the database, does below to push notifications to all clients subscribed to the same customer.
var subscribed = Hub.Clients.Group(cUSTOMER_COMPLAINTS.CUSTOMER_ID);
subscribed.addItem(cUSTOMER_COMPLAINTS);
Now it’s time to run the web app, press F5 and try to search customer id.
Fiddler shows that a HTTP GET request to search 659024 in first row and a HTTP POST request to subscribe to a group “659024” in second row.
By default, Angular $http.get
is requesting JSON in Accept
field and the web accordingly responds with JSON data as below.
If this request is replayed requesting XML on Fiddler's Composer tab, the web responds with verbose XML data as below.
Now to see SignalR is working, open another browser, Firefox for example, access the same page and search the same customer id, add a new complaint which should be appearing on both browsers.
Points of Interest
At first, I was a bit confused about Web API routing convention. On HTTP request, it decides which method to serve the request by its URL (controller name and id) and HTTP verb (GET
, POST
, PUT
, DELETE
).
When client function is being called to get notification, it is adding/deleting $scope.complaints
property but nothing happened. Turns out that needs to call $apply
as it is outside of Angular, I guess this is something unncessary if I was using Knockout observable.
When I was testing the web on localhost, Chrome and Firefox was using server-side event and Internet Explorer was using long polling, none was using Websockets. Maybe IIS Express setting on my PC?
Summary
ASP.NET Web API allowed me to write a data-centric web app where client is making Ajax calls as required and the web is responding with data in requested JSON or XML format. With Angular and SignalR, the web looks responsive showing changes made somewhere else in real time. I liked that Angular is encouraging module pattern and it somehow allows me to stay away from DOM manipulation. SignalR must have lots of use cases to replace polling. For this article, I assumed data change can happen only on the web app so I was pushing notification directly on action methods but to be more realistic, it may have to be separated from controller actions so that push notification works on change notification from database.
Hope you enjoyed my first article.
History
- 10th March, 2015: Initial upload