This article shows how to display Elasticsearch Watcher events in an Angular UI using SignalR. The Elasticsearch Watcher events are sent to a MVC 6 controller in an ASP.NET 5 application. The action method in the controller handles the watcher events and sends the data to the Angular UI using SignalR and the SignalR client library angular-signalr-hub.
Code: https://github.com/damienbod/AspNet5Watcher
2015.09.20: Updated to ASP.NET beta 7
2015.10.20: Updated to ASP.NET beta 8
- Part 1: Settings up the ASP.NET 5 application for Elasticsearch
- Part 2: Using Elasticsearch Watcher to create data events
- Part 3: Visualizing Elasticsearch Watcher events using ASP.NET 5, SignalR and Angular
Setting up SignalR in a ASP.NET 5 MVC 6 application
To use SignalR in an ASP.NET 5 project, it must be added to the project in the project.json file in the dependencies. The following configuration is required:
"Microsoft.AspNet.SignalR.Server": "3.0.0-*"
Here’s the configuration for the ASP.NET 5 project using the beta5 version.
"dependencies": { "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", "Microsoft.AspNet.Mvc": "6.0.0-beta8", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", "NEST": "1.7.0", "Microsoft.Net.Http": "2.2.29", "Microsoft.AspNet.SignalR.Server": "3.0.0-beta8-15656", "NEST.Watcher": "1.0.0-beta2" },
Now SignalR can be added to the Startup.cs class. This just requires the AddSignalR and the UseSignalR methods in the ConfigureServices and Configure methods respectively.
public IConfigurationRoot Configuration { get; set; } public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) { var builder = new ConfigurationBuilder() .SetBasePath(appEnv.ApplicationBasePath) .AddJsonFile("config.json"); Configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.Configure<ApplicationConfiguration>(Configuration.GetSection("ApplicationConfiguration")); services.AddMvc(); services.AddSignalR(options => { options.Hubs.EnableDetailedErrors = true; }); services.AddScoped<SearchRepository, SearchRepository>(); services.AddInstance(_configuration); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.MinimumLevel = LogLevel.Information; loggerFactory.AddConsole(); loggerFactory.AddDebug(); app.UseIISPlatformHandler(); app.UseExceptionHandler("/Home/Error"); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); app.UseSignalR();
Now that SignalR is up and running on the server, a Hub can be created and used. The Hub in this demo is a simple class which inherits from the Hub class.
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace AspNet5Watcher.Hubs { [HubName("alarms")] public class AlarmsHub : Hub { } }
Using the SignalR Hub in a MVC 6 Controller
In the MVC 6 controller, WatcherEventsController which is used to handle the Elasticsearch Watcher events, the AlarmsHub is used to send the events to all the SignalR clients. The Hub is added to the controller using the FromServices attribute.
private IConnectionManager _connectionManager; private IHubContext _alarmsHub; [FromServices] public IConnectionManager ConnectionManager { get { return _connectionManager; } set { _connectionManager = value; _alarmsHub = _connectionManager.GetHubContext<AlarmsHub>(); } }
Then the client can be used in the WatcherEvents/CriticalAlarm method. When Elasticsearch sends new data, the _alarmsHub is used to send the total count of critical data to the SignalR clients and also the last ten critical alarms.
//POST http://localhost:5000/api/WatcherEvents/CriticalAlarm HTTP/1.1 [HttpPost] [Route("CriticalAlarm")] public IActionResult Post([FromBody]int countNewCriticalAlarms) { if (countNewCriticalAlarms != _criticalAlarmsCount ) { var newCriticalAlarmsCount = countNewCriticalAlarms - _criticalAlarmsCount; _criticalAlarmsCount = countNewCriticalAlarms; _alarmsHub.Clients.All.sendtotalalarmscount(countNewCriticalAlarms); _alarmsHub.Clients.All.sendlasttencriticalalarms( _searchRepository.SearchForLastTenCriticalAlarms()); } return new HttpStatusCodeResult(200); }
Using SignalR with a Angular JS client
The SignalR events are implemented in Angular using angular-signalr-hub. This is not necessary but I like its simplicity when using it.
The SignalR packages are added using bower. These are added to the bower.json file in the dependencies property. This will download and install the default bower settings. If you required only the min files, then you need to customize the exportsOverride for the packages.
"angular-signalr-hub": "1.5.0", "signalr": "2.2.0"
Then the javascript files are added to the index.html file before the app.js file.
<script src="lib/jquery/js/jquery.js"></script> <script src="lib/signalr/jquery.signalr.js"></script> <script src="lib/angular-signalr-hub/signalr-hub.js"></script> <script type="text/javascript" src="app.js"></script>
Now the module can be added to your main angular module and also the new alarmsDisplay route can be configured in the app.js file.
(function () { var mainApp = angular.module("mainApp", ["ui.router", "SignalR"]); mainApp.config(["$stateProvider", "$urlRouterProvider", function ($stateProvider, $urlRouterProvider) { $urlRouterProvider.otherwise("/home/createAlarm"); $stateProvider .state("home", { abstract: true, url: "/home", templateUrl: "/templates/home.html" }) .state("createAlarm", { parent: "home", url: "/createAlarm", templateUrl: "/templates/createAlarm.html", controller: "alarmsController", }) .state("alarms", { url: "/alarms", templateUrl: "/templates/alarms.html", controller: "alarmsDisplayController", }) } ] ); })();
The application uses an angular controller to add the SignalR client to the UI. This is implemented in the AlarmsDisplayController controller. The SignalR client implements two listeners for the SignalR methods. When the listeners receive an event, the data is added to the root scope of the application.
(function () { 'use strict'; var module = angular.module("mainApp"); var AlarmsDisplayController = (function () { function AlarmsDisplayController($scope, log, alarmsService, $rootScope, Hub, $timeout) { $scope.Vm = this; this.$scope = $scope; this.alarmsService = alarmsService; this.log = log; this.$rootScope = $rootScope; this.Hub = Hub; this.$timeout = $timeout; this.log.info("AlarmsDisplayController called"); this.Name = "Displaying Watcher alarms"; var hub = new Hub('alarms', { //client side methods listeners: { 'sendtotalalarmscount': function (count) { $rootScope.AlarmsCount = count; $rootScope.$apply(); console.log("Recieved SendTotalAlarmsCount:" + count); }, 'sendlasttencriticalalarms': function (lasttenalarms) { $rootScope.LastTenCriticalAlarms = lasttenalarms; $rootScope.$apply(); console.log(lasttenalarms); }, }, //handle connection error errorHandler: function (error) { console.error(error); }, stateChanged: function (state) { switch (state.newState) { case $.signalR.connectionState.connecting: console.log("signalR.connectionState.connecting" + state.newState) break; case $.signalR.connectionState.connected: console.log("signalR.connectionState.connected" + state.newState) break; case $.signalR.connectionState.reconnecting: console.log("signalR.connectionState.reconnecting" + state.newState) break; case $.signalR.connectionState.disconnected: console.log("signalR.connectionState.disconnected" + state.newState) break; } } }); } return AlarmsDisplayController; })(); // this code can be used with uglify module.controller("alarmsDisplayController", [ "$scope", "$log", "alarmsService", '$rootScope', 'Hub', '$timeout', AlarmsDisplayController ] ); })();
Once the controller is completed, the data can be used in the alarms display HTML file in the templates folder in the wwwroot. This HTML will be updated every time a SignalR event is sent to all the listening clients.
<div class="container"> <div class="row"> <div class="col-sm-9"> <h4>{{Vm.Name}}, Total critical alarms:</h4> </div> <div class="col-sm-3"> <div class="alarmsDisplay round-corners-5px"> <p>{{$root.AlarmsCount}}</p> </div> </div> </div> </div> <div class="container"> <h4>Last ten critical alarms</h4> <table class="table"> <thead> <tr> <th>Message</th> <th>Created</th> <th>Id</th> </tr> </thead> <tbody> <tr data-ng-repeat="alarm in $root.LastTenCriticalAlarms"> <td>{{alarm.Message}}</td> <td>{{alarm.Created}}</td> <td>{{alarm.Id}}</td> </tr> </tbody> </table> </div>
Displaying the Elasticsearch Watcher Data
Every time a new critical alarm is added or created, the alarm is saved to Elasticsearch and within 10s, Elasticsearch Watcher notices this and calls the MVC 6 action method. The action method then uses SignalR and the UI is updated in real time.
The HTML page is updated is real time without a refresh using SignalR.
Conclusion
Using Elasticsearch Watcher together with SignalR and ASP.NET 5, some really cool features can be implemented which could be very useful when solving a business problem for a particular use case.
Note
At present the application uses HttpClient directly with Elasticsearch to create and delete the Elasticsearch Watcher, with a not so nice magic string. This will be updated as soon as the next NEST Elasticsearch Watcher is released and supports the watcher features used in this application.
Links:
https://github.com/JustMaier/angular-signalr-hub
https://github.com/aspnet/musicstore
https://github.com/SignalR/SignalR
https://www.elastic.co/guide/en/watcher/current/index.html
http://amsterdam.luminis.eu/2015/06/23/first-steps-with-elastic-watcher/
https://github.com/elastic/elasticsearch-watcher-net
https://www.elastic.co/guide/en/watcher/current/actions.html#actions-index
