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

Blazor Server应用EF Core DbContext实体跟踪与生命周期问题求助

问题背景

我正在开发Blazor Server应用,执行操作时遇到两个关联DbContext生命周期和EF Core实体跟踪的错误。我的解决方案分为4个项目:

  • 后端项目:采用仓储-服务架构,使用MySQL数据库
  • 类库项目:存放所有模型定义
  • Blazor Server前端项目
  • xUnit测试项目

错误1:实体跟踪冲突

System.InvalidOperationException:实体类型“Provincia”的实例无法被跟踪,因为另一个具有相同{'IdProvincia'}键值的实例已被跟踪。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。可考虑使用'DbContextOptionsBuilder.EnableSensitiveDataLogging'查看冲突的键值
该错误出现在Crear.razor中尝试持久化Localidad实体时,此时DbContext的服务生命周期为Transient。

错误2:Scoped生命周期下刷新报错

如果将DbContext的服务生命周期改为Scoped(或使用默认值),CRUD操作可以正常运行,但按F5刷新页面会报如下错误:

InvalidOperationException:上一个操作完成前,上下文又启动了第二个操作。这通常是由不同线程使用同一个DbContext实例导致的。

问题根因
  1. 服务生命周期不匹配:你将Service注册为Scoped,就算DbContext是Transient,Scoped的Service在初始化时只会解析一次DbContext,整个Blazor连接生命周期内都会复用同一个DbContext实例,导致之前查询的实体一直被跟踪,插入关联实体时触发主键冲突。
  2. Blazor Server的Scoped生命周期是跨请求的:和传统MVC/Razor Pages的请求级Scoped不同,Blazor Server的Scoped生命周期对应整个SignalR连接,用户打开页面后DbContext会一直存活,并发操作或刷新时容易触发线程安全问题。
  3. 代码存在不合理操作:插入Localidad时重复查询关联的Provincia实体,同时存在async void这类不安全的异步写法。
修复方案

步骤1:改用DbContextFactory(Blazor Server官方推荐方案)

修改Startup.cs的服务注册,替换原有DbContext注册逻辑:

public void ConfigureServices(IServiceCollection services){
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
    
    // 替换原有AddDbContext,改为AddDbContextFactory
    services.AddDbContextFactory<LengaContext>(opt => opt.UseSqlServer(
        Configuration.GetConnectionString("MyDB")), ServiceLifetime.Transient);

    services.AddScoped<IProvinciaService, ProvinciaService>();
    services.AddScoped<ILocalidadService, LocalidadService>();
    services.AddTransient<IAuthorizationHandler, IsOperadorHandler>();
    services.AddScoped<IStringLocalizer<App>, StringLocalizer<App>>();
}

步骤2:修改仓储基类,每次操作创建新的DbContext

修改Service.cs,改为注入IDbContextFactory<LengaContext>,每次数据库操作都创建新的上下文,操作完成后自动释放:

namespace CapacitacionesBE.Data
{
    public class Service<T> : IRepository<T> where T : class
    {
        protected readonly IDbContextFactory<LengaContext> _contextFactory;

        public Service(IDbContextFactory<LengaContext> contextFactory)
        {
            _contextFactory = contextFactory;
        }
        public async Task Delete(int Id)
        {
            using var context = _contextFactory.CreateDbContext();
            var ItemToDelete = context.Set<T>().Find(Id);
            if (ItemToDelete != null)
            {
                context.Set<T>().Remove(ItemToDelete);
                try
                {
                    await context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            // 不再需要手动释放Context,每次using会自动释放
        }

        public async Task<List<T>> GetAll()
        {
            try
            {
                using var context = _contextFactory.CreateDbContext();
                var Items =  await context.Set<T>().ToListAsync();
                return Items;
            }
            catch (Exception ex)
            {
                throw new Exception($"无法拉取实体列表: {ex.Message}");
            }
        }

        public async Task<T> GetById(int Id)
        {
            using var context = _contextFactory.CreateDbContext();
            var Item = await context.Set<T>().FindAsync(Id);
            return Item;
        }

        public async Task<T> Insert(T Element)
        {
            if (Element != null)
            {
                using var context = _contextFactory.CreateDbContext();
                context.Set<T>().Add(Element);
                
                try
                {
                    await context.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    throw;
                }

                return Element;
            }
            else
            {
                return null;
            }
        }

        public async Task<T> Update(T Element)
        {
            using var context = _contextFactory.CreateDbContext();
            context.Set<T>().Update(Element);

            try
            {
                await context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw;
            }

            return Element;
        }
    }
}

其他Service(比如LocalidadService)同理,修改构造函数注入的参数为IDbContextFactory<LengaContext>,所有数据库操作都用CreateDbContext创建的上下文执行。

步骤3:优化前端代码,消除冗余操作和不安全写法

修改Crear.razor的代码:

  1. 去掉冗余的Provincia查询,直接给外键字段IdProvincia赋值
  2. 把async void改为async Task,避免异步操作异常泄漏
  3. 去掉不必要的Task.Run包裹
@code {
    List<Provincia> Provincias;
    public Localidad Localidad = new Localidad();
    public int SelectedProvincia { get; set; }

    private async Task HandleSubmit()
    {
        Logger.LogInformation("Handling submit");
        await CrearLocalidad();
    }

    protected async Task CrearLocalidad()
    {
        // 直接赋值外键,不需要查询Provincia实体,避免跟踪冲突
        Localidad.IdProvincia = SelectedProvincia;
        await _serviceLocalidad.Insert(Localidad);
        NavigationManager.NavigateTo("Localidades");
    }

    protected override async Task OnInitializedAsync()
    {
        // 去掉不必要的Task.Run
        Provincias = await _serviceProvincia.GetAll();
        var PrimerProvincia = Provincias.FirstOrDefault();
        if (PrimerProvincia != null) {
            SelectedProvincia = PrimerProvincia.IdProvincia;
        }
    }

    void Cancel()
    {
        NavigationManager.NavigateTo("Localidades");
    }
}
额外优化建议

如果需要在插入后返回带关联Provincia的Localidad实体,可以在Insert操作后手动Include查询,不需要直接赋值导航属性插入,进一步降低跟踪冲突概率。

内容的提问来源于stack exchange,提问作者Mauro Alvarez

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 09:27:03