2016-11-23 2 views
1

Ich verwende LINQ-> WCF Data Services-> EF, die eine Teilmenge von LINQ mit ein paar Einschränkungen unterstützt. Ich hatte keine Probleme damit, einmal die Tricks und Workarounds für verschiedene Dinge zu lernen, aber ich würde gerne einen wiederverwendbaren Ausdruckgenerator machen, um nur den Date Teil einer DateTime zu vergleichen.Generieren eines Ausdrucks für ein IQueryable <T>

Mit regulären EF können Sie EntityFunctions.TruncateTime (EF < 6) oder DbFunctions.TruncateTime (EF6 +) verwenden, aber dies funktioniert nicht über Datendienste.

Meine Lösung war bisher immer wieder dieses Chaos einer Klausel, wo zu bauen:

.Where(x => x.DateProperty.Year == DateToCompare.Year && 
      x.DateProperty.Month == DateToCompare.Month && 
      x.DateProperty.Day == DateToCompare.Day); 

Das ist nur böse ist, immer wieder schreiben zu haben (aber es funktioniert), so dass ich versuchte, so etwas wie zu erstellen:

.WhereDate(x => x.DateProperty, DateToCompare); 

etwas ähnliches tun würde, nur kurz und süß und lesbar - ich verabscheue repetitiven unnötig Gefühl Code.

Die Struktur ist kein Problem, ich weiß, ich brauche etwas, das IQueryable<T>, Func<T, DateTime> (oder Expression<Func<T, DateTime>>) führt, und DateTime und kehrt IQueryable<T>.

public static IQueryable<T> WhereDate<T>(this IQueryable<T> data, Func<T, DateTime>> selector, DateTime date) 
{ 
    return data.Where(/*Something*/); 
}; 

Wo ich Probleme habe, ist, nehmen diese und bauen einen Ausdruck, der ohne Verletzung der Einschränkungen des Ausdrucks Bäume, die in where-Klausel gesetzt werden kann. Ich bin mir nicht ganz sicher, wie ich eine existierende Abfrage nehmen und meine eigene where-Anweisung zu dem Ausdruck hinzufügen kann, ohne eine .Where zu machen, was ich denke, könnte der Schlüssel hier sein. Ich denke, ich muss ein Expression<Func<T, DateTime>> aufnehmen und etwas bauen, das das verwendet, um ein Expression<Func<T, bool>> to the tree and return it as an IQueryable hinzuzufügen.

Jeder hat etwas Erfahrung damit, oder wissen, welche Dokumente ich lesen sollte?

Die größten Barrieren hier sind, dass Sie ein statementbasiertes Lambda nicht in einen Ausdruck umwandeln können und keine nicht unterstützten Funktionen in den Datendienst oder EF übergeben können. Das macht alle naiven Lösungen unmöglich, und so weit ich weiß, bleibt die Manipulation der manuellen Ausdrücke.

+0

