Home

Awesome

Blazor client notifications on database record change

This example uses .NET CORE 3.0 Blazor server side to real-time update a HTML page on any database record changes.

<img src="https://github.com/christiandelbianco/blazor-notification-db-record-change/blob/master/Schema.png" />

Based on the observer design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

<img src="https://github.com/christiandelbianco/blazor-notification-db-record-change/blob/master/2019-11-03at21-05-44.gif" />

Database table

Table for which I want to receive notifications every time its content changes:

CREATE TABLE [dbo].[WeatherForecasts](
	[City] [nvarchar](50) NOT NULL,
	[Temperature] [int] NOT NULL
)

Subject

Singleton instance wrapping SqlTableDependency and forwarding record table changes to subscribers:

    public delegate void WeatherForecastDelegate(object sender, WeatherForecastChangeEventArgs args);

    public class WeatherForecastChangeEventArgs : EventArgs
    {
        public WeatherForecast NewWeatherForecast { get; }
        public WeatherForecast OldWeatherForecast { get; }

        public WeatherForecastChangeEventArgs(WeatherForecast newWeatherForecast, WeatherForecast oldWeatherForecast)
        {
            this.NewWeatherForecast = newWeatherForecast;
            this.OldWeatherForecast = oldWeatherForecast;
        }
    }

    public interface IWeatherForecastService
    {
        public event WeatherForecastDelegate OnWeatherForecastChanged;
        IList<WeatherForecast> GetForecast();
    }

    public class WeatherForecastService : IWeatherForecastService, IDisposable
    {
        private const string TableName = "WeatherForecasts";
        private SqlTableDependency<WeatherForecast> _notifier;
        private IConfiguration _configuration;
        
        public event WeatherForecastDelegate OnWeatherForecastChanged;

        public WeatherForecastService(IConfiguration configuration)
        {
            _configuration = configuration;

            _notifier = new SqlTableDependency<WeatherForecast>(_configuration["ConnectionString"], TableName);
            _notifier.OnChanged += this.TableDependency_Changed;
            _notifier.Start();
        }

        private void TableDependency_Changed(object sender, RecordChangedEventArgs<WeatherForecast> e)
        { 
            if (this.OnWeatherForecastChanged != null)
            {
                this.OnWeatherForecastChanged(this, new WeatherForecastChangeEventArgs(e.Entity, e.EntityOldValues));
            }
        }

        public IList<WeatherForecast> GetForecast()
        {
            var result = new List<WeatherForecast>();

            using (var sqlConnection = new SqlConnection(_configuration["ConnectionString"]))
            {
                sqlConnection.Open();

                using (var command = sqlConnection.CreateCommand())
                {
                    command.CommandText = "SELECT * FROM " + TableName;
                    command.CommandType = CommandType.Text;

                    using (SqlDataReader reader = command.ExecuteReader())
                    {
                        if (reader.HasRows)
                        {
                            while (reader.Read())
                            {
                                result.Add(new WeatherForecast
                                {
                                    City = reader.GetString(reader.GetOrdinal("City")),
                                    Temperature = reader.GetInt32(reader.GetOrdinal("Temperature"))
                                });
                            }
                        }
                    }
                }
            }

            return result;
        }

        public void Dispose()
        {
            _notifier.Stop();
            _notifier.Dispose();
        }
    }

Registration as Singleton:

public void ConfigureServices(IServiceCollection services)
{
    ...
    ...
    services.AddSingleton<IWeatherForecastService, WeatherForecastService>();
}

Observer

Index.razor page code (event subscriber):

@page "/"

@using DataBaseRecordChaneNotificationWithBlazor.Data

@inject IWeatherForecastService ForecastService
@implements IDisposable

<h1>Weather forecast</h1>

<p>Immediate client notification on record table change with Blazor</p>

<table class="table">
    <thead>
        <tr>
            <th>City</th>
            <th>Temp. (C)</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var forecast in forecasts)
        {
            <tr>
                <td>@forecast.City</td>
                <td>@forecast.Temperature</td>
            </tr>
        }
    </tbody>
</table>

@code {
    IList<WeatherForecast> forecasts;

    protected override void OnInitialized()
    {
        this.ForecastService.OnWeatherForecastChanged += this.WeatherForecastChanged;
        this.forecasts = this.ForecastService.GetForecast();
    }

    private async void WeatherForecastChanged(object sender, WeatherForecastChangeEventArgs args)
    {
        var recordToupdate = this.forecasts.FirstOrDefault(x => x.City == args.NewWeatherForecast.City);
        if (recordToupdate == null)
        {
            this.forecasts.Add(args.NewWeatherForecast);
        }
        else
        {
            recordToupdate.Temperature = args.NewWeatherForecast.Temperature;
        }

        await InvokeAsync(() =>
        {
            base.StateHasChanged();
        });
    }

    public void Dispose()
    {
        this.ForecastService.OnWeatherForecastChanged += this.WeatherForecastChanged;
    }
}

More info on https://github.com/christiandelbianco/monitor-table-change-with-sqltabledependency.