Integrating Angular 2 and SignalR - Part 1 of 2

This post is part of a two-part series on integrating Angular 2 and SignalR. If you haven't read the first post, please check it out:

Over the years I've always been looking for an excuse to use SignalR in applications. I just think the experience real-time communication provides is great, and the library really makes it all pretty easy to do. However, it does take some work to get a proper SignalR system set up, and for my personal site I actually am just using Pusher for real time messaging, but only because they make it incredibly easy to use and have a very generous free tier.

At work however we can't normally use 3rd party services, so this was a good excuse to try to implement similar functionality using SignalR and Angular 2. This pair of posts will talk about setting up a really simple WebAPI/SignalR server hosted in a .NET console app, and then wire up an Angular 2 client that can interact and receive push notifications from the server. I am also taking some inspiration from how Pusher is designed here to abstract the SignalR components into channels and events.

This first of two articles will tackle getting the server up and running, and then in the second article we'll tackle writing the Angular 2 client so that the SignalR stuff is hidden and easy to consume. All of the code shown is available in my github repo.

<update> - March 20, 2016

As I worked on the client side of this experiment I made some changes to how the server worked. They aren't major, but include the following:
1. I updated the ChannelEvent class to include some additional information that helps the client side handle messages.
2. I added an "admin" channel by default that publishes events related to activity, but this isn't too critical for the demos
3. The OnEvent() call available on the clients now accepts the channel name as a parameter.

Outside of those changes the overall approach hasn't changed. I've updated all code snippets here to reflect the current state of things.

</update>

To set the stage for what we're building, here's a general picture of the components:

overall picture

In this example we have a SignalR Hub called EventHub that is used to send out status events generated by clients. However, these events are specific to a channel called tasks, so only clients that have joined the channel will get the messages. We're hard coding this information in this example, but in a real app these values could be generated at run-time depending on the situation.

So with that let's tackle the server!

The SignalR Server

One of the great things about the later versions of ASP.NET is the OWIN pipeline and the ability to host these web technologies in simple applications (like a console app). In a production scenario you would probably host this in a windows service, IIS or Azure, but for this demo we don't need any of that.

First off, create a new console application within Visual Studio, and add the following packages to bring in what we need for OWIN, adding WebAPI, hosting SignalR and logging with Serilog:

<?xml version="1.0" encoding="utf-8"?>  
<packages>  
  <package id="Microsoft.AspNet.Cors" version="5.0.0" targetFramework="net46" />
  <package id="Microsoft.AspNet.SignalR.Client" version="2.2.0" targetFramework="net46" />
  <package id="Microsoft.AspNet.SignalR.Core" version="2.2.0" targetFramework="net46" />
  <package id="Microsoft.AspNet.SignalR.Owin" version="1.2.2" targetFramework="net46" />
  <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net46" />
  <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net46" />
  <package id="Microsoft.AspNet.WebApi.Owin" version="5.2.3" targetFramework="net46" />
  <package id="Microsoft.Owin" version="3.0.1" targetFramework="net46" />
  <package id="Microsoft.Owin.Cors" version="3.0.1" targetFramework="net46" />
  <package id="Microsoft.Owin.Host.HttpListener" version="3.0.1" targetFramework="net46" />
  <package id="Microsoft.Owin.Hosting" version="3.0.1" targetFramework="net46" />
  <package id="Microsoft.Owin.Security" version="2.1.0" targetFramework="net46" />
  <package id="Newtonsoft.Json" version="6.0.4" targetFramework="net46" />
  <package id="Owin" version="1.0" targetFramework="net46" />
  <package id="Serilog" version="1.5.14" targetFramework="net46" />
</packages>  

Once you've got all these packages in there, update the Main method so it looks like the following. Basically we're just doing the following:

  1. Configuring the logger so that we get pretty output in the console
  2. Creating an OWIN host to run the app on http://localhost:9123
  3. Instantiating the SignalR hub and wiring up a listener to print log statements when events are fired
  4. Finally, just blocking until Enter is pressed to keep the server running
