Search code examples
c#entity-frameworkentity-framework-coreexpression-trees

Can I reuse code for selecting a custom DTO object for a child property with EF Core?


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?


Solution

  • 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

    [Computed]
    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.