0

I was surprised about a DbContext behaviour with this code:

DBContext context = new();
While(true)
{
Fubar fubar = context.Fubars.Single(f => f.Id == X);
Console.WriteLine(fubar.MyProp); //NOK!!
}

If, in the meantime, another context from another application update this entity with a different MyProp value, the DbContext will keep returning an object with the initial obsolete MyProp value.

I can easily solve it by reinstanciating the DBContext into each iteration, but I thought that reinstantiating the entity was enough:

While(true)
{
DBContext context = new();
Fubar fubar = context.Fubars.Single(f => f.Id == X);
Console.WriteLine(fubar.MyProp); //OK!!
}

Can you explain me why I do not get the upto date entity with Fubar fubar = context.Fubars.Single(f => f.Id == X); ?

A.D.
  • 1,062
  • 1
  • 13
  • 37
  • https://stackoverflow.com/questions/46205114/how-to-refresh-an-entity-framework-core-dbcontext – TheGeneral Oct 13 '21 at 08:36
  • 3
    most full ORMs implement some level of object identity caching; if you ask for Fubar 42, then *the first time* it will go to the DB, get the row, and build you an object - but in the future, if it sees that it would be creating Fubar 42, it will *hand you back that exact same instance* - no new object will be created, and the row won't be consumed; if possible, the ORM will also try to *not even talk to the DB*, if it can understand what you're doing (`Single`), and knows that it has that object in memory. Basically: use a different `DBContext` instance (and dispose the old, if `IDisposable`) – Marc Gravell Oct 13 '21 at 08:49

1 Answers1

0

DbContexts should be short-lived. If you want instances that are not tracked/cached then you can use AsNoTracking() in the query, however this would need to be done consistently as I believe an AsNoTracking() query could still return a previously tracked entity. (I.e. from another call against that DbContext instance that didn't use AsNoTracking()

To ensure a DbContext always returns the current state from the database consistently, one approach would be to hide the DbSets and expose IQueryable<TEntity> alternatives.

For instance, instead of:

public class AppDbContext : DbContext
{
    public DbSet<Orders> Orders { get; set; }
    public DbSet<Customers> Customers { get; set; }
}

public class AppReadDbContext : DbContext
{
    public DbSet<Order> _Orders { protected get; set; }
    public DbSet<Customer> _Customers { protected get; set; }

    public IQueryable<Order> Orders => _Orders.AsNoTracking();
    PUBLIC  IQueryable<Customer> Customers => _Customer.AsNoTracking();        
}

Caveat: That's an idea I haven't confirmed works or not, will try it out tomorrow morning. Update: Tested it this morning and applied a few changes. Protected DbSets don't populate so left the DbSet public with a protected Getter. This is to discourage code attempting to use the DbSet rather than the AsNoTracking() IQueryable. Verified the behaviour with the following test:

[Test]
public void TestNoTracking()
{
    using (var context = new AppReadDbContext()) // protected DbSet getter with NoTracking IQueryable getter.
    {
        var customer = context.Customers.Single(x => x.CustomerId == 2);
        Assert.AreNotEqual("Alex", customer.Name);
        using (var context2 = new AppDbContext()) // normal DbSets
        {
            var c = context2.Customers.Single(x => x.CustomerId == 2);
            c.Name = "Alex";
            context2.SaveChanges();
        }
        Assert.AreNotEqual("Alex", customer.Name);
        customer = context.Customers.Single(x => x.CustomerId == 2);
        Assert.AreEqual("Alex", customer.Name); // was fetched from DB.
    }
}

Note that this would be only really suitable for read operations. You can do updates without tracking but it is less efficient. Generally for most operations aim to use short-lived DbContexts, and ensure they are disposed afterwards. (I.e. using() block)

Steve Py
  • 26,149
  • 3
  • 25
  • 43