Building real time charts with Angular 5, Google Charts, SignalR Core, .NET Core 2, Entity Framework Core 2 and SqlTable dependency, part 1

 

This series of articles aims to provide a proof of concept for building real time charts with latest technologies.

For your information this project can be reused on other projects Angular 5 and prior.

Before preparing the project, we will review the technologies used and others requirements.

Overview

All the following instructions are based on my GitHub project

Here is the architecture of the project :

Database

Sql Server, localDb with Visual Studio 2017 is correct to make it work

Front End technologies

BackEnd technologies

SignalR Core provides real time connection to database, and SqlTableDependency  checks any changes on your Sql database and notify your app when changes occur.

Setup your environment

Enable service broker

The Service Broker object, built-in from SQL Server 2005, is to provide a database messaging tool for managing data flows between SQL Servers asynchronously, serialized, and transactional.

In our project it wil allow to notify you app on any changes that occur.

ALTER DATABASE SignalRDemo SET ENABLE_BROKER with rollback immediate

Create SQL table for the project

USE [SignalRDemo]USE [SignalRDemo]GO
/****** Object:  Table [dbo].[GaugesData]    Script Date: 2017-12-30 14:51:37 ******/SET ANSI_NULLS ONGO
SET QUOTED_IDENTIFIER ON GO
CREATE TABLE [dbo].[GaugesData]( [Id] [int] NOT NULL, [Memory] [int] NOT NULL, [Cpu] [int] NOT NULL, [Network] [int] NOT NULL, 
CONSTRAINT [PK_GaugesData] PRIMARY KEY CLUSTERED ( 
[Id] ASC
)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]) ON [PRIMARY]
GO

Then populate table with data (minimum 0, maximum 100) for each field

Install Angular-CLI 1.6.3 or later

npm install -g angular-cli@1.6.3

Create an Angular 5 project

ng new myApp

Download and install the SignalR client side library

npm install @aspnet/signalr-client

Ensure you have installed required tools and SDK for Back End

You will need Visual Studio 2017 and .Net Core 2

Install required Nuget Packages

PM > Install-Package Microsoft.AspNetCore.SignalR -Version 1.0.0-alpha2-final 

PM > Install-Package Microsoft.EntityFrameworkCore -Version 2.0.1 

PM > Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.0.1 

PM > Install-Package SqlTableDependency -Version 6.1.0

 

Now let’s see in details how the .NET Core project works : part 2

Building real time charts with Angular 5, Google Charts, SignalR Core, .NET Core 2, Entity Framework Core 2 and SqlTable dependency, part 3

 

<< Back to part 2

We are done with the project setup and the back end implementation, now let’s focus the front end solution.

What does it look like ?

 

We have :

  • A folder that contains the gauge chart component (gaugeschart.component.html and gaugeschart.component.ts)
  • A folder that contains a gauge chart service and a Google Charts base service (google-gauges-chart.service.ts and google-charts.base.service.ts)
  • A folder that contains environments files
  • A folder that contains a strongly typed model for the gauge chart (gauge.ts)
  • Finally at the root of src folder the defaults files components and module (app component files and app module file)

 

Implementation

Gauge.ts

export class GaugeModel {
    public id: number;
    public cpu: number;
    public memory: number;
    public network: number;
}

google-charts.base.service.ts and google-gauges-chart.service.ts

For more explanations about these files work, you can check a preivous article I wrote some times before : http://anthonygiretti.com/2017/10/12/using-google-charts-in-angular-4-project-part-2/

declare var google: any;

export class GoogleChartsBaseService {
  constructor() { 
    google.charts.load('current', {'packages':['corechart','gauge']});
  }

  protected buildChart(data: any[], chartFunc: any, options: any) : void {
    var func = (chartFunc, options) => {
      var datatable = google.visualization.arrayToDataTable(data);
      chartFunc().draw(datatable, options);
    };   
    var callback = () => func(chartFunc, options);
    google.charts.setOnLoadCallback(callback);
  }
  
}

 

import { GoogleChartsBaseService } from './google-charts.base.service';
import { Injectable } from '@angular/core';

declare var google: any;

@Injectable()
export class GoogleGaugesChartService extends GoogleChartsBaseService {

  constructor() { super(); }

  public BuildGaugesChart(elementId: String, data: any[], config: any) : void {
    var chartFunc = () => { return new google.visualization.Gauge(document.getElementById(elementId)); };
    this.buildChart(data, chartFunc, config);
  }
}

Gauge chart component

As you can see, the component is decorelated from Google Charts because I inject within a Google Gauge chart service, it can be easily replaced by another Javascript library such as canvas.js

import { Component, Input, OnInit } from '@angular/core';

