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

将EntityFrameworkCore从5.0升级到6.0.14遇到的问题

EF Core 5升级到6.0.14(SQLite)后的问题与解决方案

我们团队接手了一个连接SQLite数据库的C#应用,需要将其从EntityFrameworkCore 5.0升级到6.0.14,升级后出现了两处异常问题,以下是简化后的相关代码及疑问。

仓储类

public async Task AddAsync<T>(T entity, CancellationToken cancellationToken = default) where T : class
{
    if (entity == null) throw new ArgumentNullException(nameof(entity));
    await _dbContext.Set<T>().AddAsync(entity, cancellationToken);
}

public async Task<List<T>> GetListAsync<T>(Expression<Func<T, bool>> condition = null, IEnumerable<Expression<Func<T, object>>> includes = null,
                                        bool asNoTracking = false, CancellationToken cancellationToken = default) where T : class
{
    IQueryable<T> query = _dbContext.Set<T>();
    if (condition != null) query = query.Where(condition);
    if (includes != null) foreach (var expression in includes) query = query.Include(expression);
    if (asNoTracking) query = query.AsNoTracking();
    return await query.AsSingleQuery().ToListAsync(cancellationToken);
}

public void Update<T>(T entity) where T : class
{
    if (entity == null) throw new ArgumentNullException(nameof(entity));
    _dbContext.Set<T>().Update(entity);
}

模型类

public record MainEntity 
{
    [Key]
    public string Id { get; protected set; }
    public ICollection<RelatedEntity> OtherEntities { get; set; }

    public MainEntity() { }
    public MainEntity(string id)
    {            
        this.Id = id;
    }
}

public record RelatedEntity
{
    public string Id { get; set; }  // 该属性实际是指向MainEntity.Id的外键,同时与CoId构成表的主键
    public string CoId { get; set; } 
}

管理器代码

public async Task SaveStateDB(CancellationToken cancellation)
{
    using (var scope = scopeFactory.CreateScope())
    {
        var repository = scope.ServiceProvider.GetService<IRepository>();
        lock (lockToSave)
        {
            try
            {
                var includes = new List<Expression<Func<MainEntity, object>>> { x => x.OtherEntities };
                var Storage = repository.GetAsync(x => x.Id == id, includes, true, cancellation).Result;
                bool isNew = false;
                if (Storage == default)
                {
                    Storage = new MainEntity(id);
                    isNew = true;
                }
                var OtherEntities = GetOtherEntities();
                if (Storage.OtherEntities == null) Storage.OtherEntities = OtherEntities;
                else
                {
                    Storage.OtherEntities.Except(IoVariables).ForEach(x => repository.Delete(x));
                    OtherEntities.Except(Storage.OtherEntities).ForEach(x => Storage.OtherEntities.Add(x));
                }

                if (isNew) repository.AddAsync(Storage, cancellation).Wait();
                else repository.Update(Storage);

                repository.SaveAsync(cancellation);
            }
            catch (Exception ex)
            {
                try
                {
                    repository.SaveAsync(cancellation);
                }
                catch (Exception)
                {
                    logger.LogError("--------------------" + ex.Message + " " + ex.StackTrace + "--------------------");
                }
            }
        }
    }
}

升级后出现的异常

  1. 数据库为空时调用AddAsync可正常存储,但已有MainEntity后调用Update时,抛出异常:

Exception has occurred: CLR/System.InvalidOperationException
Exception thrown: 'System.InvalidOperationException' in Microsoft.EntityFrameworkCore.dll: 'Unable to track an entity of type 'RelatedEntity' because its primary key property 'Id' is null.'

  1. 解决上述问题后,调用SaveAsync(内部为SaveChangesAsync)时抛出并发异常:

Exception thrown: 'Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException' in System.Private.CoreLib.dll: 'The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded.'

具体疑问

  • 为何EF Core 5中RelatedEntity无Id时可正常存储?
  • 为何AddAsync可正常工作,Update却不行?
  • 为何启用AsNoTracking会引发并发异常,如何保留该设置解决问题?

问题解答

1. EF Core 5中RelatedEntity无Id可正常存储的原因

EF Core 5对复合主键的处理逻辑更宽松:当RelatedEntity的Id作为外键关联MainEntity.Id,且和CoId组成复合主键时,EF Core 5会在插入阶段自动从关联的MainEntity中填充Id值,即使代码未显式设置。而EF Core 6.0强化了实体跟踪的校验规则,要求被跟踪的实体必须明确设置主键属性的值,否则直接抛出主键为空的异常。

2. AddAsync正常但Update异常的原因

  • 调用AddAsync时,整个MainEntity及其关联的RelatedEntity都处于新增状态,EF Core会自动处理关联外键的填充逻辑,RelatedEntity的Id会同步MainEntity.Id,此时不会触发主键非空校验。
  • 调用Update时,MainEntity是通过AsNoTracking加载的,关联的RelatedEntity处于未被上下文跟踪的状态。当调用Update(Storage)时,EF Core会尝试将整个实体图标记为修改状态,此时未设置Id的RelatedEntity会触发EF Core 6的主键校验,导致异常。

3. 保留AsNoTracking解决并发异常的方法

并发异常的本质是:AsNoTracking加载的实体没有被上下文跟踪,调用Update时EF Core会基于实体当前属性生成更新语句,但由于没有实体的原始值,EF Core默认会检查更新行数是否匹配(乐观并发机制),最终因无法确认实体原始状态导致误判。

方案一:手动附加实体并设置状态

在调用Update前,将实体附加到上下文,并明确设置每个实体的状态:

// 替换原有的repository.Update(Storage);
var mainDbSet = _dbContext.Set<MainEntity>();
mainDbSet.Attach(Storage);
_dbContext.Entry(Storage).State = EntityState.Modified;

// 处理关联的RelatedEntity,确保Id已设置
foreach (var related in Storage.OtherEntities)
{
    related.Id = Storage.Id; // 显式关联主实体Id
    var relatedEntry = _dbContext.Entry(related);
    if (relatedEntry.State == EntityState.Detached)
    {
        // 判断是否已存在,设置对应状态
        bool exists = await _dbContext.Set<RelatedEntity>()
            .AnyAsync(x => x.Id == related.Id && x.CoId == related.CoId, cancellation);
        relatedEntry.State = exists ? EntityState.Modified : EntityState.Added;
    }
}

方案二:仅更新需要修改的属性

避免全量更新,只标记需要修改的属性,减少并发校验的影响:

// 重新加载无跟踪的原始实体
var original = await repository.GetAsync(x => x.Id == id, includes: null, asNoTracking: true, cancellation);
// 复制需要更新的属性
original.OtherEntities = Storage.OtherEntities;

// 附加并标记需要修改的集合属性
_dbContext.Attach(original);
_dbContext.Entry(original).Collection(x => x.OtherEntities).IsModified = true;

方案三:临时关闭乐观并发校验(不推荐)

在上下文配置中关闭全局乐观并发校验,或移除实体的并发令牌,但会失去并发保护能力:

// 在DbContext的OnModelCreating方法中
modelBuilder.Entity<MainEntity>()
    .Property(x => x.Id)
    .IsConcurrencyToken(false);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 03:25:09