4

When querying using Entity Framework Core, I am using expressions to convert to DTO objects, which works well for the object, and any child collections.

A simplified example:

Model:

public class Model
{
    public int ModelId { get; set; }
    public string ModelName { get; set; }

    public virtual ICollection<ChildModel> ChildModels { get; set; }

    // Other properties, collections, etc.

    public static Expression<Func<Model, ModelDto>> AsDto =>
        model => new ModelDto
        { 
            ModelId = model.ModelId,
            ModelName = model.ModelName,
            ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList()
        };
}

Query:

dbContext.Models.Where(m => SomeCriteria).Select(Model.AsDto).ToList();

My question is about trying to find a way to do something similar for a child that is not a collection. If I have added to my model:

public AnotherChildModel AnotherChildModel { get; set; }

I can add a conversion in the expression:

public static Expression<Func<Model, ModelDto>> AsDto =>
    model => new ModelDto
    { 
        ModelId = model.ModelId,
        ModelName = model.ModelName,
        ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
        AnotherChildModel = new AnotherChildModelDto
        {
            AnotherChildModelId = model.AnotherChildModelId
        }
    };

But, I have not found a good way to avoid repeating this code every time that I need to convert the second child model to a DTO object. The expressions work for the main object and any child collections, but not for single entities. Is there a way to add the equivalent of a .Select() for a single entity?

michael
  • 95
  • 7
  • Have you looked at Automapper? – Dai Feb 26 '21 at 00:27
  • I found a similar situation, maybe this can help. https://stackoverflow.com/questions/22434498/selecting-as-expression-from-a-single-object – hijinxbassist Feb 26 '21 at 00:44
  • @Dai - I have looked at Automapper some, but I am not familiar with it, and have struggled to get started with it, especially as part of the Entity Framework queries. It does seem like it might be able to do what I am looking for though. – michael Feb 26 '21 at 21:49
  • @hijinxbassist - That answer is helpful! I thought I had tried something similar in the past and had issues (with the compiled expressions resulting many separate queries per row of results), but in testing similar code to that answer now, it seems to be working. Thank you! – michael Feb 26 '21 at 21:55

2 Answers2

7

There are several libraries which allows to do that in intuitive way:

LINQKit

[Expandable(nameof(AsDtoImpl))]
public static ModelDto AsDto(Model model)
{
   _asDtoImpl ??= AsDtoImpl() .Compile();
   return _asDtoImpl(model);
}

private static Func<Model, ModelDto> _asDtoImpl;

private static Expression<Func<Model, ModelDto>> AsDtoImpl =>
    model => new ModelDto
    { 
        ModelId = model.ModelId,
        ModelName = model.ModelName,
        ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
        AnotherChildModel = new AnotherChildModelDto
        {
            AnotherChildModelId = model.AnotherChildModelId
        }
    };
dbContext.Models
   .Where(m => SomeCriteria).Select(m => Model.AsDto(m))
   .AsExpandable()
   .ToList();

UPDATE: For EF Core, LINQKit can be confugred globally and AsExpanding() can be omitted.

builder
    .UseSqlServer(connectionString)
    .WithExpressionExpanding(); // enabling LINQKit extension

NeinLinq - almost the same as in LINQKit

[InjectLambda]
public static ModelDto AsDto(Model model)
{
   _asDto ??= AsDto() .Compile();
   return _asDto(model);
}

private static Func<Model, ModelDto> _asDto;

private static Expression<Func<Model, ModelDto>> AsDto =>
    model => new ModelDto
    { 
        ModelId = model.ModelId,
        ModelName = model.ModelName,
        ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
        AnotherChildModel = new AnotherChildModelDto
        {
            AnotherChildModelId = model.AnotherChildModelId
        }
    };
dbContext.Models
   .Where(m => SomeCriteria).Select(m => Model.AsDto(m))
   .ToInjectable()
   .ToList();

UPDATE: For EF Core, NenLinq can be confugred globally and ToInjectable() can be omitted.

builder
    .UseSqlServer(connectionString)
    .WithLambdaInjection(); // enabling NeinLinq extension

DelegateDecompiler - less verbose than others

[Compute]
public static ModelDto AsDto(Model model)
  => new ModelDto
    { 
        ModelId = model.ModelId,
        ModelName = model.ModelName,
        ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
        AnotherChildModel = new AnotherChildModelDto
        {
            AnotherChildModelId = model.AnotherChildModelId
        }
    }
dbContext.Models
   .Where(m => SomeCriteria).Select(m => Model.AsDto(m))
   .Decompile()
   .ToList();

All libraries do the same thing - correct expression tree before EF Core processing. All of them need additional call to inject it's own IQueryProvider.

Svyatoslav Danyliv
  • 21,911
  • 3
  • 16
  • 32
  • Thank you for the detailed answer! With the additional call needed that you mentioned at the end, this would be the "AsExpandable()" call in LINQKit for example? So then this would be needed on any queries using these? – michael Feb 26 '21 at 21:57
  • Yes, exactly. But this call can be done in the root of the query, or in the repository. – Svyatoslav Danyliv Feb 27 '21 at 02:13
0

You can do it without any third party library. The key is to use AsQueryable() before the Select is made. You can of course get it to work without it but there is a high probability that you will fetch more columns than you actually need.

For your code it would be something like:

dbContext.Models.Where(m => SomeCriteria).AsQueryable().Select(Model.AsDto).ToList();

Example:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

using var context = new MyDbContext();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();

var blogPostIds = context.Blogs
    .Select(b => new
    {
        BlogId = b.Id,
        PostIds = b.Posts.AsQueryable().Select(Helper.Selector).ToList()
    })
    .ToList();

public static class Helper
{
    public static Expression<Func<Post, int>> Selector
        => x => x.Id;
}

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

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Data Source=(LocalDb)\\MSSQLLocalDB;Initial Catalog=Selector;Integrated Security=SSPI;")
            .LogTo(Console.WriteLine, LogLevel.Information);
}

public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    public IEnumerable<Post> Posts { get; set; }
}

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

Source: https://stackoverflow.com/a/76047514/3850405

Ogglas
  • 62,132
  • 37
  • 328
  • 418