Chuck Norris jokes live with Asp.Net Core 2.1

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.
- Running demo
- This post is part of the Geneva .Net User Group Meetup on Tuesday, October 30, 2018 <!-- Slides -->
- The sources are available on GitHub
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;
}
}
}