import { GoogleGaugesChartService } from '../services/google-gauges-chart.service';
import { OnChanges } from '@angular/core/src/metadata/lifecycle_hooks';

declare var google: any;


@Component({
  selector: 'gauges-chart',
  templateUrl: './gaugeschart.component.html'
})
export class GaugesChartComponent implements OnInit, OnChanges {

    @Input() data: any[];
    @Input() config: any;
    @Input() elementId: String;
    
    constructor(private _gaugesChartService: GoogleGaugesChartService) {}

    /* Only chart data !!!!!!! */
    ngOnChanges(changes) {
        if (changes.data != undefined) {
            this.data = changes.data.currentValue;
            this._gaugesChartService.BuildGaugesChart(this.elementId, this.data, this.config);
        }      
    }

    ngOnInit(): void {
        this._gaugesChartService.BuildGaugesChart(this.elementId, this.data, this.config); 
    }
}

I used ngOnInit for the component initialization with its first binding to data, then we need to “listen” any moditication of data that are broadcasted with the specific event ngOnChanges and we ensure it’s data that are modified if (changes.data != undefined)

Component Html :

<div id="{{elementId}}" style="width: 400px; height: 120px;"></div>

App component

This is most insteresting part, we will see hoz the client side library for SignalR works

import { AfterContentInit, AfterViewInit, Component, OnInit } from '@angular/core';

import { environment as Environment } from '../environments/environment';
import { GaugeModel } from '../models/gauge';
import { HubConnection } from '@aspnet/signalr-client';

declare var google: any;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  private _hubConnection: HubConnection;

  public data:any[] = [
    ['Label', 'Value'],
    ['Memory', 0],
    ['CPU', 0],
    ['Network', 0]
  ];
  public elementId:String = "Gauge1";
  public config:any = {
    width: 400, height: 120,
    redFrom: 90, redTo: 100,
    yellowFrom:75, yellowTo: 90,
    minorTicks: 5
  };


  constructor() {
  }

  public ngOnInit() {
    
    this._hubConnection = new HubConnection(Environment.hubUrl);
    
    this._hubConnection
      .start()
      .then(() => this._hubConnection.invoke('GetGaugesData').catch(err => console.error(err)))
      .catch(err => console.log('Error while establishing connection :('));

    var that = this;
    this._hubConnection.on('GetGaugesData', (data: GaugeModel) => {
      this.data = [
        ['Label', 'Value'],
        ['Memory', data.memory],
        ['CPU', data.cpu],
        ['Network', data.network]
      ];
    });
    
  }

}

Firstly, we need to isntantiate a HubConnection, like this : this._hubConnection = new HubConnection(Environment.hubUrl); it takes in parameter the Hub Url defined in environment.ts :

export const environment = {
production:false,
hubUrl:"http://localhost:33383/gauges"
};

Then we open the connection and define a callback when it succeeds or handle error when it occur :

this._hubConnection
.start()
.then(() => this._hubConnection.invoke('GetGaugesData').catch(err => console.error(err)))
.catch(err => console.log('Error while establishing connection :('));

When it succeed we invoke the method that populate initially our chart :

this._hubConnection.invoke('GetGaugesData')

Once we are done with the inialization, we subscribe to any change and modify data :

this._hubConnection.on('GetGaugesData', (data: GaugeModel) => {
      this.data = [
        ['Label', 'Value'],
        ['Memory', data.memory],
        ['CPU', data.cpu],
        ['Network', data.network]
      ];
    });

Component Html :

<h2>Real-Time Gaugeschart by Google with SignalR Core and EntityFramework Core 2 / SQLTableDependency</h2>

<div class="divTable">
   <divclass="divTableBody">
      <divclass="divTableRow">
         <divclass="divTableCell"><gauges-chart [data]="data" [config]="config" [elementId]="elementId"></gauges-chart></div>
       </div>
   </div>
</div>

Component Css :

.divTable{
    display: table;
    width: 100%;
}

.divTableRow {
    display: table-row;
}

.divTableHeading {
    background-color: #EEE;
    display: table-header-group;
}

.divTableCell, .divTableHead {
    border: 1px solid #999999;
    display: table-cell;
    padding: 3px10px;
    text-align: center;
    margin: auto;
}

.divTableHeading {
    background-color: #EEE;
    display: table-header-group;
    font-weight: bold;
}

.divTableFoot {
    background-color: #EEE;
    display: table-footer-group;
    font-weight: bold;
}

.divTableBody {
    display: table-row-group;
}

Appmodule.ts :

import { AppComponent } from './app.component';
import { BrowserModule } from '@angular/platform-browser';
import { GaugesChartComponent } from './charts/gaugeschart.component';
import { GoogleChartsBaseService } from './services/google-charts.base.service';
import { GoogleGaugesChartService } from './services/google-gauges-chart.service';
import { NgModule } from '@angular/core';

