Laden...

[Gelöst] Auslesen der Parameter aus einer BinaryExpression<Func<T,bool>>

Erstellt von Killerkrümel vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.861 Views
K
Killerkrümel Themenstarter:in
166 Beiträge seit 2008
vor 5 Jahren
[Gelöst] Auslesen der Parameter aus einer BinaryExpression<Func<T,bool>>

Hallo Community,

ich scheitere grade an etwas vermutlich simplem.

Folgender Quick&DirtyCode aus dem LinqPad


public interface IFoo
{
	string Name {get;set;}
	int Number {get;set;}
}

public class Foo : IFoo
{
	public string Name {get;set;}
	public int Number {get;set;}
}

public List<Foo> FooCollection = new List<Foo>
{
	new Foo(){Name = "Name1", Number = 1},
	new Foo(){Name = "Name2", Number = 2},
	new Foo(){Name = "Name3", Number = 2}
};

void Main()
{
	string name = ReturnFoosName(x => x.Name == "Name1" && x.Number == 1);
	name.Dump();
}

public string ReturnFoosName(Expression<Func<IFoo, bool>> where)
{
	//var qq = where.??? Hier hätte ich gerne den Wert "Name" und/oder den Wert "Name1"
	//var ww = where.??? Hier hätte ich gerne den Wert "Number" und/oder den Wert "1"
	return FooCollection.Single(where.Compile()).Name;
}

In der Methode ReturnFoosName würde ich jetzt gerne aus dem 'where'
den Parameter auslesen.

Mit Expression<Func<T>> komme ich über den Body an die Daten.
e.g.


var body = (System.Linq.Expressions.MethodCallExpression)expression.Body;

Aber bei obigem "Predicate" nicht.

Frage:

  • Ist es überhaupt möglich, Parameter auszulesen? (In diesem Fall 'Name')
  • Wie komme ich an diesen Parameter? (Bitte keinen Code, sondern Hilfe zur Selbsthilfe)

Viele Grüße,
Killerkruemel

1.029 Beiträge seit 2010
vor 5 Jahren

Hi,

du musst letztendlich den ExpressionTree parsen, mit Hilfe folgender Seite hatte ich's in kürzester Zeit gelöst (musst nur ein bisl abändern, da deine Expression komplizierter als das Beispiel ist):
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/index

LG

Edit:
Bei genauerem Lesen merke ich, dass du auf der Seite wahrscheinlich schon warst.

Letztendlich holst du dir aus deinem Predicate den "Body" (BinaryExpression)
Dieser Body hat "Left" (BinaryExpression für "Name == 'Name1'") und Right (BinaryExpression für "Number == 1").

Um an den Namen zu kommen - nimmst du dir also den BinaryTree "Left", der besteht widerum aus einem Left (ParameterExpression für Name) und einem Right (ConstantExpression für "Name1").

Um an den eigentlichen Wert zu kommen nimmst du also "Right" (ConstantExpression) und kannst dann das Property "Value" nehmen, in einen string casten und bist mit dem Namen durch.

--> Predicate.Body.Left.Right.Value => "Name1" (musst ein paar Zwischencasts machen)

K
Killerkrümel Themenstarter:in
166 Beiträge seit 2008
vor 5 Jahren

Hi Taipi,

Danke für die Antwort.
Ja, auf der Seite war ich schon.

Es scheitert gerade daran, das ich Body.Left.Right nicht zu einer ConstantExpression casten kann,
da es eine FieldExpression ist.

Ich habe auch versucht, über die PropertyInfo der Member an die Information zu kommen,
allerdings habe ich keinen Typ für propertyInfo.GetValue(???)...

1.029 Beiträge seit 2010
vor 5 Jahren

Hi,

na gut - dann verstehe ich deine Probleme - ist allerdings auch kein Weltuntergang.

Demzufolge sieht dein eigener Code wahrscheinlich eher folgendermaßen aus:

