如何在Blazor Server中实现多个范围限定的后台服务而不重复编写代码?

huangapple go评论65阅读模式
英文:

How Can I Have Multiple Scoped Background Services in Blazor Server Without Code Repetition?

问题

Here is the translated content you requested:

我们正在编写我们的第一个重要的Blazor Server应用程序。这个应用程序包含两个我们编写的服务。这些服务基本上作为cron作业运行。其中一个根据前一天的活动在午夜后不久发送电子邮件,而另一个则审核每天创建的Excel电子表格,然后根据它找到的内容更新一些数据库。

我们希望这些服务共享一个名为ScopedBackgroundService.cs的文件,因为它的功能很基本,理论上适用于任何服务。以下是该文件(主要基于[https://learn.microsoft.com/en-us/dotnet/core/extensions/scoped-service?pivots=dotnet-7-0][1]提供的代码):

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);
    }
}

这是IScopedProcessingService.cs

public interface IScopedProcessingService
{
    Task DoWorkAsync(CancellationToken stoppingToken);
}

最后,这是我们的Program.cs的一部分:

var builder = WebApplication.CreateBuilder(args);

...

builder.Services.AddHostedService<ScopedBackgroundService>();

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

...

然而,如果我按照上面的代码运行,第一个列出的服务将无法运行,而第二个列出的服务将运行。通过交换列出服务的顺序,我可以改变哪个服务将运行,哪个将失败。

我将ScopedBackgroundService.cs文件复制到第二个文件中(命名为ScopedBackgroundServiceB.cs),并对IScopedProcessingService.cs执行相同操作(命名为IScopedProcessingServiceB.cs)。然后,我将Program.cs更改为:

builder.Services.AddHostedService<ScopedBackgroundService>();
builder.Services.AddHostedService<ScopedBackgroundServiceB>();

builder.Services.AddScoped<IScopedProcessingService, PhaseChangeReportService>();
builder.Services.AddScoped<IScopedProcessingServiceB, PlannerOrderStatusUpdateService>();

然后两个服务都将运行!

但是,这会导致ScopedBackgroundService.csIScopedProcessingService.cs中的代码重复。如果可能的话,我想避免这种重复。

是否有可能避免这种重复的代码,还是有必要的?

具有最小复制的编辑

以下是如何实现此问题的最小复制。

  • 在Visual Studio for .NET 7中创建Blazor Server App模板。

  • 修改Program.cs如下:

var builder = WebApplication.CreateBuilder(args);

// 将服务添加到容器中。
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();

// 配置HTTP请求管道。
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 默认的HSTS值为30天。您可能需要根据生产情况进行更改,参见https://aka.ms/aspnetcore-hsts。
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();
  • 创建IScopedProcessingService.cs
public interface IScopedProcessingService
{
    Task DoWorkAsync(CancellationToken stoppingToken);
}
  • 创建IScopedProcessServiceB.cs
public interface IScopedProcessingServiceB
{
    Task DoWorkAsync(CancellationToken stoppingToken);
}
  • 创建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);
    }
}
  • 创建ScopedBackgroundServiceB.cs
public sealed class ScopedBackgroundServiceB : BackgroundService


<details>
<summary>英文:</summary>

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][1]):

public sealed class ScopedBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScopedBackgroundService> _logger;

public ScopedBackgroundService(IServiceProvider serviceProvider, ILogger&lt;ScopedBackgroundService&gt; logger)
{
    _serviceProvider = serviceProvider;
    _logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation($&quot;{nameof(ScopedBackgroundService)} is running.&quot;);

    await DoWorkAsync(stoppingToken);
}

private async Task DoWorkAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation($&quot;{nameof(ScopedBackgroundService)} is working.&quot;);

    using (IServiceScope scope = _serviceProvider.CreateScope())
    {
        IScopedProcessingService scopedProcessingService = scope.ServiceProvider.GetRequiredService&lt;IScopedProcessingService&gt;();

        await scopedProcessingService.DoWorkAsync(stoppingToken);
    }
}


public override async Task StopAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation($&quot;{nameof(ScopedBackgroundService)} is stopping.&quot;);
    
    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&#39;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&lt;ScopedBackgroundService&gt; _logger;