@NgModule({
declarations: [
AppComponent,
GaugesChartComponent
],

imports: [
BrowserModule
],

providers: [GoogleChartsBaseService,GoogleGaugesChartService],
bootstrap: [AppComponent]
})

export class AppModule { }

Index.html

<!doctype html>
<html lang="en">
<head>
   <metacharset="utf-8">
   <title>Angular5SignalRCoreEFCore2GoogleCharts</title>
   <basehref="/">
   <script type="text/javascript"src="https://www.gstatic.com/charts/loader.js"></script>
   <metaname="viewport"content="width=device-width, initial-scale=1">
   <linkrel="icon"type="image/x-icon"href="favicon.ico">
</head>
<body>
    <app-root></app-root>
</body>
</html>

Dont forget to put Google Charts reference 🙂 (loader.js)

Tha ‘s the end did you enjoy it? 🙂

Try to play with data ! 😮

Building real time charts with Angular 5, Google Charts, SignalR Core, .NET Core 2, Entity Framework Core 2 and SqlTable dependency, part 2

 

<< Back to part 1

Let’s take a look on what does the project look like :

 

To make it work this project contains :

  • A DbContext (GaugesContext.cs) for EntityFramework Core
  • A Hub (GaugeHub.cs) for SignalR that broadcast data
  • A Model that contains strongly typed data to send (Gauge.cs)
  • A Repository exposed with Entity Framework and its Interface (GaugeRepository.cs and IGaugeRepository.cs)
  • A Subscription to Gauge sql table with SqlTableDependency and its Interface (GaugeDatabaseSubscription.cs and IDatabaseSubscription)
  • Two Extension methods that extends IServiceCollection (AddDbContextFactory.cs) and IApplicationBuilder (UseSqlTableDependency.cs)
  • And Startup.cs and Program.cs

Let’s describe their implementation :

Gauge Model

using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace SignalrCoreDemoWithSqlTableDependency.Models
{
    [Table("GaugesData")]
    public class Gauge
    {
        [JsonProperty("id")]
        [Key]
        public int Id { get; set; }

        [JsonProperty("memory")]
        public int Memory { get; set; }

        [JsonProperty("cpu")]
        public int Cpu { get; set; }

        [JsonProperty("network")]
        public int Network { get; set; }
    }
}

Gauge DbContext

using Microsoft.EntityFrameworkCore;
using SignalrCoreDemoWithSqlTableDependency.Models;

namespace SignalrCoreDemoWithSqlTableDependency.EF
{
    public class GaugeContext : DbContext
    {
        public GaugeContext(DbContextOptions options)
            : base(options)
        {
        }

        public virtual DbSet Gauges { get; set; }
    }
}

Gauge Repository

using SignalrCoreDemoWithSqlTableDependency.EF;
using SignalrCoreDemoWithSqlTableDependency.Models;
using System;
using System.Linq;

namespace SignalrCoreDemoWithSqlTableDependency.Repository
{
    public interface IGaugeRepository
    {
       Gauge Gauge { get; }
    }
    public class GaugeRepository : IGaugeRepository
    {
        private Func _contextFactory;

        public Gauge Gauge => GetGauge();

        public GaugeRepository(Func context)
        {
            _contextFactory = context;
        }

        private Gauge GetGauge()
        {
            using (var context = _contextFactory.Invoke())
            {
                return context.Gauges.FirstOrDefault();
            }
        }
    }
}

Just take a look at how the GaugeContext is injected, I inject a Func that provide an instance of DbContext?

Why? because the lifecycle of our hub must be singleton, so all interdependencies requires between classes have to be singleton too, but we can’t use a DbContext in singleton because the Entity Framework context is not thread safe and in concurrence scenarios the context has a lot of issues.

Gauge Hub

using Microsoft.AspNetCore.SignalR;
using SignalrCoreDemoWithSqlTableDependency.Repository;
using System.Threading.Tasks;

namespace SignalrCoreDemoWithSqlTableDependency.Hubs
{
    public class GaugeHub : Hub
    {
        private readonly IGaugeRepository _repository;

        public GaugeHub(IGaugeRepository repository)
        {
            _repository = repository;
        }

        public async Task GetGaugesData()
        {
            await Clients.All.InvokeAsync("GetGaugesData", _repository.Gauge);
        }
    }
}

The Gauge Hub must inherit from Hub class

“GetGaugesData” is the name of the opretation the client will subscribe to get Gauges data (Clients.All.InvokeAsync(“GetGaugesData”, _repository.Gauge))

GaugeDatabaseSubscription

