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 ExpressionExtensions
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)
Func<int, bool> f1 = x => (x < 2);
Expression<Func<int, bool>> f2 = x => (x < 2);
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>> inTime = comment => comment.Created < dateTime;
.Include(inTime.InjectInto((BlogEntry b, Func<Comment, bool> f) => b.Comments.Where(c => f(c))))
.Where(inTime.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;
Expression<Func<BlogEntry, IEnumerable<Comment>>> anyInTime = blog => blog.Comments.Where(c => inTime(c));
Console.WriteLine(anyInTime);
.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();