C# LINQ 多连接 IQueryable 修改结果选择器表达式

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19644617/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 15:31:15  来源:igfitidea点击:

LINQ multiple join IQueryable modify result selector expression

c#linqentity-frameworkexpression

提问by mhand

Imagine the following table structure

想象一下下面的表结构

---------
TableA
ID
Name

---------
TableB
ID
TableAID

---------
TableC
ID
TableBID

I want to define a function that joins these three tables and accepts an Expression<Func<TableA, TableB, TableC, T>>as a selector.

我想定义一个连接这三个表并接受 anExpression<Func<TableA, TableB, TableC, T>>作为选择器的函数。

So I'd like something like the following:

所以我想要如下内容:

public IQueryable<T> GetJoinedView<T>(Expression<Func<TableA, TableB, TableC, T>> selector)
{
    return from a in DbContext.Set<TableA>()
           join b on DbContext.Set<TableB>() a.ID equals b.TableAID
           join c on DbContext.Set<TableC>() b.ID equals c.TableBID
           select selector;
}

Now, obviously the above doesn't do what I want it to do, this will give me an IQueryableof the expression type. I could use method chaining syntax, but then I end up needing multiple selectors, one for each method chain invocation. Is there a way to take the selector and apply it to an anonymous type like in the following incomplete function:

现在,显然上面没有做我想要它做的事情,这会给我一个IQueryable表达式类型。我可以使用方法链语法,但是我最终需要多个选择器,每个方法链调用一个。有没有办法采用选择器并将其应用于匿名类型,如以下不完整的函数:

public IQueryable<T> GetJoinedView<T>(Expression<Func<TableA, TableB, TableC, T>> selector)
{
    var query = from a in DbContext.Set<TableA>()
                join b on DbContext.Set<TableB>() a.ID equals b.TableAID
                join c on DbContext.Set<TableC>() b.ID equals c.TableBID
                select new
                {
                    A = a, B = b, C = c
                };

    // I need the input selector to be modified to be able to operate on
    // the above anonymous type
    var resultSelector = ModifyInputSelectorToOperatorOnAnonymousType(selector);

    return query.Select(resultSelector);
}

Any ideas on how this could be done?

关于如何做到这一点的任何想法?

采纳答案by Ocelot20

You can define a throwaway intermediary object to select into instead of using an anonymous type:

您可以定义一个一次性的中间对象来选择,而不是使用匿名类型:

public class JoinedItem
{
    public TableA TableA { get; set; }
    public TableB TableB { get; set; }
    public TableC TableC { get; set; }
}

New method:

新方法:

public IQueryable<T> GetJoinedView<T>(Expression<Func<JoinedItem, T>> selector)
{
    return DbContext.Set<TableA>()
                    .Join(DbContext.Set<TableB>(),
                          a => a.ID,
                          b => b.TableAID,
                          (ab) => new { A = a, B = b})
                    .Join(DbContext.Set<TableC>(),
                          ab => ab.B.ID,
                          c => c.TableBID
                          (ab, c) => new JoinedItem
                              {
                                  TableA = ab.A,
                                  TableB = ab.B,
                                  TableC = c
                              })
                     .Select(selector);
}

Will you really be joining on these three tables enough to make the use of this method clearer than just expressing what you want to do directly in LINQ? I would argue that the extra lines needed to create this query every time would be clearer than using this method.

你真的会加入这三个表,足以让这个方法的使用比直接在 LINQ 中表达你想做什么更清楚吗?我认为每次创建此查询所需的额外行比使用此方法更清晰。

回答by Viper

Maybe it is not the solution you are looking for, but i will post it:

也许这不是您正在寻找的解决方案,但我会发布它:

I would recommend a DataModel for each 'select' you perform against your database like the following:

我会为您对数据库执行的每个“选择”推荐一个 DataModel,如下所示:

 public class JoinedDataModel
 {
     public TableA DataA { get; set; }
     public TableB DataB { get; set; }
     public TableC DataC { get; set; }
 }

your 'select' does the same as you already do

你的“选择”和你已经做的一样

public IQueryable<JoinedDataModel> GetJoinedView( )
{
    return from a in DbContext.Set<TableA>()
           join b on DbContext.Set<TableB>() a.ID equals b.TableAID
           join c on DbContext.Set<TableC>() b.ID equals c.TableBID
           select new JoinedDataModel( )
           {
                DataA = a,
                DataB = b,
                DataC = c
           };
}

and then you need some kind of 'mapper' which represents your 'selector' or at least what i think you mean with selector:

