We are writing our first significant Blazor Server application. This application contains two services that we have written. These services essentially run as cron jobs. One of them sends out emails shortly after midnight based on activity from the day before, while the other reviews an Excel spreadsheet that is created daily and then updates some databases based on what it finds.
We would like the services to share a ScopedBackgroundService.cs
, as its functionality is basic and (in theory) would work fine for either service. Here is that file (based primarily upon code provided by https://learn.microsoft.com/en-us/dotnet/core/extensions/scoped-service?pivots=dotnet-7-0):
public sealed class ScopedBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScopedBackgroundService> _logger;
public ScopedBackgroundService(IServiceProvider serviceProvider, ILogger<ScopedBackgroundService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundService)} is running.");
await DoWorkAsync(stoppingToken);
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundService)} is working.");
using (IServiceScope scope = _serviceProvider.CreateScope())
{
IScopedProcessingService scopedProcessingService = scope.ServiceProvider.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWorkAsync(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundService)} is stopping.");
await base.StopAsync(stoppingToken);
}
}
Here is IScopedProcessingService.cs
:
public interface IScopedProcessingService
{
Task DoWorkAsync(CancellationToken stoppingToken);
}
Finally, this is from our Program.cs
:
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddHostedService<ScopedBackgroundService>();
builder.Services.AddScoped<IScopedProcessingService, PhaseChangeReportService>();
builder.Services.AddScoped<IScopedProcessingService, PlannerOrderStatusUpdateService>();
...
However, if I run the code as written above, the first listed service will fail to run, while the second listed service will run. I can change which service will run and which will fail by swapping the order in which the services are listed.
I copied the ScopedBackgroundService.cs
file into a second file (named ScopedBackgroundServiceB.cs
), and did the same to IScopedProcessingService.cs
(into IScopedProcessingServiceB.cs
). I then changed Program.cs
to:
builder.Services.AddHostedService<ScopedBackgroundService>();
builder.Services.AddHostedService<ScopedBackgroundServiceB>();
builder.Services.AddScoped<IScopedProcessingService, PhaseChangeReportService>();
builder.Services.AddScoped<IScopedProcessingServiceB, PlannerOrderStatusUpdateService>();
And then both services will run!
However, this results in duplicated code re ScopedBackgroundService.cs
and IScopedProcessingService.cs
. I’d like to avoid that if possible.
Is it possible to avoid this repeated code, or is it necessary?
Edit With Minimal Repro
Here is how to achieve a minimal repro of this issue.
-
Create a Blazor Server App template in Visual Studio for .NET 7.
-
Modify
Program.cs
to the following:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.AddHostedService<ScopedBackgroundService>();
builder.Services.AddHostedService<ScopedBackgroundServiceB>();
builder.Services.AddScoped<IScopedProcessingService, PhaseChangeReportService>();
builder.Services.AddScoped<IScopedProcessingServiceB, PlannerOrderStatusUpdateService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
- Create
IScopedProcessingService.cs
:
public interface IScopedProcessingService
{
Task DoWorkAsync(CancellationToken stoppingToken);
}
- Create
IScopedProcessServiceB.cs
:
public interface IScopedProcessingServiceB
{
Task DoWorkAsync(CancellationToken stoppingToken);
}
- Create
ScopedBackgroundService.cs
:
public sealed class ScopedBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScopedBackgroundService> _logger;
public ScopedBackgroundService(IServiceProvider serviceProvider, ILogger<ScopedBackgroundService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_logger.LogInformation($"{nameof(ScopedBackgroundService)} has been constructed.");
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundService)} is running.");
await DoWorkAsync(stoppingToken);
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundService)} is working.");
using (IServiceScope scope = _serviceProvider.CreateScope())
{
IScopedProcessingService scopedProcessingService = scope.ServiceProvider.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWorkAsync(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundService)} is stopping.");
await base.StopAsync(stoppingToken);
}
}
- Create
ScopedBackgroundServiceB.cs
:
public sealed class ScopedBackgroundServiceB : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScopedBackgroundServiceB> _logger;
public ScopedBackgroundServiceB(IServiceProvider serviceProvider, ILogger<ScopedBackgroundServiceB> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_logger.LogInformation($"{nameof(ScopedBackgroundServiceB)} has been constructed.");
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundServiceB)} is running.");
await DoWorkAsync(stoppingToken);
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundServiceB)} is working.");
using (IServiceScope scope = _serviceProvider.CreateScope())
{
IScopedProcessingServiceB scopedProcessingService = scope.ServiceProvider.GetRequiredService<IScopedProcessingServiceB>();
await scopedProcessingService.DoWorkAsync(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(ScopedBackgroundServiceB)} is stopping.");
await base.StopAsync(stoppingToken);
}
}
- Create
PhaseChangeReportService.cs
:
public sealed class PhaseChangeReportService : IScopedProcessingService
{
private readonly ILogger<PhaseChangeReportService> _logger;
public PhaseChangeReportService(ILogger<PhaseChangeReportService> logger)
{
_logger = logger;
_logger.LogInformation($"{nameof(PhaseChangeReportService)} is constructed.");
}
public async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(PhaseChangeReportService)} is working.");
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000 * 10, stoppingToken);
_logger.LogInformation($"{nameof(PhaseChangeReportService)} has produced a 10-second unit of work.");
}
}
}
- Create
PlannerOrderStatusUpdateService.cs
:
public class PlannerOrderStatusUpdateService : IScopedProcessingServiceB
{
private readonly ILogger<PlannerOrderStatusUpdateService> _logger;
public PlannerOrderStatusUpdateService(ILogger<PlannerOrderStatusUpdateService> logger)
{
_logger = logger;
_logger.LogInformation($"{nameof(PlannerOrderStatusUpdateService)} is constructed.");
}
public async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($"{nameof(PlannerOrderStatusUpdateService)} is working.");
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000 * 10, stoppingToken);
_logger.LogInformation($"{nameof(PlannerOrderStatusUpdateService)} has produced a 10-second unit of work.");
}
}
}
At this point you can run the solution and the console will output the logs of the creation and execution of the various services. Notably, both PhaseChangeReportService and PlannerOrderStatusUpdateService will execute on their 10-second timers.
However, if you…
- comment out
builder.Services.AddHostedService<ScopedBackgroundServiceB>();
fromProgram.cs
, - change
builder.Services.AddScoped<IScopedProcessingServiceB, PlannerOrderStatusUpdateService>();
tobuilder.Services.AddScoped<IScopedProcessingService, PlannerOrderStatusUpdateService>();
inProgram.cs
, and - update
PlannerOrderStatusUpdateService.cs
to implement ScopedBackgroundService (instead of ScopedBackgroundServiceB),
then only PlannerOrderStatusUpdateService will execute. Nothing
will be logged regarding PhaseChangeReportService in this
circumstance.
Currently the only fix I know for this is the original setup, which involves repeating the code within IScopedProcessingService.cs
and ScopedBackgroundService.cs
. As this violates a fundamental principle of coding (Don’t Repeat Yourself), I want to see if there is another way.
2
Answers
You can just do this:
and use:
You don’t have to register a service with an interface, but will mean your implementation would be through the class and not the interface, which could impact reusability and maintainability when migrating to different technologies.
Unless you do something like this:
You can use a similar snippet like the above to add your services in your
Program.cs
In your first approach, you register one backgroundservice and two dependent services to execute. GetRequiredService just picks the first.
You can make it work by getting all service implementations:
I would rename
IScopedProcessingService
to something likeIProcessingServiceHandler
, that makes the relations clearer.