dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.77k stars 3.18k forks source link

Reusing Include Statements #34661

Closed AndreqGav closed 1 month ago

AndreqGav commented 1 month ago

Data structure

public class MyEntity
{
    public Guid Id { get; set; }

    public SubEntity1 SubEntity1 { get; set; }

    public SubEntity2 SubEntity2 { get; set; }
}

public class SubEntity1
{
    public Guid Id { get; set; }

    public SubEntity2 SubEntity2 { get; set; }
}

public class SubEntity2
{
    public Guid Id { get; set; }
}

public class ContainerEntity1
{
    public Guid Id { get; set; }

    public MyEntity MyEntity { get; set; }
}

public class ContainerEntity2
{
    public Guid Id { get; set; }

    public List<MyEntity> MyEntities { get; set; }
}

Usage

I need to get data that contains MyEntity with all fields included

    var query1 = context.Set<MyEntity>()
        .Include(q => q.SubEntity1)
        .ThenInclude(q => q.SubEntity2)
        .Include(q => q.SubEntity2)
        .Where(x => ...);

    var query2 = context.Set<ContainerEntity1>()
        .Include(q => q.MyEntity)
        .ThenInclude(q => q.SubEntity1).ThenInclude(q => q.SubEntity2)
        .Include(q => q.MyEntity.SubEntity2)
        .Where(x => ...);

    var query3 = context.Set<ContainerEntity2>()
        .Include(q => q.MyEntities)
        .ThenInclude(q => q.SubEntity1).ThenInclude(q => q.SubEntity2)
        .Include(q => q.MyEntities).ThenInclude(q => q.SubEntity2)
        .Where(x => ...);

Question

How can I reuse the include statements to avoid duplication?

I would like to be able to create IncludeDetails extensions

    var query1 = context.Set<MyEntity>()
        .IncludeDetails();

    var query2 = context.Set<ContainerEntity1>()
        .Include(q => q.MyEntity).IncludeDetails();

    var query3 = context.Set<ContainerEntity2>()
        .Include(q => q.MyEntities).IncludeDetails();

I don't need to use AutoInclude, because there are different scenarios where a different set of included fields is needed

roji commented 1 month ago

You can define an extension method over e.g. IQueryable<MyEntity> that would internally add the various Include/ThenIncludes on that, and then call that - just like any regular extension methods. Are you seeing any problem with doing that?

AndreqGav commented 1 month ago

Thanks for the answer. I think this option won't work because the source isn't always IQueryable<MyEntity>.

For example here:

    var query2 = context.Set<ContainerEntity1>()
        .Include(q => q.MyEntity).IncludeDetails()
        .Where(x => ...);

    var query3 = context.Set<ContainerEntity2>()
        .Include(q => q.MyEntities).IncludeDetails()
        .Where(x => ...);

The extension for IQueryable<MyEntity> will not work here.

I tried to do it but ran into 2 problems:

  1. It took 3 extension methods for each option, and that's duplicating the logic of including fields.

    public static class MyExtensions
    {
    public static IQueryable<MyEntity> IncludeDetails(this IQueryable<MyEntity> source)
    {
        return source
            .Include(q => q.SubEntity1)
            .ThenInclude(q => q.SubEntity2)
            .Include(q => q.SubEntity2);
    } 
    
    public static IQueryable<T> IncludeDetails<T>(this IIncludableQueryable<T, MyEntity> source)
    where T: class
    {
        return source
            .ThenInclude(q => q.SubEntity1)
            .ThenInclude(q => q.SubEntity2)
            // .Include(q => q.SubEntity2)
            .Include(q => q);
    }
    
    public static IQueryable<T> IncludeDetails<T>(this IIncludableQueryable<T, IEnumerable<MyEntity>> source)
        where T: class
    {
        return source
            .ThenInclude(q => q.SubEntity1)
            .ThenInclude(q => q.SubEntity2)
            // .Include(q => q.SubEntity2)
            .Include(q => q);
    }
    }
  2. Failed to include the SubEntity2 field for query2 and query3

I managed to bypass the first problem, but the second problem persists because of this, only one MyEntity field can be included.

roji commented 1 month ago

IIncludableQueryable extends IQueryable, so that shouldn't be a problem. You can also make your IncludeDetails method generic over the interface, with a constraint for your MyEntity:

public static T IncludeDetails<T>(this T source) where T : IQueryable<MyEntity>
{ ... }

This extension should work on IQueryable, IIncludableQueryable and all the other interfaces which extend IQueryable.

AndreqGav commented 1 month ago

For my case, if I take this code

    var query2 = context.Set<ContainerEntity1>()
        .Include(q => q.MyEntity).IncludeDetails()
        .Where(x => ...);

Then context.Set<ContainerEntity1>().Include(q => q.MyEntity) returns IncludableQueryable<ContainerEntity1, MyEntity>, which is not suitable for IQueryable<MyEntity>

roji commented 1 month ago

@AndreqGav Include() doesn't change the type being queried (ContainerEntity1) - it only expresses that something (MyEntity) needs to be included on that type - so that's by design.

What you could do here, is accept an Expression lambda parameter on IncludeDetails() that tells it how to reach (include) MyEntity on the source queryable (ContainerEntity1 in the above cased):

public static IQueryable<T> IncludeDetails<T>(this IQueryable<T> source, Expression<Func<T, MyEntity>> entitySelector)
    => source.Include(entitySelector).ThenInclude(...);

Then you can use it as follows:

var query2 = context.Set<ContainerEntity1>()
    .IncludeDetails(q => q.MyEntity)
    .Where(x => ...);

... or something along those lines, depending on exactly what you're trying to achieve; it's a matter of getting the method signature correct and passing the right expression tree selector.

AndreqGav commented 1 month ago

Thank you for your answer.

Reflecting on the solution, I also arrived at a similar approach, but then got stuck because I don't understand how to make it work for including a collection List<MyEntity> :

public class ContainerEntity2
{
    public Guid Id { get; set; }

    public List<MyEntity> MyEntities { get; set; }
}

I can add another method specifically for collections and repeat the logic of including fields for MyEntity

public static IQueryable<T> IncludeDetails<T>(this IQueryable<T> source, Expression<Func<T, IEnumerable<MyEntity>>> entitySelector)
    => source.Include(entitySelector).ThenInclude(...);

But that would be duplication—which I want to avoid.

I would like to be able to define the logic for including fields in one method and use it in different situations: for loading the entity itself, and for loading as navigation properties (simple and collection).

The provided solution works for the entity itself and simple navigation, but is not suitable for including a List<MyEntity> collection

    var query3 = context.Set<ContainerEntity2>()
        .IncludeDetails(q => q.MyEntities)
        .Where(x => ...);
AndreqGav commented 1 month ago

@roji Any thoughts on this?