我可以重用代码来使用 EF Core 为子属性选择自定义 DTO 对象吗?

问题描述 投票:0回答:3

使用 Entity Framework Core 进行查询时,我使用表达式转换为 DTO 对象,这对于对象和任何子集合都很有效。

一个简化的例子:

型号:

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

查询:

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

我的问题是试图找到一种方法为非集合的孩子做类似的事情。如果我已添加到我的模型中:

public AnotherChildModel AnotherChildModel { get; set; }

我可以在表达式中添加转换:

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

但是,我还没有找到一种好方法来避免每次需要将第二个子模型转换为 DTO 对象时重复此代码。这些表达式适用于主对象和任何子集合,但不适用于单个实体。有没有办法为单个实体添加 .Select() 的等效项?

c# entity-framework entity-framework-core expression-trees
3个回答
8
投票

有几个库可以以直观的方式做到这一点:

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();

更新:对于EF Core,LINQKit可以全局配置,

AsExpanding()
可以省略。

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

NeinLinq - 几乎与 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();

更新:对于 EF Core,NenLinq 可以全局配置,

ToInjectable()
可以省略。

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

DelegateDecompiler - 比其他的更简洁

[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();

所有库都执行相同的操作 - 在 EF Core 处理之前更正表达式树。它们都需要额外的调用来注入它自己的

IQueryProvider


0
投票

无需任何第三方库即可完成。关键是在制作

AsQueryable()
之前先使用
Select
。当然,您可以在没有它的情况下让它工作,但您很可能会获取比实际需要更多的列。

对于你的代码来说,它会是这样的:

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

示例:

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

来源: https://stackoverflow.com/a/76047514/3850405


0
投票

这可以在不使用任何第三方工具包的情况下通过子类化

ExpressionVisitor
并使用它来编辑外部
AsDto()
方法的表达式树并注入
AsDto()
的内部
AnotherChildModel
方法来实现。这里的基本思想是首先定义 AsDto() 表达式的
curried
版本,将
Func<AnotherChildModel, AnotherChildModelDto>
作为附加参数,然后使用
ExpressionVisitor
创建一个修改后的副本,用表达式替换 curried 函数参数
AnotherChildModel.AsDto
。由于此方法直接使用表达式树,因此它独立于您正在使用的特定 LINQ 框架。

要了解如何做到这一点,首先定义以下扩展方法,将一些

Expression<Func<T, TResult>>
注入和/或组合到某些包含表达式树中:

public static partial class ExpressionExtensions
{
    // Uncurry and compose an Expression<Func<T1, Func<T2, T3>, TResult>> into an Expression<Func<T1, TResult>> by composing with an Expression<Func<T2, T3>>
    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]);

    // Compose two Func<Tx, Ty> expressions with compatible generic parameters into a third.
    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(new [] {(outer.Parameters[0], inner.Body)}).Visit(outer.Body), 
            false, inner.Parameters[0]);    
}

class InvokeReplacer : ExpressionVisitor
{
    // Replace an Invoke() with the body of a lambda, replacing the formal paramaters of the lambda with the arguments of the invoke.
    // TODO: Handle replacing of functions that are not invoked but just passed as parameters to some external method, e.g.
    //   collection.Select(map) instead of collection.Select(i => map(i))
    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
{
    // Replace formal parameters (e.g. of a lambda body) with some containing expression in scope.
    readonly Dictionary<ParameterExpression, Expression> parametersToReplace;
    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);
}

现在你可以写

Model.AsDto()
如下:

// Lazily create the expression once and cache its value for performance.
static Lazy<Expression<Func<Model, ModelDto>>> AsDtoExpression = new(static () => {
    // Create a curried lambda with a Func<AnotherChildModel, AnotherChildModelDto> passed in:
    Expression<Func<Model, Func<AnotherChildModel, AnotherChildModelDto>, ModelDto>> asDtoCurried = 
        (model, map) => new ModelDto
        {
            // Set the values we can set directly:
            ModelId = model.ModelId,
            ModelName = model.ModelName,
            ChildModels = model.ChildModels.AsQueryable().Select(ChildModel.AsDto).ToList(),
            // But we can't call AnotherChildModel.AsDto() directly so sub in map for now.
            AnotherChildModelDto = map(model.AnotherChildModel), 
        };
    // Then inject AnotherChildModel.AsDto in place of the curried function:
    return asDtoCurried.Inject(AnotherChildModel.AsDto);
}); 

public static Expression<Func<Model, ModelDto>> AsDto => AsDtoExpression.Value;

演示小提琴#1 这里

备注:

  • 简单地从

    AnotherChildModel.AsDto()
    内部调用
    Model.Dto()
    可能很诱人。虽然这可能适用于 LINQ to Objects,但它不适用于 Entity Framework 或其他 LINQ to Entities 应用程序,原因请参见 “LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”” - 难住了! .

  • 我纯粹出于性能原因懒惰地构建表达式树并缓存结果。

  • 同样的想法可以用于注入

    ChildModel.AsDto()
    ,消除对
    .AsQueryable()
    的需要:

    // Lazily create the expression once and cache its value for performance.
    static Lazy<Expression<Func<Model, ModelDto>>> AsDtoExpression = new(static () => {
        // Create a curried lambda with a Func<AnotherChildModel, AnotherChildModelDto> passed in:
        Expression<Func<Model, Func<ChildModel, ChildModelDto>, Func<AnotherChildModel, AnotherChildModelDto>, ModelDto>> asDtoCurried = 
            (model, map1, map2) => new ModelDto
            {
                // Set the values we can set directly:
                ModelId = model.ModelId,
                ModelName = model.ModelName,
                // And sum in maps for the child models' AsDto() methods for now.
                ChildModels = model.ChildModels.Select(c => map1(c)).ToList(),
                AnotherChildModelDto = map2(model.AnotherChildModel), 
            };
        // Now inject the child AsDto() methods in place of the curried functions.
        return asDtoCurried.Inject(ChildModel.AsDto, AnotherChildModel.AsDto);
    }); 
    
    public static partial class ExpressionExtensions
    {
        public static Expression<Func<T1, TResult>> Inject<T1, T2, T3, T4, T5, TResult>(
            this Expression<Func<T1, Func<T2, T3>, Func<T4, T5>, TResult>> outer, 
            Expression<Func<T2, T3>> inner1, Expression<Func<T4, T5>> inner2) =>
            Expression.Lambda<Func<T1, TResult>>(
                new InvokeReplacer((outer.Parameters[1], inner1), (outer.Parameters[2], inner2)).Visit(outer.Body), 
                false, 
                outer.Parameters[0]);
    }
    

    请注意,虽然实现了替换

    Select(c => map1(c))
    ,但未实现替换
    Select(map1)

    演示小提琴 #2 这里

  • 有关编辑表达式树的更多内容,请参阅翻译表达式树为什么我要使用ExpressionVisitor?

© www.soinside.com 2019 - 2024. All rights reserved.