2

Since no-tracking queries in Entity Framework Core have lazy-loading disabled, I've been looking for a way to eagerly load all needed properties.

I'm trying to create a class that will handle the generation of all navigational paths from a certain entity class. I would use these paths to call .Include(path) on an IQueryable<TEntity> of that entity, so they can get eagerly loaded.

This is the method that handles the generation of navigational paths:

private IEnumerable<string> GenIncludePaths(int depth, IEntityType entityType, HashSet<INavigation> includedNavigations = null)
        {
            if (depth <= 0)
                yield break;

            if (includedNavigations == null)
                includedNavigations = new HashSet<INavigation>();

            var entityNavigations = new List<INavigation>();
            foreach (var navigation in entityType.GetNavigations())
            {

                //Check if duplicate
                if (includedNavigations.Add(navigation))
                    entityNavigations.Add(navigation);

                //Add inverse to avoid infinite loop
                var inverseNavigation = navigation.FindInverse();
                if (inverseNavigation != null)
                    includedNavigations.Add(inverseNavigation);
            }

            foreach (var navigation in entityNavigations)
            { 
                //Recursively search for navigations
                foreach (var nestedNavigation in GenIncludePaths(depth - 1, navigation.GetTargetType(), includedNavigations))
                {
                    yield return $"{navigation.Name}.{nestedNavigation}";
                }
                yield return navigation.Name;

            }
        }

This method is a recursive version of the method that was suggested by Ivan in this answer. The method is organized recursively so that I can have control over how deep I want to search for navigable properties.

With the next method, I include each generated path to the inline LINQ:

        public IQueryable<TEntity> IncludeAll(IQueryable<TEntity> source)
        {
            return this.propertyPaths.Aggregate(source, (query, path) => query.Include(path));
        }

All instances of IQueryable<TEntity> that I use this method on have tracking turned OFF, and the problem arises when I include paths from the depth of 2 or more, since some of those generated paths are cyclic. When I try to execute the query (with .ToList()) to retrieve the results I get the following error:

 System.InvalidOperationException: The Include path 'CompaniesAdditionalData->Company' results in a cycle. Cycles are not allowed in no-tracking queries. Either use a tracking query or remove the cycle.

Is there any way I can check whether a navigation would result in a cycle? Or would I have to move away from the automatic generation of paths? Any feedback is welcome.

I tried to explain the problem as concisely as I could, if more information is needed be sure to tell me.

EDIT: I have implemented the changes that Ivan suggested in the comments, by passing the HashSet<INavigation> as a parameter in the recursive calls, state is maintained and no cyclic paths are generated. But now, another problem emerges. When there are multiple navigation properties of the same type in an entity, only one of them will be recursively searched for paths.

This happens because when the first property of that type is searched, every navigational property from that type is added to the includedNavigations. This means that any subsequent attempt to search navigational properties of that type will fail, as they are all treated as already included.

  • Well, you seem to be modifying the working code from my answer here https://stackoverflow.com/questions/49593482/entity-framework-core-2-0-1-eager-loading-on-all-nested-related-entities/49597502#49597502. The problem here (apart from the lack of attribution) is that the original code is iterative, thus easily controls cycles. There is absolutely no need to turn it into recursive, since `stack.Count` there IS the depth at any time, thus can easily be used to limit the max depth. – Ivan Stoev Jun 27 '20 at 19:27
  • @IvanStoev Yes, I am modifying your code, I didn't understand your method fully and I didn't want to just copy something I don't understand so I wrote a recursive version which controls depth easily. So, seeing you marked my question as a duplicate, could you help me modify your code to control depth effectively? – Djordje Nikolic Jun 27 '20 at 21:30
  • @IvanStoev Also there is another reason why I didn't want to use your code, your code doesn't generate the path to the root property when the property leads to other paths. For example, if my class `Company` has a property `Customers`, and that leads to entities with their own props, it won't generate the path "Customers", it will only generate the paths leading from it, like "Customers.Data", "Customers.Data2" etc. This is obviously a problem if I want to separate paths by depth, as "Customers" on its own is a valid depth 1 path, but it won't be generated. – Djordje Nikolic Jun 27 '20 at 21:34
  • Why do you think not generating separate `Include` for parts of of the path " obviously is a problem"? Because it's not at all. `Include("Customers.Data1")` is the equivalent of `Include(e => e.Customers).ThenInclude(e => e.Data1)`, i.e. is including all the properties from the path. What about easily controlling the depth, as I mentioned in the first comment `stack.Count` is the current depth there. And "going down" (the equivalent of recursive call) there is this single line `stack.Push(entityNavigations.GetEnumerator());`. Add `if (stack.Count + 1 < depth)` above it and you are done. – Ivan Stoev Jun 28 '20 at 02:45
  • The problem with recursive implementations in general is the shared state. Here the shared state which prevents the cycles is the `includedNavigations`. So if you really want recursive method, you should make `includedNavigations` a parameter of that method and pass it down. Which will require the first call to be `GenIncludePaths` to pass `new HashSet();`, which is nit good for the callers. Or the typical solution for similar recursive problems of creating 2 methods - public non recursive which calls private recursive. Explicit `Stack` based implementations avoid that problem. – Ivan Stoev Jun 28 '20 at 03:07
  • @IvanStoev You are right that `Include("Customers.Data1")` would include "Customers" as well, but the problem arises when we separate the paths by depth. In case that I only want to generate the paths that are of depth 1, "Customers" would not be generated, and it would also not generate "Customers.Data1", as that is a depth 2 path. Because of this, "Customers" would never be included to the queryable, that's the reason why it's a problem. – Djordje Nikolic Jun 28 '20 at 08:39
  • @IvanStoev I've added the `if (stack.Count < depth)` and it works, but we would also need to find a way to generate the root path, as I explained in the previous comment. Experimentally, I've also tried to get the recursive version to work, using the solution you suggested, but I still get cyclic paths. I've added an optional parameter for the `includedNavigations` that is set to `null` by default, in the method I do `if (includedNavigations == null) includedNavigations = new HashSet();`, and in the recursive calls I pass the object further. Am I missing something? – Djordje Nikolic Jun 28 '20 at 08:44
  • I was wrong for the `if` - it should be on another place. Rather than explaining how to do that, I've just added `maxDepth` parameter to my implementation. Regarding recursive implementation, I guess you should figure it out yourself. If you want, I can reopen the question, just let me know. – Ivan Stoev Jun 28 '20 at 11:55
  • @IvanStoev I have just tried your new code, and now it generates root paths correctly, which is excellent. But I have noticed something that I think is a bug. If an Entity has two navigational properties of the same type, the code generates only the paths to one of those. For example, if my entity Company has two Customer properties, CustomerS and CustomerR, the generated paths are: "CustomerR.Data, CustomerR.Data2, CustomerS", but they should be "CustomerR.Data, CustomerR.Data2, CustomerS.Data, CustomerS.Data2". – Djordje Nikolic Jun 30 '20 at 12:34
  • @IvanStoev It would be great if you could reopen the question, as I would like to find out how to properly create the recursive algorithm, if only for academic purposes. – Djordje Nikolic Jun 30 '20 at 12:35
  • @DjordjeNikolic did you figure out how to handle multiple nav entities of the same type? – arao6 Jan 23 '22 at 16:51
  • @arao6 unfortunately, no. We ended up just ignoring the problem and limiting the use of this method for only the first level of properties. – Djordje Nikolic Jan 27 '22 at 21:43

0 Answers0