Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework - Different proxy objects for the same entity. And Include behavior with multiple paths to same destination

I noticed that when I navigate to the same entity object via a different "member path" I get a different object. (I'm using change-tracking proxies, so I get a different change-tracking proxy object.)

Here is an example to show what I mean.

var joesInfo1 = context.People.Single(p => p.Name == "Joe").Info;

var joesInfo2 = context.People.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info;

Even though joesInfo1 & joesInfo2 refer to the same record in the DB (the same entity), they are different objects. I thought that Entity Framework made sure to use the same object in these cases.

Question #1: Is this really how it is? Or is my observation wrong?

This is a problem when eager loading via Include. For example,

IQueryable<Person> allPeople = null;

using(context)
{
       allPeople = context.People
                          //.AsNoTracking()
                          .Include(p => p.Info)
                          .Include(p => p.Children)
                          .Include(p => p.Parent)
                          .ToList();

}


var joesInfo1 = allPeople.Single(p => p.Name == "Joe").Info;  // OK, Info is already there because it was eagerly loaded

var joesInfo2 = allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info;  
// ERROR: "Object context disposed...", Info is not in the Person object, even though the Person object refers to the same entity (Joe) as above.

So, it looks like to get eager loading to work, you have to specify all possible "member access paths" that you will take in your program. This is not possible in some cases like this one. Because your Person object might be floating around in your program and the navigation properties "Parent" or "Children" could be called on it (and it's parents/children) any number of times.

Question #2: Is there any way to get this to work without specifying all of the "member access paths" that you will take in your program?

Thanks.


ANSWER:

So, here's what I have concluded, based on bubi's answer.

It is possible to get different "entity objects" if you use AsNoTracking(). (In other words, in the example above, depending on what path you take to get to the "Joe" Person entity, it's possible that you will get a different object.) If you don't use AsNoTracking all the Joes will be the same object.

Here is what this means:

You CAN eagerly load a whole hierarchical or recursive object graph and use it outside of a context. How? JUST DON'T USE AsNoTracking().

like image 529
N73k Avatar asked Oct 17 '22 08:10

N73k


1 Answers

About your code, in the second question you are running the first query (allPeople is an IQueryable)

var joesInfo1 = allPeople.Single(p => p.Name == "Joe").Info;  // OK, Info is already there because it was eagerly loaded

with the context already disposed so it won't run.

Anyway, I suppose this is your model

[Table("People67")]
public class Person
{
    public Person()
    {
        Children = new List<Person>();
    }

    public int Id { get; set; }
    [MaxLength(50)]
    public string Name { get; set; }

    public virtual Info Info { get; set; }

    public virtual ICollection<Person> Children { get; set; }
}

public class Info
{
    public int Id { get; set; }
    [MaxLength(50)]
    public string Description { get; set; }
}

After seed the database this code works (look at the assertion)

using (var context = new Context(GetConnection()))
{
    var joes1 = context.People.Single(p => p.Name == "Joe");
    var joes2 = context.People.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe");

    Assert.IsTrue(object.ReferenceEquals(joes1, joes2);
    Assert.IsTrue(object.ReferenceEquals(joes1.Info.GetType(), joes2.Info.GetType()));
    Assert.IsTrue(object.ReferenceEquals(joes1.Info, joes2.Info));
}

so about your first question proxies are of the same type and the reference is the same.
A little bit deeper, if you have a look to the queries

ExecuteDbDataReader==========
SELECT TOP 2 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
[Extent1].[Person_Id] AS [Person_Id], 
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE 'Joe' = [Extent1].[Name]
ExecuteDbDataReader==========
SELECT TOP 2 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
[Extent1].[Person_Id] AS [Person_Id], 
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE 'Joe''s Dad' = [Extent1].[Name]
ExecuteDbDataReader==========
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
[Extent1].[Person_Id] AS [Person_Id], 
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE ([Extent1].[Person_Id] IS NOT NULL) AND ([Extent1].[Person_Id] = @EntityKeyValue1)
EntityKeyValue1 = 1
ExecuteDbDataReader==========
SELECT 
[Extent2].[Id] AS [Id], 
[Extent2].[Description] AS [Description]
FROM ( [People67] AS [Extent1]
INNER JOIN [Infoes] AS [Extent2] ON ([Extent1].[Info_Id] = [Extent2].[Id]))
WHERE ([Extent1].[Info_Id] IS NOT NULL) AND ([Extent1].[Id] = @EntityKeyValue1)
EntityKeyValue1 = 2

you can understand that EF merge the entities in memory (look at the third query).

Now, to be more precise, this behaviour does not change if you add also a property Parent_Id to Person. The third query is run also if EF should know that Joe is already in memory.

===================
Now the second part

As I said at the beginning of the answer, your code does not work at all because you are accessing to an IQueryable with a disposed context also in the first query.

In this case I suppose this is your code.

List<Person> allPeople;

using (var context = new Context(GetConnection()))
{
    allPeople = context.People
        .Include(_ => _.Info)
        .Include(_ => _.Children)
        .ToList();
}

// This is an in memory query because to the previous ToList
// Take care of == because is an in memory case sensitive query!
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe").Info);
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info);
Assert.IsTrue(object.ReferenceEquals(allPeople.Single(p => p.Name == "Joe").Info, allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info));

If you activate the profiler you will see that EF does not run query after the ToList().

===================
So, what does not work? Several things if you insert AsNoTracking(). In that case the EF behaviour is different, the entities are not in the context (are not tracked) and EF needs to access database to retrieve entities that it should have in memory.

For example, this code does not work.

List<Person> allPeople;

using (var context = new Context(GetConnection()))
{
    allPeople = context.People
        .Include(_ => _.Info)
        .Include(_ => _.Children)
        .AsNoTracking()
        .ToList();
}

// This throw an exception
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info);

EDIT
You can solve the different issues you find using AsNoTracking in different ways. I don't know if there is "the solution".
I usually implement == (and Equals, !=, GetHashCode and so on) taking care of character casing (DBMSs often are case insensitive so also == must be case insensitive) to avoid '==' issues (different references to same db entity).
Then, if I need, I cache in entities in memory and I search for entities in memory instead of navigating properties.
At the end the code is not so clean as using navigation properties but it works(Knuth said, "optimization is the root of all evils").

like image 109
bubi Avatar answered Oct 21 '22 07:10

bubi