Search code examples
c#entity-framework-coreincludeef-code-firstrepository-pattern

EF Core Including Other Entities (Generic Repository pattern)


I'm using Generic repository /UoW patter in my application c# I was using EF6 ,then i moved to EF core . My app worked well excpet for some reason my includes doesn't work , and i got exception

Interface :

TEntity GetFirstOrDefault(
        Expression<Func<TEntity, bool>> filter = null,
        params Expression<Func<TEntity, object>>[] includes);

Implementation (EF core):

    public virtual TEntity GetFirstOrDefault(Expression<Func<TEntity, bool>> filter = null, 
                            params Expression<Func<TEntity, object>>[] includes)
    {
        IQueryable<TEntity> query = dbSet;

        query = includes.Aggregate(query, (current, item) => EvaluateInclude(current, item));
        return query.FirstOrDefault(filter);
    }

In Entity Framework EF6 , it was :

        foreach (Expression<Func<TEntity, object>> include in includes)
            query = query.Include(include);

The EvaluateInclude function is :

private IQueryable<TEntity> EvaluateInclude(IQueryable<TEntity> current, Expression<Func<TEntity, object>> item)
    {
        if (item.Body is MethodCallExpression)
        {
            var arguments = ((MethodCallExpression)item.Body).Arguments;
            if (arguments.Count > 1)
            {
                var navigationPath = string.Empty;
                for (var i = 0; i < arguments.Count; i++)
                {
                    var arg = arguments[i];
                    var path = arg.ToString().Substring(arg.ToString().IndexOf('.') + 1);

                    navigationPath += (i > 0 ? "." : string.Empty) + path;
                }
                return current.Include(navigationPath);
            }
        }

        return current.Include(item);
    }

When I call GetFirstOrDefault function like this way , it works :

internal Domain.Entities.Project GetProject(int projectId)
    {
        Expression<Func<Domain.Entities.Project, bool>> funcWhere = j => (!j.IsDisabled && j.ProjectId == projectId);
      return  UnitOfWork.Repository<Domain.Entities.Project>().GetFirstOrDefault(funcWhere,
            p => p.StatusProject,
            p => p.ProjectRoles.Select(t => t.Employee),
            //p => p.ProjectTeams.Select(t => t.Team.TeamEmployees.Select(e => e.Employee)),
            );

    }

But when I un-comment the extra include , it fails :

 internal Domain.Entities.Project GetProject(int projectId)
    {
        Expression<Func<Domain.Entities.Project, bool>> funcWhere = j => (!j.IsDisabled && j.ProjectId == projectId);
      return  UnitOfWork.Repository<Domain.Entities.Project>().GetFirstOrDefault(funcWhere,
            p => p.StatusProject,
            p => p.ProjectRoles.Select(t => t.Employee),
            p => p.ProjectTeams.Select(t => t.Team.TeamEmployees.Select(e => e.Employee)),
            );

    }

System.InvalidOperationExceptionInvalid include path: 'Project.ProjectTeams.Team.TeamEmployees.Select(e => e.Employee)' - couldn't find navigation for: 'Select(e => e'

enter image description here


Solution

  • Solved :

    following this answer link I added this code that parse my lambda expression of includes :

        // This method is a slight modification of EF6 source code
        private bool TryParsePath(Expression expression, out string path)
        {
            path = null;
            var withoutConvert = RemoveConvert(expression);
            var memberExpression = withoutConvert as MemberExpression;
            var callExpression = withoutConvert as MethodCallExpression;
    
            if (memberExpression != null)
            {
                var thisPart = memberExpression.Member.Name;
                string parentPart;
                if (!TryParsePath(memberExpression.Expression, out parentPart))
                {
                    return false;
                }
                path = parentPart == null ? thisPart : (parentPart + "." + thisPart);
            }
            else if (callExpression != null)
            {
                if (callExpression.Method.Name == "Select"
                    && callExpression.Arguments.Count == 2)
                {
                    string parentPart;
                    if (!TryParsePath(callExpression.Arguments[0], out parentPart))
                    {
                        return false;
                    }
                    if (parentPart != null)
                    {
                        var subExpression = callExpression.Arguments[1] as LambdaExpression;
                        if (subExpression != null)
                        {
                            string thisPart;
                            if (!TryParsePath(subExpression.Body, out thisPart))
                            {
                                return false;
                            }
                            if (thisPart != null)
                            {
                                path = parentPart + "." + thisPart;
                                return true;
                            }
                        }
                    }
                }
                else if (callExpression.Method.Name == "Where")
                {
                    throw new NotSupportedException("Filtering an Include expression is not supported");
                }
                else if (callExpression.Method.Name == "OrderBy" || callExpression.Method.Name == "OrderByDescending")
                {
                    throw new NotSupportedException("Ordering an Include expression is not supported");
                }
                return false;
            }
    
            return true;
        }
    
        // Removes boxing
        private Expression RemoveConvert(Expression expression)
        {
            while (expression.NodeType == ExpressionType.Convert
                   || expression.NodeType == ExpressionType.ConvertChecked)
            {
                expression = ((UnaryExpression)expression).Operand;
            }
    
            return expression;
        }
    
        #endregion
    

    Then change my EvaluateInclude function to :

      private IQueryable<TEntity> EvaluateInclude(IQueryable<TEntity> current, Expression<Func<TEntity, object>> item)
        {
            if (item.Body is MethodCallExpression)
            {
                string path;
                TryParsePath(item.Body, out path);
                return current.Include(path);
    
            }
    
            return current.Include(item);
        }
    

    And it works