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

时间:2013-10-28 20:35:24

标签: c# linq entity-framework expression

想象一下下面的表结构

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

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

3 个答案:

答案 0 :(得分:13)

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

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

你是否真的加入这三个表足以使这个方法的使用更清晰,而不仅仅是在LINQ中直接表达你想要做的事情?我认为每次创建此查询所需的额外行比使用此方法更清晰。

答案 1 :(得分:1)

所以我们可以做的就是从将数据加入匿名对象的确切方法开始。

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

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

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

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

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

其余的你已经拥有。

答案 2 :(得分:0)

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

对于您对数据库执行的每个'select',我建议使用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
           };
}

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

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传递给方法,但我认为这不会起作用,但也许有人想出了解决方案。