【问题标题】:EF Core: dbcontext issues in blazor server appEF Core:blazor 服务器应用程序中的 dbcontext 问题
【发布时间】:2021-09-28 11:49:12
【问题描述】:

我正在 Blazor 服务器中开发应用程序,当我尝试执行某些操作时遇到不同的错误。

我的解决方案分为 3 个项目

  • 带有存储库的后端项目 - 服务架构和 MySql DB
  • 类库中用于存储我的模型的模型
  • Blazor 服务器前端。
  • 一个 xUnit 测试项目

我收到以下错误

System.InvalidOperationException:无法跟踪实体类型“Provincia”的实例,因为已经在跟踪具有相同键值的另一个实例 {'IdProvincia'}。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值

当我尝试在我的Crear.razor 中持久化Localidad 实体时出现此错误,我的 dbcontext 此处的生命周期服务等于Transient

其他问题是,当我更改 DbContext 服务的生命周期时,如果我将其设置为 Scoped或删除它,这是默认值),CRUD 操作正在运行,但是如果我在页面中进行刷新(按 F5),则会出现以下错误

InvalidOperationException:在前一个操作完成之前在此上下文中启动了第二个操作。这通常是由使用相同 DbContext 实例的不同线程引起的。有关如何避免 DbContext 线程问题的更多信息,请参阅https://go.microsoft.com/fwlink/?linkid=2097913

这是我的代码(省略了导入

Localidad.cs

namespace CapacitacionModelos.Modelos
{
    [Table("Localidad")]
    public class Localidad
    {
        [Column("IdLocalidad")]
        public int LocalidadId { get; set; }
        [Column("Descripcion")]
        [Required]
        public string Nombre { get; set; }
        [ForeignKey("IdProvincia")]
        public int IdProvincia { get; set; }
        public virtual Provincia Provincia { get; set; }
        [StringLength(50)]
        public string Abreviatura { get; set; }
        [Column("IdMPF")]
        public byte? IdMpf { get; set; }

        public Localidad()
        {
         
        }
        public Localidad(string Nombre, Provincia Provincia)
        {
            this.Nombre = Nombre;
            this.Provincia = Provincia;
        }
    }
}

Provincia.cs

namespace CapacitacionModelos.Modelos
{
    [Table("Provincia")]
    public class Provincia
    {
        [Key]
        public int IdProvincia { get; set; }
        [Column("Descripcion")]
        [StringLength(80)]
        public string Nombre { get; set; }

        public Provincia()
        {
        }
        public Provincia(string Nombre)
        {
            this.Nombre = Nombre;
        }
    }
}

LocalidadService.cs

namespace CapacitacionesBE.Data
{
    public interface ILocalidadService : IRepository<Localidad>
    {
        public Task<List<Localidad>> GetLocalidadesByProvincia(int idProvincia);
        public Task<List<Localidad>> GetLocalidadesWithProvincias();
    }
    public class LocalidadService : Service<Localidad>, ILocalidadService
    {
        public LocalidadService(LengaContext context) : base(context)
        {

        }

        public async Task<List<Localidad>> GetLocalidadesByProvincia(int idProvincia)
        {
            return await _context
                .Localidades
                .Where(localidad => localidad.Provincia.IdProvincia == idProvincia)
                .ToListAsync();
        }

        public async Task<List<Localidad>> GetLocalidadesWithProvincias()
        {
            return await _context
                .Localidades
                .Include(localidad => localidad.Provincia)
                .ToListAsync();
        }
    }
}

IRepository.cs

namespace CapacitacionesBE.Data
{
    public interface IRepository<T> : IDisposable
    {
        public Task<List<T>> GetAll();
        public Task<T> GetById(int Id);
        public Task<T> Insert(T Element);
        public Task<T> Update(T Element);
        public Task Delete(int Id);

    }
}

Service.cs

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

        public Service(LengaContext context)
        {
            _context = context;
        }
        public async Task Delete(int Id)
        {
            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.Dispose();
        }

        public async Task<List<T>> GetAll()
        {
            try
            {
                var Items =  await _context.Set<T>().ToListAsync();
                return Items;
            }
            catch (Exception ex)
            {
                throw new Exception($"Error no se pueden traer las entidades: {ex.Message}");
            }
        }

        public async Task<T> GetById(int Id)
        {
            var Item = await _context.Set<T>().FindAsync(Id);

            return Item;
        }

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

                return Element;
            }
            else
            {
                return null;
            }
        }

        public async Task<T> Update(T Element)
        {
            _context.Set<T>().Update(Element);

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

            return Element;
        }
    }
}

