Chuck Norris jokes live with Asp.Net Core 2.1

This post explains how to send Chuck Norris joke from an Asp.Net Core application to a Javascript client every five seconds. Technically there is a service running in the background on the server and notifications to the browser using SignalR.

This will be the opportunity to introduce some new features of Asp.Net Core 2.1: BackgroundService, HttpClientFactory, SignalR Hub, SignalR Javascript client and the MediatR library with IServiceScopeFactory. We'll also have a look at LibMan for managing client libraries.

We will make two version of the application:

  • the first is a direct implementation focused on the result
  • the second will use new Asp.Net Core functionality to improve the implementation

Basic version

Create a new Asp.Net Core 2.1 Razor web application:

Background service

There is a new class BackgroundService in Asp.Net Core 2.1 to implement background service (see also Implementing background tasks in .NET Core 2.x webapps or microservices with IHostedService and the BackgroundService class)

Create a new class ChuckNorrisJokesService that extends BackgroundService and implement abstact method Task ExecuteAsync(CancellationToken stoppingToken) :

public class ChuckNorrisJokesService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var joke = await GetJoke();
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }

    private async Task<string> GetJoke()
    {
        using (var httpClient = new HttpClient())
        {
            var result = await httpClient.GetStringAsync("https://api.chucknorris.io/jokes/random");
            return result;
        }
    }
}

Configure the service:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IHostedService, ChuckNorrisJokesService>();
}

SignalR Hub

Create a empty class that extend Hub

public class ChuckNorrisJokesHub : Hub
{
}

Configure the service: services.AddSignalR(); and add a route for the hub :

app.UseSignalR(routes =>
{
    routes.MapHub<ChuckNorrisJokesHub>("/chucknorrisjokeshub");
});

Background service with SignalR hub

public class ChuckNorrisJokesService : BackgroundService
{
    private readonly IHubContext<ChuckNorrisJokesHub> _hubContext;

    public ChuckNorrisJokesService(IHubContext<ChuckNorrisJokesHub> hubContext)
    {
        _hubContext = hubContext;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var joke = await GetJoke();
            await _hubContext.Clients.All.SendAsync("ReceiveJoke", joke, stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }

    private async Task<string> GetJoke()
    {
        using (var httpClient = new HttpClient())
        {
            var result = await httpClient.GetStringAsync("https://api.chucknorris.io/jokes/random");
            return result;
        }
    }
}

Razor page and SignalR Javascript client

With LibMan add the SignalR javascript client:

Add a new Razor page Joke :

@page
@model ChuckNorrisJokesLiveBasic.Pages.JokeModel

@{
    ViewData["Title"] = "Joke";
}

<h2>Chuck Norris Joke</h2>

<p><strong><span id="joke">Waiting to start...</span></strong></p>

@section Scripts
{
    <script src="~/lib/signalr/dist/browser/signalr.js"></script>
    <script>
        var connection = new signalR.HubConnectionBuilder().withUrl("/chucknorrisjokeshub").build();
        connection.on("ReceiveJoke",
            function (joke) {
                var json = JSON.parse(joke);
                document.getElementById("joke").innerHTML = json.value;
            });
        connection.start().catch(function (err) {
            return console.error(err.toString());
        });
    </script>
}

Start the application and you see the client not connected:

and after a few moment the server hub send Chuck Norris joke to the client every fice seconds:

Improvements

HttpClientFactory

Use HttpClientFactory to implement resilient HTTP requests

Modify the background service :

public class ChuckNorrisJokesService : BackgroundService
    {
        private readonly IHubContext<ChuckNorrisJokesHub> _hubContext;
        private readonly IHttpClientFactory _httpClientFactory;

        public ChuckNorrisJokesService(IHubContext<ChuckNorrisJokesHub> hubContext, IHttpClientFactory httpClientFactory)
        {
            _hubContext = hubContext;
            _httpClientFactory = httpClientFactory;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var joke = await GetJoke();
                await _hubContext.Clients.All.SendAsync("ReceiveJoke", joke, stoppingToken);
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }

        private async Task<string> GetJoke()
        {
            using (var httpClient = _httpClientFactory.CreateClient())
            {
                var result = await httpClient.GetStringAsync("https://api.chucknorris.io/jokes/random");
                return result;
            }
        }
    }

Typed SignalR Hub

Add the interface and class implementation :

public interface IChuckNorrisJokesHub
    {
        Task ReceiveJoke(string joke);
    }

    public class ChuckNorrisJokesHub : Hub<IChuckNorrisJokesHub>
    {
        public async Task SendJoke(string joke)
        {
            await Clients.All.ReceiveJoke(joke);
        }
    }

Modify the background service:

    public class ChuckNorrisJokesService : BackgroundService
    {
        private readonly IHubContext<ChuckNorrisJokesHub, IChuckNorrisJokesHub> _hubContext;
        private readonly IHttpClientFactory _httpClientFactory;

        public ChuckNorrisJokesService(IHubContext<ChuckNorrisJokesHub, IChuckNorrisJokesHub> hubContext, IHttpClientFactory httpClientFactory)
        {
            _hubContext = hubContext;
            _httpClientFactory = httpClientFactory;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var joke = await GetJoke();
                await _hubContext.Clients.All.ReceiveJoke(joke);
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }

        private async Task<string> GetJoke()
        {
            using (var httpClient = _httpClientFactory.CreateClient())
            {
                var result = await httpClient.GetStringAsync("https://api.chucknorris.io/jokes/random");
                return result;
            }
        }
    }

Decouple background service and hub with MediatR

MediatR Simple, unambitious mediator implementation in .NET

Add the Nuget packages:

    <PackageReference Include="MediatR" Version="5.1.0" />
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="5.1.0" /> 

Update Startup.cs with services.AddMediatR();

Add the command and command handler:

public class SendJokeCommand : IRequest<Unit>
    {
        public string Joke { get; set; }
    }

    public class MediatRCommand : IRequestHandler<SendJokeCommand, Unit>
    {
        private readonly IHubContext<ChuckNorrisJokesHub, IChuckNorrisJokesHub> _hubContext;

        public MediatRCommand(IHubContext<ChuckNorrisJokesHub, IChuckNorrisJokesHub> hubContext)
        {
            _hubContext = hubContext;
        }

        public async Task<Unit> Handle(SendJokeCommand request, CancellationToken cancellationToken)
        {
            await _hubContext.Clients.All.ReceiveJoke(request.Joke);
            return Unit.Value;
        }
    }

Modify the background service:

    public class ChuckNorrisJokesService : BackgroundService
    {
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly IMediator _mediator;
    
        public ChuckNorrisJokesService(IHttpClientFactory httpClientFactory, IMediator mediator)
        {
            _httpClientFactory = httpClientFactory;
            _mediator = mediator;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var joke = await GetJoke();
                var sendJokeCommand = new SendJokeCommand { Joke = joke };
                await _mediator.Send(sendJokeCommand, stoppingToken);
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }

        private async Task<string> GetJoke()
        {
            using (var httpClient = _httpClientFactory.CreateClient())
            {
                var result = await httpClient.GetStringAsync("https://api.chucknorris.io/jokes/random");
                return result;
            }
        }
    }

Try the application and... boom:

A singleton service cannot depend on a scoped service. To resolve the problem we use IServiceScopeFactory in the background service (see: IServiceScopeFactory documentation, ASP.NET Core Dependency Injection):

    public class ChuckNorrisJokesService : BackgroundService
    {
        private readonly IHttpClientFactory _httpClientFactory;
        private readonly IServiceScopeFactory _scopeFactory;

        public ChuckNorrisJokesService(IHttpClientFactory httpClientFactory, IServiceScopeFactory scopeFactory)
        {
            _httpClientFactory = httpClientFactory;
            _scopeFactory = scopeFactory;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var joke = await GetJoke();
                var sendJokeCommand = new SendJokeCommand { Joke = joke };
                using (var scope = _scopeFactory.CreateScope())
                {
                    var mediator = scope.ServiceProvider.GetService<IMediator>();
                    await mediator.Send(sendJokeCommand, stoppingToken);
                }
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
        }

        private async Task<string> GetJoke()
        {
            using (var httpClient = _httpClientFactory.CreateClient())
            {
                var result = await httpClient.GetStringAsync("https://api.chucknorris.io/jokes/random");
                return result;
            }
        }
    }