当我的表被另一方更新时,.NET Core中的数据库上下文仍然返回旧值,我该如何强制Db上下文刷新?
我已经做了研究,但我只发现人们使用Reload
方法来强制上下文刷新,但它在EF Core中不可用。
其他一些解决方案建议在使用后处理上下文,但我收到错误消息说DB上下文是由依赖注入创建的,我不应该去干扰它。
当我的表被另一方更新时,.NET Core中的数据库上下文仍然返回旧值,我该如何强制Db上下文刷新?
我已经做了研究,但我只发现人们使用Reload
方法来强制上下文刷新,但它在EF Core中不可用。
其他一些解决方案建议在使用后处理上下文,但我收到错误消息说DB上下文是由依赖注入创建的,我不应该去干扰它。
哦,这个问题让我纠结了好几天。
我正在使用带有.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 花费了我数天时间...
在这里也有一篇优秀的文章描述了解决此缓存问题的各种方法。
AsNoTracking()
相同。在任何情况下,我仍然建议使用 NoTrackingWithIdentityResolution
,因为上述方法将在单个查询中为相同的实体返回不同的实例,而此选项将首先合并它们。 - Xriuk您提到当您尝试重新创建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);
DbContextOptions<T>
,其中T
是您的上下文类型。因此,您实际上可以注入它(例如DbContextOptions<RRStoreContext> rrContextOptions
),然后根据需要创建全新的上下文(using (var db = new RRContext(rrContextOptions))
)。您需要判断这是否比其他答案更好,但有时这是最简单的方法,因为它们不会公开重置功能(它确实存在并由DbContext池功能使用)。 - Simon_WeaverReload
和 ReloadAsync
自 Entity 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();
随着.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;
}
}
}
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();
}
public void Refresh()
{
(Context as DbContext).Database.CloseConnection();
(Context as DbContext).Database.OpenConnection();
}
这是我的解决方案,希望能对你有所帮助。
仓库
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);
DbContext
重新加载已加载的实体数据。如果您执行以下代码 var thing1 = myContext.Things.Find(1); /* something updates the database in the meantime */ var thing2 = myContext.Things.Find(1);
,则 thing2 == thing1
(相同对象实例),由于 DbContext
正在使用其缓存而不是重新加载数据,因此数据库中的更改将不会被加载到其中。 - binki