Search code examples
c#asp.net-coreblazorblazor-server-sidehangfire

Service not being injected within Hangfire job in Blazor Server App


Disclaimer: I'm very new to the C#, ASP.NET Core and Dependency Injection world. I've created a simple Blazor Server App from the default template, which scaffolds a mock weather service and shows data fetched from it in a table. Now I want the table to be automatically updated every five seconds, for which I am using the Hangfire.AspNetCore and Hangfire.MemoryStorage packages. So I've slightly modified the FetchData.razor component to look like this:

@page "/fetchdata"

@using WeatherTest.Data
@using Hangfire

@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    public async Task UpdateForecasts()
    {
      forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
      BackgroundJob.Schedule(() => UpdateForecasts(), TimeSpan.FromSeconds(5));
    }

    protected override async Task OnInitializedAsync()
    {
      await UpdateForecasts();
    }
}

This is the weather service:

using System;
using System.Linq;
using System.Threading.Tasks;

namespace WeatherTest.Data
{
    public class WeatherForecastService
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
        {
            var rng = new Random();
            return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            }).ToArray());
        }
    }
}

And the services configuration:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddHangfire(c => c.UseMemoryStorage());
    services.AddHangfireServer();
}

The problem is that UpdateForecasts() succeeds when called from OnInitializedAsync(), but fails when called by Hangfire throwing the following exception at forecasts = await ForecastService.GetForecastAsync(DateTime.Now);:

System.NullReferenceException: 'Object reference not set to an instance of an object.'

WeatherTest.Pages.FetchData.ForecastService.get returned null.

It seems to me that, since the Hangfire worker runs in another thread, then the WeatherForecastService doesn't get injected. Am I right? Is it possible for a singleton to be used from multiple threads, or each thread should have its own service instance? At last, how should I solve this?


Solution

  • Here's the FetchData component that uses a standard Timer.

    A few points:

    1. I've added a Task.Delay in to emulate a slow connection and to show that the page is refreshing.
    2. StateHasChanged is wrapped like this await this.InvokeAsync(StateHasChanged) to ensure it gets run on the UI context thread.
    3. Implements IDisposable as we have the Timer event handler to disconnect.
    @page "/fetchdata"
    @implements IDisposable
    <PageTitle>Weather forecast</PageTitle>
    
    @using StackOverflow.Server.Data
    @inject WeatherForecastService ForecastService
    
    <h1>Weather forecast</h1>
    
    <p>This component demonstrates fetching data from a service.</p>
    
    <div class="p-2">
        <button class="btn @this.btnCss" @onclick="ToggleTimer">@this.btnText</button>
    </div>
    
    @if (forecasts == null)
    {
        <p><em>Loading...</em></p>
    }
    else
    {
        <table class="table">
            <thead>
                <tr>
                    <th>Date</th>
                    <th>Temp. (C)</th>
                    <th>Temp. (F)</th>
                    <th>Summary</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var forecast in forecasts)
                {
                    <tr>
                        <td>@forecast.Date.ToShortDateString()</td>
                        <td>@forecast.TemperatureC</td>
                        <td>@forecast.TemperatureF</td>
                        <td>@forecast.Summary</td>
                    </tr>
                }
            </tbody>
        </table>
    }
    
    @code {
        private System.Timers.Timer aTimer = new System.Timers.Timer(2000);
    
        private WeatherForecast[]? forecasts;
    
        private string btnCss => aTimer.Enabled ? "btn-danger" : "btn-success";
    
            private string btnText => aTimer.Enabled ? "Stop" : "Start";
    
        protected override async Task OnInitializedAsync()
        {
            aTimer.Elapsed += TimerElapsed;
            aTimer.AutoReset = true;
            aTimer.Enabled = true;
            forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
        }
    
        public async void TimerElapsed(Object? sender, System.Timers.ElapsedEventArgs e)
        {
            forecasts = null;
            await this.InvokeAsync(StateHasChanged);
            await Task.Delay(500);
            forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
            await this.InvokeAsync(StateHasChanged);
        }
    
        public void ToggleTimer()
        {
            aTimer.Enabled = !aTimer.Enabled;
            this.InvokeAsync(StateHasChanged);
        }
    
        public void Dispose()
           => aTimer!.Elapsed -= TimerElapsed;
    }