Search code examples
c#ef-core-3.1

How to make an EF Core query using Expression?


How to query a database with EF Core using Expression?

I'd like to build a dynamic query according to metadata, and query/edit data in a dynamic way, probably it could be with Expression. How could I do that?

Target:

db.Blogs.AsNoTracking().FirstOrDefault(p => p.BlogId == 1);

Code?

var funcAsNoTracking = Expression.Call(dbSet.GetType().GetMethod("AsNoTracking")!);
var funcFirstOrDefault = Expression.Call(typeof(IQueryable<>).GetMethod("FirstOrDefault")!);
/* what to do next or else? */
//Code

Other code:

public class BloggingContext : DbContext
{
     public DbSet<Blog> Blogs { get; set; }
     public DbSet<Post> Posts { get; set; }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public List<Post> Posts { get; } = new();
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

Solution

  • Thanks for your answer @phuzi

    After a nice weekend rest, I can focus on the implementation and figure it out myself, share it hoping help others.

    The conclusion, EF core could make this, still I would like try another way witch like LINQ to SQL to build dynamic solution.

    Note, this is just some demo code only show the purpose, the full code is large.

    Part A: Metadata

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        public Dictionary<string, object> AllDbSets
        {
            get
            {
                //TODO: setup with dynamic registration and cache
                var dic = new Dictionary<string, object>() { { nameof(Blogs), Blogs }, { nameof(Posts), Posts } };
                return dic;
            }
        }
    }
    

    The newly added AllDbSets

    Part B: Dynamic search model

    internal class Query
    {
        public Query(List<string> fileds, List<IFilter> filters)
        {
            this.Fileds = fileds;
            this.Filters = filters;
        }
    
        public List<string> Fileds { get; set; }
        public List<IFilter> Filters { get; set; }
    }
    
    internal interface IFilter
    {
    
    }
    
    internal class EqualFilter : IFilter
    {
        public EqualFilter(string field, object value)
        {
            this.Filed = field;
            this.Value = value;
        }
    
        public string Filed { get; set; }
        public object Value { get; set; }
    }
    

    Note: Just for this demo

    Part C: Query Engine process with demo

    static void Main(string[] args)
    {
        var query = new Query(new List<string> { "Blogs.BlogId", "Blogs.Url" }, new List<IFilter>() { new EqualFilter("Blogs.BlogId", 1) });
    
        var queryDBSetName = query.Fileds.Select(p => p.Split(".")[0]).Distinct().First();
    
    
        using BloggingContext db = new();
        {
            //Get DB set and basic metadata from ef core
            var set = db.AllDbSets[queryDBSetName];
            var setType = set.GetType();
            var entityType = setType.GenericTypeArguments.First();
    
            //AsNoTracking methodinfo
            var methodInfoAsNoTracking = typeof(EntityFrameworkQueryableExtensions).GetTypeInfo().GetDeclaredMethod(nameof(EntityFrameworkQueryableExtensions.AsNoTracking))!.MakeGenericMethod(entityType);
            //Build AsNoTracking expression
            var sourceParam = Expression.Parameter(typeof(IQueryable<>).MakeGenericType(entityType), "source");
            var asNoTrackingExpression = Expression.Call(methodInfoAsNoTracking, sourceParam);
    
            //FirstOrDefault methodinfo
            var methodInfoFirstOrDefault = typeof(Queryable).GetTypeInfo().DeclaredMethods.First(p => p.Name == nameof(Queryable.FirstOrDefault) && p.GetParameters().Length == 2 && p.GetParameters()[1].Name == "predicate")!.MakeGenericMethod(entityType);
    
            //Build property predicate expression
            var propertyInfo = entityType.GetProperty("BlogId")!;
            var param = Expression.Parameter(entityType, "p");
            var left = Expression.Property(param, propertyInfo);
            var right = Expression.Constant(1, typeof(int));
    
            var predicateExpression = Expression.Equal(left, right);
    
            //Make lambda filter expression
            Expression filterExpression = Expression.Lambda(predicateExpression, param);
    
            //FirstOrDefault expression with filter
            var callFirstOrDefault = Expression.Call(methodInfoFirstOrDefault, Expression.Lambda(asNoTrackingExpression, sourceParam).Body, filterExpression);
    
            //Make the call of expression
            var result = Expression.Lambda(callFirstOrDefault, sourceParam).Compile().DynamicInvoke(set);
        }
    }
    

    The filter from query ignored, but does not matter for demo.

    Again, it is just a test demo, lots of work to make it perfect.