public ScopedBackgroundService(IServiceProvider serviceProvider, ILogger&lt;ScopedBackgroundService&gt; logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_logger.LogInformation($&quot;{nameof(ScopedBackgroundService)} has been constructed.&quot;);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(ScopedBackgroundService)} is running.&quot;);
await DoWorkAsync(stoppingToken);
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(ScopedBackgroundService)} is working.&quot;);
using (IServiceScope scope = _serviceProvider.CreateScope())
{
IScopedProcessingService scopedProcessingService = scope.ServiceProvider.GetRequiredService&lt;IScopedProcessingService&gt;();
await scopedProcessingService.DoWorkAsync(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(ScopedBackgroundService)} is stopping.&quot;);
await base.StopAsync(stoppingToken);
}
}
```
- Create `ScopedBackgroundServiceB.cs`:
```
public sealed class ScopedBackgroundServiceB : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger&lt;ScopedBackgroundServiceB&gt; _logger;
public ScopedBackgroundServiceB(IServiceProvider serviceProvider, ILogger&lt;ScopedBackgroundServiceB&gt; logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_logger.LogInformation($&quot;{nameof(ScopedBackgroundServiceB)} has been constructed.&quot;);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(ScopedBackgroundServiceB)} is running.&quot;);
await DoWorkAsync(stoppingToken);
}
private async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(ScopedBackgroundServiceB)} is working.&quot;);
using (IServiceScope scope = _serviceProvider.CreateScope())
{
IScopedProcessingServiceB scopedProcessingService = scope.ServiceProvider.GetRequiredService&lt;IScopedProcessingServiceB&gt;();
await scopedProcessingService.DoWorkAsync(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(ScopedBackgroundServiceB)} is stopping.&quot;);
await base.StopAsync(stoppingToken);
}
}
```
- Create `PhaseChangeReportService.cs`:
```
public sealed class PhaseChangeReportService : IScopedProcessingService
{
private readonly ILogger&lt;PhaseChangeReportService&gt; _logger;
public PhaseChangeReportService(ILogger&lt;PhaseChangeReportService&gt; logger)
{
_logger = logger;
_logger.LogInformation($&quot;{nameof(PhaseChangeReportService)} is constructed.&quot;);
}
public async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(PhaseChangeReportService)} is working.&quot;);
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000 * 10, stoppingToken);
_logger.LogInformation($&quot;{nameof(PhaseChangeReportService)} has produced a 10-second unit of work.&quot;);
}
}
}
```
- Create `PlannerOrderStatusUpdateService.cs`:
```
public class PlannerOrderStatusUpdateService : IScopedProcessingServiceB
{
private readonly ILogger&lt;PlannerOrderStatusUpdateService&gt; _logger;
public PlannerOrderStatusUpdateService(ILogger&lt;PlannerOrderStatusUpdateService&gt; logger)
{
_logger = logger;
_logger.LogInformation($&quot;{nameof(PlannerOrderStatusUpdateService)} is constructed.&quot;);
}
public async Task DoWorkAsync(CancellationToken stoppingToken)
{
_logger.LogInformation($&quot;{nameof(PlannerOrderStatusUpdateService)} is working.&quot;);
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000 * 10, stoppingToken);
_logger.LogInformation($&quot;{nameof(PlannerOrderStatusUpdateService)} has produced a 10-second unit of work.&quot;);
}
}
}
```
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&lt;ScopedBackgroundServiceB&gt;();` from `Program.cs`,
- change `builder.Services.AddScoped&lt;IScopedProcessingServiceB, PlannerOrderStatusUpdateService&gt;();` to `builder.Services.AddScoped&lt;IScopedProcessingService, PlannerOrderStatusUpdateService&gt;();` 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&#39;t Repeat Yourself), I want to see if there is another way.
[1]: https://learn.microsoft.com/en-us/dotnet/core/extensions/scoped-service?pivots=dotnet-7-0
</details>
# 答案1
**得分**: 1
```plaintext
你可以这样做:
```csharp
builder.Services.AddScoped<PhaseChangeReportService>();
builder.Services.AddScoped<PlannerOrderStatusUpdateService>();
```
然后使用:
```csharp
public SomeClassContstructor(PhaseChangeReportService phaseChangeReport...) { ... }
```
你不必为接口注册服务,但这意味着你的实现将通过类而不是接口进行,这可能会影响到在迁移到不同技术时的可重用性和可维护性。
除非你像这样做一些事情:
```csharp
using IServiceScope scope = _serviceProvider.CreateScope();
var implementationTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(x =>
!x.IsInterface //可选,如果你可能在其他接口上使用接口
&& x.GetInterface(typeof(IScopedProcessingService).Name) != null
);
foreach (var implementationType in implementationTypes)
{
var service = scope.ServiceProvider.GetService(implementationType);
await (service as IScopedProcessingService).DoWorkAsync(...);
}
```
你可以使用类似上面的代码段来在你的 `Program.cs` 中添加服务。
```
<details>
<summary>英文:</summary>
You can just do this:
```
builder.Services.AddScoped&lt;PhaseChangeReportService&gt;();
builder.Services.AddScoped&lt;PlannerOrderStatusUpdateService&gt;();
```
and use:
```
public SomeClassContstructor(PhaseChangeReportService phaseChangeReport...) { ... }
```
You don&#39;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 =&gt;
!x.IsInterface //Optional if you might use the interface on other interfaces
&amp;&amp; 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`
</details>

huangapple
  • 本文由 发表于 2023年6月1日 02:47:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76376468.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定