How to refresh an Entity Framework Core DBContext?

前端 未结 5 1555
隐瞒了意图╮
隐瞒了意图╮ 2020-12-10 01:58

When my table is updated by another party, the db context in dotnet core still return the old value, how can I force the Db context to refresh?

I\'ve done research b

相关标签:
5条回答
  • 2020-12-10 02:22

    Oh, this issue had me in knots for days.

    I'm using Visual Studio 2017 with .Net Core 2.1, and my EF Core code looked something like this:

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

    At step 3, it would simply set "updatedUser" to the original version of the [User] record, rather than attempting to load in a fresh copy. So, if, after step 3, I modified that [User] record, I'd actually lose whatever settings the web service had applied to it.

    I - eventually - found two solutions.

    I could change the ChangeTracker settings. This worked, but I was concerned about the side-effects of doing this:

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

    Or, I could slip in the following command, before attempting to reload the [User] record...

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

    This seems to force .Net Core to reload that [User] record, and life is good again.

    I hope this is useful...

    Tracking down, and fixing this bug took me days....

    There's also an excellent article describing the various ways to get around this caching issue here.

    0 讨论(0)
  • 2020-12-10 02:24

    Dependency Injection and DbContext

    You mention that when you try to recreate your DbContext, you get an error about the context being managed by your dependency injection (DI) system. There are two different styles of using a dependency injection system for object creation. The DI can either create a global singleton instance that is shared as a service between all consumers or it can create an instance per scope/unit of work (e.g., per request in a web server).

    If your DI system is configured to create a single global shared instance of DbContext, then you will encounter various problems associated with long-lived DbContext.

    • DbContext, by design, never automatically removes objects from its cache because it is not designed to be long-lived. Thus, a long-lived DbContext will retain memory wastefully.
    • Your code will never see changes to items loaded into its cache without manually reloading each entity it loads.
    • DbContext only allows one query to run at any time and is not threadsafe. If you try to run multiple queries on a globally shared instance, it will throw DbConcurrencyException (at least on its async interface, not sure about its sync interface).

    Thus, the best practice is to use a single DbContext per unit of work. Your DI system can help you with this by being configured to provide a fresh instance for each request your application processes within a scope. For example, ASP.NET Core’s Dependency Injection system supports scoping instances by request.

    Refreshing a Single Entity

    The easiest way to get fresh data is to create a new DbContext. However, within your unit of work, or within the constraints of the granularity of scoping provided by your DI system, you may trigger an external process which is supposed to modify your entity directly in the database. You may need to see that change before exiting your DI’s scope or completing your unit of work. In that case, you can force a reload by detaching your instance of the data object.

    To do this, first get the EntityEntry<> for your object. This is an object which lets you manipulate DbContext’s internal cache for that object. You can then mark this entry detached by assigning EntitytState.Detached to its State property. I believe that this leaves the entry in the cache but causes the DbContext to remove and replace it when you actually load the entry in the future. What matters is that it causes a future load to return a freshly loaded entity instance to your code. For example:

    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);
    
    0 讨论(0)
  • 2020-12-10 02:28

    Here is my solution, I hope it will help you.

    • The detach function will detach all nested entities and array of entities.
    • The findByIdAsync will have an optional parameter to detach the entity and will reload it.

    Repository

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

    So for exemple with:

    Classes:

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

    Manager

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

    Controller

        SampleManager.FindByIdAsync(id, forceRefresh: true);
    
    0 讨论(0)
  • 2020-12-10 02:39

    You would have to detach the entity from the context, or implement you own extension for .Reload()

    Here's the .Reload() implementation. Source: 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;
    }
    

    Where 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();
    }
    
    0 讨论(0)
  • 2020-12-10 02:40

    Ideally the other party would notify you when an update is made. This could be achieved via a message queue.

    Among others: Azure Service Bus, Rabbit MQ, 0MQ

    0 讨论(0)
提交回复
热议问题