vendredi 14 juillet 2023

Composing Queryable with "ThenInclude" with Dynamic Generic Arguments

Environment: .NET 6, EF 6.0.19

This might not be possible, but I am trying to use reflection to generate a query for my generic types which crawls the navigations from the root type and includes/thenincludes all related types.

Getting the includes (first-order-relationships) to work is pretty trivial and works:

// Get all the navigation properties for the type so we can include them
IEnumerable<INavigation> navigations = _dbContext.Model.FindEntityType(typeof(T)).GetNavigations();

// Add include clauses for each navigation property so we get the full model
foreach (INavigation prop in navigations)
{
    query = query.Include(prop.Name);
}

where query is IQueryable<T> from System.Linq.

However, ThenInclude() is a method on IIncludableQueryable<T, U> where U is some property in the navigation path from T. However, because we don't know U at compile-time, we need to use reflection to determine the type and

  1. create a generic type for the IIncludableQueryable
  2. Create the expression for the new part of the query and
  3. amend the query (or return a new query based on the original query)

However, using Activator to make an IIncludableQueryable fails because of a perceived type mismatch during creation; however, what I'm unclear is on where the type massaging would normally happen in a standard ThenIncludes call. Below is an example of what I'm running and where it's failing (ignore that this doesn't cover all edge cases and doesn't recurse properly)

private IQueryable<T> AddNavigationProperties<T>(IQueryable<T> query)
        where T : class, IMyInterface
{
    // Get all the navigation properties for the type so we can include them
    IEnumerable<INavigation> navigations = _dbContext.Model.FindEntityType(typeof(T)).GetNavigations();

    // Add include clauses for each navigation property so we get the full model
    foreach (INavigation prop in navigations)
    {
        query = query.Include(prop.Name);

        if (prop.ClrType.IsGenericType && prop.ClrType.IsCollection())
        {
            var propNavs = _dbContext.Model.FindEntityType(prop.ClrType.GetGenericArguments()[0]).GetNavigations();
            var genericType = prop.ClrType.GetGenericArguments()[0];

            foreach (INavigation nav in propNavs)
            {                    
                Type delegateType = typeof(Func<,>).MakeGenericType(genericType, nav.ClrType);
                ParameterExpression parameter = Expression.Parameter(genericType, nav.Name);
                MemberExpression memberExpression = Expression.Property(parameter, genericType.GetProperty(nav.Name));
                LambdaExpression expression = Expression.Lambda(delegateType, memberExpression, parameter); // Create the expression that defines how we access this property from the previous entity

                // Make the types we will need for creating the new query instance
                var enumerableType = typeof(IEnumerable<>).MakeGenericType(nav.ClrType);
                var includableType = typeof(IncludableQueryable<,>).MakeGenericType(typeof(T), nav.ClrType);

                // ==== THIS LINE FAILS ==== 
                var includable = Activator.CreateInstance(
                        includableType,
                        // This is the pattern used for resolving the constructor parameter in EntityFrameworkQueryableExtensions
                        query.Provider is EntityQueryProvider ?
                            query.Provider.CreateQuery<T>(
                                Expression.Call(
                                    instance: null,
                                // This static MethodInfo is based on the one in EntityFrameworkQueryableExtensions
                                method: ThenIncludeAfterEnumerableMethodInfo.MakeGenericMethod(typeof(T), enumerableType, nav.ClrType),
                                arguments: new[] { query.Expression, expression }
                            ))
                        : query);
                }
            }
        }
    }

    return query;
}

The error that occurs on the failing line is essentially

Expression of type 'System.Linq.IQueryable[T] cannot be used for parameter Microsoft.EntityFrameworkCore.Query.IIncludableQueryable[T, TPreviousEntity, U] (Parameter: 'arg0')

However, it's unclear to me why that expression is not an acceptable parameter. From reading the file definitions in EF it seems like that would be the exact expression I would need.

FWIW here is the call signature of the "ThenIncludes" I am trying to invoke:

public static IIncludableQueryable<TEntity, TProperty> ThenInclude<TEntity, TPreviousProperty, TProperty>(
    this IIncludableQueryable<TEntity, IEnumerable<TPreviousProperty>> source,
    Expression<Func<TPreviousProperty, TProperty>> navigationPropertyPath)
    where TEntity : class




Aucun commentaire:

Enregistrer un commentaire