Novitzky
Novitzky

Reputation: 4876

EntityFramework doesn't refresh navigation properties

I've got a simple relationship

enter image description here

I've created a simple app with the Model like the above one. Model in the application must be updated every time DB changes. I can get the latest changes by calling GetDBChanges Stored Procedure. (see method T1Elapsed)

here is the app:

class Program
{
    private static int? _lastDbChangeId;
    private static readonly MASR2Entities Model = new MASR2Entities();
    private static readonly Timer T1 = new Timer(1000);
    private static readonly Timer T2 = new Timer(1000);
    private static Strategy _strategy = null;

    static void Main(string[] args)
    {
        using (var ctx = new MASR2Entities())
        {
            _lastDbChangeId = ctx.GetLastDbChangeId().SingleOrDefault();
        }
        _strategy = Model.Strategies.FirstOrDefault(st => st.StrategyId == 224);

        T1.Elapsed += T1Elapsed;
        T1.Start();

        T2.Elapsed += T2Elapsed;
        T2.Start();

        Console.ReadLine();
    }

    static void T2Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("All rules: " + Model.StrategyRules.Count());
        Console.WriteLine("Strategy: name=" + _strategy.Name + " RulesCount=" + _strategy.StrategyRules.Count);
    }

    private static void T1Elapsed(object sender, ElapsedEventArgs e)
    {
        T1.Stop();
        try
        {
            using (var ctx = new MASR2Entities())
            {
                var changes = ctx.GetDBChanges(_lastDbChangeId).ToList();
                foreach (var dbChange in changes)
                {
                    Console.WriteLine("DbChangeId:{0} {1} {2} {3}", dbChange.DbChangeId, dbChange.Action, dbChange.TableName, dbChange.TablePK);
                    switch (dbChange.TableName)
                    {
                        case "Strategies":
                            {
                                var id = Convert.ToInt32(dbChange.TablePK.Replace("StrategyId=", ""));
                                Model.Refresh(RefreshMode.StoreWins, Model.Strategies.AsEnumerable());
                            }
                            break;
                        case "StrategyRules":
                            {
                                var id = Convert.ToInt32(dbChange.TablePK.Replace("StrategyRuleId=", ""));
                                Model.Refresh(RefreshMode.StoreWins, Model.StrategyRules.AsEnumerable());
                            }
                            break;
                    }
                    _lastDbChangeId = dbChange.DbChangeId;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("ERROR: " + ex.Message);
        }
        finally
        {
            T1.Start();
        }
    }
}

When I run it, this is a sample output:

All rules: 222
Strategy: name=Blabla2 RulesCount=6

then I add a row to the child table (Strategy Rule),

DbChangeId:1713 I StrategyRules StrategyRuleId=811
All rules: 223
Strategy: name=Blabla2 RulesCount=7

and finally, I remove the row from StrategyRules

DbChangeId:1714 D StrategyRules StrategyRuleId=811
All rules: 222
Strategy: name=Blabla2 RulesCount=7

Why RulesCount is still 7? How I can force EF to refresh "Navigation Property"?

What I am missing here?

---EDIT--- to cover Slauma's answer

case "StrategyRules":
{
   var id = Convert.ToInt32(dbChange.TablePK.Replace("StrategyRuleId=", ""));
   if (dbChange.Action == "I")
   {
       //Model.Refresh(RefreshMode.StoreWins, Model.StrategyRules.AsEnumerable());       
   }
   else if (dbChange.Action == "D")
   {
      var deletedRule1 = Model.StrategyRules.SingleOrDefault(sr => sr.Id == id); 
      //the above one is NULL as expected

      var deletedRule2 = _strategy.StrategyRules.SingleOrDefault(sr => sr.Id == id);
      //but this one is not NULL - very strange, because _strategy is in the same context
      //_strategy = Model.Strategies.FirstOrDefault(st => st.StrategyId == 224);
   }  
}

Upvotes: 0

Views: 3226

Answers (1)

Slauma
Slauma

Reputation: 177133

ObjectContext.Refresh refreshes the scalar properties of the entities you pass into the method along with any keys that refer to related entities. If an entity you pass into the method does not exist anymore in the database because it has been deleted in the meantime Refresh does nothing with the attached entity and just ignores it. (That's a guess from my side, but I could not explain otherwise why you 1) don't get an exception on Refresh (like "cannot refresh entity because it has been deleted") and 2) the entity is apparently still attached to the context.)

Your Insert case does not work because you call Refresh but it works because you load the whole StrategyRules table into memory in this line:

Model.Refresh(RefreshMode.StoreWins, Model.StrategyRules.AsEnumerable())

Refresh enumerates the collection in the second parameter internally. By starting the iteration it triggers the query which is just Model.StrategyRules = load the whole table. AsEnumerable() is only a switch from LINQ-to-Entities to LINQ-to-Objects, that is, every LINQ operator you would apply after AsEnumerable() is performed in memory, not on the database. Since you don't apply anything, AsEnumerable() actually has no effect on your query.

Because you load the whole table the recently inserted StrategyRule will be loaded as well and together will the key to the _strategy entity. The ObjectContext's automatic relationship fixup establishes the relationship to the navigation collection in _strategy and _strategy.StrategyRules.Count will be 7. (You could remove the Refresh call and just call Model.StrategyRules.ToList() and the result would still be 7.)

Now, all this does not work in the Delete case. You still run a query to load the whole StrategyRules table from the database but EF won't remove or detach entities from the context that are not in the result set anymore. (And as far as I know there is no option to force such an automatic removal.) The deleted entity is still in the context with its key refering to strategy and the count will remain 7.

What I am wondering is why you don't leverage that your set of DBChanges apparenty knows exactly what has been removed in the dbChange.TablePK property. Instead of using Refresh couldn't you use something like:

case "StrategyRules":
{
    switch (dbChange.Action)
    {
        case "D":
        {
            var removedStrategyRule = _strategy.StrategyRules
                .SingleOrDefault(sr => sr.Id == dbChange.TablePK);
            if (removedStrategyRule != null)
                _strategy.StrategyRules.Remove(removedStrategyRule);
        }
        break;

        case ...
    }
}
break;

Upvotes: 2

Related Questions