テーブルが別のパーティによって更新された場合、dotnetコアのdbコンテキストはまだ古い値を返しますが、Dbコンテキストを強制的に更新するにはどうすればよいですか?
私は調査を行いましたが、EFコアでは使用できないReload
メソッドを使用して、コンテキストを強制的に更新していることがわかりました。
他の解決策は、使用後にコンテキストを破棄することを提案しますが、DBコンテキストは依存性注入によって作成され、それを台無しにすべきではないというエラーが表示されます。
DbContext
を再作成しようとすると、依存性注入(DI)システムによって管理されているコンテキストに関するエラーが発生することに言及しています。オブジェクト作成に依存性注入システムを使用するには、2つの異なるスタイルがあります。 DIは、すべてのコンシューマー間でサービスとして共有されるグローバルシングルトンインスタンスを作成するか、スコープ/作業単位(たとえば、Webサーバーでのリクエストごと)にインスタンスを作成できます。
DIシステムがDbContext
の単一のグローバル共有インスタンスを作成するように構成されている場合、長期にわたるDbContext
に関連するさまざまな問題が発生します。
DbContext
は、設計により、キャッシュからオブジェクトを自動的に削除することはありません。これは、オブジェクトが長期間存続するように設計されていないためです。したがって、長寿命のDbContext
はメモリを無駄に保持します。DbContext
では、常に1つのクエリのみを実行でき、スレッドセーフではありません。グローバルに共有されたインスタンスで複数のクエリを実行しようとすると、 throw DbConcurrencyException
(少なくともその非同期インターフェイスで、その同期インターフェイスが不明)。したがって、 ベストプラクティスは、作業単位ごとに単一のDbContext
を使用することです 。 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);
ああ、この問題は何日も結びついていました。
.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)]レコードのoriginalバージョンに設定するだけで、新しいコピーをロードしようとしません。したがって、ステップ3の後、その[ユーザー]レコードがImodifiedの場合、Webサービスが適用した設定はすべて失われます。
私は-最終的に-2つのソリューションを見つけました。
ChangeTracker
設定を変更できます。これは機能しましたが、これを行うことの副作用が心配でした。
dbContext.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;
または、[User]レコードをリロードする前に、次のコマンドを入力することもできます...
await dbContext.Entry(usr).ReloadAsync();
これにより、.Net Coreがその[ユーザー]レコードをリロードするように強制されるようであり、人生は再び良好です。
これが役立つことを願っています...
このバグを追跡して修正するには、数日かかりました。
このキャッシュの問題を回避するさまざまな方法を説明する優れた記事もあります here 。
コンテキストのエンティティを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();
}
_
ここに私の解決策があります、それがあなたを助けることを願っています。
リポジトリ
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<TPrimaryKey>
{
[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);