0

How can I convert an EntityQueryable to an IIncludableQueryable? I get this error...

Object of type 'Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable1[Extenso.TestLib.Data.Entities.ProductModel]' cannot be converted to type 'Microsoft.EntityFrameworkCore.Query.IIncludableQueryable2[Extenso.TestLib.Data.Entities.ProductModel,System.Collections.Generic.IEnumerable`1[Extenso.TestLib.Data.Entities.Product]]'.

...when attempting this:

public static Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> MapInclude<TModel, TEntity>(
    Expression<Func<IQueryable<TModel>, IQueryable<TModel>>> includeExpression)
{
    ArgumentNullException.ThrowIfNull(includeExpression);

    if (includeExpression.Body is MethodCallExpression methodCall)
    {
        if (methodCall.Method.Name is "Include" or "ThenInclude")
        {
            if (methodCall.Arguments.Count > 1 &&
                methodCall.Arguments[1] is UnaryExpression unary &&
                unary.Operand is LambdaExpression lambda)
            {
                var parameter = Expression.Parameter(typeof(TEntity), "x");
                var visitor = new ExpressionMappingVisitor<TModel, TEntity>(parameter);
                var body = visitor.Visit(lambda.Body);
                var mappedLambda = Expression.Lambda(body, parameter);

                return query =>
                {
                    // For first-level includes, convert to IIncludableQueryable
                    if (methodCall.Method.Name == "Include")
                    {
                        return ConvertToIncludable(query, mappedLambda);
                    }

                    // For ThenInclude, we should already have an IIncludableQueryable
                    var includeMethod = typeof(EntityFrameworkQueryableExtensions)
                        .GetMethods()
                        .First(m =>
                            m.Name == "ThenInclude" &&
                            m.GetParameters().Length == 2 &&
                            m.GetGenericArguments().Length == 3);

                    var prevPropType = methodCall.Method.GetGenericArguments()[1];
                    var mappedPrevType = ExpressionMappingVisitor<TModel, TEntity>.GetMappedType(prevPropType) ?? prevPropType;

                    includeMethod = includeMethod.MakeGenericMethod(typeof(TEntity), mappedPrevType, body.Type);

                    // Problem is here
                    object result = includeMethod.Invoke(null, [query, mappedLambda]);
                    return (IIncludableQueryable<TEntity, object>)result;
                };
            }
        }
    }

    throw new ArgumentException("Invalid include expression format", nameof(includeExpression));
}

The caller looks like this:

private IQueryable<TEntity> BuildBaseQuery(DbContext context, SearchOptions<TModel> options)
{
    var query = context.Set<TEntity>().AsNoTracking();

    if (options.Include is not null)
    {
        var mappedInclude = MapInclude(options.Include);
        query = mappedInclude(query); // Happens here. I believe because the `query` variable is an EntityQueryable
    }

    if (options.Query is not null)
    {
        var mappedPredicate = MapPredicate(options.Query);
        query = query.Where(mappedPredicate);
    }

    if (options.OrderBy is not null)
    {
        var mappedOrderBy = MapOrderBy(options.OrderBy);
        query = mappedOrderBy(query);
    }

    return query;
}

The caller of that is a unit test doing this:

var result = repository.Find(new SearchOptions<ProductModelViewModel>
{
    Query = x => x.ProductModelId == productModel.ProductModelId,
    Include = query => query
        .Include(x => x.Products)
        .ThenInclude(x => x.ProductSubcategory)
}).First();

And the Include property on SearchOptions<T> here is as follows:

public Expression<Func<IQueryable<TEntity>, IQueryable<TEntity>>> Include { get; set; }

As expected, if I only do Include(), all is well.. but if I do a ThenInclude(), I have a problem..

How can I achieve what I am looking to do?

5
  • Another example when repository pattern is useless. Include is a part of EF Core. You can achieve the similar result by just Extension methods over IQueryable. Commented May 23 at 13:09
  • @SvyatoslavDanyliv See my response to Charlieface's answer Commented May 24 at 0:19
  • Don't want participate in this another useless monster. As easy solution, ust transfer your includes to overload which accepts string as include path - query.Include("Products.ProductSubcategory") Commented May 24 at 5:59
  • @SvyatoslavDanyliv I need a solution, not your opinion of what is useless or not. Commented May 24 at 12:44
  • Well, I forgot that it can contain filter. Will see what can be done. Commented May 24 at 13:41

2 Answers 2

1

You can either extend your existing ExpressionMappingVisitor to achieve similar functionality or use my alternative implementation, ExpressionTypeMapper. If I'm correct, this replacement can significantly simplify your code, especially for handling OrderBy and other mappings. It transforms the entire expression tree, including lambdas and method calls.

Updated code: Note that this version returns a Func<IQueryable<TEntity>, IQueryable<TEntity>> instead of IIncludableQueryable<TEntity, object>, which is still valid for how it's used within the library.

public static Func<IQueryable<TEntity>, IQueryable<TEntity>> MapInclude<TModel, TEntity>(
    Expression<Func<IQueryable<TModel>, IQueryable<TModel>>> includeExpression)
{
    ArgumentNullException.ThrowIfNull(includeExpression);

    var mapping = new Dictionary<Type, Type> { { typeof(TModel), typeof(TEntity) } };
    var newInclude = (LambdaExpression)ExpressionTypeMapper.ReplaceTypes(includeExpression, mapping);

    var newBody = Expression.Convert(newInclude.Body, typeof(IQueryable<TEntity>));
    var lambda = Expression.Lambda<Func<IQueryable<TEntity>, IQueryable<TEntity>>>(
        newBody, newInclude.Parameters[0]);

    return lambda.Compile();
}
Sign up to request clarification or add additional context in comments.

3 Comments

This is exactly what I needed! That is absolutely fantastic. Much appreciated
You should consider making a NuGet package for that ExpressionTypeMapper. Super useful
@Matt, it was an interesting couple of hours developing mapper. Adding another open-source library feels like overkill - these days, all my free time goes to linq2db.
0

It's not clear to me why expressions are needed here at all. Just use a normal lambda for the Include

public Func<IQueryable<TEntity>, IQueryable<TEntity>> Include { get; set; }

The caller can still do

    Include = query => query
        .Include(x => x.Products)
        .ThenInclude(x => x.ProductSubcategory)

Note that IIncludableQueryable is also a IQueryable so this will work fine.

Then you don't need any of that complex expression code.

if (options.Include is not null)
{
    query = options.Include(query);
}

In fact, you could pretty much combine all of those SearchOptions lambdas into a single lambda.

2 Comments

Because I am unable to share all of the code in one SO post.. what's happening here is I have a lightweight alternative to AutoMapper.. I am mapping an expression from a model to an entity. Hence the method, public static Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> MapInclude<TModel, TEntity>( Expression<Func<IQueryable<TModel>, IQueryable<TModel>>> includeExpression)
If you want to see the code, you can find it on GitHub: github.com/gordon-matt/Extenso. See develop branch. Open "Extenso.Data.Entity.Tests" proj, and run "ExtensoMapperEntityFrameworkRepositoryTests". You will see the multi-level include tests fail.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.