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