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

后端 未结 4 1164
离开以前
离开以前 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 Include(this IQueryable 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)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 P1s { get; set; }
    }
    
    public class P3 : P
    {
        public ICollection P1s { get; set; }
    }
    
    public class P4 : P
    {
        public ICollection P2s { get; set; }
    }
    
    public class MyDbContext : DbContext
    {
        public DbSet P1s { get; set; }
        public DbSet P2s { get; set; }
        public DbSet P3s { get; set; }
        public DbSet P4s { get; set; }
    
        // ...
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity().HasOne(e => e.P2).WithMany(e => e.P1s).HasForeignKey("P2Id").IsRequired();
            modelBuilder.Entity().HasOne(e => e.P3).WithMany(e => e.P1s).HasForeignKey("P3Id").IsRequired();
            modelBuilder.Entity().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
    Include
    (
        this IQueryable source,
        Expression> 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
    ThenInclude
    (
        this IIncludableQueryable> source,
        Expression> navigationPropertyPath
    )
    

    and

    public static IIncludableQueryable
    ThenInclude
    (
        this IIncludableQueryable source,
        Expression> 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.

提交回复
热议问题