0

How can I use an Expression in another Expression. For a set I can use blog.Posts.Select(postMapper.ProjectPost). But How can I use it for a single object? I don't want to call compile, I need to use that in EF sql translator. I try some hacks like new List<Blog>{post.Blog}.Select(blogMapper.ProjectBlog).First() but it's not working.

    public class BloggingContext : DbContext
    {
        private readonly ILoggerFactory loggerFactory;

        public BloggingContext(ILoggerFactory loggerFactory)
        {
            this.loggerFactory = loggerFactory;
        }

        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
        
        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseLoggerFactory(loggerFactory).UseSqlServer(@"Server=(LocalDB)\MSSqlLocalDb;Database=EFExpressionMapper;Trusted_Connection=True");
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        public List<Post> Posts { get; } = new List<Post>();
    }

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

    class Program
    {
        static async Task Main(string[] args)
        {
            IServiceCollection serviceCollection = new ServiceCollection();
            serviceCollection.AddLogging(builder => builder
                .AddConsole()
                .AddFilter(level => level >= LogLevel.Information)
            );
            var loggerFactory = serviceCollection.BuildServiceProvider().GetService<ILoggerFactory>();

            await using var dbContext = new BloggingContext(loggerFactory);
            
            dbContext.Add(new Blog
            {
                Url = "http://blogs.msdn.com/sample-blog",
                Posts =
                {
                    new Post {Title = "Post 1", Content = "Post 1 content"},
                    new Post {Title = "Post 2", Content = "Post 2 content"},
                    new Post {Title = "Post 3", Content = "Post 3 content"},
                }
            });
            await dbContext.SaveChangesAsync();

            var postMapper = new PostMapper(new BlogMapper());

            var posts = await dbContext.Posts.Select(postMapper.ProjectPost).ToArrayAsync();

            foreach (var post in posts)
            {
                Console.WriteLine($"{post.Title} {post.Blog.Url}");
            }
        }
    }

    public class PostMapper
    {
        public Expression<Func<Post, PostDto>> ProjectPost { get; }

        public PostMapper(BlogMapper blogMapper)
        {
//TODO USE blogMapper.ProjectBlogList WITHOUT COMPILE
            ProjectPost = post => new PostDto(post.PostId, post.Title, post.Content, blogMapper.ProjectBlogList.Compile()(post.Blog));
        }
    }

    public class BlogMapper
    {
        public Expression<Func<Blog, BlogListDto>> ProjectBlogList { get; } = blog => new BlogListDto(blog.BlogId, blog.Url);
    }

    public class BlogListDto
    {
        public int BlogId { get; }
        public string Url { get; }
        
        public BlogListDto(int blogId, string url)
        {
            BlogId = blogId;
            Url = url;
        }
    }

    public class PostDto
    {
        public int PostId { get; }
        public string Title { get; }
        public string Content { get; }
        
        public BlogListDto Blog { get; }

        public PostDto(int postId, string title, string content, BlogListDto blog)
        {
            PostId = postId;
            Title = title;
            Content = content;
            Blog = blog;
        }
    }

Look into PostMapper constructor. I'm used a Compile method there. But it's not good for EF

Peter Pollen
  • 83
  • 1
  • 6

1 Answers1

0

Actually LINQKit may correct your query and make EF happy when using Compile. Just add AsExpandable() to your query.

But I suggest to do not create mapping class for each DTO but collect them in logical one:

public static class DtoMapper
{
    [Expandable(nameof(AsDtoPost))]
    public static PostDto AsDto(this Post post)
        => throw new NotImplementedException();

    [Expandable(nameof(AsDtoBlogList))]
    public static BlogListDto AsDto(this Blog blog)
        => throw new NotImplementedException();

    static Expression<Func<Post, PostDto>> AsDtoPost()
        => post => new PostDto(post.PostId, post.Title, post.Content, post.Blog.AsDto()));

    static Expression<Func<Blog, BlogListDto>> AsDtoBlogList()
        => blog => new BlogListDto(blog.BlogId, blog.Url);
}

So your sample can be rewritten

var posts = await dbContext.Posts
    .AsExpandable()
    .Select(p => p.AsDto()).ToArrayAsync();

Similar but answer already created before, which covers other libraries which do the same lambda expression expanding. https://stackoverflow.com/a/66386142/10646316 This answer is focused on LINQKit realisation.

Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • IT's not working with asynchronou versions. But it's interesting solution `Unhandled exception. System.InvalidOperationException: The source 'IQueryable' doesn't implement 'IAsyncEnumerable'. Only sources that implement 'IAsyncEnumerable' can be used for Entity Framework asynchronous operations.` – Peter Pollen Apr 05 '21 at 18:28
  • Async should work, you have to install correct version: https://www.nuget.org/packages/LinqKit.Microsoft.EntityFrameworkCore/ – Svyatoslav Danyliv Apr 05 '21 at 18:36