Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AsNoTracking() performs partial identity resolution #29152

Closed
ComptonAlvaro opened this issue Sep 19, 2022 · 9 comments
Closed

AsNoTracking() performs partial identity resolution #29152

ComptonAlvaro opened this issue Sep 19, 2022 · 9 comments

Comments

@ComptonAlvaro
Copy link

I want to do a query with no identity resolution, so I am trying to use AsNoTracking(), but it performs identity resolution.

I am trying this:

            using (Context miDbContext = new Context(_optionsDbContext))
            {
                return await miDbContext.Orders
                    .AsNoTracking()
                    .Include(x => x.IDCategoryNavigation)
                    .ToListAsync();
           }

The Order has a property Category and category has a property that is a collection o orders, I mean, it is bidirectional navigation properties.

Supose I have 2 orders of the category A. If I don't use AsNoTracking(), if i access the category through an order, in the colletion of the category I have 2 orders, it is the expected.

But if I use AsNoTracking(), if I access to the category through and order, the category has only one order, that is the same instance than the order.

Is this the expected behaviour? I mean, in the documentation, it is tell that if I use AsNoTracking, it will be created two istances for the same ID.

Thanks.

@ajcvickers
Copy link
Member

This issue is lacking enough information for us to be able to fully understand what is happening. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

@ComptonAlvaro
Copy link
Author

I include a solution with an example, that include the code and a .bak with a Sql Server database.

But I whill explain the two cases.

I have orders and cateogories. I have two orders for the category 1.

When I use AsNoTracking:

List<Ordene> miLstOrdenes = await miDbContext.Ordenes
                        .Include(x => x.IDCategoriaNavigation)
                        .AsNoTrackingWithIdentityResolution()
                        .ToListAsync();

Result: 1 order in the collection, the order thorugh I access the category. So if I access from order 1, the category has the order one, if I access from order 2, the category has the order 2.

My expected result: I am not using indentity resolution, so I wouldn't expect any order in the collection of the category. I would expect order in the category if I explicitly set the include with .Include(x => x.IDCategoriaNavigation).ThenInclude(x => x.Ordenes). But what I get is a partial include, only 1 order, so it could be a bit confuse in some cases.

When I use AsNoTraccking including the orders of the category:

List<Ordene> miLstOrdenes = await miDbContext.Ordenes
                        .Include(x => x.IDCategoriaNavigation).ThenInclude(x => x.Ordenes)
                        .AsNoTracking()
                        .ToListAsync();

Result: exception because of cycle includes.

My expected result: how it is not used identity resolution, I wouldn't expect an exception, because I would like to can tell that I want the order of the category.

EfCoreIdentityResolutionTest.zip

@ajcvickers
Copy link
Member

@ComptonAlvaro Unfortunately, I'm really struggling to understand what you are saying. So let me explain what is happening in each of your queries and why; hopefully that will help.

List<Ordene> miLstOrdenes = await miDbContext.Ordenes
                        .Include(x => x.IDCategoriaNavigation)
                        .AsNoTrackingWithIdentityResolution()
                        .ToListAsync();

Each Ordene in the list will have its IDCategoriaNavigation set to its associated Categoria instance. There will only ever be one Ordene instance with a given key value, and only ever one Categoria instance with a given key value. If multiple Ordene instances are associated with the same Categoria, then IDCategoriaNavigation on each of these instances will point to the single Categoria instance with that key. The Ordenes collection navigation on each Categoria will contain all the instances Ordene that are related to that Categoria.

List<Ordene> miLstOrdenes = await miDbContext.Ordenes
                        .Include(x => x.IDCategoriaNavigation).ThenInclude(x => x.Ordenes)
                        .AsNoTracking()
                        .ToListAsync();

As before, each Ordene in the list will have its IDCategoriaNavigation set to its associated Categoria instance. However, rather than there being a single Categoria instance with a given key value, there will instead be a new Categoria instance for each Ordene, regardless of its key value. This is because there is no identity resolution to resolve two Categoria instances with the same key value into a single instance. The Ordenes collection navigation on each of these Categoria instances will contain only the Ordene instance that references it. Note that the ThenInclude(x => x.Ordenes) call here does nothing--you should see a warning in the logs indicating that it was ignored.

@ComptonAlvaro
Copy link
Author

With AsNoTrackingWithIdentities, I don't have problems, for me it works as I expected. It is with AsNoTracking.

To try to use the same concepts, I will explain what I understand about AsNoTracking, if in some point it is wrong, please, correct me. I am base in this document: https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking?view=efcore-6.0

AsNoTracking means that there is no identity resolution.

No identity resolution for me means 2 things.

First, if I don't specify an explicit include, then don't populate the collections or navigation properties of the included entities. Then I have two cases.

