using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
public static partial class EntityFrameworkQueryableExtensions
public static IIncludableQueryable<TEntity, IEnumerable<TItem>> Include<TEntity, TItem> (
this IQueryable<TEntity> source,
Expression<Func<TEntity, IEnumerable<TItem>>> navigationPropertyPath,
Expression<Func<TItem, bool>> filter) where TEntity : class =>
source.Include(navigationPropertyPath.Where(filter));
public static IQueryable<TEntity> Where<TEntity, TItem> (
this IQueryable<TEntity> source,
Expression<Func<TEntity, IEnumerable<TItem>>> navigationPropertyPath,
Expression<Func<IEnumerable<TItem>, bool>> filter) where TEntity : class =>
source.Where(filter.Compose(navigationPropertyPath));
public static partial class ExpressionExtensions
public static Expression<Func<T, IEnumerable<TItem>>> Where<T, TItem>(
this Expression<Func<T, IEnumerable<TItem>>> outer, Expression<Func<TItem, bool>> predicate) =>
((Expression<Func<IEnumerable<TItem>, Func<TItem, bool>, IEnumerable<TItem>>>) ((q, f) => q.Where(i => f(i))))
public static Expression<Func<T1, TResult>> Compose<T1, T2, TResult>(this Expression<Func<T2, TResult>> outer, Expression<Func<T1, T2>> inner) =>
Expression.Lambda<Func<T1, TResult>>(
new ParameterReplacer((outer.Parameters[0], inner.Body)).Visit(outer.Body),
false, inner.Parameters[0]);
public static Expression<Func<T1, TResult>> InjectInto<T1, T2, T3, TResult>(this Expression<Func<T2, T3>> inner, Expression<Func<T1, Func<T2, T3>, TResult>> outer) =>
public static Expression<Func<T1, TResult>> Inject<T1, T2, T3, TResult>(this Expression<Func<T1, Func<T2, T3>, TResult>> outer, Expression<Func<T2, T3>> inner) =>
Expression.Lambda<Func<T1, TResult>>(
new InvokeReplacer((outer.Parameters[1], inner)).Visit(outer.Body),
false, outer.Parameters[0]);
class InvokeReplacer : ExpressionVisitor
readonly Dictionary<Expression, LambdaExpression> funcsToReplace;
public InvokeReplacer(params (Expression func, LambdaExpression replacement) [] funcsToReplace) =>
this.funcsToReplace = funcsToReplace.ToDictionary(p => p.func, p => p.replacement);
protected override Expression VisitInvocation(InvocationExpression invoke) =>
funcsToReplace.TryGetValue(invoke.Expression, out var lambda)
? (invoke.Arguments.Count != lambda.Parameters.Count
? throw new InvalidOperationException("Wrong number of arguments")
: new ParameterReplacer(lambda.Parameters.Zip(invoke.Arguments)).Visit(lambda.Body))
: base.VisitInvocation(invoke);
class ParameterReplacer : ExpressionVisitor
readonly Dictionary<ParameterExpression, Expression> parametersToReplace;
public ParameterReplacer(params (ParameterExpression parameter, Expression replacement) [] parametersToReplace)
: this(parametersToReplace.AsEnumerable()) { }
public ParameterReplacer(IEnumerable<(ParameterExpression parameter, Expression replacement)> parametersToReplace) =>
this.parametersToReplace = parametersToReplace.ToDictionary(p => p.parameter, p => p.replacement);
protected override Expression VisitParameter(ParameterExpression p) =>
parametersToReplace.TryGetValue(p, out var e) ? e : base.VisitParameter(p);
private static void Main(string[] args)
using var ctx = MyContext.MakeInMemoryContext();
var blogs = GetBlogsInlined(ctx, DateTime.Today).ToList();
Console.WriteLine("GetBlogsInlined(ctx, DateTime.Today).ToList(); = {0}\n", JsonSerializer.Serialize(blogs));
var blogs2 = GetBlogsExpression(ctx, DateTime.Today).ToList();
Console.WriteLine("GetBlogsExpression(ctx, DateTime.Today).ToList() = {0}", JsonSerializer.Serialize(blogs2));
var blogs3 = GetBlogsFunction(ctx, DateTime.Today).ToList();
Console.WriteLine("GetBlogsFunction(ctx, DateTime.Today).ToList() = {0}\n", JsonSerializer.Serialize(blogs3));
Console.WriteLine("\nWhen calling {0}, caught expected exception {1}", nameof(GetBlogsFunction), ex.Message);
public static IQueryable<BlogEntryDTO> GetBlogsInlined(MyContext ctx, DateTime dateTime)
.Blogs.Include(blog => blog.Comments.Where(comment => comment.Created < dateTime))
.Where(blog => blog.Comments.Any(comment => comment.Created < dateTime))
.Select(b => new BlogEntryDTO(b));
public static IQueryable<BlogEntryDTO> GetBlogsExpression(MyContext ctx, DateTime dateTime)
Expression<Func<Comment, bool>> commentFilter = comment => comment.Created < dateTime;
.Include(b => b.Comments, commentFilter)
.Where(commentFilter.InjectInto((BlogEntry b, Func<Comment, bool> f) => b.Comments.Any(c => f(c))))
.Select(b => new BlogEntryDTO(b));
public static IQueryable<BlogEntryDTO> GetBlogsFunction(MyContext ctx, DateTime dateTime)
Func<Comment, bool> inTime = comment => comment.Created < dateTime;
.Blogs.Include(blog => blog.Comments.Where(inTime))
.Where(blog => blog.Comments.Any(inTime))
.Select(b => new BlogEntryDTO(b));
public class MyContext(DbContextOptions<MyContext> options) : DbContext(options)
public DbSet<BlogEntry> Blogs { get; set; }
public DbSet<Comment> Comments { get; set; }
public static MyContext MakeInMemoryContext()
var builder = new DbContextOptionsBuilder<MyContext>().UseInMemoryDatabase("context");
var ctx = new MyContext(builder.Options);
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
private void SetupBlogs()
Created = DateTime.Now.AddDays(-3),
new Comment { Content = "c1", Created = DateTime.Now.AddDays(-2) },
new Comment { Content = "c2", Created = DateTime.Now.AddDays(-1) }
Created = DateTime.Now.AddDays(-2),
Comments = [new Comment { Content = "c3", Created = DateTime.Now.AddDays(-1) }]
new BlogEntry { Name = "2", Created = DateTime.Now.AddDays(-1), Comments = [] }
public int Id { get; set; }
public string Name { get; set; }
public DateTime Created { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
public int Id { get; set; }
public string Content { get; set; }
public DateTime Created { get; set; }
public int BlogEntryId { get; set; }
public virtual BlogEntry BlogEntry { get; set; }
public class BlogEntryDTO(BlogEntry blogEntry)
public int Id { get; set; } = blogEntry.Id;
public string Name { get; set; } = blogEntry.Name;
public string[] Comments { get; set; } = blogEntry.Comments.Select(c => c.Content).ToArray();