public void Main()
        {
            string myName = "Name1";
            string name = ReturnFoosName(x => x.Name == myName && x.Number == 1);
            //name.Dump();
        }

Das führt dann zu einer FieldExpression, womit 2 Probleme erwachsen:
a) Wie kann man in eine FieldExpression casten
b) Wie kriegt man aus einer FieldExpression den Wert des Felds

Zu a)
FieldExpression ist (genau wie PropertyExpression) internal - hat aber den Basistyp MemberExpression, welcher widerum öffentlich zugreifbar ist. --> Casten in MemberExpression

Zu b)
Da musste ich dann auch wieder googeln, Ergebnis:
https://stackoverflow.com/questions/2616638/access-the-value-of-a-member-expression

-->


MemberExpression nameExpression = (MemberExpression)left.Right;
string name = (string)Expression.Lambda(nameExpression).Compile().DynamicInvoke();

LG

Edit:
Hinweis - das Expression.Lambda.Compile ist offenbar nicht immer des gelbe vom Ei - solltest dir den StackOverflow-Thread genau durchlesen und evaluieren was am ehesten zu deiner Anwendung passt.

K
Killerkrümel Themenstarter:in
166 Beiträge seit 2008
vor 5 Jahren

Jetzt bekomme ich den Typ MethodBinaryExpression, welcher nicht in MemberExpression gecastet werden kann.

Liegt es daran, das mein T aus Expression<Func<T,bool>> ein Interface ist?

K
Killerkrümel Themenstarter:in
166 Beiträge seit 2008
vor 5 Jahren

... das ist das komplizierteste "Ich spare mir einen Parameter" welches ich je geschrieben habe -.-*

Lösung:


var operation = (BinaryExpression)expression.Body;

BinaryExpression testCode = (BinaryExpression)operation.Left;

MemberExpression testCode_Right = (MemberExpression) testCode.Right;

string name = (string)Expression.Lambda(testCode_Right).Compile().DynamicInvoke();

Eidt sagt:
Danke Taipi!

16.834 Beiträge seit 2008
vor 5 Jahren

Ich versteh ehrlich gesagt auch nicht, wieso Du das so komplex machst (zudem langsam in der Laufzeit).

ReturnFoosName(x => x.Name == myName && x.Number == 1)

Durch den fixen Inhalt von ReturnFoosName raubst Du Dir die gesamte Flexibilität, die Du mit Linq an der Stelle hast.

Warum erwartest Du an der Methode nicht einfach die beiden Parameter?
Am Linq kannst ja nix ändern; knallt ansonsten ja Innen.

K
Killerkrümel Themenstarter:in
166 Beiträge seit 2008
vor 5 Jahren

Hallo Abt,

die Methode ist nur Exemplarisch.

Meine Repositories implementieren alle ein Interface, welches 4 Methoden vorgibt:


protected abstract IEnumerable<T> FindAll(Expression<Func<T, bool>> @where = null);
protected abstract void Update(T entity);
protected abstract bool Delete(T entity);
protected abstract T Insert(T entity);

Innerhalb des Repos ist das dann so gelöst



protected override IEnumerable<IPlanningObject> FindAll(Expression<Func<IPlanningObject, bool>> where = null)
{
//DoSth with where and its parameter
}

//Andere Klasse
protected override IEnumerable<IFooObject> FindAll(Expression<Func<IFooObject, bool>> where = null)
{
//DoSth with where and its parameter
}


da ich innerhalb FindAll mit Dapper arbeite, welches teilweise die in den where befindlichen Parameter nutzt, entstand mein Post.

16.834 Beiträge seit 2008
vor 5 Jahren

Ganz klare Empfehlung (auch von den Machern) hier: lass Dapper und Linq sein.
Es wurde von den Machern ganz bewusst auf eine entsprechende Expression Schnittstelle verzichtet; es widerspricht auch dem Dapper Gedanken.
Die Flexibilität und die Performance, die Einfachheit von Dapper begräbst Du damit quasi. Dann kannst gleich EF Core nutzen, wenn Du auf Linq nicht verzichten willst - was ok ist 😉