然后你需要某种“映射器”来代表你的“选择器”或者至少我认为你对选择器的意思:

public static Mapper( )
{
    private static Dictionary<MapTuple, object> _maps = new Dictionary<MapTuple, object>();

    public static void AddMap<TFrom, TTo>(Action<TFrom, TTo, DateTime> map)
    {
        Mapper._maps.Add(MapTuple.Create(typeof(TFrom), typeof(TTo)), map);
    }

    public static TTo Map<TFrom, TTo>( TFrom srcObj )
    {
        var typeFrom = typeof(TFrom);
        var typeTo = typeof(TTo);
        var key = MapTuple.Create(typeFrom, typeTo);
        var map = (Action<TFrom, TTo, DateTime>) Mapper._maps[key];

        TTo targetObj = new TTo( );

        map( srcObj, targetObj );

        return targetObj;
    }

then you need to define at least one mapping method:

那么你至少需要定义一种映射方法:

AddMap<JoinedDataModel, YourResultModel>( 
    ( src, trg ) =>
    {
        trg.SomePropertyA = src.DataA.SomeProperty;
        trg.SomePropertyB = src.DataB.SomeProperty;
    }
);

then you can simply call:

那么你可以简单地调用:

 public IList<YourResultModel> CallDb( )
 {
      return ( from item in GetJoinedView( )
               select Mapper.MapTo<JoinedDataModel, YourResultModel>( item ) 
             ).ToList( );
 }

i know that you want to pass in some kind of Expressioninto the method but i think this isn't going to work, but maybe someone came up with a solution.

我知道你想传入某种Expression方法,但我认为这行不通,但也许有人想出了一个解决方案。

回答by Servy

So what we can do is start out with the exact method that you have of joining the data into an anonymous object.

所以我们可以做的是从将数据连接到匿名对象的确切方法开始。

The first thing we'll do is start out with this simple helper class and method to allow us to replace all instance of one expression with another expression in a given expression:

我们要做的第一件事是从这个简单的帮助类和方法开始,允许我们用给定表达式中的另一个表达式替换一个表达式的所有实例:

public class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

Now for our actual method. In order to map a sequence of these anonymous objects using a three parameter constructor what we can do is have our method accept an expression representing mapping the input sequence into the first parameter, as well as selectors for the other two parameters. We can then replace all instances if the first parameter in the body of the "real" selector with the body of the first parameter's selector.

现在为我们的实际方法。为了使用三参数构造函数映射这些匿名对象的序列,我们可以做的是让我们的方法接受一个表达式,该表达式表示将输入序列映射到第一个参数,以及其他两个参数的选择器。如果“真实”选择器的主体中的第一个参数与第一个参数的选择器的主体,则我们可以替换所有实例。

Note that we need to have a parameter added to the start to allow for type inference on the anonymous type.

请注意,我们需要在开头添加一个参数以允许对匿名类型进行类型推断。

public static Expression<Func<TInput, TOutput>>
    ModifyInputSelectorToOperatorOnAnonymousType
    <TInput, TOutput, TParam1, TParam2, TParam3>(
    //this first param won't be used; 
    //it's here to allow type inference
    IQueryable<TInput> exampleParam,
    Expression<Func<TInput, TParam1>> firstSelector,
    Expression<Func<TInput, TParam2>> secondSelector,
    Expression<Func<TInput, TParam3>> thirdSelector,
    Expression<Func<TParam1, TParam2, TParam3, TOutput>> finalSelector)
{
    var parameter = Expression.Parameter(typeof(TInput), "param");

    var first = firstSelector.Body.Replace(firstSelector.Parameters.First(),
        parameter);
    var second = secondSelector.Body.Replace(secondSelector.Parameters.First(),
        parameter);
    var third = thirdSelector.Body.Replace(thirdSelector.Parameters.First(),
        parameter);

    var body = finalSelector.Body.Replace(finalSelector.Parameters[0], first)
        .Replace(finalSelector.Parameters[1], second)
        .Replace(finalSelector.Parameters[2], third);

    return Expression.Lambda<Func<TInput, TOutput>>(body, parameter);
}

Now to call it we can pass in the query, just to satisfy type inference, then a selector for the anonymous object's first, second, and third parameters, as well as our final selector:

现在调用它,我们可以传入查询,只是为了满足类型推断,然后是匿名对象的第一个、第二个和第三个参数的选择器,以及我们的最终选择器:

var resultSelector = ModifyInputSelectorToOperatorOnAnonymousType(
    query, x => x.A, x => x.B, x => x.C, selector);

And the rest you already have.

其余的你已经有了。