如何刷新Entity Framework Core的DBContext?

56

当我的表被另一方更新时,.NET Core中的数据库上下文仍然返回旧值,我该如何强制Db上下文刷新?

我已经做了研究,但我只发现人们使用Reload方法来强制上下文刷新,但它在EF Core中不可用。

其他一些解决方案建议在使用后处理上下文,但我收到错误消息说DB上下文是由依赖注入创建的,我不应该去干扰它。


他们共享同一个上下文吗?在保存更改后,不同的上下文应该共享相同的状态。 - Fals
@Fals 很不幸,它们没有共享相同的上下文。 - MiDaa
1
如果你只需要读取数据,那么使用"AsNoTracking"可能会对你有所帮助。EF Core: https://learn.microsoft.com/en-us/ef/core/querying/tracking 这里提供更多信息:EF缓存破坏: http://codethug.com/2016/02/19/Entity-Framework-Cache-Busting/ - A.J.Bauer
8个回答

64

哦,这个问题让我纠结了好几天。

我正在使用带有.Net Core 2.1的Visual Studio 2017,我的EF Core代码大致如下:

//  1.  Load a [User] record from our database 
int chosenUserID = 12345;
User usr = dbContext.Users.FirstOrDefault(s => s.UserID == chosenUserID);

//  2. Call a web service, which updates that [User] record
HttpClient client = new HttpClient()
await client.PostAsync("http://someUrl", someContent);

//  3. Attempt to load an updated copy of the [User] record
User updatedUser = dbContext.Users.FirstOrDefault(s => s.UserID == chosenUserID);
在第三步,它将简单地将“updatedUser”设置为[User]记录的原始版本,而不是尝试加载新副本。 因此,如果在第三步之后,我修改了该[User]记录,实际上会失去Web服务应用的任何设置。

最终,我找到了两个解决方案。

我可以更改ChangeTracker设置。 这有效,但我担心这样做会产生副作用:

dbContext.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;

或者,在尝试重新加载[用户]记录之前,我可以插入以下命令...

await dbContext.Entry(usr).ReloadAsync();

这似乎迫使 .Net Core 重新加载 [User] 记录,然后一切都好了。

希望这对你有用......

追踪和修复这个 bug 花费了我数天时间...

这里也有一篇优秀的文章描述了解决此缓存问题的各种方法。


2
我理解你的挫败感。然而,这是我的建议和目前的理解,在EF的设计中,DbContext对象是工作单元模式的一种实现。它应该是短暂的,并且不应该被不同的事务使用。你可以尝试使用另一个新的DbContext来进行第二个操作。 :) - MiDaa
2
虽然晚了近一年,但使用Reload解决方案后,我遇到了另一个错误:“在上一个操作完成之前,在此上下文上启动了第二个操作。这通常是由不同的线程使用DbContext实例引起的,但不能保证实例成员是线程安全的。如果客户端上评估了嵌套查询,则也可能会导致此问题,如果是这种情况,请重写查询以避免嵌套调用。”由于后续调用而出现。在这种情况下,可能ChangeTracker更好,但我对副作用不确定。 - Shinigamae
引用自 https://learn.microsoft.com/en-us/ef/core/querying/tracking#tracking-queries: 当跟踪查询返回结果时,EF Core 将检查实体是否已经存在于上下文中。如果 EF Core 找到现有实体,则返回相同的实例。EF Core 不会使用数据库值覆盖实体条目中的当前和原始值。 因此,这说明了为什么从数据库再次查询实体时,EF Core 不会更新它,这在我看来很奇怪。 - Xriuk
@Shinigamae 在运行时禁用查询跟踪的副作用是以下查询将不会被跟踪,但之前跟踪的所有实体应该仍然存在于上下文中。这与在查询末尾调用 AsNoTracking() 相同。在任何情况下,我仍然建议使用 NoTrackingWithIdentityResolution,因为上述方法将在单个查询中为相同的实体返回不同的实例,而此选项将首先合并它们。 - Xriuk

39

依赖注入和DbContext

您提到当您尝试重新创建DbContext时,会出现有关上下文由依赖注入(DI)系统管理的错误。使用依赖注入系统进行对象创建有两种不同的风格。DI可以创建一个全局单例实例,该实例在所有消费者之间作为服务共享,或者它可以为每个范围/工作单元(例如Web服务器中的每个请求)创建一个实例。

如果您的DI系统配置为创建单个全局共享的DbContext实例,则会遇到与长时间运行的DbContext相关的各种问题。

  • DbContext从不自动从其缓存中删除对象,因为它不是设计为长期运行的。因此,长时间运行的DbContext将浪费内存。
  • 您的代码没有看到加载到其缓存中的项目的更改,除非手动重新加载每个实体。
  • DbContext仅允许同时运行一个查询,并且不是线程安全的。如果您尝试在全局共享实例上运行多个查询,它将抛出DbConcurrencyException异常(至少在其异步接口上,不确定其同步接口)。

