How does Entity Framework work with recursive hierarchies? Include() seems not to work with it

前端 未结 15 1562
再見小時候
再見小時候 2020-11-28 21:53

I have an Item. Item has a Category.

Category has ID, Name, Parent

相关标签:
15条回答
  • 2020-11-28 22:09

    You could also create a tablevalued function in the database and add that to your DBContext. Then you can call that from your code.

    This example requires that you import EntityFramework.Functions from nuget.

    public class FunctionReturnType
    {
        public Guid Id { get; set; } 
    
        public Guid AnchorId { get; set; } //the zeroPoint for the recursion
    
        // Add other fields as you want (add them to your tablevalued function also). 
        // I noticed that nextParentId and depth are useful
    }
    
    public class _YourDatabaseContextName_ : DbContext
    {
        [TableValuedFunction("RecursiveQueryFunction", "_YourDatabaseContextName_")]
        public IQueryable<FunctionReturnType> RecursiveQueryFunction(
            [Parameter(DbType = "boolean")] bool param1 = true
        )
        {
            //Example how to add parameters to your function
            //TODO: Ask how to make recursive queries with SQL 
            var param1 = new ObjectParameter("param1", param1);
            return this.ObjectContext().CreateQuery<FunctionReturnType>(
                $"RecursiveQueryFunction(@{nameof(param1)})", param1);
        }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            //add both (Function returntype and the actual function) to your modelbuilder. 
            modelBuilder.ComplexType<FunctionReturnType>();
            modelBuilder.AddFunctions(typeof(_YourDatabaseContextName_), false);
    
            base.OnModelCreating(modelBuilder);
        }
    
        public IEnumerable<Category> GetParents(Guid id)
        {
            //this = dbContext
            return from hierarchyRow in this.RecursiveQueryFunction(true)
                join yourClass from this.Set<YourClassThatHasHierarchy>()
                on hierarchyRow.Id equals yourClass.Id
                where hierarchyRow.AnchorId == id
                select yourClass;
        }
    }
    
    0 讨论(0)
  • 2020-11-28 22:10

    @parliament gave me an idea for EF6. Example for Category with Methods to load all parents up to root node and all children.

    NOTE: Use this only for non performance critical operation. Example with 1000 nodes performance from http://nosalan.blogspot.se/2012/09/hierarchical-data-and-entity-framework-4.html.

    Loading 1000 cat. with navigation properties took 15259 ms 
    Loading 1000 cat. with stored procedure took 169 ms
    

    Code:

    public class Category 
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
    
        public string Name { get; set; }
    
        public int? ParentId { get; set; }
    
        public virtual Category Parent { get; set; }
    
        public virtual ICollection<Category> Children { get; set; }
    
        private IList<Category> allParentsList = new List<Category>();
    
        public IEnumerable<Category> AllParents()
        {
            var parent = Parent;
            while (!(parent is null))
            {
                allParentsList.Add(parent);
                parent = parent.Parent;
            }
            return allParentsList;
        }
    
        public IEnumerable<Category> AllChildren()
        {
            yield return this;
            foreach (var child in Children)
            foreach (var granChild in child.AllChildren())
            {
                yield return granChild;
            }
        }   
    }
    
    0 讨论(0)
  • 2020-11-28 22:11
    public static class EntityFrameworkExtensions
    {
        public static ObjectContext GetObjectContext(this DbContext context) 
        {
            ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
    
            return objectContext;
        }
    
        public static string GetTableName<T>(this ObjectSet<T> objectSet) 
            where T : class
        {
            string sql = objectSet.ToTraceString();
            Regex regex = new Regex("FROM (?<table>.*) AS");
            Match match = regex.Match(sql);
    
            string table = match.Groups["table"].Value;
            return table;
        }
    
        public static IQueryable<T> RecursiveInclude<T>(this IQueryable<T> query, Expression<Func<T, T>> navigationPropertyExpression, DbContext context)
            where T : class
        {
            var objectContext = context.GetObjectContext();
    
            var entityObjectSet = objectContext.CreateObjectSet<T>();
            var entityTableName = entityObjectSet.GetTableName();
            var navigationPropertyName = ((MemberExpression)navigationPropertyExpression.Body).Member.Name;
    
            var navigationProperty = entityObjectSet
                .EntitySet
                .ElementType
                .DeclaredNavigationProperties
                .Where(w => w.Name.Equals(navigationPropertyName))
                .FirstOrDefault();
    
            var association = objectContext.MetadataWorkspace
                .GetItems<AssociationType>(DataSpace.SSpace)
                .Single(a => a.Name == navigationProperty.RelationshipType.Name);
    
            var pkName = association.ReferentialConstraints[0].FromProperties[0].Name;
            var fkName = association.ReferentialConstraints[0].ToProperties[0].Name;
    
            var sqlQuery = @"
                    EXEC ('
                        ;WITH CTE AS
                        (
                            SELECT 
                                [cte1].' + @TABLE_PK + '
                                , Level = 1
                            FROM ' + @TABLE_NAME + ' [cte1]
                            WHERE [cte1].' + @TABLE_FK + ' IS NULL
    
                            UNION ALL
    
                            SELECT 
                                [cte2].' + @TABLE_PK + '
                                , Level = CTE.Level + 1
                            FROM ' + @TABLE_NAME + ' [cte2]
                                INNER JOIN CTE ON CTE.' + @TABLE_PK + ' = [cte2].' + @TABLE_FK + '
                        )
                        SELECT 
                            MAX(CTE.Level)
                        FROM CTE 
                    ')
                ";
    
            var rawSqlQuery = context.Database.SqlQuery<int>(sqlQuery, new SqlParameter[]
                {
                    new SqlParameter("TABLE_NAME", entityTableName),
                    new SqlParameter("TABLE_PK", pkName),
                    new SqlParameter("TABLE_FK", fkName)
                });
    
            var includeCount = rawSqlQuery.FirstOrDefault();
    
            var include = string.Empty;
    
            for (var i = 0; i < (includeCount - 1); i++)
            {
                if (i > 0)
                    include += ".";
    
                include += navigationPropertyName;
            }
    
            return query.Include(include);
        }
    }
    
    0 讨论(0)
  • 2020-11-28 22:12

    I found out that if you include "two parent levels", you will get the whole parent hierarchy, like that:

    var query = Context.Items
                .Include(i => i.Category)
                .Include(i => i.Category.Parent.Parent)
    
    0 讨论(0)
  • 2020-11-28 22:15

    Use this extension method which calls the hard-coded version of Include, to achieve a dynamic depth level of inclusion, it works great.

    namespace System.Data.Entity
    {
      using Linq;
      using Linq.Expressions;
      using Text;
    
      public static class QueryableExtensions
      {
        public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source,
          int levelIndex, Expression<Func<TEntity, TEntity>> expression)
        {
          if (levelIndex < 0)
            throw new ArgumentOutOfRangeException(nameof(levelIndex));
          var member = (MemberExpression)expression.Body;
          var property = member.Member.Name;
          var sb = new StringBuilder();
          for (int i = 0; i < levelIndex; i++)
          {
            if (i > 0)
              sb.Append(Type.Delimiter);
            sb.Append(property);
          }
          return source.Include(sb.ToString());
        }
      }
    }
    

    Usage:

    var affiliate = await DbContext.Affiliates
      .Include(3, a => a.Referrer)
      .SingleOrDefaultAsync(a => a.Id == affiliateId);
    

    Anyway, meanwhile, join the discussion about it on the EF repo.

    0 讨论(0)
  • 2020-11-28 22:17

    Instead of using the Include method you could use Load.

    You could then do a for each and loop through all the children, loading their children. Then do a for each through their children, and so on.

    The number of levels down you go will be hard coded in the number of for each loops you have.

    Here is an example of using Load: http://msdn.microsoft.com/en-us/library/bb896249.aspx

    0 讨论(0)
提交回复
热议问题