Periodic background task in Blazor wasm

Viewed 4658

In Blazor wasm, I would like to periodically execute a job (code), even if the user is navigating through the pages (every x min for example).

Is that possible? What would be a practical way?

4 Answers

Create a Service to manager the timer

public class JobExecutedEventArgs : EventArgs {}


public class PeriodicExecutor : IDisposable
{
    public event EventHandler<JobExecutedEventArgs> JobExecuted;
    void OnJobExecuted()
    {
        JobExecuted?.Invoke(this, new JobExecutedEventArgs());
    }

    Timer _Timer;
    bool _Running;

    public void StartExecuting()
    {
        if (!_Running)
        {
            // Initiate a Timer
            _Timer= new Timer();
            _Timer.Interval = 300000;  // every 5 mins
            _Timer.Elapsed += HandleTimer;
            _Timer.AutoReset = true;
            _Timer.Enabled = true;

            _Running = true;
        }
    }
    void HandleTimer(object source, ElapsedEventArgs e)
    {
        // Execute required job

        // Notify any subscribers to the event
        OnJobExecuted();
    }
    public void Dispose()
    {
        if (_Running)
        {
            // Clear up the timer
        }
    }
}

Register it in Program.cs

builder.Services.AddSingleton<PeriodicExecutor>();

Request it and start it in home page initialization

@page "/home"
@inject PeriodicExecutor PeriodicExecutor

@code {
    protected override void OnInitialized()
    {
        PeriodicExecutor.StartExecuting();
    }
}

In any component if you want to do something when job executes

@inject PeriodicExecutor PeriodicExecutor
@implements IDisposable

<label>@JobNotification</label>

@code {

   protected override void OnIntiialized()
   {
       PeriodicExecutor.JobExecuted += HandleJobExecuted;
   }
   public void Dispose()
   {
       PeriodicExecutor.JobExecuted -= HandleJobExecuted;
   }

   string JobNotification;
   void HandleJobExecuted(object sender, JobExecutedEventArgs e)
   {
        JobNotification = $"Job Executed: {DateTime.Now}";
   }
}

You can treat MainLayout.razor or App.razor as 'normal pages' for this.

Use MainLayout when you have something to show on screen:

MainLayout.razor

.... 
<div>Time=@theTime</div>

@code
{
    protected override void OnInitialized()
    {
        Ticker();
        base.OnInitialized();
    }

    string theTime;

    // use an async-void or a timer. An async-void needs no cleanup. 
    async void Ticker()
    {
        while(true)
        {
            await Task.Delay(2_000);
            theTime = DateTime.Now.ToLongTimeString();

            StateHasChanged();   // refresh everything
        }
    }

}

If you are using an ASP.NET Core Hosted flavor of Blazor WebAssembly, you can use a BackgroundService. For example:

MyBackgroundService.cs

public class MyBackgroundService : BackgroundService, IDisposable
{
    private readonly ILogger<CollectionService> _logger;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public MyBackgroundService(ILogger<CollectionService> logger, IServiceScopeFactory serviceScopeFactory)
    {
        _logger = logger;
        _serviceScopeFactory = serviceScopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("My Background Service is starting.");

        //Do your work here...

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<MyBackgroundService>();
    services.AddHostedService(provider => provider.GetService<MyBackgroundService>());

One benefit of this solution is the service will start running regardless of whether a user navigates to any particular page on the site. Or even if no user access the site.

The simplest way within a Blazor component would be to create a Timer, subscribe to Elapsed, and call InvokeAsync to update any state:

// In a @code block, or a Component.razor.cs file:
private readonly Timer _jobTimer = new() 
{
    AutoReset = true,
    Enabled = true,
    Interval = 60000, // 1 minute
};

protected override void OnInitialized() {
    _jobTimer.Elapsed += JobMethod; 
    // ^ don't forget to unsubscribe and dispose in IDisposable.Dispose!
}

private async void JobMethod(object sender, ElapsedEventArgs e) {
    // do extraneous stuff here
    await InvokeAsync(() => {
        // update state here
        StateHasChanged();
    }
}

protected virtual void Dispose(bool disposing) {
    // however you implement this, but at the very least least:
    _jobTimer.Elapsed -= JobMethod;
    _jobTimer.Dispose();
}

Use this if the job is localized to a single component. For reusability, prefer Neil's answer.

Related