I’ve spent some time recently working with the asynchronous web event messaging engine SignalR. This framework uses JavaScript (with jQuery) on the client and ASP.NET on the server to enable very interactive communication patterns. The coolest part is being able to have the server-side application call a JavaScript function on each connected browser client. While many SignalR demos you see have focused on scenarios like chat applications, I was thinking of how to use SignalR to notify business users of interesting events within an enterprise. Wouldn’t it be fascinating if business events (e.g. “Project X requirements document updated”, “Big customer added in US West region”, “Production Mail Server offline”, “FAQ web page visits up 78% today”) were published from source applications and pushed to a live dashboard-type web application available to users? If I want to process these fast moving events and perform rich aggregations over windows of events, then Microsoft StreamInsight is a great addition to a SignalR-based solution. In this blog post, I’m going to walk through a demonstration of using SignalR to push business events through StreamInsight and into a Tweetdeck-like browser client.
Solution Overview
So what are we building? To make sure that we keep an eye on the whole picture while building the individual components, I’ve summarized the solution here.
Basically, the browser client will first (through jQuery) call a server operation that adds that client to a message group (e.g. “security events”). Events are then sent from source applications to StreamInsight where they are processed. StreamInsight then calls a WCF service that is part of the ASP.NET web application. Finally, the WCF Service uses the SignalR framework to invoke the “addEventMsg()” function on each connected browser client. Sound like fun? Good. Let’s jump in.
Setting up the SignalR application
I started out by creating a new ASP.NET web application. I then used the NuGet extension to locate the SignalR libraries that I wanted to use.
Once the packages were chosen from NuGet, they got automatically added to my ASP.NET app.
The next thing to do was add the appropriate JavaScript references at the top of the page. Note the last one. It is a virtual JavaScript location (you won’t find it in the design-time application) that is generated by the SignalR framework. This script, which you can view in the browser at runtime, holds all the JavaScript code that corresponds to the server/browser methods defined in my ASP.NET application.
After this, I added the HTML and ASP.NET controls necessary to visualize my Tweetdeck-like event viewer. Besides a column where each event shows up, I also added a listbox that holds all the types of events that someone might subscribe to. Maybe one set of users just want security-oriented events, or another wants events related to a given IT project.
With my look-and-feel in place, I then moved on to adding some server-side components. I first created a new class (BizEventController.cs) that uses the SignalR “Hubs” connection model. This class holds a single operation that gets called by the JavaScript in the browser and adds the client to a given messaging group. Later, I can target a SignalR message to a given group.
using System; using System.Collections.Generic; using System.Linq; using System.Web; //added reference to SignalR using SignalR.Hubs; /// <summary> /// Summary description for BizEventController /// </summary> public class BizEventController : Hub { public void AddSubscription(string eventType) { AddToGroup(eventType); } }
I then switched back to the ASP.NET page and added the JavaScript guts of my SignalR application. Specifically, the code below (1) defines an operation on my client-side hub (that gets called by the server) and (2) calls the server side controller that adds clients to a given message group.
$(function () { //create arrays for use in showing formatted date string var days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thur', 'Fri', 'Sat']; var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']; // create proxy that uses in dynamic signalr/hubs file var bizEDeck = $.connection.bizEventController; // Declare a function on the chat hub so the server can invoke it bizEDeck.addEventMsg = function (message) { //format date var receiptDate = new Date(); var formattedDt = days[receiptDate.getDay()] + ' ' + months[receiptDate.getMonth()] + ' ' + receiptDate.getDate() + ' ' + receiptDate.getHours() + ':' + receiptDate.getMinutes(); //add new "message" to deck column $('#deck').prepend('</pre> <div>' + message + '' + formattedDt + ' via StreamInsight</div> <pre> '); }; //act on "subscribe" button $("#groupadd").click(function () { //call subscription function in server code bizEDeck.addSubscription($('#group').val()); //add entry in "subscriptions" section $('#subs').append($('#group').val() + '</pre> <hr /> <pre>'); }); // Start the connection $.connection.hub.start(); });
Building the web service that StreamInsight will call to update browsers
The UI piece was now complete. Next, I wanted a web service that StreamInsight could call and pass in business events that would get pushed to each browser client. I’m leveraging a previously-built StreamInsight WCF adapter that can be used to receive web service request and call web service endpoints. I built a WCF service and in the underlying class, I pull the list of all connected clients and invoke the JavaScript function.
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; using SignalR; using SignalR.Infrastructure; using SignalR.Hosting.AspNet; using StreamInsight.Samples.Adapters.Wcf; using Seroter.SI.AzureAppFabricAdapter; public class NotificationService : IPointEventReceiver { //implement the operation included in interface definition public ResultCode PublishEvent(WcfPointEvent result) { //get category from key/value payload string cat = result.Payload["Category"].ToString(); //get message from key/value payload string msg = result.Payload["EventMessage"].ToString(); //get SignalR connection manager IConnectionManager mgr = AspNetHost.DependencyResolver.Resolve(); //retrieve list of all connected clients dynamic clients = mgr.GetClients(); //send message to all clients for given category clients[cat].addEventMsg(msg); //also send message to anyone subscribed to all events clients["All"].addEventMsg(msg); return ResultCode.Success; } }
Preparing StreamInsight to receive, aggregate and forward events
The website is ready, the service is exposed, and all that’s left is to get events and process them. Specifically, I used a WCF adapter to create an endpoint and listen for events from sources, wrote a few queries, and then sent the output to the WCF service created above.
The StreamInsight application is below. It includes the creation of the embedded server and all other sorts of fun stuff.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.ComplexEventProcessing; using Microsoft.ComplexEventProcessing.Linq; using Seroter.SI.AzureAppFabricAdapter; using StreamInsight.Samples.Adapters.Wcf; namespace SignalRTest.StreamInsightHost { class Program { static void Main(string[] args) { Console.WriteLine(":: Starting embedded StreamInsight server ::"); //create SI server using(Server server = Server.Create("RSEROTERv12")) { //create SI application Application app = server.CreateApplication("SeroterSignalR"); //create input adapter configuration WcfAdapterConfig inConfig = new WcfAdapterConfig() { Password = "", RequireAccessToken = false, Username = "", ServiceAddress = "http://localhost:80/StreamInsightv12/RSEROTER/InputAdapter" }; //create output adapter configuration WcfAdapterConfig outConfig = new WcfAdapterConfig() { Password = "", RequireAccessToken = false, Username = "", ServiceAddress = "http://localhost:6412/SignalRTest/NotificationService.svc" }; //create event stream from the source adapter CepStream input = CepStream.Create("BizEventStream", typeof(WcfInputAdapterFactory), inConfig, EventShape.Point); //build initial LINQ query that is a simple passthrough var eventQuery = from i in input select i; //create unbounded SI query that doesn't emit to specific adapter var query0 = eventQuery.ToQuery(app, "BizQueryRaw", string.Empty, EventShape.Point, StreamEventOrder.FullyOrdered); query0.Start(); //create another query that latches onto previous query //filters out all individual web hits used in later agg query var eventQuery1 = from i in query0.ToStream() where i.Category != "Web" select i; //another query that groups events by type; used here for web site hits var eventQuery2 = from i in query0.ToStream() group i by i.Category into EventGroup from win in EventGroup.TumblingWindow(TimeSpan.FromSeconds(10)) select new BizEvent { Category = EventGroup.Key, EventMessage = win.Count().ToString() + " web visits in the past 10 seconds" }; //new query that takes result of previous and just emits web groups var eventQuery3 = from i in eventQuery2 where i.Category == "Web" select i; //create new SI queries bound to WCF output adapter var query1 = eventQuery1.ToQuery(app, "BizQuery1", string.Empty, typeof(WcfOutputAdapterFactory), outConfig, EventShape.Point, StreamEventOrder.FullyOrdered); var query2 = eventQuery3.ToQuery(app, "BizQuery2", string.Empty, typeof(WcfOutputAdapterFactory), outConfig, EventShape.Point, StreamEventOrder.FullyOrdered); //start queries query1.Start(); query2.Start(); Console.WriteLine("Query started. Press [Enter] to stop."); Console.ReadLine(); //stop all queries query1.Stop(); query2.Stop(); query0.Stop(); Console.Write("Query stopped."); Console.ReadLine(); } } private class BizEvent { public string Category { get; set; } public string EventMessage { get; set; } } } }
Everything is now complete. Let’s move on to testing with a simple event generator that I created.
Testing the solution
I built a simple WinForm application that generates business events or a user-defined number of simulated website visits. The business events are passed through StreamInsight, and the website hits are aggregated so that StreamInsight can emit the count of hits every ten seconds.
To highlight the SignalR experience, I launched three browser instances with two different group subscriptions. The first two subscribe to all events, and the third one subscribes just to website-based events. For the latter, the client JavaScript function won’t get invoked by the server unless the events are in the “Web” category.
The screenshot below shows the three browser instances launched (one in IE, two in Chrome).
Next, I launched my event-generator app and StreamInsight host. I sent in a couple of business (not web) events and hoped to see them show up in two of the browser instances.
As expected, two of the browser clients were instantly updated with these events, and the other subscriber was not. Next, I sent in a handful of simulated website hit events and observed the results.
Cool! So all three browser instances were instantly updated with ten-second-counts of website events that were received.
Summary
SignalR is an awesome framework for providing real-time, interactive, bi-directional communication between clients and servers. I think there’s a lot of value of using SignalR for dashboards, widgets and event monitoring interfaces. In this post we saw a simple “business event monitor” application that enterprise users could leverage to keep up to date on what’s happening within enterprise systems. I used StreamInsight here, but you could use BizTalk Server or any application that can send events to web services.
What do you think? Where do you see value for SignalR?
I would say it is valuable supporting broadcast pattern.
Awesome! Thanks for sharing.
I was playing with signalR and found this interesting information. I was trying to implement something like this with SQL Server Service Broker but your solution is more reliable
Thanks
I seemed to have a slight problem with your example. Here’s what I did:
1. Have 2 browsers open; one subscribes to the Web cateogry and the other “Doc Repository Event”
2. Use the Windows app to send in a “New Design Spec Version” event
3. One browser correctly displays the event
4. Send it 10 web hits event
5. The other browser subscribed to “Web” shows nothing
6. Send in another “New Design Spec Version” event
7. Now the browser subscribed to “Web” shows “10 web hits”
It seems for some reason the “Web” event won’t get broadcasted until another type of event has been sent.
Hmm. Well, StreamInsight pushes out events when other events come in and advance it’s application clock. It doesn’t use “system time” but relies on inbound events (and corresponding CTIs) to move time forward. In your case, is it only when a different event type comes in?
Thanks useful info, will you be sharing on implementing this with StreamInsight 2.1?
Good question. I should try that soon!
Great article. What did you use to create the solution overview picture?
Thanks Anders. I did the picture in good old PowerPoint.
Very nice solution. Is there any information available on how to set up the wcf endpoint you reference for WCFAdapter input – “http://localhost:80/StreamInsightv12/RSEROTER/InputAdapter” ? Seems as tho there may be another project or setup I am missing, or just a lack of knowledge on StreamInsight.\. I can get this sample to run when “run as Administrator” is used, or from Visual Studio, but not with normal creds across different machines. The endpoint comes up missing.
Hi Dave, I think I used the WCF adapter that comes in the StreamInsight samples (from Codeplex), judging from my “using” statements. If the WCF service isn’t already up, you may need elevated credentials to listen on port 80.
Great sample!
I am wondering what version of StreamInsight this sample was built with?
Thanks John. I built this with the 1.2, I think. Definitely not the new object model that StreamInsight offers.