2013-10-28 17 views
6

Immaginate la seguente struttura della tabellaLINQ Unione multipla IQueryable modificare l'espressione del selettore di risultato

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

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

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

voglio definire una funzione che unisce questi tre tavoli e accetta un Expression<Func<TableA, TableB, TableC, T>> come selettore.

Così mi piacerebbe qualcosa di simile al seguente:

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

Ora, ovviamente quanto sopra non faccio quello che voglio fare, questo mi darà una IQueryable del tipo di espressione. Potrei usare la sintassi del metodo di concatenazione, ma alla fine ho bisogno di più selettori, uno per ogni chiamata alla catena del metodo. C'è un modo per prendere il selettore e applicarlo a un tipo anonimo, come nella seguente funzione incompleta:

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

Delle idee su come questo potrebbe essere fatto?

risposta

10

È possibile definire un oggetto intermediario usa e getta per selezionare in invece di utilizzare un tipo anonimo:

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

Nuovo metodo:

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

Sarà davvero si unirà su questi tre tavoli abbastanza per fare l'uso di questo metodo è più chiaro rispetto all'espressione di ciò che vuoi fare direttamente in LINQ? Direi che le righe aggiuntive necessarie per creare questa query ogni volta sarebbero più chiare rispetto all'utilizzo di questo metodo.

+0

Il metodo viene chiamato da diversi punti e deve essere in grado di eseguire la selezione sul database. Non credo che la soluzione fornita verrà eseguita sul database, potrebbe effettivamente ottenere una TargetInvocationException – mhand

+0

I stand corretti, EF consente di utilizzare tipi non mappati negli alberi di espressioni IQueryable. – mhand

+1

Sì, finché l'albero delle espressioni risultante può essere convertito in SQL, va tutto bene. – Ocelot20

0

Forse non è la soluzione che state cercando, ma mi post it:

mi sento di raccomandare un DataModel per ogni 'seleziona' si esegue sul database come il seguente:

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

tua 'selezionare' fa lo stesso come già fate

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

e quindi è necessario un qualche tipo di 'mapper', che rappresenta il 'selector', o almeno quello che penso si intende con selecto R:

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

allora avete bisogno di definire almeno un metodo di mappatura:

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

allora si può semplicemente chiamare:

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

so che si desidera passare a un qualche tipo di Expression nel metodo, ma penso che questo non funzionerà, ma forse qualcuno ha trovato una soluzione.

1

Quindi, ciò che possiamo fare è iniziare con il metodo esatto che avete di unire i dati in un oggetto anonimo.

La prima cosa che faremo è iniziare con questo semplice classe di supporto e il metodo per permetterci di sostituire tutte le istanze di un'espressione con un'altra espressione in una data espressione:

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

Ora per il nostro attuale metodo. Per mappare una sequenza di questi oggetti anonimi usando un costruttore di tre parametri, ciò che possiamo fare è che il nostro metodo accetti un'espressione che rappresenta il mapping della sequenza di input nel primo parametro, nonché i selettori per gli altri due parametri. Possiamo quindi sostituire tutte le istanze se il primo parametro nel corpo del selettore "reale" con il corpo del selettore del primo parametro.

Si noti che è necessario disporre di un parametro aggiunto all'inizio per consentire l'inferenza di tipo sul tipo anonimo.

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

Ora chiamarlo possiamo passare nella query, solo per soddisfare inferenza di tipo, quindi un selettore per dell'oggetto anonimo primo, secondo e terzi parametri, così come il nostro selettore di finale:

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

E il resto lo hai già.