Aber wenn Du in Dapper einen Filter benötigst, dann erstell dafür spezifische Methoden.
That's the way to go.

1.029 Beiträge seit 2010
vor 5 Jahren

Hi,

kann ich Abt beipflichten - man kann bei Dapper vll noch hingehen und ein paar super einfache Standard-Statements providerspezifisch generieren zu lassen - ein komplett flexibles where-Statement widerspricht dem kompletten Gedanken. Wenn du das mit Linq haben möchtest - ist EF-Core schlicht die bessere Wahl.

Abgesehen davon - falls du das jetzt doch so umsetzt - möchte ich dir nochmals empfehlen den StackOverflow-Thread durchzulesen - die Methode die ich verwendet habe um den Parameterwert auszulesen ist die auf dieser Basis wohl ineffizienteste Variante für dein Szenario.

Behalte bitte im Hinterkopf, dass jedes Mal, für jeden einzelnen Parameter komplett neuer IL-Code geschrieben und kompiliert wird - die eigentliche Ausführung ist dann zwar super schnell - das hilft aber nichts, da du den kompilierten Kram eigentlich cachen müsstest - obendrauf kommt, dass die Parameter wahrscheinlich meist nur einmal gelesen werden müssten, womit du mit einem Weg auf Reflection-Basis noch besser bedient wärest...

LG

K
Killerkrümel Themenstarter:in
166 Beiträge seit 2008
vor 5 Jahren

Ich schließe mich da eurer Argumentation an und werde vermutlich auf EFCore wechseln.

Der Grundgedanke war, das ich die Repositories nicht unnötig mit Code und parameterisierten Methoden aufblähen wollte. Ich habe ja alle Parameter, welche ich für die Abfragen in Dapper brauche, direkt in den Expressions.

Aber gerade der Performanceaspekt von Taipi und der Aspekt von Abt, das Dapper dies garnicht vorsieht, lässt mich gerade massiv daran zweifeln, dass das die richtige Lösung ist.

Ich lasse den Thread trotzdem auf gelöst wenn das ok ist, jedoch mit Vorbehalt.

Vielen Dank euch beiden für eure Hilfe.

Gruß, Killerkruemel

16.834 Beiträge seit 2008
vor 5 Jahren

An für sich geht man im Design an der Stelle aber genau den anderen Weg: Linq-Fähigkeit im Parameter vermeiden.
Für ne Demo ist das okay (erwische mich dabei selbst ab und zu) - aber nicht für ein Projekt*.

Grund: durch einen Expression-Parameter erleichterst Du allen beteiligen an vielen Stelle Code zu wiederholen. Genau das will man nicht und daher ist das Repository ist genau der Platz, an dem das gebündelt werden soll (DRY/SoC).
Nur in einem Repository sollte eine Abfrage definiert werden; nicht mit Hilfe einer Expression irgendwo im Logik-Code.

Mein Rat: verzichte komplett auf den Expression Parameter an der Stelle, bau Dir ein Basis Repository für CRUD und biete spezifische Methoden direkt in den implementierenden Repositories an.
Siehe mein Beispiel in: https://github.com/BenjaminAbt/2018-Talks-ModernApiDevelopment/tree/master/src/Common/Database

*Hier habe ich auch eine Expression als Parameter; weil dies an dieser Stelle mit einer großen Ausnahme wirklich sinnvoll sein kann: OData.
Das ist dann trotzdem nicht im Sinne von Dapper, weil Dapper kein IQueryable kann und damit für Linq sinnfrei ist.

K
Killerkrümel Themenstarter:in
166 Beiträge seit 2008
vor 5 Jahren

Mein Rat: verzichte komplett auf den Expression Parameter an der Stelle, bau Dir ein Basis Repository für CRUD und biete spezifische Methoden direkt in den implementierenden Repositories an.
Siehe mein Beispiel in:
>

Das ist der Ansatz, den ich verfolgen wollte. Vielen Dank!