因此,最佳实践是每个工作单元使用一个单独的DbContext。您的DI系统可以通过被配置为在范围内为应用程序处理的每个请求提供新实例来帮助您进行此操作。例如,ASP.NET Core的依赖注入系统支持按请求范围实例化。

刷新单个实体

获取新鲜数据的最简单方法是创建一个新的DbContext。但是,在您的工作单元内或在DI系统提供的粒度约束内,您可能会触发直接在数据库中修改实体的外部过程。您可能需要在退出DI范围或完成工作单元之前查看该更改。在这种情况下,您可以通过分离数据对象的实例来强制重新加载。

要做到这一点,请首先获取您的对象的EntityEntry<>。这是一个对象,它允许您操作DbContext对该对象的内部缓存。然后,您可以通过将EntitytState.Detached分配给其State属性来标记此条目已分离。我相信这会使条目保留在缓存中,但会在将来实际加载条目时导致DbContext将其删除并替换它。重要的是,它会导致未来的加载将一个新加载的实体实例返回给您的代码。例如:

var thing = context.Things.Find(id);
if (thing.ShouldBeSentToService) {
    TriggerExternalServiceAndWait(id);

    // Detach the object to remove it from context’s cache.
    context.Entities(thing).State = EntityState.Detached;

    // Then load it. We will get a new object with data
    // freshly loaded from the database.
    thing = context.Things.Find(id);
}
UseSomeOtherData(thing.DataWhichWasUpdated);

2
很好的答案,我在那个项目中使用了一个短暂的上下文来解决问题。谢谢,希望这个答案能帮助其他人。 - MiDaa
4
值得注意的是,当您注册一个DbContext时,它也会注册DbContextOptions<T>,其中T是您的上下文类型。因此,您实际上可以注入它(例如DbContextOptions<RRStoreContext> rrContextOptions),然后根据需要创建全新的上下文(using (var db = new RRContext(rrContextOptions)))。您需要判断这是否比其他答案更好,但有时这是最简单的方法,因为它们不会公开重置功能(它确实存在并由DbContext池功能使用)。 - Simon_Weaver
1
仅供后来者参考,EntityFrameworkCore更改跟踪器允许重新加载单个实体: Entities(thing).Reload(); - XIU
1
这在EF 5.0.11中对我有所帮助。我必须使用.Detached,然后(item.SubItemInAnotherTable).ReloadAsync(),然后再次使用.FirstOrDefault()请求该项。 - Omzig
1
@Omzig,所以你确认我的答案可以按照原样工作?;-) - binki
显示剩余2条评论

22

其他两个答案已经提到了这些方法。 - Gert Arnold
4
非常正确,由于OP声明“我只发现人们使用Reload方法,而EF Core中没有该方法”,因此我决定发表一个回答,并且我认为短而完整的示例是缺失的。 - Ogglas
很棒的回答,不知道有没有人知道是否有一种以这种方式进行收集的选项?在这种情况下,最好是分离所有这些实体并重新运行你所拥有的任何查询。看起来EF可能会实现这种形式的收集,但我还没有看到。 - undefined

7

随着.NET 5.0和Entity Framework Core 5.0的发布,推荐使用DBContext工厂这种模式。在Startup.cs中我修改了:

services.AddDbContext<MyDbContext>...

to

services.AddDbContextFactory<MyDbContext>...

我所有的仓库类都使用相同的基类,这里我在构造函数中创建上下文:

protected BaseRepository(IDbContextFactory<MyDbContext> contextFactory)
{
    _context = contextFactory.CreateDbContext();
}

我使用一个非常简单的仓储工厂来确保每次需要时都能获得一个新的仓储和数据库上下文实例:

using System;
using Data.Models.Entities;
using Microsoft.Extensions.DependencyInjection;

namespace Data.Repository
{
    public class RepositoryFactory : IRepositoryFactory
    {
        private readonly IServiceProvider _serviceProvider;

