C # - kod do zamówienia według właściwości przy użyciu nazwy właściwości jako ciągu

92

Jaki jest najprostszy sposób kodowania właściwości w języku C #, gdy mam nazwę właściwości jako ciąg? Na przykład chcę zezwolić użytkownikowi na uporządkowanie niektórych wyników wyszukiwania według wybranej właściwości (za pomocą LINQ). W interfejsie użytkownika wybiorą właściwość „order by” - oczywiście jako ciąg znaków. Czy istnieje sposób na użycie tego ciągu bezpośrednio jako właściwości zapytania linq, bez konieczności używania logiki warunkowej (if / else, switch) do mapowania ciągów na właściwości? Odbicie?

Logicznie rzecz biorąc, chciałbym to zrobić:

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

Aktualizacja: początkowo nie określiłem, że używam Linq do Entities - wydaje się, że odbicie (przynajmniej podejście GetProperty, GetValue) nie przekłada się na L2E.

Jeremy
źródło
Myślę, że musiałbyś użyć odbicia i nie jestem pewien, czy możesz użyć odbicia w wyrażeniu lambda ... cóż, prawie na pewno nie w Linq do SQL, ale może wtedy, gdy używasz Linq przeciwko liście lub coś w tym stylu.
CodeRedick
@Telos: Nie ma powodu, dla którego nie możesz używać odbicia (ani żadnego innego interfejsu API) w lambdzie. To, czy zadziała, jeśli kod zostanie oceniony jako wyrażenie i przetłumaczone na coś innego (jak LINQ-to-SQL, jak sugerujesz), to zupełnie inna kwestia.
Adam Robinson
Dlatego zamiast odpowiedzi zamieściłem komentarz. ;) Głównie używany do Linq2SQL ...
CodeRedick
1
Po prostu musiałem pokonać ten sam problem ... zobacz moją odpowiedź poniżej. stackoverflow.com/a/21936366/775114
Mark Powell

Odpowiedzi:

129

Oferowałbym tę alternatywę dla tego, co opublikowali wszyscy inni.

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

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

Pozwala to uniknąć powtarzających się wywołań interfejsu API odbicia w celu uzyskania właściwości. Teraz jedynym powtarzanym wezwaniem jest uzyskanie wartości.

jednak

Zalecałbym użycie PropertyDescriptorzamiast tego, ponieważ pozwoli TypeDescriptorto na przypisanie niestandardowych s do twojego typu, umożliwiając lekkie operacje pobierania właściwości i wartości. W przypadku braku niestandardowego deskryptora i tak powróci do refleksji.

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

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

Jeśli chodzi o przyspieszenie, sprawdź HyperDescriptorprojekt Marca Gravela w CodeProject. Użyłem tego z wielkim sukcesem; to ratunek dla wysokowydajnych powiązań danych i dynamicznych operacji właściwości na obiektach biznesowych.

Adam Robinson
źródło
Zauważ, że odbite wywołanie (np. GetValue) jest najbardziej kosztowną częścią refleksji. Pobieranie metadanych (tj. GetProperty) jest w rzeczywistości mniej kosztowne (o rząd wielkości), więc buforując tę ​​część, nie oszczędzasz tak dużo. To będzie kosztować mniej więcej tyle samo, a ten koszt będzie duży. Coś do zapamiętania.
jrista
1
@jrista: inwokacja jest z pewnością najbardziej kosztowna. Jednak „mniej kosztowne” nie oznacza „darmowe”, ani nawet bliskie temu. Pobieranie metadanych zajmuje niebanalną ilość czasu, więc buforowanie ich jest zaletą i nie ma wady (chyba że czegoś tu brakuje). Prawdę mówiąc, powinno to być tak naprawdę użycie a PropertyDescriptor(w celu uwzględnienia niestandardowych deskryptorów, które mogą sprawić, że pobieranie wartości będzie lekką operacją).
Adam Robinson
Szukano godzin w celu znalezienia czegoś podobnego do programowego sortowania widoku ASP.NET GridView: PropertyDescriptor prop = TypeDescriptor.GetProperties (typeof (ScholarshipRequest)). Find (e.SortExpression, true);
Baxter
1
stackoverflow.com/questions/61635636/… Wystąpił problem z odbiciem, który nie wyszedł w EfCore 3.1.3. Wygląda na to, że wyświetla błąd w EfCore 2, który należy aktywować, aby wyświetlić ostrzeżenia. Skorzystaj z odpowiedzi @Mark poniżej
armourshield
1
Otrzymuję następujące informacje: InvalidOperationException: Wyrażenie LINQ 'DbSet <MyObject> .Where (t => t.IsMasterData) .OrderBy (t => t.GetType (). GetProperty ("Address"). GetValue (obj: t, index: null) .GetType ()) 'nie może zostać przetłumaczone. Albo przepisz zapytanie w formularzu, który można przetłumaczyć, albo przełącz się do oceny klienta jawnie, wstawiając wywołanie AsEnumerable (), AsAsyncEnumerable (), ToList () lub ToListAsync ().
bbrinck
67

Trochę się spóźniłem na przyjęcie, mam jednak nadzieję, że to może być pomocne.

Problem z używaniem odbicia polega na tym, że wynikowe drzewo wyrażeń prawie na pewno nie będzie obsługiwane przez innych dostawców Linq niż wewnętrzny dostawca .Net. Jest to dobre w przypadku kolekcji wewnętrznych, jednak nie zadziała, jeśli sortowanie ma być wykonywane u źródła (np. SQL, MongoDb itp.) Przed paginacją.

Poniższy przykład kodu zawiera metody rozszerzenia IQueryable dla OrderBy i OrderByDescending i może być używany w następujący sposób:

query = query.OrderBy("ProductId");

Metoda rozszerzenia:

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

Pozdrawiam, Mark.

Mark Powell
źródło
Doskonałe rozwiązanie - właśnie tego szukałem. Naprawdę muszę zagłębić się w drzewach Expression. Wciąż bardzo debiutant. @Mark, jakieś rozwiązanie do wykonywania zagnieżdżonych wyrażeń? Powiedzmy, że mam typ T z właściwością „Sub” typu TSub, która sama ma właściwość „Wartość”. Teraz chciałbym uzyskać wyrażenie Expression <Func <T, object >> dla ciągu znaków „Sub.Value”.
Simon Scheurer
4
Dlaczego potrzebujemy Expression.Convertkonwersji propertyna object? Otrzymuję Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.błąd i jego usunięcie wydaje się działać.
ShuberFu
@Demodave, jeśli dobrze pamiętam. var propAsObject = Expression.Convert(property, typeof(object));i po prostu użyj propertyzamiastpropAsObject
ShuberFu
Złoto. Dostosowany do .Net Core 2.0.5.
Chris Amelinckx
2
Mam błądLINQ to Entities only supports casting EDM primitive or enumeration types
Mateusz Puwałowski
35

Podobała mi się odpowiedź od @Mark Powell , ale jak powiedział @ShuberFu , podaje błąd LINQ to Entities only supports casting EDM primitive or enumeration types.

Usuwanie var propAsObject = Expression.Convert(property, typeof(object));nie działało z właściwościami, które były typami wartości, takimi jak liczba całkowita, ponieważ nie spowoduje to niejawnego zapakowania int do obiektu.

Korzystając z Pomysłów Kristofera Anderssona i Marca Gravella , znalazłem sposób na skonstruowanie funkcji Queryable przy użyciu nazwy właściwości i sprawienie, by nadal działała z Entity Framework. Dołączyłem również opcjonalny parametr IComparer. Przestroga: parametr IComparer nie działa z Entity Framework i należy go pominąć, jeśli używasz Linq do Sql.

Poniższe działa z Entity Framework i Linq to Sql:

query = query.OrderBy("ProductId");

I @Simon Scheurer to również działa:

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

A jeśli nie używasz Entity Framework lub Linq to Sql, działa to:

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

Oto kod:

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
źródło
Jezu, człowieku, jesteś Microsoft? :) Ten Aggregatefragment jest niesamowity! JoinDba o wirtualne widoki utworzone z modelu EF Core , ponieważ używam właściwości takich jak „T.Property”. W przeciwnym razie zamówienie po Joinbyłoby niemożliwe, aby wyprodukować albo InvalidOperationExceptionalbo NullReferenceException. I muszę zamawiać PO Join, ponieważ większość zapytań jest stała, a zamówienia w widokach nie.
Harry
@Złupić. Dzięki, ale naprawdę nie mogę przypisać sobie zbyt wiele uznania za ten Aggregatefragment. Myślę, że było to połączenie kodu Marca Gravella i zalecenia Intellisense. :)
David Specht
@DavidSpecht Po prostu uczę się drzew wyrażeń, więc wszystko w nich jest teraz dla mnie czarną magią. Ale szybko się uczę, interaktywne okno C # w VS bardzo pomaga.
Harry
jak tego używać?
Dat Nguyen
@Dat Nguyen Zamiast tego products.OrderBy(x => x.ProductId)możesz użyćproducts.OrderBy("ProductId")
David Specht
12

Tak, myślę, że nie ma innego sposobu niż Reflection.

Przykład:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));
Alon Gubkin
źródło
Otrzymuję błąd "LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."Jakieś przemyślenia lub rady, proszę?
Florin Vîrdol
5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

Próbuję przypomnieć sobie dokładną składnię z całej mojej głowy, ale myślę, że to prawda.

dkackman
źródło
2

Refleksja jest odpowiedzią!

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

Istnieje wiele rzeczy, które możesz zrobić, aby buforować odzwierciedlone PropertyInfo, sprawdzić złe ciągi, napisać funkcję porównywania zapytań itp., Ale w gruncie rzeczy to właśnie robisz.

Sebastian Good
źródło
2

Możesz użyć dynamicznego Linq - sprawdź ten blog.

Sprawdź również ten post StackOverFlow ...

Partha Choudhury
źródło
To najlepsza odpowiedź dla mnie
Demodave
2

Bardziej produktywne niż rozszerzenie refleksji na dynamiczne elementy zamówienia:

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

Przykład:

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

Być może będziesz musiał buforować zgodne lambka (np. W Słowniku <>)

gdbdable
źródło
1

Również wyrażenia dynamiczne mogą rozwiązać ten problem. Możesz używać zapytań opartych na ciągach za pośrednictwem wyrażeń LINQ, które mogły zostać dynamicznie utworzone w czasie wykonywania.

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

Myślę, że możemy użyć potężnego narzędzia o nazwie Expression iw tym przypadku użyć go jako metody rozszerzenia w następujący sposób:

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
źródło