You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何确保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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 10:05:03