static void Main(string[] args)  
{
    Log.Logger = new LoggerConfiguration()
        .WriteTo.ColoredConsole()
        .CreateLogger();

    string baseAddress = "http://localhost:9123/";

    // Start OWIN host 
    using (WebApp.Start<Startup>(url: baseAddress))
    {
        // Let's wire up a SignalR client here to easily inspect what
        //  calls are happening
        //
        var hubConnection = new HubConnection(baseAddress);
        IHubProxy eventHubProxy = hubConnection.CreateHubProxy("EventHub");
        eventHubProxy.On<string, ChannelEvent>("OnEvent", (channel, ev) => Log.Information("Event received on {channel} channel - {@ev}", channel, ev));
        hubConnection.Start().Wait();

        // Join the channel for task updates in our console window
        //
        eventHubProxy.Invoke("Subscribe", Constants.AdminChannel);
        eventHubProxy.Invoke("Subscribe", Constants.TaskChannel);

        Console.WriteLine($"Server is running on {baseAddress}");
        Console.WriteLine("Press <enter> to stop server");
        Console.ReadLine();

    }
}

Now this won't compile yet because we also need to add a file called Startup.cs that will configure the OWIN pipeline. Create one that contains the following class. This is basically doing:

  1. Enabling CORS so that our Angular 2 client can connect from a different origin
    • This is very important, but may not apply depending on your specific need!
  2. Adding the SignalR middle-ware
  3. Adding WebAPI middle-ware and telling it that we'll use attributes on our API classes to define the routing
public class Startup  
{
    public void Configuration(IAppBuilder app)
    {
        // This server will be accessed by clients from other domains, so
        //  we open up CORS. This needs to be before the call to
        //  .MapSignalR()!
        //
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

        // Add SignalR to the OWIN pipeline
        //
        app.MapSignalR();

        // Build up the WebAPI middleware
        //
        var httpConfig = new HttpConfiguration();

        httpConfig.MapHttpAttributeRoutes();

        app.UseWebApi(httpConfig);
    }
}

