如何确保Blazor后台服务与Razor组件共用EF Core数据缓存?
环境
- Blazor
- net8.0-windows
- Microsoft.EntityFrameworkCore 8.0.1
问题根源
后台服务是Singleton生命周期,Blazor组件是Scoped生命周期——即使你把IDbContextFactory注册为Scoped,后台服务作为Singleton服务,获取到的DbContext会属于根服务范围,和组件的Scoped服务范围完全隔离,两者依然会创建独立的DbContext实例,各自维护实体缓存,所以组件订阅的实体和后台更新的实体根本不是同一个对象,事件自然不会触发。
下面是几种可行的解决思路:
方案1:使用Singleton状态管理服务(推荐)
放弃依赖EF Core的实体缓存共享状态,改用专门的Singleton服务统一管理打印机状态,后台服务更新这个服务的状态,组件监听该服务的事件。这种方式最稳妥,避免EF生命周期冲突。
步骤1:创建Singleton状态服务
public class PrinterStatusService : INotifyPropertyChanged { private bool _isReady; public bool IsReady { get => _isReady; set { if (_isReady != value) { _isReady = value; OnPropertyChanged(nameof(IsReady)); } } } public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
步骤2:注册服务
在Program.cs中注册为Singleton:
builder.Services.AddSingleton<PrinterStatusService>();
步骤3:后台服务更新状态
注入状态服务和DbContextFactory,检查状态后同步更新数据库和状态服务:
public class PrinterCheckService : BackgroundService { private readonly PrinterStatusService _statusService; private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory; public PrinterCheckService(PrinterStatusService statusService, IDbContextFactory<ApplicationDbContext> dbContextFactory) { _statusService = statusService; _dbContextFactory = dbContextFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var context = _dbContextFactory.CreateDbContext(); var printer = await context.LabelPrinters.FirstOrDefaultAsync(stoppingToken); if (printer != null) { bool newStatus = CheckPrinterStatus(); // 替换为你的状态检查逻辑 if (printer.IsReady != newStatus) { printer.IsReady = newStatus; await context.SaveChangesAsync(stoppingToken); _statusService.IsReady = newStatus; // 同步到共享状态服务 } } await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } } // 自定义打印机状态检查逻辑 private bool CheckPrinterStatus() => true; }
步骤4:Blazor组件监听状态
注入状态服务,订阅事件并更新UI:
@inject PrinterStatusService StatusService @implements IDisposable <div>打印机状态:@(StatusService.IsReady ? "就绪" : "未就绪")</div> @code { protected override void OnInitialized() { StatusService.PropertyChanged += OnStatusChanged; } private void OnStatusChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(StatusService.IsReady)) { StateHasChanged(); // 触发UI更新 } } public void Dispose() { StatusService.PropertyChanged -= OnStatusChanged; } }
方案2:强制共享Singleton DbContext(不推荐)
EF Core的DbContext设计为短生命周期使用,Singleton模式会带来线程安全、内存泄漏等风险,仅在特殊场景下尝试:
步骤1:注册Singleton DbContext
在Program.cs中把DbContext注册为Singleton:
builder.Services.AddDbContext<ApplicationDbContext>( options => options.UseSqlServer(connectionString), ServiceLifetime.Singleton);
步骤2:后台服务和组件共用DbContext
后台服务注入ApplicationDbContext,注意用锁保证线程安全:
public class PrinterCheckService : BackgroundService { private readonly ApplicationDbContext _context; public PrinterCheckService(ApplicationDbContext context) { _context = context; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // 必须加锁保证线程安全 lock (_context) { var printer = _context.LabelPrinters.FirstOrDefault(); if (printer != null) { bool newStatus = CheckPrinterStatus(); if (printer.IsReady != newStatus) { printer.IsReady = newStatus; _context.SaveChanges(); } } } await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } } }
Blazor组件直接注入ApplicationDbContext,获取实体并订阅事件:
@inject ApplicationDbContext Context @implements IDisposable <div>打印机状态:@(_printer?.IsReady ?? false ? "就绪" : "未就绪")</div> @code { private LabelPrinter? _printer; protected override void OnInitialized() { _printer = Context.LabelPrinters.FirstOrDefault(); if (_printer != null) { _printer.PropertyChanged += OnPrinterStatusChanged; } } private void OnPrinterStatusChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(LabelPrinter.IsReady)) { StateHasChanged(); } } public void Dispose() { if (_printer != null) { _printer.PropertyChanged -= OnPrinterStatusChanged; } } }
警告:Singleton DbContext会导致缓存永远不会释放,并发操作时必须严格加锁,否则会出现数据不一致或异常。
方案3:使用SignalR推送状态变更
后台服务更新数据库后,通过SignalR向所有客户端推送最新状态,组件接收推送后更新UI,这种方式适合多客户端同步场景:
步骤1:添加SignalR服务
在Program.cs中注册SignalR:
builder.Services.AddSignalR();
步骤2:创建SignalR Hub
public class PrinterHub : Hub { // 推送打印机状态到所有客户端 public async Task SendPrinterStatus(bool isReady) { await Clients.All.SendAsync("ReceivePrinterStatus", isReady); } }
步骤3:配置Hub端点
app.MapHub<PrinterHub>("/printerHub");
步骤4:后台服务推送状态
注入IHubContext<PrinterHub>,更新数据库后推送状态:
public class PrinterCheckService : BackgroundService { private readonly IHubContext<PrinterHub> _hubContext; private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory; public PrinterCheckService(IHubContext<PrinterHub> hubContext, IDbContextFactory<ApplicationDbContext> dbContextFactory) { _hubContext = hubContext; _dbContextFactory = dbContextFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var context = _dbContextFactory.CreateDbContext(); var printer = await context.LabelPrinters.FirstOrDefaultAsync(stoppingToken); if (printer != null) { bool newStatus = CheckPrinterStatus(); if (printer.IsReady != newStatus) { printer.IsReady = newStatus; await context.SaveChangesAsync(stoppingToken); // 推送状态到所有客户端 await _hubContext.Clients.All.SendAsync("ReceivePrinterStatus", newStatus, stoppingToken); } } await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } } }
步骤5:Blazor组件接收推送
@using Microsoft.AspNetCore.SignalR.Client @inject NavigationManager NavigationManager @implements IDisposable <div>打印机状态:@(_isReady ? "就绪" : "未就绪")</div> @code { private HubConnection? _hubConnection; private bool _isReady; protected override async Task OnInitializedAsync() { _hubConnection = new HubConnectionBuilder() .WithUrl(NavigationManager.ToAbsoluteUri("/printerHub")) .Build(); // 监听推送的状态 _hubConnection.On<bool>("ReceivePrinterStatus", isReady => { _isReady = isReady; InvokeAsync(StateHasChanged); }); await _hubConnection.StartAsync(); } public async void Dispose() { if (_hubConnection != null) { await _hubConnection.DisposeAsync(); } } }
内容的提问来源于stack exchange,提问作者Will

