Angenommen, ich habe die folgenden Variablen definiert:
IQueryable<MyClass> myQueryable;
Dictionary<string, Expression<Func<MyClass, bool>>> extraFields;
// the dictionary is keyed by a field name
Jetzt möchte ich einige dynamische Felder an die IQueryable anheften, damit sie eine IQueryable<ExtendedMyClass>
, wobei ExtendedMyClass
folgt definiert ist:
class ExtendedMyClass
{
public MyClass MyObject {get; set;}
public IEnumerable<StringAndBool> ExtraFieldValues {get; set;}
}
class StringAndBool
{
public string FieldName {get; set;}
public bool IsTrue {get; set;}
}
Mit anderen Worten, ich möchte für jeden Wert in extraFields
einen Wert in ExtendedMyClass.ExtraFieldValues
haben, der extraFields
, ob dieser Ausdruck für diese Zeile True ergibt oder nicht.
Ich habe das Gefühl, dass dies in dynamischem Linq und LinqKit machbar sein sollte, obwohl ich das noch nie ernsthaft benutzt habe. Ich bin auch offen für andere Vorschläge, insbesondere wenn dies in einem guten alten, stark typisierten Linq möglich ist.
Ich verwende Linq to Entities, daher muss die Abfrage in SQL übersetzt werden.
Wir werden hier also viele Schritte haben, aber jeder einzelne Schritt sollte ziemlich kurz, in sich geschlossen, wiederverwendbar und relativ verständlich sein.
Als erstes erstellen wir eine Methode, mit der Ausdrücke kombiniert werden können. Es wird ein Ausdruck verwendet, der eine Eingabe akzeptiert und einen Zwischenwert generiert. Dann wird ein zweiter Ausdruck benötigt, der als Eingabe dieselbe Eingabe wie der erste, den Typ des Zwischenergebnisses, akzeptiert und dann ein neues Ergebnis berechnet. Es wird ein neuer Ausdruck zurückgegeben, der die Eingabe des ersten und die Ausgabe des zweiten übernimmt.
public static Expression<Func<TFirstParam, TResult>>
Combine<TFirstParam, TIntermediate, TResult>(
this Expression<Func<TFirstParam, TIntermediate>> first,
Expression<Func<TFirstParam, TIntermediate, TResult>> second)
{
var param = Expression.Parameter(typeof(TFirstParam), "param");
var newFirst = first.Body.Replace(first.Parameters[0], param);
var newSecond = second.Body.Replace(second.Parameters[0], param)
.Replace(second.Parameters[1], newFirst);
return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}
Dazu ersetzen wir einfach alle Instanzen des zweiten Parameters im Hauptteil des zweiten Ausdrucks durch den Hauptteil des ersten Ausdrucks. Wir müssen auch sicherstellen, dass beide Implementierungen dieselbe Parameterinstanz für den Hauptparameter verwenden.
Diese Implementierung erfordert eine Methode, um alle Instanzen eines Ausdrucks durch einen anderen zu ersetzen:
internal 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);
}
Als nächstes schreiben wir eine Methode, die eine Folge von Ausdrücken akzeptiert, die dieselbe Eingabe akzeptieren und denselben Ausgabetyp berechnen. Dies wird in einen einzelnen Ausdruck umgewandelt, der dieselbe Eingabe akzeptiert, als Ergebnis jedoch eine Sequenz der Ausgabe berechnet, in der jedes Element in der Sequenz das Ergebnis jedes Eingabeausdrucks darstellt.
Diese Implementierung ist ziemlich einfach; Wir erstellen ein neues Array und verwenden den Hauptteil jedes Ausdrucks (wobei die Parameter durch einen konsistenten ersetzt werden) als jedes Element im Array.
public static Expression<Func<T, IEnumerable<TResult>>> AsSequence<T, TResult>(
this IEnumerable<Expression<Func<T, TResult>>> expressions)
{
var param = Expression.Parameter(typeof(T));
var body = Expression.NewArrayInit(typeof(TResult),
expressions.Select(selector =>
selector.Body.Replace(selector.Parameters[0], param)));
return Expression.Lambda<Func<T, IEnumerable<TResult>>>(body, param);
}
Nachdem wir alle diese Allzweck-Hilfsmethoden aus dem Weg geräumt haben, können wir mit der Arbeit an Ihrer spezifischen Situation beginnen.
Der erste Schritt besteht darin, Ihr Wörterbuch in eine Folge von Ausdrücken StringAndBool
, die jeweils eine MyClass
akzeptieren und einen StringAndBool
, der dieses Paar darstellt. Zu diesem Zweck verwenden wir " Combine
für den Wert des Wörterbuchs und verwenden dann ein Lambda als zweiten Ausdruck, um das Zwischenergebnis zum Berechnen eines StringAndBool
Objekts zu verwenden und den Schlüssel des Paares zu schließen.
IEnumerable<Expression<Func<MyClass, StringAndBool>>> stringAndBools =
extraFields.Select(pair => pair.Value.Combine((foo, isTrue) =>
new StringAndBool()
{
FieldName = pair.Key,
IsTrue = isTrue
}));
Jetzt können wir unsere AsSequence
Methode verwenden, um dies von einer Folge von Selektoren in einen einzelnen Selektor umzuwandeln, der eine Folge auswählt:
Expression<Func<MyClass, IEnumerable<StringAndBool>>> extrafieldsSelector =
stringAndBools.AsSequence();
Jetzt sind wir fast fertig. Wir müssen jetzt nur noch Combine
für diesen Ausdruck verwenden, um unser Lambda für die Auswahl einer MyClass
in eine ExtendedMyClass
zu schreiben, während wir den zuvor generierten Selektor für die Auswahl der zusätzlichen Felder verwenden:
var finalQuery = myQueryable.Select(
extrafieldsSelector.Combine((foo, extraFieldValues) =>
new ExtendedMyClass
{
MyObject = foo,
ExtraFieldValues = extraFieldValues,
}));
Wir können denselben Code verwenden, die Zwischenvariable entfernen und uns auf die Typinferenz verlassen, um sie auf eine einzelne Anweisung zu reduzieren, vorausgesetzt, Sie finden sie nicht zu unhöflich:
var finalQuery = myQueryable.Select(extraFields
.Select(pair => pair.Value.Combine((foo, isTrue) =>
new StringAndBool()
{
FieldName = pair.Key,
IsTrue = isTrue
}))
.AsSequence()
.Combine((foo, extraFieldValues) =>
new ExtendedMyClass
{
MyObject = foo,
ExtraFieldValues = extraFieldValues,
}));
Es ist erwähnenswert, dass ein wesentlicher Vorteil dieses allgemeinen Ansatzes darin besteht, dass die Verwendung der übergeordneten Expression
zu Code führt, der zumindest einigermaßen verständlich ist, aber auch zur Kompilierungszeit statisch überprüft werden kann, um typsicher zu sein . Es gibt hier eine Handvoll allgemein verwendbarer, wiederverwendbarer, überprüfbarer, überprüfbarer Erweiterungsmethoden, die es uns nach dem Schreiben ermöglichen, das Problem nur durch die Zusammensetzung von Methoden und Lambdas zu lösen, und die keine tatsächliche Manipulation des Ausdrucks erfordern, was beides ist komplex, fehleranfällig und entfernt alle Arten von Sicherheit. Jede dieser Erweiterungsmethoden ist so konzipiert, dass der resultierende Ausdruck immer gültig ist, solange die Eingabeausdrücke gültig sind und die Eingabeausdrücke hier alle als gültig gelten, da es sich um Lambda-Ausdrücke handelt, die der Compiler überprüft für die Typensicherheit.
Ich denke, es ist hier hilfreich, ein Beispiel für extraFields
zu nehmen, sich vorzustellen, wie der Ausdruck aussehen würde, den Sie benötigen, und dann herauszufinden, wie er tatsächlich erstellt wird.
Also, wenn Sie haben:
var extraFields = new Dictionary<string, Expression<Func<MyClass, bool>>>
{
{ "Foo", x => x.Foo },
{ "Bar", x => x.Bar }
};
Dann möchten Sie etwas generieren wie:
myQueryable.Select(
x => new ExtendedMyClass
{
MyObject = x,
ExtraFieldValues =
new[]
{
new StringAndBool { FieldName = "Foo", IsTrue = x.Foo },
new StringAndBool { FieldName = "Bar", IsTrue = x.Bar }
}
});
Jetzt können Sie die Ausdrucksbaum-API und LINQKit verwenden, um diesen Ausdruck zu erstellen:
public static IQueryable<ExtendedMyClass> Extend(
IQueryable<MyClass> myQueryable,
Dictionary<string, Expression<Func<MyClass, bool>>> extraFields)
{
Func<Expression<Func<MyClass, bool>>, MyClass, bool> invoke =
LinqKit.Extensions.Invoke;
var parameter = Expression.Parameter(typeof(MyClass));
var extraFieldsExpression =
Expression.Lambda<Func<MyClass, StringAndBool[]>>(
Expression.NewArrayInit(
typeof(StringAndBool),
extraFields.Select(
field => Expression.MemberInit(
Expression.New(typeof(StringAndBool)),
new MemberBinding[]
{
Expression.Bind(
typeof(StringAndBool).GetProperty("FieldName"),
Expression.Constant(field.Key)),
Expression.Bind(
typeof(StringAndBool).GetProperty("IsTrue"),
Expression.Call(
invoke.Method,
Expression.Constant(field.Value),
parameter))
}))),
parameter);
Expression<Func<MyClass, ExtendedMyClass>> selectExpression =
x => new ExtendedMyClass
{
MyObject = x,
ExtraFieldValues = extraFieldsExpression.Invoke(x)
};
return myQueryable.Select(selectExpression.Expand());
}