Efcore: Proposal: Improved Include() syntax

Created on 28 Oct 2020  ·  9Comments  ·  Source: dotnet/efcore

Normally when you want to load navigations like these:

Order.OrderDetails.Product.Categories
Order.OrderDetails.Product.Foo
Order.OrderDetails.Offer

You have to write something like this:

```c#
var orders = await context.Orders
.Include(o => o.OrderDetails)
.ThenInclude(od => od.Product)
.ThenInclude(p => p.Categories)
.Include(o => o.OrderDetails)
.ThenInclude(od => od.Product)
.ThenInclude(p => p.Foo)
.Include(o => o.OrderDetails)
.ThenInclude(od => od.Offer)
.ToArrayAsync();

What if it, instead, could be expressed in this way:

```c#
var orders = await context.Orders
    .Include(o => o.OrderDetails, c => 
        c.Include(od => od.Product, c2 =>
            c2.Include(p => p.Categories)
              .Include(p => p.Foo))
         .Include(od => od.Offer))
    .ToArrayAsync();

_[Includes can be chained and in expression bodies]_

The proposal would “include” a new overload for Include() that takes a lambda (with configuration object) as a second argument that allows you to configure nested child-navigations via nested Include calls.

This syntax would be easier to handle since it avoids repetition by expressing includes in a single hierarchy.

customer-reported type-enhancement

Most helpful comment

See also #4490 in which several ideas for different Include syntax is discussed.

All 9 comments

See also #4490 in which several ideas for different Include syntax is discussed.

@robertsundstrom The syntax looks quite nice, but we're not sure that it can be implemented. Consider this:

```C#
var blogs = context.Blogs
.Include(e => e.Posts)
.ThenInclude(e => e.Tags);

Expanding the generic types here reveals:

```C#
var blogs = context.Blogs
    .Include<Blog, ICollection<Post>>(e => e.Posts)
    .ThenInclude<Blog, Post, ICollection<Tag>>(e => e.Tags);

Notice that the generic type of ThenInclude is based on the return value of preceding Include, which allows the generics to be inferred while still allowing strongly-typed lambda expression in ThenInclude.

If the ThenInclude is instead an additional lambda in the first call, then I don't think it will be possible to resolve generic types like this, meaning the generics would have to be all specified explicitly, which impacts both the readability and the Intellisense experience.

@ajcvickers My thought was to create a new Include type that wraps all the nested methods. That is what gets returned at the end.

It acts more like a builder in that sense.

@robertsundstrom It would be great if you could implement what the API surface would look like in a simple C# project so that we can see how the compiler handles it.

@ajcvickers I have been experimenting with getting the type inference to work. It is kind of frustrating that type arguments from other arguments don't flow up.

I was also hoping that constraints would help in choosing the right overload of Include. The compiler doesn't seem to prioritize the overloads based on type compatibility. I expect the one with a TProperty : ICollection<TItem> to be chosen for properties that are of a type that implement that interface.

I don't know if this could be an improvement to the C# compiler itself. Or most importantly, if it would break existing code.

The IIncludableQueryable<TEntity, TProperty> would be replaced with something else.

```c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;

namespace test
{
class Program
{
static async Task Main(string[] args)
{
var context = new AppContext();
var items = await context.Foos.Include(f => f.Items, c =>
c.Include(i => i.Bar)
.Include(i => i.Alice, c =>
c.Include(c2 => c2.Item)))
.ToArrayAsync();

        Console.WriteLine("Hello World!");
    }
}

public class AppContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }
}

class Foo
{
    public ICollection<Item> Items { get; set; }
}

class Item
{
    public Bar Bar { get; set; }

    public Alice Alice { get; set; }
}

class Bar
{

}

class Alice
{
    public Item Item { get; set; }
}

public class IncludeBuilder<TEntity, TProperty>
    where TEntity : class
    where TProperty : class
{
    public IncludeBuilder<TEntity, TProperty> Include(Expression<Func<TEntity, TProperty>> navigationPropertyPath, Action<IncludeBuilder<TEntity, TProperty>> builder)
    {
        return null;
    }
}

public class IncludeBuilderCollection<TEntity>
    where TEntity : class
{
    public IncludeBuilder<TEntity, TProperty> Include<TItem, TProperty>(Expression<Func<TEntity, TProperty>> navigationPropertyPath, Action<IncludeBuilder<TEntity, TItem>> builder)
        where TProperty : class, ICollection<TItem>
        where TItem : class
    {
        return null;
    }
}

public static class IncludeExt
{
    public static IIncludableQueryable<TEntity, TProperty> Include<TEntity, TProperty>(this IQueryable<TEntity> source, Expression<Func<TEntity, TProperty>> navigationPropertyPath, Action<IncludeBuilder<TEntity, TProperty>> builder) 
        where TEntity : class
        where TProperty : class
    {
        return null;
    }

    public static IIncludableQueryable<TEntity, TProperty> IncludeCollection<TEntity, TProperty, TItem>(this IQueryable<TEntity> source, Expression<Func<TEntity, TProperty>> navigationPropertyPath, Action<IncludeBuilderCollection<TItem>> builder) 
        where TEntity : class
        where TProperty : class, ICollection<TItem>
        where TItem : class
    {
        return null;
    }
}

}
```

@robertsundstrom - Can you also put function body for those methods? We need to create actual expression tree at the end. Action does not help in tree building.

@smitpatel Sorry. I have not implemented anything in the method bodies.

The Action parameters are just for configuring.

The outer Include will return an object that has been constructed for all those inner Includes.

You could see outer Include as the method that actually constructs the expression trees.

Of course, I have not thought about everything yet.

But it could translate into the regular method call.

@robertsundstrom Have you made any progress here? Is this somethin you are still pursuing?

Sorry. I have been moving so have not had the time. Let me see in the coming days.

Something that I would like to explore is conditional nested includes.

Was this page helpful?
0 / 5 - 0 ratings