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; }
}
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.