This is the nost exciting part, this class provides subscription to any modification on Gauge table and notify our app of these modifications, then SignalR will boradcoast it to the client, let’s take a look :

using Microsoft.AspNetCore.SignalR;
using SignalrCoreDemoWithSqlTableDependency.Hubs;
using SignalrCoreDemoWithSqlTableDependency.Models;
using SignalrCoreDemoWithSqlTableDependency.Repository;
using System;
using TableDependency.Enums;
using TableDependency.EventArgs;
using TableDependency.SqlClient;

namespace SignalrCoreDemoWithSqlTableDependency.SqlTableDependencies
{
    public interface IDatabaseSubscription
    {
       void Configure(string connectionString);
    }
    public class GaugeDatabaseSubscription : IDatabaseSubscription
    {
        private bool disposedValue = false;
        private readonly IGaugeRepository _repository;
        private readonly IHubContext _hubContext;
        private SqlTableDependency _tableDependency;

        public GaugeDatabaseSubscription(IGaugeRepository repository, IHubContext hubContext)
        {
            _repository = repository;
            _hubContext = hubContext;
        }

        public void Configure(string connectionString)
        {
            _tableDependency = new SqlTableDependency(connectionString, null, null, null, null, DmlTriggerType.All);
            _tableDependency.OnChanged += Changed;
            _tableDependency.OnError += TableDependency_OnError;
            _tableDependency.Start();

            Console.WriteLine("Waiting for receiving notifications...");
        }

        private void TableDependency_OnError(object sender, TableDependency.EventArgs.ErrorEventArgs e)
        {
            Console.WriteLine($"SqlTableDependency error: {e.Error.Message}");
        }

        private void Changed(object sender, RecordChangedEventArgs e)
        {
            if (e.ChangeType != ChangeType.None)
            {
                // TODO: manage the changed entity
                var changedEntity = e.Entity;
                _hubContext.Clients.All.InvokeAsync("GetGaugesData", _repository.Gauge);
            }
        }

        #region IDisposable

        ~GaugeDatabaseSubscription()
        {
            Dispose(false);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    _tableDependency.Stop();
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion
    }
}

Important things  :

We want to be notified on any changes on the Gaube table, thta’s why I put DmlTriggerType.All on the SqlTableDependency constructor

In Changed event we invoke the broadcast data modification via GaugeHub (_hubContext.Clients.All.InvokeAsync(“GetGaugesData”, _repository.Gauge))

AddDbContextFactory and UseSqlTableDependency

We need these extensions methods to apply in the Startup.cs  our app injection dependency for our GaugeContext and apply as well our SqlTableDependency service

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;

namespace SignalrCoreDemoWithSqlTableDependency
{
    public static class AddDbContextFactoryHelper
    {
        public static void AddDbContextFactory(this IServiceCollection services, string connectionString) where DataContext : DbContext
        {
            services.AddSingleton<Func>((ctx) =>
            {
                var options = new DbContextOptionsBuilder()
                    .UseSqlServer(connectionString)
                    .Options;

                return () => (DataContext)Activator.CreateInstance(typeof(DataContext), options);
            });
        }
    }
}
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SignalrCoreDemoWithSqlTableDependency.SqlTableDependencies;

namespace SignalrCoreDemoWithSqlTableDependency
{
    public static class UseSqlTableDependencyHelpers
    {
        public static void UseSqlTableDependency(this IApplicationBuilder services, string connectionString) where T : IDatabaseSubscription
        {
            var serviceProvider = services.ApplicationServices;
            var subscription = serviceProvider.GetService();
            subscription.Configure(connectionString);
        }
    }
}

At last Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using SignalrCoreDemoWithSqlTableDependency.EF;
using SignalrCoreDemoWithSqlTableDependency.Hubs;
using SignalrCoreDemoWithSqlTableDependency.Repository;
using SignalrCoreDemoWithSqlTableDependency.SqlTableDependencies;

namespace SignalrCoreDemoWithSqlTableDependency
{
    public class Startup
    {
        private const string ConnectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog=SignalRDemo;Integrated Security=SSPI;";
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddSignalR();

            // dependency injection
            services.AddDbContextFactory(ConnectionString);
            services.AddSingleton<IGaugeRepository, GaugeRepository>();
            services.AddSingleton<IDatabaseSubscription, GaugeDatabaseSubscription>();
            services.AddSingleton<IHubContext, HubContext>();

            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader());
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseCors("CorsPolicy");

            app.UseSignalR(routes =>
            {
                routes.MapHub("gauges");
            });

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            app.UseSqlTableDependency(ConnectionString);

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}

 

The back end is now completly set up 🙂

Let’s go to see how we setup the front end: part 3