EntityFramework: Eager loading with excludes instead of includes?
Asked Answered
S

1

4

My data model has a lot of nested entities and I would like to eagerly load the whole object tree ... except for a view entities that will be explicitly loaded on demand.

Using include paths I have to specify many paths and every time I add a new entity I will have to adapt those include paths. I currently use following method of my repository to load all entities of a type:

public virtual IQueryable<TEntity> All(string commaSeperatedIncludePropertyPaths = "")
    {
      IQueryable<TEntity> initialQuery = Context.Set<TEntity>();
      string[] includePaths = commaSeperatedIncludePropertyPaths.Split(new[] { ','}, StringSplitOptions.RemoveEmptyEntries);    
      return includePaths.Aggregate(initialQuery, (currentQuery, includeProperty) => currentQuery.Include(includeProperty));
    }

The passed include paths already fill a whole screen.

Therefore I would prefer that the EntityFramework automatically loads all navigation properties eagerly, except for the ones I specify with exclude paths:

public virtual IQueryable<TEntity> All(string commaSeperatedExcludePropertyPaths = "")
    {
      //... how to implement?
    }

The exclude paths would help to avoid circular dependencies and to filter out the few entities I don't want to load eagerly. Specifying excludes instead of includes would reduce boilerplate code for me.

Is this possible with EF 6.1.3 or planned for EF 7? If not, what are reasons against that option?

Did anyone already try to read entity meta data and apply it for "auto eager loading" and failed?

Related (old) questions and articles:

  • Overview on options for loading navigation properties:

https://msdn.microsoft.com/en-us/magazine/hh205756.aspx

  • Auto eager load

Entity framework auto eager load

Entity Framework - Is there a way to automatically eager-load child entities without Include()?

Entity framework linq query Include() multiple children entities

  • Type save includes

Entity Framework .Include() with compile time checking?

Satinet answered 3/5, 2017 at 7:39 Comment(3)
One way that comes to mind is inspect target type (entity) via reflection, figure out all navigation properties and Include them (except those listed in your parameter).Chief
Even if it were doable, I think this might be road to hell in long run from performance perspective. EF becomes dramatically slower with every Include you use in query. From my experience using more than two includes is a signal to rework loading approach, separate to several smaller queries etcJohanson
Technically it should be doable. I've tried once, but working with EF6 metadata is quite non intuitive, and this combined with directed graph processing, so I gave up.Involuted
S
1

Below is a first draft for a solution. I still have to find out if it's practicable ... and I'll consider to rework the loading approach (as Lanorkin suggested), too. Thank you for your comments.

Edit

It turned out that, while excludes might make sense when developing an application ...doing many changes to the domain model..., excludes are not more elegant than includes for a "real world example" that I just considered.

a) I went through my entities and counted the number of included and excluded navigation properties. The average number of excluded properties was not significantly smaller then the number of included properties.

b) If I do consider a distinct navigation property "foos" for the exclusions, I will be forced to consider exclusions for the sub entities of type Foo ... if I do not want to use its properties at all.

On the other hand, using inclusions, I just need to specify the navigation property "foos" and do not need to specify anything else for the sub entities.

Therefore, while excludes might save some specs for one level, they dent to require more specs for the next level ... (when excluding some intermediate entities and not only entities that are located at the leaves of the loaded object tree).

c) Furthermore, the includes/excludes might not only depend on the type of the entity but also on the path that is used to access it. Then an exclude needs to be specified like "exclude properties xy when loading the entity for one purpose and exclude properties z when loading the entity for another purpose".

=> As a result of this considerations I will go on using inclusions.

I implemented type save inclusions that are based on inclusion dictionaries instead of strings:

  private static readonly Inclusions<Person> _personInclusionsWithCompanyParent = new Inclusions<Person>(typeof(Company))
      {
        {e => e.Company, false},
        {e => e.Roles, true}        
      };

I have a method that creates the query from a list of inclusions. That method also checks if all existing navigation properties are considered in the dictionaries. If I add a new entity and forget to specify corresponding inclusions, an exception will be thrown.


Nevertheless, here is an experimental solution for using excludes instead of includes:

private const int MAX_EXPANSION_DEPTH = 10;

private DbContext Context { get; set; } //set during construction of my repository


 public virtual IQueryable<TEntity> AllExcluding(string excludeProperties = "")
    {
      var propertiesToExclude = excludeProperties.Split(new[]
                                                        {
                                                          ','
                                                        },
                                                        StringSplitOptions.RemoveEmptyEntries);


      IQueryable<TEntity> initialQuery = Context.Set<TEntity>();
      var elementType = initialQuery.ElementType;

      var navigationPropertyPaths = new HashSet<string>();
      var navigationPropertyNames = GetNavigationPropertyNames(elementType);
      foreach (var propertyName in navigationPropertyNames)
      {
        if (!propertiesToExclude.Contains(propertyName))
        {
          ExtendNavigationPropertyPaths(navigationPropertyPaths, elementType, propertyName, propertyName, propertiesToExclude, 0);
        }
      }

      return navigationPropertyPaths.Aggregate(initialQuery, (current, includeProperty) => current.Include(includeProperty));
    }

    private void ExtendNavigationPropertyPaths(ISet<string> navigationPropertyPaths,
                                               Type parentType,
                                               string propertyName,
                                               string propertyPath,
                                               ICollection<string> propertiesToExclude,
                                               int expansionDepth)
    {
      if (expansionDepth > MAX_EXPANSION_DEPTH)
      {
        return;
      }

      var propertyInfo = parentType.GetProperty(propertyName);

      var propertyType = propertyInfo.PropertyType;

      var isEnumerable = typeof(IEnumerable).IsAssignableFrom(propertyType);
      if (isEnumerable)
      {
        propertyType = propertyType.GenericTypeArguments[0];
      }

      var subNavigationPropertyNames = GetNavigationPropertyNames(propertyType);
      var noSubNavigationPropertiesExist = !subNavigationPropertyNames.Any();
      if (noSubNavigationPropertiesExist)
      {
        navigationPropertyPaths.Add(propertyPath);
        return;
      }

      foreach (var subPropertyName in subNavigationPropertyNames)
      {
        if (propertiesToExclude.Contains(subPropertyName))
        {
          navigationPropertyPaths.Add(propertyPath);
          continue;
        }

        var subPropertyPath = propertyPath + '.' + subPropertyName;
        ExtendNavigationPropertyPaths(navigationPropertyPaths,
                                      propertyType,
                                      subPropertyName,
                                      subPropertyPath,
                                      propertiesToExclude,
                                      expansionDepth + 1);
      }
    }

    private ICollection<string> GetNavigationPropertyNames(Type elementType)
    {
      var objectContext = ((IObjectContextAdapter)Context).ObjectContext;
      var entityContainer = objectContext.MetadataWorkspace.GetEntityContainer(objectContext.DefaultContainerName, DataSpace.CSpace);
      var entitySet = entityContainer.EntitySets.FirstOrDefault(item => item.ElementType.Name.Equals(elementType.Name));
      if (entitySet == null)
      {
        return new List<string>();
      }
      var entityType = entitySet.ElementType;
      return entityType.NavigationProperties.Select(np => np.Name)
                       .ToList();
    }
Satinet answered 3/5, 2017 at 10:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.