LINQ 多重联接 IQueryable 修改结果选择器表达式



想象一下下面的表格结构

---------
TableA
ID
Name
---------
TableB
ID
TableAID
---------
TableC
ID
TableBID

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

所以我想要如下的东西:

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

现在,显然上面没有做我想要它做的事情,这将给我一个表达式类型的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);
}

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

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

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

新方法:

public IQueryable<T> GetJoinedView<T>(Expression<Func<JoinedItem, T>> selector)
{
    return DbContext.Set<TableA>()
                    .Join(DbContext.Set<TableB>(),
                          a => a.ID,
                          b => b.TableAID,
                          (a, b) => 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);
}

您是否真的要加入这三个表,使此方法的使用比直接在 LINQ 中表达要执行的操作更清晰?我认为每次创建此查询所需的额外行比使用此方法更清晰。

因此,

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

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

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

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

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

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

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

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

其余的你已经有了。

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

我建议您对数据库执行的每个"选择"使用DataModel,如下所示:

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

您的"选择"与您已经执行的操作相同

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

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

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

然后,您需要定义至少一个映射方法:

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

然后,您只需调用:

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

我知道您想将某种Expression传递到该方法中,但我认为这行不通,但也许有人想出了一个解决方案。

最新更新