Can a String based Include alternative be created in Entity Framework Core?

后端 未结 4 1159
离开以前
离开以前 2020-12-15 06:35

On an API I need dynamic include but EF Core does not support String based include.

Because of this I created a mapper which maps Strings to lambda expressions added

相关标签:
4条回答
  • 2020-12-15 07:07

    Update:

    Starting with v1.1.0, the string based include is now part of EF Core, so the issue and the below solution are obsolete.

    Original answer:

    Interesting exercise for the weekend.

    Solution:

    I've ended up with the following extension method:

    public static class IncludeExtensions
    {
        private static readonly MethodInfo IncludeMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo()
            .GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.Include)).Single(mi => mi.GetParameters().Any(pi => pi.Name == "navigationPropertyPath"));
    
        private static readonly MethodInfo IncludeAfterCollectionMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo()
            .GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude)).Single(mi => !mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter);
    
        private static readonly MethodInfo IncludeAfterReferenceMethodInfo = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo()
            .GetDeclaredMethods(nameof(EntityFrameworkQueryableExtensions.ThenInclude)).Single(mi => mi.GetParameters()[0].ParameterType.GenericTypeArguments[1].IsGenericParameter);
    
        public static IQueryable<TEntity> Include<TEntity>(this IQueryable<TEntity> source, params string[] propertyPaths)
            where TEntity : class
        {
            var entityType = typeof(TEntity);
            object query = source;
            foreach (var propertyPath in propertyPaths)
            {
                Type prevPropertyType = null;
                foreach (var propertyName in propertyPath.Split('.'))
                {
                    Type parameterType;
                    MethodInfo method;
                    if (prevPropertyType == null)
                    {
                        parameterType = entityType;
                        method = IncludeMethodInfo;
                    }
                    else
                    {
                        parameterType = prevPropertyType;
                        method = IncludeAfterReferenceMethodInfo;
                        if (parameterType.IsConstructedGenericType && parameterType.GenericTypeArguments.Length == 1)
                        {
                            var elementType = parameterType.GenericTypeArguments[0];
                            var collectionType = typeof(ICollection<>).MakeGenericType(elementType);
                            if (collectionType.IsAssignableFrom(parameterType))
                            {
                                parameterType = elementType;
                                method = IncludeAfterCollectionMethodInfo;
                            }
                        }
                    }
                    var parameter = Expression.Parameter(parameterType, "e");
                    var property = Expression.PropertyOrField(parameter, propertyName);
                    if (prevPropertyType == null)
                        method = method.MakeGenericMethod(entityType, property.Type);
                    else
                        method = method.MakeGenericMethod(entityType, parameter.Type, property.Type);
                    query = method.Invoke(null, new object[] { query, Expression.Lambda(property, parameter) });
                    prevPropertyType = property.Type;
                }
            }
            return (IQueryable<TEntity>)query;
        }
    }
    

    Test:

    Model:

    public class P
    {
        public int Id { get; set; }
        public string Info { get; set; }
    }
    
    public class P1 : P
    {
        public P2 P2 { get; set; }
        public P3 P3 { get; set; }
    }
    
    public class P2 : P
    {
        public P4 P4 { get; set; }
        public ICollection<P1> P1s { get; set; }
    }
    
    public class P3 : P
    {
        public ICollection<P1> P1s { get; set; }
    }
    
    public class P4 : P
    {
        public ICollection<P2> P2s { get; set; }
    }
    
    public class MyDbContext : DbContext
    {
        public DbSet<P1> P1s { get; set; }
        public DbSet<P2> P2s { get; set; }
        public DbSet<P3> P3s { get; set; }
        public DbSet<P4> P4s { get; set; }
    
        // ...
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<P1>().HasOne(e => e.P2).WithMany(e => e.P1s).HasForeignKey("P2Id").IsRequired();
            modelBuilder.Entity<P1>().HasOne(e => e.P3).WithMany(e => e.P1s).HasForeignKey("P3Id").IsRequired();
            modelBuilder.Entity<P2>().HasOne(e => e.P4).WithMany(e => e.P2s).HasForeignKey("P4Id").IsRequired();
            base.OnModelCreating(modelBuilder);
        }
    }
    

    Usage:

    var db = new MyDbContext();
    
    // Sample query using Include/ThenInclude
    var queryA = db.P3s
        .Include(e => e.P1s)
            .ThenInclude(e => e.P2)
                .ThenInclude(e => e.P4)
        .Include(e => e.P1s)
            .ThenInclude(e => e.P3);
    
    // The same query using string Includes
    var queryB = db.P3s
        .Include("P1s.P2.P4", "P1s.P3");
    

    How it works:

    Given a type TEntity and a string property path of the form Prop1.Prop2...PropN, we split the path and do the following:

    For the first property we just call via reflection the EntityFrameworkQueryableExtensions.Include method:

    public static IIncludableQueryable<TEntity, TProperty>
    Include<TEntity, TProperty>
    (
        this IQueryable<TEntity> source,
        Expression<Func<TEntity, TProperty>> navigationPropertyPath
    )
    

    and store the result. We know TEntity and TProperty is the type of the property.

    For the next properties it's a bit more complex. We need to call one of the following ThenInclude overloads:

    public static IIncludableQueryable<TEntity, TProperty>
    ThenInclude<TEntity, TPreviousProperty, TProperty>
    (
        this IIncludableQueryable<TEntity, ICollection<TPreviousProperty>> source,
        Expression<Func<TPreviousProperty, TProperty>> navigationPropertyPath
    )
    

    and

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

    source is the current result. TEntity is one and the same for all calls. But what is TPreviousProperty and how we decide which method to call.

    Well, first we use a variable to remember what was the TProperty in the previous call. Then we check if it is a collection property type, and if yes, we call the first overload with TPreviousProperty type extracted from the generic arguments of the collection type, otherwise simply call the second overload with that type.

    And that's all. Nothing fancy, just emulating an explicit Include / ThenInclude call chains via reflection.

    0 讨论(0)
  • 2020-12-15 07:08

    String-based Include() shipped in EF Core 1.1. I would suggest you try upgrading and removing any workarounds you had to add to your code to address this limitation.

    0 讨论(0)
  • 2020-12-15 07:22

    Creating an "IncludeAll" extension on query will require a different approach from what you have initially done.

    EF Core does expression interpretation. When it sees the .Include method, it interprets this expression into creating additional queries. (See RelationalQueryModelVisitor.cs and IncludeExpressionVisitor.cs).

    One approach would be to add an additional expression visitor that handles your IncludeAll extension. Another (and probably better) approach would be to interpret the expression tree from .IncludeAll to the appropriate .Includes and then let EF handle the includes normally. An implementation of either is non-trivial and beyond the scope of a SO answer.

    0 讨论(0)
  • 2020-12-15 07:23

    String-based Include() shipped in EF Core 1.1. If you keep this extension you will get error "Ambiguous match found". I spent half day to search solution for this error. Finally i removed above extension and error was resolved.

    0 讨论(0)
提交回复
热议问题