We also need to create our SignalR hub itself, so we need two classes like these. The hub exposes three methods for the clients to call:

  • Subscribe
    • This lets a client join a SignalR room (or channel in the abstraction we'll create later)
  • Unsubscribe
    • This let's a client leave a room/channel
  • Publish
    • This gives the client a mechanism to publish events to a specific channel

You'll notice that we're not saving what clients are in what groups, but that's because this is a simple demo. I might try to tackle an approach for saving that data in a future post, but for now we're not gonna worry about that.

Finally, SignalR is a dynamic thing, meaning that the methods shared between the server and client aren't strictly typed. So you need to pay special care to the name and casing used. In this hub we're using a single client method called OnEvent that the server will invoke with any new events raised by other clients.

/// <summary>
/// A signalR hub that provides channel-based event broadcasting
/// that clients can subscribe to
/// </summary>
public class EventHub : Hub  
{
    public async Task Subscribe(string channel)
    {
        await Groups.Add(Context.ConnectionId, channel);

        var ev = new ChannelEvent
        {
            ChannelName = Constants.AdminChannel,
            Name = "user.subscribed",
            Data = new
            {
                Context.ConnectionId,
                ChannelName = channel
            }
        };

        await Publish(ev);
    }

    public async Task Unsubscribe(string channel)
    {
        await Groups.Remove(Context.ConnectionId, channel);

        var ev = new ChannelEvent
        {
            ChannelName = Constants.AdminChannel,
            Name = "user.unsubscribed",
            Data = new
            {
                Context.ConnectionId,
                ChannelName = channel
            }
        };

        await Publish(ev);
    }


    public Task Publish(ChannelEvent channelEvent)
    {
        Clients.Group(channelEvent.ChannelName).OnEvent(channelEvent.ChannelName, channelEvent);

        if (channelEvent.ChannelName != Constants.AdminChannel)
        {
            // Push this out on the admin channel
            //
            Clients.Group(Constants.AdminChannel).OnEvent(Constants.AdminChannel, channelEvent);
        }

        return Task.FromResult(0);
    }


    public override Task OnConnected()
    {
        var ev = new ChannelEvent
        {
            ChannelName = Constants.AdminChannel,
            Name = "user.connected",
            Data = new
            {
                Context.ConnectionId,
            }
        };

        Publish(ev);

        return base.OnConnected();
    }


    public override Task OnDisconnected(bool stopCalled)
    {
        var ev = new ChannelEvent
        {
            ChannelName = Constants.AdminChannel,
            Name = "user.disconnected",
            Data = new
            {
                Context.ConnectionId,
            }
        };

        Publish(ev);

        return base.OnDisconnected(stopCalled);
    }

}

I also define a class called ChannelEvent that is a container for any event that will be published. It contains the channel name, the event name, a timestamp and a generic Data property that can contain anything. We also expose a Json property that will set the serialized form of the Data object.

/// <summary>
/// A generic object to represent a broadcasted event in our SignalR hubs
/// </summary>
public class ChannelEvent  
{
    /// <summary>
    /// The name of the event
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// The name of the channel the event is associated with
    /// </summary>
    public string ChannelName { get; set; }

    /// <summary>
    /// The date/time that the event was created
    /// </summary>
    public DateTimeOffset Timestamp { get; set; }

    /// <summary>
    /// The data associated with the event
    /// </summary>
    public object Data
    {
        get { return _data; }
        set
        {
            _data = value;
            this.Json = JsonConvert.SerializeObject(_data);
        }
    }
    private object _data;

    /// <summary>
    /// A JSON representation of the event data. This is set automatically
    /// when the Data property is assigned.
    /// </summary>
    public string Json { get; private set; }

    public ChannelEvent()
    {
        Timestamp = DateTimeOffset.Now;
    }
}

The final piece we need in this console app is the API endpoints that clients can call to do work. We will simulate that using a "task" controller that exposes a long running operation, and a shorter running one. Some key things to see here include:

  1. We're just grabbing a reference to the SignalR context from the static GlobalHost property. Normally we'd inject that somehow.
  2. Each API method defines the event name that it will fire while running. This is something the clients would need to know in advance to properly subscribe to them.
  3. The PublishEvent method makes a call invoke a call on the clients using the context. It cannot call the public methods of the EventHub class.
    • To me this is the part I don't like about SignalR...it's too easy to mess this up.
[RoutePrefix("tasks")]
public class TaskController : ApiController  
{
    private IHubContext _context;

    // This can be defined a number of ways
    //
    private string _channel = Constants.TaskChannel;

    public TaskController()
    {
        // Normally we would inject this
        //
        _context = GlobalHost.ConnectionManager.GetHubContext<EventHub>();
    }


    [Route("long")]
    [HttpGet]
    public IHttpActionResult GetLongTask()
    {
        Log.Information("Starting long task");

        double steps = 10;
        var eventName = "longTask.status";

        ExecuteTask(eventName, steps);

        return Ok("Long task complete");
    }



    [Route("short")]
    [HttpGet]
    public IHttpActionResult GetShortTask()
    {
        Log.Information("Starting short task");

        double steps = 5;
        var eventName = "shortTask.status";

        ExecuteTask(eventName, steps);

        return Ok("Short task complete");
    }

    private void ExecuteTask(string eventName, double steps)
    {
        var status = new Status
        {
            State = "starting",
            PercentComplete = 0.0
        };

        PublishEvent(eventName, status);

        for (double i = 0; i < steps; i++)
        {
            // Update the status and publish a new event
            //
            status.State = "working";
            status.PercentComplete = (i / steps) * 100;
            PublishEvent(eventName, status);

            Thread.Sleep(500);
        }

        status.State = "complete";
        status.PercentComplete = 100;
        PublishEvent(eventName, status);
    }

    private void PublishEvent(string eventName, Status status)
    {
        // From .NET code like this we can't invoke the methods that
        //  exist on our actual Hub class...because we only have a proxy
        //  to it. So to publish the event we need to call the method that
        //  the clients will be listening on.
        //
        _context.Clients.Group(_channel).OnEvent(Constants.TaskChannel, new ChannelEvent
        {
            ChannelName = Constants.TaskChannel,
            Name = eventName,
            Data = status
        });
    }
}


public class Status  
{
    public string State { get; set; }

    public  double PercentComplete { get; set; }
}

With that code written we'll all set to fire up the server. Compile and run it and you should see the following:

server console screenshot

If you use a tool like Postman to make an REST call to the long task you should see the status events being sent out over the SignalR connection:

signalr messages

So with this we've got a simple WebAPI and SignalR hub all hosted in a console app with some simple logging to let us see what messages are being sent out.

In the second part of this series we'll walk through the Angular 2 client and demonstrate how easy it is to consume these SignalR notifications.

comments powered by Disqus