A couple of months ago I wrote about a self-hosted SignalR application that I've been working on as part of a larger project. This particular application runs as a service and hosts a SignalR Hub that interacts with a message queue and pushes messages into an application dashboard for live information of the queue's status. Basically the queue service application asynchronously notifies the SignalR hub when messages are processed and the hub pushes these messages out for display into a Web interface to any connected SignalR clients. It's a wonderfully interactive tool that replaces an old WinForms based front end interface with a Web interface that is now accessible from anwhere, but it's a little different SignalR implementation in that the main broadcast agent aren't browser clients, but rather the Windows Service backend. The backend broadcasts messages from a self-hosted SignalR server.
As I mentioned in that last post, being able to host SignalR inside of external non-Web applications opens up all sorts of opportunities and the process of self-hosting - while not as easy as hosting in IIS - is pretty straight forward. In this post I'll describe the steps to set up SignalR for self-hosting and using it inside of a Windows Service.
Self-Hosting SignalR
The process of self-hosting SignalR is surprisingly simple. SignalR relies on the new OWIN architecture that bypasses ASP.NET for a more lightweight hosting environment. There's no dependencies on System.Web and so the hosting process tends to be pretty lean.
Creating a OWin Startup class
The first step is to create a startup class that is called when OWIN initializes. The purpose of this class is to allow you to configure the OWIN runtime, hook in middleware components (think of it like HttpModules) etc. If you're a consumer of a high level tool like SignalR, the OWIN configuration class simply serves as the entry point to hook up the SignalR configuration. In my case I'm using hubs and so all I have here is a HubConfiguration:
public class SignalRStartup
{
public static IAppBuilder App = null;
public void Configuration(IAppBuilder app)
{
var hubConfiguration = new HubConfiguration {
EnableCrossDomain = true,
EnableDetailedErrors = true
};
app.MapHubs(hubConfiguration);
}
}
SignalR provides the HubConfiguration class and IAppBuilder extension method called MapHubs that's used for the hub routing. MapHubs uses Reflection to find all the Hub classes in your app and auto-registers them for you. If you're using Connections then the MapConnect<T> class is used to register each connection class individually.
If you're using SignalR 2.0 (currently in RC) then the configuration looks a little different:
public void Configuration(IAppBuilder app)
{
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true,
EnableJSONP = true
};
map.RunSignalR(hubConfiguration);
});
}
Note the explicit CORS configuration, which enabled cross domain calls for XHR requests, has been migrated to the OWIN middleware rather than being directly integrated in SignalR. You'll need the Microsoft.OWIN.Cors NuGet package for this functionalty to become available. This is pretty much required on every self-hosted server accessed from the browser since self-host always implies a different domain or at least a different port which most browsers (except IE) also interpret as cross-domain.
And that's really all you need to do to configure SignalR.
Starting up the OWIN Runtime
Next we need to kickstart OWIN to use the Startup class created above. This is done by calling the WebApp.Start<T> factory method passing the startup class as a generic parameter:
SignalR = WebApp.Start<SignalRStartup>("http://*:8080/");
Start<T> passes in the startup class as generic parameter and the hosting URI. Here I'm using the root site as the base on port 8080. If you're hosting under SSL, you'd use https://*:8080/.
The method returns an instance of the Web app that you can hold onto. The result is a plain IDisposable interface and when it goes out of scope so does the SignalR service. In order to keep the app alive, it's important to capture the instance and park it somewhere for the lifetime of your application.
In my service application I create the SignalR instance on the service's Start() method and attach it to a SignalR property I created on the service. The service sticks around for the lifetime of the application and so this works great.
Running in a Windows Service
Creating a Windows Service in .NET is pretty easy - you simply create a class that inherits from System.ServiceProcess.ServiceBase
and then override the OnStart() and OnStop() and Dispose() methods at a minimum.
Here's an example of my implementation of ServiceBase including the SignalR loading and unloading:
public class MPQueueService : ServiceBase
{
MPWorkflowQueueController Controller { get; set; }
IDisposable SignalR { get; set; }
public void Start()
{
Controller = new MPWorkflowQueueController(App.AdminConfiguration.ConnectionString);
var config = QueueMessageManagerConfiguration.Current;
Controller.QueueName = config.QueueName;
Controller.WaitInterval = config.WaitInterval;
Controller.ThreadCount = config.ControllerThreads;
SignalR = WebApp.Start<SignalRStartup>(App.AdminConfiguration.MonitorHostUrl);
// Spin up the queue
Controller.StartProcessingAsync();
LogManager.Current.LogInfo(String.Format("QueueManager Controller Started with {0} threads.",
Controller.ThreadCount));
// Allow access to a global instance of this controller and service
// So we can access it from the stateless SignalR hub
Globals.Controller = Controller;
Globals.WindowsService = this;
}
public new void Stop()
{
LogManager.Current.LogInfo("QueueManager Controller Stopped.");
Controller.StopProcessing();
Controller.Dispose();
SignalR.Dispose();
Thread.Sleep(1500);
}
/// <summary>
/// Set things in motion so your service can do its work.
/// </summary>
protected override void OnStart(string[] args)
{
Start();
}
/// <summary>
/// Stop this service.
/// </summary>
protected override void OnStop()
{
Stop();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (SignalR != null)
{
SignalR.Dispose();
SignalR = null;
}
}
}
There's not a lot to the service implementation. The Start() method starts up the Queue Manager that does the real work of the application, as well as SignalR which is used in the processing of Queue Requests and sends messages out through the SignalR hub as requests are processed.
Notice the use of Globals.Controller and Globals.WindowsService in the Start() method. SignalR Hubs are completely stateless and they have no context to the application they are running inside of, so in order to pass the necessary state logic and perform tasks like getting information out of the queue or managing the actual service interface, any of these objects that the Hub wants access to have to be available somewhere globally.
public static class Globals
{
public static MPWorkflowQueueController Controller;
public static MPQueueService WindowsService;
}
By using a global class with static properties to hold these values they become accessible to the SignalR Hub which can then act on them. So inside of a hub class I can do things like Globals.Controller.Pause() to pause the queue manager's queue processing. Anything with persistent state you need to access from within a Hub has to be exposed in a similar fashion.
Bootstrapping the Windows Service
Finally you also need to also bootstrap the Windows service so it can start and respond to Windows ServiceManager requests in your main program startup (program.cs).
[STAThread]
static void Main(string[] args)
{
string arg0 = string.Empty;
if (args.Length > 0)
arg0 = (args[0] ?? string.Empty).ToLower();
if (arg0 == "-service" )
{
RunService();
return;
}
if (arg0 == "-fakeservice")
{
FakeRunService();
return;
}
}
static void RunService()
{
var ServicesToRun = new ServiceBase[] { new MPQueueService() };
LogManager.Current.LogInfo("Queue Service started as a Windows Service.");
ServiceBase.Run(ServicesToRun);
}
static void FakeRunService()
{
var service = new MPQueueService();
service.Start();
LogManager.Current.LogInfo("Queue Service started as FakeService for debugging.");
// never ends but waits
Console.ReadLine();
}
Once installed a Windows Service calls the service EXE with a -service command line switch to start the service the first time it runs. At that point ServiceBase.Run is called on our custom service instance and now the service is running. While running the Windows Service Manager can then call into OnStart(),OnStop() etc. as these commands are applied against the service manager. After a OnStop() operation the service is shut down, which shuts down the EXE.
Note that I also add support for a -fakeservice command line switch. I use this switch for debugging, so that I can run the application for testing under debug mode using the same Service interface. FakeService simply instantiates the service class and explicitly calls the Start() method which simulates the OnStart() operation from the Windows Service Manager. In short this allows me to debug my service by simply starting a regular debug process in Visual Studio, rather than using Attach Process and attaching to a live Windows Service. Much easier and highly recommended while you're developing the service.
Windows Service Registration
Another thing I like to do with my services is provide the ability to have them register themselves. My startup program also corresponds to -InstallService and -UninstallService flags which allow self-registration of the service. .NET doesn't include a native interface for doing this however, with some API calls to the Service Manager API's it's short work to accomplish this. I'm not going to post the code here, but I have a self-contained C# code file that provides this functionality:
With this class in place, you can now easily do something like this in the startup program when checking for command line arguments:
else if (arg0 == "-installservice" || arg0 == "-i")
{
WindowsServiceManager SM = new WindowsServiceManager();
if (!SM.InstallService(Environment.CurrentDirectory + "\\MPQueueService.exe -service",
"MPQueueService", "MP Queue Manager Service"))
MessageBox.Show("Service install failed.");
return;
}
else if (arg0 == "-uninstallservice" || arg0 == "-u")
{
WindowsServiceManager SM = new WindowsServiceManager();
if (!SM.UnInstallService("MPQueueService"))
MessageBox.Show("Service failed to uninstall.");
return;
}
So now we have the service in place - let's look a little closer at the SignalR specific details.
SSL Configuration
Once you deploy your service you'll most likely need to run it under SSL as well. Although not required, if any of your client applications run SSL, the service also needs to run SSL in order to avoid browser mixed content warnings. Unfortunately configuration of an SSL certificate is a little more work than just installing a certificate in IIS - you need to run a command line utility with some magic values to make this work.
If you need to do this, I have a follow up post that discusses SSL installation and creation in more detail.
Hub Implementation
The key piece of the SignalR specific implementation of course is the SignalR hub. The SignalR Hub is just a plain hub with any of the SignalR logic you need to perform. If you recall typical hub methods are called from the client and then typically use the Clients.All.clientMethodToCall to broadcast messages to all (or a limited set) of connected clients.
The following is a very truncated example of the QueueManager hub class that includes a few instance broadcast methods for JavaScript clients, as well as several static methods to be used by the hosting EXE to push messages to the client from the server:
public class QueueMonitorServiceHub : Hub
{
/// <summary>
/// Writes a message to the client that displays on the status bar
/// </summary>
public void StatusMessage(string message, bool allClients = false)
{
if (allClients)
Clients.All.statusMessage(message);
else
Clients.Caller.statusMessage(message);
}
/// <summary>
/// Starts the service
/// </summary>
public void StartService()
{
// unpause the QueueController to start processing again
Globals.Controller.Paused = false;
Clients.All.startServiceCallback(true);
Clients.All.writeMessage("Queue starting with " +
Globals.Controller.ThreadCount.ToString() +
" threads.",
"Info", DateTime.Now.ToString("HH:mm:ss"));
}
public void StopService()
{
// Pause - we can't stop service because that'll exit the server
Globals.Controller.Paused = true;
Clients.All.stopServiceCallback(true);
Clients.All.writeMessage("Queue has been stopped.","Info",
DateTime.Now.ToString("HH:mm:ss"));
}
…
/// <summary>
/// Context instance to access client connections to broadcast to
/// </summary>
public static IHubContext HubContext
{
get
{
if (_context == null)
_context = GlobalHost.ConnectionManager.GetHubContext<QueueMonitorServiceHub>();
return _context;
}
}
static IHubContext _context = null;
/// <summary>
/// Writes out message to all connected SignalR clients
/// </summary>
/// <param name="message"></param>
public static void WriteMessage(string message, string id = null,
string icon = "Info", DateTime? time = null)
{
if (id == null)
id = string.Empty;
// if no id is passed write the message in the ID area
// and show no message
if (string.IsNullOrEmpty(id))
{
id = message;
message = string.Empty;
}
if (time == null)
time = DateTime.UtcNow;
// Write out message to SignalR clients
HubContext.Clients.All.writeMessage(message,
icon,
time.Value.ToString("HH:mm:ss"),
id,
string.Empty);
}
/// <summary>
/// Writes out a message to all SignalR clients
/// </summary>
/// <param name="queueItem"></param>
/// <param name="elapsed"></param>
/// <param name="waiting"></param>
public static void WriteMessage(QueueMessageItem queueItem,
int elapsed = 0, int waiting = -1,
DateTime? time = null)
{
string elapsedString = string.Empty;
if (elapsed > 0)
elapsedString = (Convert.ToDecimal(elapsed) / 1000).ToString("N2");
var msg = HtmlUtils.DisplayMemo(queueItem.Message);
if (time == null)
time = DateTime.UtcNow;
// Write out message to SignalR clients
HubContext.Clients.All.writeMessage(msg,
queueItem.Status,
time.Value.ToString("HH:mm:ss"),
queueItem.Id,
elapsedString,
waiting);
}
}
This hub includes a handful of instance hub methods that are called from the client to update other clients. For example the ShowStatus method is used by browser clients to broadcast a status bar update on the UI of the browser app. Start and Stop Service operations start and stop the queue processing and also update the UI. This is the common stuff you'd expect to see in a SignalR hub.
Calling the Hub from within the Windows Service
However, the static methods in the Hub class are a little less common. These methods are called from the Windows Service application to push messages from the server to the client. So rather than having the browser initiate the SignalR broadcasts, we're using the server side EXE and SignalR host to push messages from the server to the client. The methods are static because there is no 'active' instance of the Hub and so every method call basically has to establish the context for the Hub broadcast request.
The key that makes this work is this snippet:
GlobalHost.ConnectionManager
.GetHubContext<QueueMonitorServiceHub>()
.Clients.All.writeMessage(msg,
queueItem.Status,
time.Value.ToString("HH:mm:ss"),
queueItem.Id,
elapsedString,
waiting);
which gives you access to the Hub from within a server based application.
The GetHubContext<T>() method is a factory that creates a fully initialized Hub that you can pump messages into from the server. Here I simply call out to a writeMessage() function in the browser application, which is propagated to all active clients.
In the browser in JavaScript I then have a mapping for this writeMessage endpoint on the hub instance:
hub.client.writeMessage = self.writeMessage;
where self.writeMessage is a function on page that implements the display logic:
// hub callbacks
writeMessage: function (message, status, time, id, elapsed, waiting) {
…
}
If you recall SignalR requires that you map a server-side method to a handler function (the first snippet) on the client, but beyond that there are no additional requirements. SignalR's client library simply calls the mapped method and passes any parameters you fired on the server to the JavaScript function.
For context, the result of all of this looks like the figure below where the writeMessage function is responsible for writing out the individual line request lines in the list display. The writeMessage code basically uses a handlebars.js template to merge the received data into HTML to be rendered in the page.
It's very cool to see this in action especially with multiple browser windows open. Even at very rapid queue processing of 20+ requests a second (for testing) you can see multiple browser windows update nearly synchronously. Very cool.
Summary
Using SignalR as a mechanism for pushing server side processing messages to the client is a powerful feature that opens up many opportunities for dashboard and notification style applications that used to run in server isolated silos previously. By being able to host SignalR in a Windows Service or any EXE based application really, you can now offload many UI tasks that previously required custom desktop applications and protocols, and push the output directly to browser based applications in real time. It's a wonderful way to rethink browser based UIs fed from server side data. Give it some thought and see what opportunities you can find to open up your server interfaces.
Resources
Other Posts you might also like