skip to Main Content

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>(); from Program.cs,
  • change builder.Services.AddScoped<IScopedProcessingServiceB, PlannerOrderStatusUpdateService>(); to builder.Services.AddScoped<IScopedProcessingService, PlannerOrderStatusUpdateService>(); in Program.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


  1. You can just do this:

    builder.Services.AddScoped<PhaseChangeReportService>();
    builder.Services.AddScoped<PlannerOrderStatusUpdateService>();
    

    and use:

    public SomeClassContstructor(PhaseChangeReportService phaseChangeReport...) { ... }
    

    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:

    using IServiceScope scope = _serviceProvider.CreateScope();
    
    var implementationTypes = Assembly.GetExecutingAssembly()
            .GetTypes()
            .Where(x =>
                !x.IsInterface //Optional if you might use the interface on other interfaces
                && x.GetInterface(typeof(IScopedProcessingService).Name) != null
            );
    
    foreach (var implementationType in implementationTypes)
    {
        var service = scope.ServiceProvider.GetService(implementationType);
    
        await (service as IScopedProcessingService).DoWorkAsync(...);
    }
    

    You can use a similar snippet like the above to add your services in your Program.cs

    Login or Signup to reply.
  2. 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:

    private async Task DoWorkAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation($"{nameof(ScopedBackgroundService)} is working.");
    
        using (IServiceScope scope = _serviceProvider.CreateScope())
        {
            var scopedProcessingServices =  scope.ServiceProvider
                .GetServices<IScopedProcessingService>();
    
            // sequential execution. You can opt for parallel  with WaitAll() or similar
            foreach(var scopedProcessingService in scopedProcessingServices)
            {
               await scopedProcessingService.DoWorkAsync(stoppingToken);
            }
        }
    }
    

    I would rename IScopedProcessingService to something like IProcessingServiceHandler , that makes the relations clearer.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search