Startup.cs 我只复制了 configureServices 方法,文件的其余部分不需要

    public void ConfigureServices(IServiceCollection services){

                services.AddRazorPages();
                services.AddServerSideBlazor();
                services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
    
                services.AddDbContext<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>>();
   }

Cear.razor 这是我的看法

@page "/Localidades/Crear"
@using CapacitacionModelos.Modelos
@using Microsoft.Extensions.Localization;
@using LengaFE.Shared.Components;
@using Microsoft.Extensions.Logging
@inject CapacitacionesBE.Data.ILocalidadService _serviceLocalidad
@inject CapacitacionesBE.Data.IProvinciaService _serviceProvincia
@inject NavigationManager NavigationManager
@inject ILogger<Crear> Logger

@inject IStringLocalizer<App> L;

<h3>Crear</h3>

<EditForm Model="@Localidad" OnValidSubmit="@HandleSubmit">
    <div class="row">
        <div class="col-md-8">
            <div class="form-group">
                <label for="Localidad" class="control-label">@L["localidad"]</label>
                <input form="Nombre" class="form-control" @bind="@Localidad.Nombre" />
            </div>
        </div>
        <div class="col-md-8">
            <div class="form-group">
                <label for="Provincia" class="control-label">@L["provincia"]</label>
                <CustomInputSelect @bind-Value="SelectedProvincia" id="Provincia" class="form-control">
                    @if (Provincias is null)
                    {
                        <option>Cargando...</option>
                    }
                    else
                    {
                        @foreach (var provincia in Provincias)
                        {
                            <option value="@provincia.IdProvincia">@provincia.Nombre</option>
                        }
                    }
                </CustomInputSelect>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-md-4">
            <FormButtons PrimaryActionLabel=@L["crear"]
                         SecondaryActionLabel=@L["cancelar"]
                         ClickActionPrimary=@CrearLocalidad
                         ClickActionSecondary=@Cancel />
        </div>
    </div>
</EditForm>

@code {

    List<Provincia> Provincias;

    public Localidad Localidad = new Localidad();

    private void HandleSubmit()
    {
        Logger.LogInformation("Handling submit");
        CrearLocalidad();
    }

    public int SelectedProvincia { get; set; }

    protected async void CrearLocalidad()
    {
        var provincia = await _serviceProvincia.GetById(SelectedProvincia);

        //Localidad.IdProvincia = SelectedProvincia;
        Localidad.Provincia = provincia;

        await _serviceLocalidad.Insert(Localidad);

        NavigationManager.NavigateTo("Localidades");
    }

    protected override async Task OnInitializedAsync()
    {
        Provincias = await Task.Run(() => _serviceProvincia.GetAll());
        var PrimerProvincia = Provincias.FirstOrDefault();
        if (PrimerProvincia != null) {
            SelectedProvincia = PrimerProvincia.IdProvincia;
        }
    }

    void Cancel()
    {
        NavigationManager.NavigateTo("Localidades");
    }
}

FormButtons.razor

@using Microsoft.Extensions.Localization;
@inject IStringLocalizer<App> L;

<div class="form-group">
    <input type="button" class="btn btn-primary" @onclick="@ClickActionPrimary" value=@PrimaryActionLabel />
    <input type="button" class="btn btn-primary" @onclick="@ClickActionSecondary" value=@SecondaryActionLabel />