Werfen Sie einen Blick auf auf [Link] (https://msdn.microsoft.com/en-us/library/bb882637 (v = vs.110) .aspx) . –

+0

Warum können Sie nicht einfach Ihren ersten Ausdruck in der Erweiterungsmethode zurückgeben? – user2697817

+0

@ user2697817 Weil OP will es generisch und nützlich für jede Entität – DavidG

Antwort

2

Hier ist die Lösung kam ich mit nach viel über das Thema zu lesen:

private static IQueryable<T> _whereDate<T>(this IQueryable<T> data, MemberExpression date1Expression, ParameterExpression parameter, DateTime date) 
{ 
    var date1Year = Expression.Property(date1Expression, "Year"); 
    var date1Month = Expression.Property(date1Expression, "Month"); 
    var date1Day = Expression.Property(date1Expression, "Day"); 
    var date2Year = Expression.Constant(date.Year); 
    var date2Month = Expression.Constant(date.Month); 
    var date2Day = Expression.Constant(date.Day); 
    var yearsEqual = Expression.Equal(date1Year, date2Year); 
    var monthsEqual = Expression.Equal(date1Month, date2Month); 
    var daysEqual = Expression.Equal(date1Day, date2Day); 
    var allPartsEqual = Expression.AndAlso(Expression.AndAlso(daysEqual, monthsEqual), yearsEqual); //Day->Month->Year to efficiently remove as many as possible as soon as possible. 
    var whereClause = Expression.Call(typeof(Queryable), "Where", new Type[] { data.ElementType }, data.Expression, Expression.Lambda(allPartsEqual, parameter)); 
    return data.Provider.CreateQuery<T>(whereClause); 
} 

public static IQueryable<T> WhereDate<T>(this IQueryable<T> data, Expression<Func<T, DateTime?>> selector, DateTime date) 
{ 
    var selectorMemberExpression = ((MemberExpression)selector.Body); 
    var nullableDateProperty = (PropertyInfo)selectorMemberExpression.Member; 
    var entityExpression = Expression.Parameter(typeof(T)); 
    var date1Expression = Expression.Property(entityExpression, nullableDateProperty); 
    return data._whereDate(Expression.PropertyOrField(date1Expression, "Value"), entityExpression, date); 
} 

public static IQueryable<T> WhereDate<T>(this IQueryable<T> data, Expression<Func<T, DateTime>> selector, DateTime date) 
{ 
    var selectorMemberExpression = ((MemberExpression)selector.Body); 
    var dateProperty = (PropertyInfo)selectorMemberExpression.Member; 
    var entityExpression = Expression.Parameter(typeof(T)); 
    return data._whereDate(Expression.Property(entityExpression, dateProperty), entityExpression, date); 
} 

Es in mehrere Funktionen aufgeteilt ist redundanten Code zu reduzieren und sowohl DateTime und DateTime? zu unterstützen.

Ich weiß, es gibt keine Überprüfung auf einen Mangel an Wert in der NULL-fähigen Version - das ist etwas, das ich bald hinzufügen werde, aber ich wollte die Lösung für jeden anderen von lernen und sicherstellen, dass niemand ihre Zeit damit verschwendet, zu erklären Das zu mir. Ich gehe meinen Code immer ein paar Mal durch, um die Effizienz und Lesbarkeit zu überprüfen, die Funktionen zu dokumentieren, unklare Dinge zu kommentieren und sicherzustellen, dass keine unerwarteten Exception s entstehen können, aber das ist vorher. Denken Sie daran, wenn Sie diesen Code wörtlich verwenden (und wenn Sie das tun, lassen Sie es mich wissen, ich würde gerne wissen, dass ich die Mühe nicht verschwendet habe, dies zu veröffentlichen).

+0

Gut gemacht, damit es funktioniert. Plötzlich so ausführlich, wo der Ausdruck nicht schlecht aussieht. – user2697817

+1

@ user2697817 Es ist ... ziemlich schlecht. Es ist ziemlich fragil, da es stark von der spezifischen Implementierung des Ausdrucks abhängig ist, anstatt allgemein genug zu sein, um beliebige Ausdrücke zu unterstützen. Es ist einfach nicht so wartbar oder leicht an andere Situationen anpassbar, es ist ziemlich schwer zu lesen, und es macht einfach eine Menge Dinge unnötig; Es macht viel mehr manuellen Ausdruck Manipulation, die tatsächlich benötigt wird, um dies zu erreichen. Den allgemeineren Fall zu lösen, ist tatsächlich * einfacher *. – Servy

+1

@Servy Wie ich schon sagte, ich habe es nicht aufgeräumt. Ich plane, es zu verallgemeinern, um jeden beliebigen Ausdruck zu akzeptieren, redundante Ausdrücke zu reduzieren, usw. Dies war nur ein "Prototyp", wenn Sie so wollen - es funktioniert, ohne sich Gedanken darüber zu machen, ob es schon der beste Weg war. Ich habe mich noch nicht so mucken lassen, dass sich meine Ausdrucksweise so entwickelt, also ist es klobig. Ich schätze, wenn ich die aufgeräumte Version später nicht mehr bearbeite, werde ich mehr Urteilsvermögen erleiden? : P – Yushatak

0

Sie können den erforderlichen Ausdruck immer mit den Klassenmethoden System.Linq.Expressions.Expression erstellen. Es ist jedoch nervig, knifflig und fehleranfällig.

Stattdessen können Sie eine Kompilierung Prototyp Ausdrücke verwenden, um die Parameter mit aktuellen Werten ersetzt ein kleines Hilfsprogramm verwenden ich für meine Antwort auf Entity Framework + DayOfWeek erstellt haben:

public static class ExpressionUtils 
{ 
    public static Expression<Func<TResult>> Expr<TResult>(Expression<Func<TResult>> e) => e; 
    public static Expression<Func<T, TResult>> Expr<T, TResult>(Expression<Func<T, TResult>> e) => e; 
    public static Expression<Func<T1, T2, TResult>> Expr<T1, T2, TResult>(Expression<Func<T1, T2, TResult>> e) => e; 
    public static Expression<Func<T1, T2, T3, TResult>> Expr<T1, T2, T3, TResult>(Expression<Func<T1, T2, T3, TResult>> e) => e; 
    public static Expression<Func<T1, T2, T3, T4, TResult>> Expr<T1, T2, T3, T4, TResult>(Expression<Func<T1, T2, T3, T4, TResult>> e) => e; 
    public static Expression WithParameters(this LambdaExpression expression, params Expression[] values) 
    { 
     return expression.Parameters.Zip(values, (p, v) => new { p, v }) 
      .Aggregate(expression.Body, (e, x) => e.ReplaceParameter(x.p, x.v)); 
    } 
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target) 
    { 
     return new ParameterReplacer { Source = source, Target = target }.Visit(expression); 
    } 
    class ParameterReplacer : ExpressionVisitor 
    { 
     public ParameterExpression Source; 
     public Expression Target; 
     protected override Expression VisitParameter(ParameterExpression node) 
     { 
      return node == Source ? Target : base.VisitParameter(node); 
     } 
    } 
} 

