如何刷新实体框架核心 DBContext?

Posted

技术标签:

【中文标题】如何刷新实体框架核心 DBContext?【英文标题】:How to refresh an Entity Framework Core DBContext? 【发布时间】:2017-09-13 19:06:52 【问题描述】:

当我的表被另一方更新时,dotnet core中的db context仍然返回旧值,如何强制db context刷新?

我进行了研究,但我只发现人们使用Reload 方法来强制刷新上下文。

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

【问题讨论】:

他们共享相同的上下文?保存更改后,不同的上下文应该共享相同的状态。 @Fals 不幸的是,他们不共享相同的上下文。 如果您只需要读取数据,那么“AsNoTracking”可以帮助您。 EF Core:docs.microsoft.com/en-us/ef/core/querying/tracking,更多信息请参见:EF Cache Busting:codethug.com/2016/02/19/Entity-Framework-Cache-Busting 【参考方案1】:

哦,这个问题困扰了我好几天。

我正在使用带有 .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);

在第 3 步,它将简单地将“updatedUser”设置为 [User] 记录的原始 版本,而不是尝试加载新副本。因此,如果在第 3 步之后,我修改了 [User] 记录,我实际上会丢失 Web 服务对其应用的任何设置。

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

我可以更改ChangeTracker 设置。这行得通,但我担心这样做的副作用:

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

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

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

这似乎迫使 .Net Core 重新加载该 [User] 记录,生活再次美好。

希望对你有用……

追踪并修复这个错误花了我几天的时间......

还有一篇出色的文章描述了解决此缓存问题的各种方法here。

【讨论】:

我明白这有多烦人。然而,这是我的建议和我目前的理解,在 EF 的设计中,DbContext 对象是工作单元模式的实现。它应该是短暂的,而不是被不同的翻译使用。您可以尝试使用另一个新的 DbContext 来执行第二个操作。 :) 晚了将近一年,但使用此解决方案使用 Reload,我遇到另一个错误,即“在前一个操作完成之前在此上下文上启动了第二个操作。这通常是由使用同一实例的不同线程引起的的 DbContext,但是实例成员不能保证是线程安全的。这也可能是由在客户端上评估嵌套查询引起的,如果是这种情况,请重写查询以避免嵌套调用。由于后续调用。在这种情况下,ChangeTracker 可能更好,但我也不确定副作用。【参考方案2】:

依赖注入和 DbContext

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

如果您的 DI 系统配置为创建 DbContext 的单个全局共享实例,那么您将遇到与长寿命 DbContext 相关的各种问题。

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

因此,the best practice is to use a single DbContext per unit of work。您的 DI 系统可以通过配置为在一个范围内为您的应用程序处理的每个请求提供一个新实例来帮助您解决此问题。例如,ASP.NET Core 的 Dependency Injection 系统支持按请求对实例进行作用域。

刷新单个实体

获取新数据的最简单方法是创建一个新的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);

【讨论】:

很好的答案,我使用了一个短暂的上下文来解决该项目中的问题。谢谢,希望这个答案能帮助其他人。 另外值得注意的是,当您注册 DbContext 时,它还会注册DbContextOptions<T>,其中T 是您的上下文类型。所以你实际上可以注入它(例如DbContextOptions<RRStoreContext> rrContextOptions)并根据需要创建一个全新的上下文(using (var db = new RRContext(rrContextOptions))。你必须判断这是否最适合与其他答案相比,但有时它是最简单的方法,因为它们不会公开重置功能(它确实存在并且由 DbContext 池功能使用)。 为了以后到达这里的每个人,EntityFrameworkCore 更改跟踪器允许重新加载单个实体:Entities(thing).Reload(); 所以这一半在 EF 5.0.11 中帮助了我。我确实必须 .Detached,然后 (item.SubItemInAnotherTable).ReloadAsync(),然后用 .FirstOrDefault() 再次询问该项目 @Omzig 所以你确认我的答案是书面的吗? ;-)【参考方案3】:

ReloadReloadAsyncEntity Framework Core 1.1 以来一直可用

例子:

//test.Name is test1
var test = dbContext.Tests.FirstOrDefault();
test.Name = "test2";

//test.Name is now test1 again
dbContext.Entry(test).Reload();

https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.entityentry.reload?view=efcore-1.1

【讨论】:

其他两个答案已经提到了这些方法。 @GertArnold 非常正确,鉴于 OP 声明 I only found people use Reload method, which is not available in EF core,我决定发布答案。而且我认为缺少一个简短而完整的示例。【参考方案4】:

随着 .NET 5.0 和 Entity Framework Core 5.0 的发布,推荐的模式是使用 DBContext factory。在 Statup.cs 我改变了:

services.AddDbContext<MyDbContext>...

services.AddDbContextFactory<MyDbContext>...

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

protected BaseRepository(IDbContextFactory<MyDbContext> contextFactory)

    _context = contextFactory.CreateDbContext();

我使用一个非常简单的存储库工厂来确保每次需要时都能获得一个新的存储库实例和 dbcontext:

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 上下文是由依赖注入创建的”错误并消除了对 Reload()/Refresh() 方法的需求。

【讨论】:

【参考方案5】:

您必须 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();

【讨论】:

嗨,我需要使用“GetEntityKey”吗? DbContext 不包含“GetEntityKey”的定义。 @CedricArnould,GetEntityKey 是一种扩展方法,用于提取主键Id 的元数据。我将对其进行编辑并将其添加到我的答案中。 加 1 用于分离和重新查找。就这么简单。【参考方案6】:

这个简单的序列刷新了 EFCore 5.0 下的整个上下文:

public void Refresh()

    (Context as DbContext).Database.CloseConnection();
    (Context as DbContext).Database.OpenConnection();

【讨论】:

【参考方案7】:

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

分离函数将分离所有嵌套实体和实体数组。 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;
    

例如:

类:

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);

【讨论】:

如果您想完全重新加载您的实体(及其子实体),那么这是一个很好的解决方案! IEntity 在哪里?【参考方案8】:

理想情况下,另一方会在进行更新时通知您。这可以通过message queue 来实现。

其中包括:Azure 服务总线、Rabbit MQ、0MQ

【讨论】:

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正在使用它的缓存而不是重新加载数据。

以上是关于如何刷新实体框架核心 DBContext?的主要内容,如果未能解决你的问题,请参考以下文章

实体框架核心 DbContext 和依赖注入

无法使用 DbContext 实体框架核心 SaveChanges()

应用程序中断访问 dbcontext、Asp .net 核心 web api 2.0 与实体框架核心 2.0 数据库第一种方法

实体框架核心和多线程

dbcontext 不包含“刷新”的定义

dotnet 核心中的 Dbcontext 对象瞬态