        public RepositoryFactory(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
        
        public IApplicationRepository BuildApplicationRepository()
        {
            var service = _serviceProvider.GetService<IApplicationRepository>();

            return service;
        }
    }
}

使用所描述的模式解决了“DB context is created by dependency injection”错误,并消除了对 Reload()/Refresh() 方法的需求。

4
你需要将实体从上下文中“分离(detach)”,或者实现自己的扩展以使用“.Reload()”功能。
以下是“.Reload()”方法的实现。来源:https://weblogs.asp.net/ricardoperes/implementing-missing-features-in-entity-framework-core
public static TEntity Reload<TEntity>(this DbContext context, TEntity entity) where TEntity : class
{
    return context.Entry(entity).Reload();
}

public static TEntity Reload<TEntity>(this EntityEntry<TEntity> entry) where TEntity : class
{
    if (entry.State == EntityState.Detached)
    {
        return entry.Entity;
    }

    var context = entry.Context;
    var entity = entry.Entity;
    var keyValues = context.GetEntityKey(entity);

    entry.State = EntityState.Detached;

    var newEntity = context.Set<TEntity>().Find(keyValues);
    var newEntry = context.Entry(newEntity);

    foreach (var prop in newEntry.Metadata.GetProperties())
    {
        prop.GetSetter().SetClrValue(entity, 
        prop.GetGetter().GetClrValue(newEntity));
    }

    newEntry.State = EntityState.Detached;
    entry.State = EntityState.Unchanged;

    return entry.Entity;
}

GetEntityKey()是什么:

public static object[] GetEntityKey<T>(this DbContext context, T entity) where T : class
{
    var state = context.Entry(entity);
    var metadata = state.Metadata;
    var key = metadata.FindPrimaryKey();
    var props = key.Properties.ToArray();

    return props.Select(x => x.GetGetter().GetClrValue(entity)).ToArray();
}

你好,我需要使用哪个Using来使用“GetEntityKey”? DbContext没有包含“GetEntityKey”的定义。 - Cedric Arnould
1
@CedricArnould,“GetEntityKey” 是一种扩展方法,用于提取主键“Id”的元数据。我会编辑并将其添加到我的答案中。 - penleychan
加1以进行分离和重新查找。就这么简单。 - Avrohom

4
这个简单的序列可以在EFCore 5.0下刷新整个上下文:
public void Refresh()
{
    (Context as DbContext).Database.CloseConnection();
    (Context as DbContext).Database.OpenConnection();
}

有关这个的解释吗?刷新是指什么?只是连接吗?还是"整个上下文"?包括变更跟踪器吗? - undefined

3

这是我的解决方案,希望能对你有所帮助。

  • detach函数将分离所有嵌套的实体和实体数组。
  • findByIdAsync函数将具有可选参数以分离实体并重新加载它。

仓库

    public void Detach(TEntity entity)
    {
        foreach (var entry in _ctx.Entry(entity).Navigations)
        {
            if (entry.CurrentValue is IEnumerable<IEntity> children)
            {
                foreach (var child in children)
                {
                    _ctx.Entry(child).State = EntityState.Detached;
                }
            }
            else if (entry.CurrentValue is IEntity child)
            {
                _ctx.Entry(child).State = EntityState.Detached;
            }
        }
        _ctx.Entry(entity).State = EntityState.Detached;
    }

所以举个例子,有以下内容:
类:

Classes:

public interface IEntity : IEntity<Guid>
{
}

public interface IEntity<TPrimaryKey>
{
    [JsonProperty("id")]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    TPrimaryKey Id { get; set; }
}

public class Sample : IEntity
{
    public Guid Id { get; set; }
    public string Text { get; private set; }   
    public Guid? CreatedByUserId { get; set; }
    public virtual User CreatedByUser { get; set; }     
    public List<SampleItem> SampleItems { get; set; } = new List<SampleItem>();
}

public class SampleItem : IEntity
{
    public Guid Id { get; set; }
    public string Text { get; private set; }   
    public Guid? CreatedByUserId { get; set; }
    public virtual User CreatedByUser { get; set; }     
}

经理

    public async Task<Sample> FindByIdAsync(Guid id, bool includeDeleted = false, bool forceRefresh = false)
    {
        var result = await GetAll()
            .Include(s => s.SampleItems)
            .IgnoreQueryFilters(includeDeleted)
            .FirstOrDefaultAsync(s => s.Id == id);

        if (forceRefresh)
        {
            _sampleRepository.Detach(result);
            return await FindByIdAsync(id, includeDeleted);
        }

        return result;
    }

控制器

    SampleManager.FindByIdAsync(id, forceRefresh: true);

1
如果您想完全重新加载实体(及其子实体),那么这是一个很好的解决方案! - mirind4
IEntity在哪里? - Maulik Modi

-4
理想情况下,另一方在更新时会通知您。这可以通过消息队列实现。
其中包括:Azure Service Bus、Rabbit MQ、0MQ。

4
OP 询问如何强制 EntityFramework 的 DbContext 重新加载已加载的实体数据。如果您执行以下代码 var thing1 = myContext.Things.Find(1); /* something updates the database in the meantime */ var thing2 = myContext.Things.Find(1);,则 thing2 == thing1(相同对象实例),由于 DbContext 正在使用其缓存而不是重新加载数据,因此数据库中的更改将不会被加载到其中。 - binki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接