首页 > 解决方案 > EF Core:blazor 服务器应用程序中的 dbcontext 问题

问题描述

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

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

我收到以下错误

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

当我尝试Localidad在我的 dbcontext 中持久化实体时会出现此错误Crear.razor,我的 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);

    }
}

服务.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>>();
   }

Crear.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");
    }
}

表单按钮.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 中使用瞬态,它为每个请求创建一个新实例。对于与其他模型没有关系的实体,CRUD 可以正常工作,但对于包含Provincia 实体的Localidad 实体,它会失败。另外我认为解决方案可能与实体状态有关,但我还没有深入挖掘。我正在努力在我的工作 atm 上取得一些进展。关于实体状态,我在其他项目中搭建了一些剃须刀页面,并且正在视图中跟踪实体状态,也许我应该这样做。

对改进此代码的任何帮助都会受到重视,我是网络核心和后端的初学者(我只使用前端技术),我仍在阅读一些书籍,如网络核心在行动中并做一些课程,但是这个超出了我目前的知识。

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

解决方案


无论如何,绝对需要根据 Panagiotis Kanavos 的评论DbContextFactoryMS 文档中实施,这将解决大多数此错误的情况。

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

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

推荐阅读