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.


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.


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.


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

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

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.


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.



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


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

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);
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.