First case, Order.Inlcude(x => x.IDCategoryNavigation). In this case, I don't expect that collection Category.Orders has items, because I didn't tell explicitly with an include (ThenInclude in this case). But it includes one order, that is the same instance that the that I get as result of the query. So EF core decides by itself to populate the collection, when I didn't tell do that. For me this is what I understand for entity resolution, to related entities with the same key and use the same instace. For that I have tracking and entity resolution and also I can filtrer the included entities, so I don't understand why with AsNoTracking EF decide to populate the collection.

Another problem for me in this case is that the collection of orders of the category has only one order, not two, when really there are two order of this category. This is because no entity resolution, I know, but for me it is partial entity resolution, because add to the collection one order and is the same instance than the root return as result. It is a bit confuse for consumer, why to get only one, when the consumer expect no entity resolution and no order in the collection (from my point of view).

The second case:

Order.Inlcude(x => x.IDCategoryNavigation).ThenInclude(x => x.Orders). In this case, I get an exception because of circle includes. What I would expect it is don't get this exception, but I get this exception because entity core tries to populate the collection of the orders. How I comment above, I would expect that in the case of no entity resolution, EF only populate the collection if I tell explicitly with the ThenInclude(x => x.).

So the result expected in this case it would be no an exception, but the order with the category and the collection of the category of all the orders of the category, because i didn't filter the included entities.

I guess that perhaps this is the correct behaviour by design. If this is the case, perhaps it could be possible to add a new method, for example, in the same way there is a AsNoTracking, to have AsNoTrackingWithExplicitIncludes() for example that it could work in this way:

Order.AsNotrackingWithExcplicitIncludes().ToList(): no include any navigation property.

Order.AsNotrackingWithExcplicitIncludes().Include(x => x.IDCategoryNavigation): in this case Order has the category property navigation and the collection of orders of the category would be empty, I didn't tell I want to inlcude it.

Order.AsNotrackingWithExcplicitIncludes().Include(x => x.IDcategoryNavigation).ThenInclude(x => x.Orders): in this case, the collection of orders of the category would have all the orthers of the category. It includes the entity of the root query, but it is different instace. How I didn't include another explicit ThenInclude, the orders of the collection of the category wouldn't have the navigation property of the category.

With that from my point of view I have the following advanteges:

1.- I indicate explicitly what i want, so EF doesn't decide what to include in the collection. Nothing if I don't tell, no partial includes. So it breaks cicle references.

2.- How it is not used identity resolution, I have different instances for the same entity if it is has to be include in many places.

The Idea of all of this thinking in the serialization. If I get the result of a query and it has circle references, many serializers has problems with that, so I have to create DTOs to break that circle references. If from EF I get a result without this circle references because EF doesn't populate the collection itself, because I can decide the level of includes, then I could serialize it directly.

Perhaps some could think that always it is a good idea to create the DTOs, but really if it is enough with the result of EF, I can save this resources, and in this way I am free to decide if create DTOs or not, not only create because I get circle references from EF.

I hope now it would be more clear instead of more confuse.

@ajcvickers
Copy link
Member

First, if I don't specify an explicit include, then don't populate the collections or navigation properties of the included entities.

This is not correct. AsNoTracking doesn't change the behavior of Include.

@ComptonAlvaro
Copy link
Author

With as NoTracking() EF decide to include in the collection of the order of the category the order, why is it included this order when I didn't tell with ThenInclude()?

@ajcvickers
Copy link
Member

EF always fixes up both sides of the relationship. See #11564.

@ComptonAlvaro
Copy link
Author

Yes, it is the same than the other user request, decide if to fix up or not the relationship.

As you comment the other issue, this is the bahavior that EF has since the begging, so it has no sense to break it for that, but perhaps would it is possible to add a new option, for example AsNoTracnkingWithNoFixUpRelationships?

In this way I could decide when to fix up and when not fix up the relationships.

For example:

orders.AsNoTracking().Include(x => x.IDCategoryNavigation): this will fix up the relationship, so the collection of orders will have one order, and it will be the same instance than root order. Correct, it is the actual beahavior.

Orders.AsNoTrackingWithNoFixUpRelationships().Inlcude(x => x.IDCategoryNavigation): this will not fix up the relationship, so the collection of the category will not have any order.

Orders.AsNoTracking().Include(x => x.IDCategoryNavigation).ThenInclude(x => x.Orders). Exception because of circle references. It is the actual bahaviour. Correct to don't break anctual behavior.

Orders.AsNoTrackingWithNoFixUpRelationships(x => x.IDCategoryNavigation).ThenInclude(x => x.Orders): no exception, and the collection of the orders will have all the orders of the category, icnluded the root order, but it will be both different instances. Or perhaps the same to save memery, for serialization the problem if I am not wrong, it is circular types reference, no circle instances references.

@ajcvickers
Copy link
Member

@ComptonAlvaro Anything we may do in this area is tracked by #11564.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants