C # - Code zum Bestellen nach einer Eigenschaft unter Verwendung des Eigenschaftsnamens als Zeichenfolge

90

Was ist der einfachste Weg, um gegen eine Eigenschaft in C # zu codieren, wenn ich den Eigenschaftsnamen als Zeichenfolge habe? Zum Beispiel möchte ich dem Benutzer erlauben, einige Suchergebnisse nach einer Eigenschaft seiner Wahl (unter Verwendung von LINQ) zu ordnen. Sie wählen die Eigenschaft "order by" in der Benutzeroberfläche - natürlich als Zeichenfolgenwert. Gibt es eine Möglichkeit, diese Zeichenfolge direkt als Eigenschaft der linq-Abfrage zu verwenden, ohne bedingte Logik (if / else, switch) verwenden zu müssen, um die Zeichenfolgen Eigenschaften zuzuordnen. Reflexion?

Logischerweise möchte ich Folgendes tun:

query = query.OrderBy(x => x."ProductId");

Update: Ich habe ursprünglich nicht angegeben, dass ich Linq to Entities verwende - es scheint, dass Reflection (zumindest der GetProperty-, GetValue-Ansatz) nicht in L2E übersetzt wird.

Jeremy
quelle
Ich denke, Sie müssten Reflektion verwenden, und ich bin nicht sicher, ob Sie Reflektion in einem Lambda-Ausdruck verwenden können ... Nun, mit ziemlicher Sicherheit nicht in Linq to SQL, aber vielleicht, wenn Sie Linq gegen eine Liste oder etwas anderes verwenden.
CodeRedick
@Telos: Es gibt keinen Grund, warum Sie Reflection (oder eine andere API) in einem Lambda nicht verwenden können. Ob es funktioniert, wenn der Code als Ausdruck ausgewertet und in etwas anderes übersetzt wird (wie LINQ-to-SQL, wie Sie vorschlagen), ist eine ganz andere Frage.
Adam Robinson
Aus diesem Grund habe ich einen Kommentar anstelle einer Antwort gepostet. ;) Meistens an Linq2SQL gewöhnt ...
CodeRedick
1
Musste nur das gleiche Problem überwinden .. siehe meine Antwort unten. stackoverflow.com/a/21936366/775114
Mark Powell

Antworten:

123

Ich würde diese Alternative zu dem anbieten, was alle anderen gepostet haben.

System.Reflection.PropertyInfo prop = typeof(YourType).GetProperty("PropertyName");

query = query.OrderBy(x => prop.GetValue(x, null));

Dies vermeidet wiederholte Aufrufe der Reflection-API zum Abrufen der Eigenschaft. Jetzt wird nur noch der Wert wiederholt.

jedoch

Ich würde die Verwendung von a PropertyDescriptorempfehlen, da dies TypeDescriptordie Zuweisung von benutzerdefinierten s zu Ihrem Typ ermöglicht, wodurch einfache Operationen zum Abrufen von Eigenschaften und Werten möglich sind. Wenn kein benutzerdefinierter Deskriptor vorhanden ist, wird er ohnehin auf die Reflexion zurückgreifen.

PropertyDescriptor prop = TypeDescriptor.GetProperties(typeof(YourType)).Find("PropertyName");

query = query.OrderBy(x => prop.GetValue(x));

HyperDescriptorSchauen Sie sich Marc Gravels Projekt auf CodeProject an, um es zu beschleunigen. Ich habe dies mit großem Erfolg verwendet; Es ist ein Lebensretter für leistungsstarke Datenbindung und dynamische Eigenschaftsoperationen für Geschäftsobjekte.

Adam Robinson
quelle
Beachten Sie, dass der reflektierte Aufruf (dh GetValue) der teuerste Teil der Reflexion ist. Das Abrufen von Metadaten (dh GetProperty) ist tatsächlich weniger kostspielig (um eine Größenordnung). Wenn Sie also diesen Teil zwischenspeichern, sparen Sie sich nicht wirklich so viel. Dies wird in beiden Fällen ziemlich gleich viel kosten, und diese Kosten werden hoch sein. Nur etwas zu beachten.
Jrista
1
@ jrista: Aufruf ist am teuersten, um sicher zu sein. "Weniger kostspielig" bedeutet jedoch nicht "kostenlos" oder sogar in der Nähe davon. Das Abrufen von Metadaten dauert nicht trivial, daher hat das Zwischenspeichern einen Vorteil und keinen Nachteil (es sei denn, ich vermisse hier etwas). In Wahrheit sollte dies PropertyDescriptorsowieso eine sein (um benutzerdefinierte Typdeskriptoren zu berücksichtigen, die das Abrufen von Werten zu einer einfachen Operation machen könnten ).
Adam Robinson
Stundenlang nach so etwas gesucht, um das programmgesteuerte Sortieren einer ASP.NET-GridView zu handhaben: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Find (e.SortExpression, true);
Baxter
stackoverflow.com/questions/61635636/… Hatte ein Problem mit der Reflexion, das in EfCore 3.1.3 nicht funktioniert hat. Es scheint einen Fehler in EfCore 2 auszulösen, der für die Warnungen aktiviert werden muss. Verwenden Sie die Antwort von @Mark unten
Armourshield
Ich erhalte Folgendes: InvalidOperationException: Der LINQ-Ausdruck 'DbSet <MyObject> .Where (t => t.IsMasterData) .OrderBy (t => t.GetType (). GetProperty ("Address"). GetValue (obj: t, index: null) .GetType ()) 'konnte nicht übersetzt werden. Schreiben Sie die Abfrage entweder in einer Form um, die übersetzt werden kann, oder wechseln Sie explizit zur Clientbewertung, indem Sie einen Aufruf von AsEnumerable (), AsAsyncEnumerable (), ToList () oder ToListAsync () einfügen.
Bbrinck
62

Ich bin etwas spät zur Party, aber ich hoffe, das kann hilfreich sein.

Das Problem bei der Verwendung von Reflection besteht darin, dass der resultierende Ausdrucksbaum mit ziemlicher Sicherheit von keinem anderen Linq-Anbieter als dem internen .NET-Anbieter unterstützt wird. Dies ist für interne Sammlungen in Ordnung, funktioniert jedoch nicht, wenn die Sortierung vor der Paginierung an der Quelle (z. B. SQL, MongoDb usw.) erfolgen soll.

Das folgende Codebeispiel enthält IQueryable-Erweiterungsmethoden für OrderBy und OrderByDescending und kann wie folgt verwendet werden:

query = query.OrderBy("ProductId");

Verlängerungsmethode:

public static class IQueryableExtensions 
{
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderBy(ToLambda<T>(propertyName));
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderByDescending(ToLambda<T>(propertyName));
    }

    private static Expression<Func<T, object>> ToLambda<T>(string propertyName)
    {
        var parameter = Expression.Parameter(typeof(T));
        var property = Expression.Property(parameter, propertyName);
        var propAsObject = Expression.Convert(property, typeof(object));

        return Expression.Lambda<Func<T, object>>(propAsObject, parameter);            
    }
}

Grüße, Mark.

Mark Powell
quelle
Ausgezeichnete Lösung - genau das habe ich gesucht. Ich muss mich wirklich in die Ausdrucksbäume vertiefen. Immer noch sehr Anfänger. @ Mark, gibt es eine Lösung für verschachtelte Ausdrücke? Angenommen, ich habe einen Typ T mit einer Eigenschaft "Sub" vom Typ TSub, die selbst die Eigenschaft "Value" hat. Jetzt möchte ich den Ausdruck Ausdruck <Func <T, Objekt >> für die Zeichenfolge "Sub.Value" erhalten.
Simon Scheurer
4
Warum brauchen wir die Expression.Convertkonvertieren propertyzu object? Ich erhalte eine Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.Fehlermeldung und das Entfernen scheint zu funktionieren.
ShuberFu
@ Demodave, wenn ich mich richtig erinnere. var propAsObject = Expression.Convert(property, typeof(object));und nur propertyanstelle vonpropAsObject
ShuberFu
Gold. Angepasst für einen .Net Core 2.0.5.
Chris Amelinckx
2
Erhaltener FehlerLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski
34

Ich mochte die Antwort von @Mark Powell , aber wie @ShuberFu sagte, gibt es den Fehler LINQ to Entities only supports casting EDM primitive or enumeration types.

Das Entfernen var propAsObject = Expression.Convert(property, typeof(object));funktionierte nicht mit Eigenschaften, bei denen es sich um Werttypen handelte, wie z. B. Ganzzahl, da das int-Objekt nicht implizit eingerahmt wurde.

Unter Verwendung von Ideen von Kristofer Andersson und Marc Gravell habe ich einen Weg gefunden, die abfragbare Funktion unter Verwendung des Eigenschaftsnamens zu erstellen und sie weiterhin mit Entity Framework funktionieren zu lassen. Ich habe auch einen optionalen IComparer-Parameter eingefügt. Achtung: Der IComparer-Parameter funktioniert nicht mit Entity Framework und sollte bei Verwendung von Linq to Sql weggelassen werden.

Folgendes funktioniert mit Entity Framework und Linq to Sql:

query = query.OrderBy("ProductId");

Und @Simon Scheurer das funktioniert auch:

query = query.OrderBy("ProductCategory.CategoryId");

Und wenn Sie Entity Framework oder Linq to Sql nicht verwenden, funktioniert dies:

query = query.OrderBy("ProductCategory", comparer);

Hier ist der Code:

public static class IQueryableExtensions 
{    
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderBy", propertyName, comparer);
}

public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderByDescending", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenBy", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenByDescending", propertyName, comparer);
}

/// <summary>
/// Builds the Queryable functions using a TSource property name.
/// </summary>
public static IOrderedQueryable<T> CallOrderedQueryable<T>(this IQueryable<T> query, string methodName, string propertyName,
        IComparer<object> comparer = null)
{
    var param = Expression.Parameter(typeof(T), "x");

    var body = propertyName.Split('.').Aggregate<string, Expression>(param, Expression.PropertyOrField);

    return comparer != null
        ? (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param),
                Expression.Constant(comparer)
            )
        )
        : (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param)
            )
        );
}
}
David Specht
quelle
Meine Güte, Mann, sind Sie Microsoft? :) Das AggregateFragment ist großartig! Es kümmert sich um virtuelle Ansichten, die aus dem EF Core-Modell mit erstellt wurden Join, da ich Eigenschaften wie "T.Property" verwende. Andernfalls Joinwäre eine Bestellung nach InvalidOperationExceptionoder entweder nicht möglich NullReferenceException. Und ich muss NACH bestellen Join, da die meisten Abfragen konstant sind, die Bestellungen in Ansichten nicht.
Harry
@ Harry. Danke, aber ich kann das AggregateFragment wirklich nicht genug würdigen. Ich glaube, es war eine Kombination aus Marc Gravells Code und einer Intellisense-Empfehlung. :)
David Specht
@DavidSpecht Ich lerne gerade Ausdrucksbäume, also ist alles an ihnen jetzt noch schwarze Magie für mich. Aber ich lerne schnell, das interaktive C # -Fenster in VS hilft sehr.
Harry
Wie benutzt man das?
Dat Nguyen
@Dat Nguyen Stattdessen products.OrderBy(x => x.ProductId)könnten Sie verwendenproducts.OrderBy("ProductId")
David Specht
12

Ja, ich glaube nicht, dass es einen anderen Weg als Reflection gibt.

Beispiel:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));
Alon Gubkin
quelle
5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

Ich habe versucht, die genaue Syntax auf den Kopf zu stellen, aber ich denke, das ist richtig.

dkackman
quelle
2

Reflexion ist die Antwort!

typeof(YourType).GetProperty("ProductId").GetValue(theInstance);

Es gibt viele Dinge, die Sie tun können, um die reflektierten PropertyInfo zwischenzuspeichern, nach fehlerhaften Zeichenfolgen zu suchen, Ihre Abfragevergleichsfunktion zu schreiben usw., aber im Kern tun Sie dies.

Sebastian Gut
quelle
2

Sie können dynamisches Linq verwenden - lesen Sie diesen Blog.

Schauen Sie sich auch diesen StackOverFlow-Beitrag an ...

Partha Choudhury
quelle
Dies ist die beste Antwort für mich
Demodave
2

Produktiver als Reflexionserweiterung auf dynamische Auftragspositionen:

public static class DynamicExtentions
{
    public static object GetPropertyDynamic<Tobj>(this Tobj self, string propertyName) where Tobj : class
    {
        var param = Expression.Parameter(typeof(Tobj), "value");
        var getter = Expression.Property(param, propertyName);
        var boxer = Expression.TypeAs(getter, typeof(object));
        var getPropValue = Expression.Lambda<Func<Tobj, object>>(boxer, param).Compile();            
        return getPropValue(self);
    }
}

Beispiel:

var ordered = items.OrderBy(x => x.GetPropertyDynamic("ProductId"));

Möglicherweise müssen Sie auch konforme Lambas zwischenspeichern (z. B. im Wörterbuch <>).

gdbdable
quelle
1

Auch dynamische Ausdrücke können dieses Problem lösen. Sie können stringbasierte Abfragen über LINQ-Ausdrücke verwenden, die zur Laufzeit möglicherweise dynamisch erstellt wurden.

var query = query
          .Where("Category.CategoryName == @0 and Orders.Count >= @1", "Book", 10)
          .OrderBy("ProductId")
          .Select("new(ProductName as Name, Price)");
ali-myousefi
quelle
0

Ich denke, wir können einen leistungsstarken Werkzeugnamen Ausdruck verwenden und in diesem Fall wie folgt als Erweiterungsmethode verwenden:

public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string ordering, bool descending)
{
    var type = typeof(T);
    var property = type.GetProperty(ordering);
    var parameter = Expression.Parameter(type, "p");
    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
    var orderByExp = Expression.Lambda(propertyAccess, parameter);
    MethodCallExpression resultExp = 
        Expression.Call(typeof(Queryable), (descending ? "OrderByDescending" : "OrderBy"), 
            new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));
    return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(resultExp);
}
Abolfazl
quelle