</div>

@code {

    [Parameter]
    public string PrimaryActionLabel { get; set; }

    [Parameter]
    public string SecondaryActionLabel { get; set; }

    [Parameter]
    public EventCallback ClickActionPrimary { get; set; }

    [Parameter]
    public EventCallback ClickActionSecondary { get; set; }

}

我一直在阅读有关生命周期服务的信息,这就是我在 dbcontext 中使用 transient 的原因,它为每个请求创建一个新实例。 对于与其他模型没有关系的实体,CRUD 可以正常工作,但对于包含 Provincia 实体Localidad 实体,它会失败。 另外我认为解决方案可能与 Entity State 有关,但我还没有深入挖掘。我正在努力在我的工作 atm 上取得一些进展。 关于实体状态,我在其他项目中搭建了一些剃须刀页面,并在视图中跟踪实体状态,也许我应该这样做。

任何改进此代码的帮助都将得到重视,我是网络核心和后端的初学者(我只使用前端技术),我仍在阅读一些书籍,例如net core in action 并做了一些课程,但这超出了我目前的知识范围。

【问题讨论】:

  • EF Core 之上的整个“通用存储库”是一个丑陋的反模式,您的 Update 可以轻松执行 43 次删除。您也丢失了 DbContext 的事务行为,这意味着您现在需要使用显式数据库事务。不过,这不是该错误的原因(仅是间接的)。是否使用 Transient 无关紧要,因为 Blazor 服务器应用程序中的范围是用户电路(会话)。 IServiceIRepository 实例在会话期间保持不变,因此使用相同的 DbContext 实例
  • ASP.NET Core Blazor Server with Entity Framework Core (EFCore) 解释了应如何在 Blazor 服务器应用程序中使用 EF Core。代替AddDbContextAddDbContextFactory 用于注册 DbContextFactory。组件使用该工厂创建 DbContext 实例,它们在不再需要它们时将其释放。
  • 感谢@PanagiotisKanavos,我一直在看一些关于通用存储库的帖子。我找到了这个softwareengineering.stackexchange.com/a/220126,所以我已经有了一个由 dbcontext 表示的工作单元,我的存储库将由 dbset 表示。所以 ef core 为我提供了这个功能。因此我可以为每个模型都有一个接口并直接使用 dbsets 实现每个模型的类中的方法吗?很抱歉问了这么多问题……但非常感谢!
  • Scope to the component lifetime 解释了如何将 DbContext 限定为组件的生命周期,以便它再次作为工作单元运行。 1) 组件在初始化期间使用`DbFactory.CreateDbContext();` 创建 2) 组件实现IDisposable 并释放DbContext。这样,除非您调用 SaveChanges,否则当您离开组件时,任何修改都会被丢弃
  • 当实体从它们各自的 DbContext 中分离出来,然后通过 Update 等方法附加到其他 DbContext 实例时,您一定会遇到各种问题,尤其是当这些实体具有相关实体时。需要根据 DbContext 跟踪的实例检查这些相关实体以替换引用(如果存在),如果不存在则附加引用。 (否则可能面临重复数据或 PK 冲突异常)将泛型添加到混合中会增加麻烦,因为泛型不知道特定于类的详细信息,因此无法看到引用。

标签: c# entity-framework-core asp.net-core-3.1 blazor-server-side


【解决方案1】:

无论如何,绝对需要根据 Panagiotis Kanavos 的评论从 MS documentation 实施 DbContextFactory,这将解决此错误的大多数情况。

然而,Steve Py 关于实体在尝试Update 具有相关实体的实体时与它们各自的 DbContext 分离的评论是针对更高级的情况。如果这是原因,我发现有两种解决方法是:

  1. 在调用Update之前将相关实体清空
  2. EntityEntry.State 设置为EntityState.Modified 而不是调用Update

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-08-14
    • 2018-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-02
    相关资源
    最近更新 更多