Die Idee ist einfach. Sie erstellen einen Prototyp Lambda-Ausdruck mit Parametern:

var protoExpr = ExpressionUtils.Expr((DateTime x, DateTime y) => 
    x.Year == y.Year && x.Month == y.Month && x.Day == y.Day); 

und ersetzen Sie dann die Parameter mit tatsächlichen Ausdrücken.

var actualExpr = protoExpr.WithParameters(expr1, expr2); 

Zum Beispiel könnte das Verfahren in Frage wie diese umgesetzt werden:

public static class WcfQueryableExtensions 
{ 
    public static IQueryable<T> WhereEqual<T>(this IQueryable<T> source, Expression<Func<T, DateTime>> selector, DateTime date) 
    { 
     var dateValue = ExpressionUtils.Expr(() => date).Body; 
     var predicate = Expression.Lambda<Func<T, bool>>(
      ExpressionUtils.Expr((DateTime x, DateTime y) => 
       x.Year == y.Year && x.Month == y.Month && x.Day == y.Day) 
       .WithParameters(selector.Body, dateValue), 
      selector.Parameters); 
     return source.Where(predicate); 
    } 
} 

Allerdings gibt allgemeineren Ansatz ist, die auch für die Abfragesyntax funktioniert. Sie schreiben die Abfragen mit dem natürlichen Stil LINQ to Objects (mit CLR-Methoden/properties/operators) und verwenden dann eine einzige Erweiterungsmethode, um die Abfrage in ein WCF-kompatibles Format zu konvertieren. Die Methode selbst verwendet ExpressionVistor zum Umschreiben des Abfrageausdrucks. Zum Beispiel, hier ist der Startpunkt der Umsetzung DateTime Gleichheit:

public static class WcfQueryableExtensions 
{ 
    public static IQueryable<T> AsWcfQueryable<T>(this IQueryable<T> source) 
    { 
     var expression = new WcfConverter().Visit(source.Expression); 
     if (expression == source.Expression) return source; 
     return source.Provider.CreateQuery<T>(expression); 
    } 

    class WcfConverter : ExpressionVisitor 
    { 
     protected override Expression VisitBinary(BinaryExpression node) 
     { 
      if (node.NodeType == ExpressionType.Equal && node.Left.Type == typeof(DateTime)) 
       return ExpressionUtils.Expr((DateTime x, DateTime y) => 
        x.Year == y.Year && x.Month == y.Month && x.Day == y.Day) 
        .WithParameters(Visit(node.Left), Visit(node.Right)); 
      return base.VisitBinary(node); 
     } 
    } 
} 

Sie können relativ einfach andere Umwandlungen hinzufügen, sobald Sie sie benötigen. Es könnte innerhalb der oben genannten Methode oder durch Abfangen anderer Visit Methoden wie im verknüpften Beitrag sein.

Verwendungsbeispiel:

var query = (from x in myQueryable 
      where x.DateProperty == DateToCompare 
      ... 
      select ... 
      ).AsWcfQueryable() 
Verwandte Themen