Q:

Translating generic eager load method from EF6 to EF Core

For EF6, I had a method in my generic repository that I exposed to all service layers in order to retrieve entities from the database with any nested properties as needed:
public IQueryable<T> OldMethod(params Expression<Func<T, object>>[] includeProperties)
{
    var queryable = set.AsQueryable();

    return includeProperties.Aggregate(queryable, (current, includeProperty) => current.Include(includeProperty));
}

This way, I could use the method in the following way:
var data = repo.OldMethod(x => x.Papers, => x.People.Select(y => y.Addresses)).ToList();

In EF6, this would load the Papers navigation property, the People navigation property, and the Addresses navigation property on each person. This, as expected, throws an exception in EFCore. Because of the switch to Include-->ThenInclude method in EFCore, I'm not quite sure how to easily replicate this at my service layer which I'd like to not require any information about EntityFramework.

A:

This has been asked many times since the initial release of EF Core. Earlier prerelease versions of EF Core even were supporting it, but then it has been removed from EF Core code (I guess in order to promote the new Include / ThenInclude pattern).
While Include / ThenInclude pattern looks more clear (besides the current Intellisense issues), it has one major drawback - requires access to EntityFrameworkQueryableExtensions, thus reference to Microsoft.EntityFrameworkCore assembly. While paramsExpression>` pattern has no such requirement.
The good thing is the one can relatively easily add that functionality. The EF6 source code is publicly available on GitHub, and from there we can see that it uses a method called TryParsePath to build dot separated string path which then is passed to the string overload of Include method.
The same can be applied in EF Core. We can probably use the EF6 code, but I'm going to provide my own version. It can be easily be seen that the supported constructs are member accessors or calls to method called Select with 2 arguments, the second being LambdaExpression.  
Following is my interpretation of the above, encapsulated in two custom extension methods:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Microsoft.EntityFrameworkCore
{
    public static class IncludeExtensions
    {
        public static IQueryable<T> Include<T>(this IQueryable<T> source, IEnumerable<string> includePaths) where T : class
            => includePaths.Aggregate(source, (query, path) => query.Include(path));

        public static IQueryable<T> Include<T>(this IQueryable<T> source, IEnumerable<Expression<Func<T, object>>> includePaths) where T : class
            => source.Include(includePaths.Select(e => GetIncludePath(e?.Body)));

        static string GetIncludePath(Expression source, bool allowParameter = false)
        {
            if (allowParameter && source is ParameterExpression)
                return null; // ok
            if (source is MemberExpression member)
                return CombinePaths(GetIncludePath(member.Expression, true), member.Member.Name);
            if (source is MethodCallExpression call && call.Method.Name == "Select"
                && call.Arguments.Count == 2 && call.Arguments[1] is LambdaExpression selector)
                return CombinePaths(GetIncludePath(call.Arguments[0]), GetIncludePath(selector.Body));
            throw new Exception("Invalid Include path.");
        }

        static string CombinePaths(string path1, string path2)
            => path1 != null ? path1 + "." + path2 : path2;
    }
}

The first is simply helper for calling multiple string includes (taken from my answer to Entity Framework Core 2.0.1 Eager Loading on all nested related entities). The second is the method in question, which converts the expressions to strings and call the first. The main work is done by GetIncludePath private method which recursively processes the expression based on the aforementioned rules, plus one additional rule - when navigating bottom up, it should end with lambda parameter.
Now the implementation of the method is question is simple as that:
public IQueryable<T> OldMethod(params Expression<Func<T, object>>[] includeProperties)
    => set.Include(includeProperties);

