【发布时间】: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 服务器应用程序中的范围是用户电路(会话)。IService和IRepository实例在会话期间保持不变,因此使用相同的 DbContext 实例 -
ASP.NET Core Blazor Server with Entity Framework Core (EFCore) 解释了应如何在 Blazor 服务器应用程序中使用 EF Core。代替
AddDbContext,AddDbContextFactory用于注册 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