[厨余回收] Entity Framework Core 错误:「...cannot be tracked becaus

从应用程式日誌看到下面这个 Entity Framework Core(以下简称 EF Core)发出的例外错误:

The instance of entity type 'MyTable' cannot be tracked because another instance with the same key value for {'Key'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

它的意思是,相同主索引键的实体无法被追蹤,因为已经有另一个实体正在被追蹤。我们来看看怎么回事。

我的应用程式是主控台应用程式,使用 EF Core 操作资料库,并且加入了 Microsoft.Extensions.DependencyInjection 套件实作 DI,因此在服务注册的阶段,很自然地使用 ServiceCollection.AddDbContext<T>() 来注册 DbContext。

serviceCollection.AddDbContext<TestContext>(
    options =>
        {
            // 预设使用 No-tracking queries
            options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
            options.UseSqlServer(testConnStr);
        });

我有一个服务是新增或更新资料,下面是模拟的程式码:

public class MyService(IServiceProvider serviceProvider)
{
    public void Upsert(string key, string value)
    {
        var testCtx = serviceProvider.GetService<TestContext>();

        var myTable = testCtx.MyTables.SingleOrDefault(x => x.Key == key);

        if (myTable == null)
        {
            myTable = new MyTable { Key = key };

            testCtx.Entry(myTable).State = EntityState.Added;
        }
        else
        {
            testCtx.Entry(myTable).State = EntityState.Modified;
        }

        myTable.Value = value;

        testCtx.SaveChanges();
    }
}

相同主索引键的资料会被至少更新 2 次以上,下面模拟多次更新:

var serviceProvider = serviceCollection.BuildServiceProvider();

// 第一次更新
serviceProvider.GetService<MyService>().Upsert("test", "test1");

// 第二次更新
serviceProvider.GetService<MyService>().Upsert("test", "test2");

然后就在第二次更新的时候,爆出了开头所说的例外错误。

问题原因

众所周知,服务是有所谓的生命週期,ServiceCollection.AddDbContext<T>() 在注册 DbContext 的时候,预设的 Lifetime 是 ServiceLifetime.Scoped,如果是 ASP.NET Core 应用程式,DbContext 会跟随每一个 Request 的生命週期而生灭。

但是主控台应用程式没有 Request 的概念,所以如果我们的服务注册为 Scoped,其生命週期会跟随 DI 容器的 Root Scope,这几乎等同于 Singleton,我们的 DbContext 一旦建立,就不会被释放,当第二次更新的时候,相同主索引键的实体因无法被追蹤而爆出例外错误。

解决错误方法有三种:

解法一:EntityState.Detached

将资料实体的 State 设为 EntityState.Detached,变成无追蹤状态。

public class MyService(IServiceProvider serviceProvider)
{
    public void Upsert(string key, string value)
    {
        ...

        EntityEntry<MyTable> myTableEntry;

        if (myTable == null)
        {
            ...

            myTableEntry = testCtx.Entry(myTable);
            myTableEntry.State = EntityState.Added;
        }
        else
        {
            myTableEntry = testCtx.Entry(myTable);
            myTableEntry.State = EntityState.Modified;
        }

        ...

        myTableEntry.State = EntityState.Detached;
    }
}

解法二:ServiceLifetime.Transient

在注册 DbContext 时,将其生命週期改为 ServiceLifetime.Transient,意即每解析服务一次就重新建立实例。

serviceCollection.AddDbContext<TestContext>(
    options =>
        {
            ...
        },
    ServiceLifetime.Transient);

解法三:手动建立 Scope

解法三有两种写法,一种是在服务内部建立 Scope:

public class MyService(IServiceProvider serviceProvider)
{
    public void Upsert(string key, string value)
    {
        using var scope = serviceProvider.CreateScope();

        var testCtx = scope.ServiceProvider.GetService<TestContext>();

        ...
    }
}

另一种则是在注册服务时就建立 Scope:

serviceCollection.AddTransient(provider => new MyService(provider.CreateScope().ServiceProvider));

以上针对「相同主索引键的实体无法被追蹤」这个错误,提供三种解法供大家参考。

相关资源

C# 指南
ASP.NET 教学
ASP.NET MVC 指引
Azure SQL Database 教学
SQL Server 教学
Xamarin.Forms 教学

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章

5 点赞(415) 阅读(67)