Over the weekend I had fun getting NServiceBus to publish events to the browser via SignalR. Both libraries are easy to work with; most time was spent learning Backbone and battling json serialisation of dates .
In the blotter demo, a view is bound to #commandForm. ModelBinding ensures a command is populated with the form inputs – providing similar binding to Knockout.
// Form controller Labs.Form = (function (labs, backbone) { var form = {}; // Model var command = backbone.Model.extend({ url: "api/command" }); // View var commandView = backbone.View.extend({ el: "#commandForm", events: { "submit": "submitCommand" }, initialize: function () { backbone.ModelBinding.bind(this); }, submitCommand: function () { this.model.save(); } }); // Initialisers labs.addInitializer(function () { form.CommandView = new commandView({ model: new command() }); }); return form; })(Labs, Backbone);
When the form is submitted a command is sent (PUT) to an ApiController which sends it on to the MessageBus. Quite nicely, Backbone PUTs new models and POSTs updated.
The CommandHandler simulates an aggregate root by handling the command and publishing an event every few seconds for a total of thirty.
public class CommandHandler : IHandleMessages<Command> { public IBus Bus { get; set; } public void Handle(Command command) { Log.For(this).DebugFormat("Handling command - {0}".FormatWith(command)); var raiseEventObservable = Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)).Timestamp(); var stopObservable = Observable.Timer(TimeSpan.FromSeconds(30)).Timestamp(); raiseEventObservable.TakeUntil(stopObservable).Subscribe(timestamped => { var @event = new Event { Id = Guid.NewGuid(), AggregateId = command.AggregateId, Created = DateTime.UtcNow }; Log.For(this).Debug("Publishing event - {0}".FormatWith(@event)); Bus.Publish(@event); }); } }
RX is probably overkill here and more than likely incorrectly implemented. However, I’m fascinated about the relationship between Event Sourcing, CEP and RX. And I need the practice.
The web project subscribes to the events. Whenever an event is raised it forwards it on to all clients connected to the events hub.
public class EventHandler : IHandleMessages<Event> { public void Handle(Event @event) { Log.For(this).Debug("Handling event - {0}".FormatWith(@event)); var connectionManager = AspNetHost.DependencyResolver.Resolve<IConnectionManager>(); var clients = connectionManager.GetClients<Hubs.EventsHub>(); clients.handle(@event); } }
Thanks to the magic of SignalR and dynamic, server code invokes the ‘handle’ function on the events-hub proxy within the browser.
// Signalr client Labs.Hub = (function (labs) { var hub = {}; // Initialisers labs.addInitializer(function () { // Start signalr connection $.connection.hub.start(); // Create events proxy var events = $.connection.events; // Handler for new event events.handle = function (event) { labs.vent.trigger("labs:hub:receivedEvent", event); }; }); return hub; })(Labs);
Fiddler reveals that the client issued a long-poll get request when the application started. Whenever a message is dispatched from the server, this request ends and another starts. Given a better browser websockets would be used.
The Hub raises an ‘labs:hub:receivedEvent’ event along with the data from the server. The blotter handles the message – ensuring the Hub and the Blotter don’t have mixed responsibilities. The Blotter adds the event to the eventCollection and renders it as a row within a table using a cached jquery template.
// Blotter controller Labs.Blotter = (function (labs, backbone) { var blotter = {}; // Models var event = backbone.Model.extend({}); var eventCollection = backbone.Collection.extend({ mode: event }); // Display an individual event var eventView = labs.ItemView.extend({ tagName: "tr", template: "#event-template" }); // Display the list of events. var eventsView = labs.CollectionView.extend({ el: "table", itemView: eventView }); // Event handlers labs.vent.bind("labs:hub:receivedEvent", function (evt) { blotter.Events.add(evt); }); // Initialisers labs.addInitializer(function () { blotter.Events = new eventCollection(); blotter.EventsView = new eventsView({ collection: blotter.Events }); }); return blotter; })(Labs, Backbone);
While the demo illustrates how easy it is to integrate these libraries, there are several issues that ought to be addressed:-
- Functionally, a blotter should probably display events in reverse order and then only n-number of events. This should be quite trivial to implement with Backbone using the Comparator function on the eventCollection.
- The lifetime of the events-hub proxy needs more consideration. Should it really connect as soon as the application starts? What should happen when an error occurs?
- The commandView shouldn’t be responsible for saving its own model. Instead it should raise an event that’s handled by a separate function.
- The empty EventsHub feels like a smell. Its sole purpose is to define an endpoint for clients and to allow the EventHandler to gain access to those clients. SignalR’s PeristentConnection may overcome this as it provides more low-level control.
- Despite using Json.NET SignalR does’t provide a means to control json serialisation when using Hubs; adding a formatter to the global configuration had no effect . Again, the PersistentConnection may improve things.