Comment spécifier l'argument Linq OrderBy de manière dynamique?

94

Comment spécifier l'argument transmis à l' orderbyaide d'une valeur que je prends comme paramètre?

Ex:

List<Student> existingStudends = new List<Student>{ new Student {...}, new Student {...}}

Actuellement mise en œuvre:

List<Student> orderbyAddress = existingStudends.OrderBy(c => c.Address).ToList();

Au lieu de c.Address, comment puis-je prendre cela comme paramètre?

Exemple

 string param = "City";
 List<Student> orderbyAddress = existingStudends.OrderByDescending(c => param).ToList();
Sreedhar
la source
4
Vous recherchez peut-être Dynamic Linq: weblogs.asp.net/scottgu/archive/2008/01/07/…
BrokenGlass
@Nev_Rahd: J'ai essayé de clarifier un peu la question. En outre, OrderByest une fonctionnalité Linq, et est activée IEnumerable, pas une fonctionnalité spécifique à List. N'hésitez pas à annuler la modification ou à la modifier davantage :)
Merlyn Morgan-Graham
Copie
Michael Freidgeim

Réponses:

129

Voici une possibilité d'utiliser la réflexion ...

var param = "Address";    
var propertyInfo = typeof(Student).GetProperty(param);    
var orderByAddress = items.OrderBy(x => propertyInfo.GetValue(x, null));
codeConcussion
la source
3
Mais est-ce vrai en ce qui concerne les expressions Linq interprétées par des fournisseurs, comme Entity Framework (serveur SQL, ou autre) ??
a.boussema
2
@vijay - utilisez la ThenByméthode .
codeConcussion
7
Lorsque j'essaie cela, j'obtiens l'erreur: LINQ to Entities ne reconnaît pas la méthode 'System.Object GetValue (System.Object, System.Object [])', et cette méthode ne peut pas être traduite en une expression de magasin. Cette réponse s'applique-t-elle uniquement à Linq To SQL?
philreed
4
Aucune erreur avec .AsEnumerable (): var orderByAddress = items.AsEnumerable (). OrderBy (x => propertyInfo.GetValue (x, null));
César
1
Comment puis-je décider dynamiquement de commander par asc ou desc
Hitesh Modha
123

Vous pouvez utiliser un peu de réflexion pour construire l'arbre d'expression comme suit (il s'agit d'une méthode d'extension):

public static IQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string orderByProperty,
                          bool desc) 
{
     string command = desc ? "OrderByDescending" : "OrderBy";
     var type = typeof(TEntity);
     var property = type.GetProperty(orderByProperty);
     var parameter = Expression.Parameter(type, "p");
     var propertyAccess = Expression.MakeMemberAccess(parameter, property);
     var orderByExpression = Expression.Lambda(propertyAccess, parameter);
     var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { type, property.PropertyType },
                                   source.Expression, Expression.Quote(orderByExpression));
     return source.Provider.CreateQuery<TEntity>(resultExpression);
}

orderByPropertyest le nom de la propriété que vous souhaitez descclasser et s'il est passé à true comme paramètre pour , le tri sera par ordre décroissant; sinon, triera par ordre croissant.

Vous devriez maintenant pouvoir faire existingStudents.OrderBy("City",true);ouexistingStudents.OrderBy("City",false);

Icare
la source
10
Cette réponse est géniale et bien meilleure que la réponse de réflexion. Cela fonctionne en fait avec d'autres fournisseurs comme le framework d'entité.
Sam
2
Je voterais dix fois plus si je pouvais !!! Où apprenez-vous à écrire une méthode d'extension comme celle-ci ?? !!
Jach
3
Cela devrait-il renvoyer un IOrderedQueryable, tout comme le OrderBy intégré? De cette façon, vous pourriez appeler .ThenBy dessus.
Patrick Szalapski
4
Cela semble ne plus fonctionner lors de l'utilisation d'EFCore 3.0, j'obtiens une erreur d'exécution où il ne peut pas traduire la requête.
Mildan
3
Oui, @Mildan, cela casse en 3.0 et 3.1 pour moi aussi. avec l'erreur ~ "cant translate". J'utilise Pomelo pour MySQl si cela est pertinent. Le problème est l'expression. Si vous codez l'expression, cela fonctionne. Donc, au lieu de Lambda.Expression (), fournissez simplement quelque chose comme: LambdaExpression orderByExp1 = (Expression <Func <AgencySystemBiz, string >>) (x => x.Name);
Menace le
10

Pour développer la réponse de @Icarus : si vous voulez que le type de retour de la méthode d'extension soit un IOrderedQueryable au lieu d'un IQueryable, vous pouvez simplement convertir le résultat comme suit:

public static IOrderedQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string orderByProperty, bool desc)
{
    string command = desc ? "OrderByDescending" : "OrderBy";
    var type = typeof(TEntity);
    var property = type.GetProperty(orderByProperty);
    var parameter = Expression.Parameter(type, "p");
    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
    var orderByExpression = Expression.Lambda(propertyAccess, parameter);
    var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { type, property.PropertyType },
        source.Expression, Expression.Quote(orderByExpression));
    return (IOrderedQueryable<TEntity>)source.Provider.CreateQuery<TEntity>(resultExpression);
}
Ciaran
la source
2
Il semble que d'autres réponses n'étaient pas appropriées pour Entity Framework. C'est une solution parfaite pour EF car Linq to Entities ne prend pas en charge GetProperty, GetValue
Bill
1
Cette méthode semble échouer pour moi en 3.0 et 3.1 (elle a fonctionné en 2.2). J'utilise Pomelo pour MySql donc cela pourrait être pertinent. Il y a un travail mais c'est moche. Voir mon commentaire ci-dessus.
Menace le
Cela a fonctionné pour moi dans EF 3.0. Cependant, vous devez modifier la ligne suivante afin que le frontal n'ait pas besoin de respecter la casse: var property = type.GetProperty (OrderByProperty, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
King Arthur the Third le
Est-ce toujours optimisé pour Core 3.1?
Chris Go
8

1) Installez System.Linq.Dynamic

2) Ajoutez le code suivant

public static class OrderUtils
{
    public static string ToStringForOrdering<T, TKey>(this Expression<Func<T, TKey>> expression, bool isDesc = false)
    {
        var str = expression.Body.ToString();
        var param = expression.Parameters.First().Name;
        str = str.Replace("Convert(", "(").Replace(param + ".", "");
        return str + (isDesc ? " descending" : "");
    }
}

3) Écrivez votre commutateur pour sélectionner la fonction Lambda

public static class SortHelper
{
    public static Expression<Func<UserApp, object>> UserApp(string orderProperty)
    {
        orderProperty = orderProperty?.ToLowerInvariant();
        switch (orderProperty)
        {
            case "firstname":
                return x => x.PersonalInfo.FirstName;
            case "lastname":
                return x => x.PersonalInfo.LastName;
            case "fullname":
                return x => x.PersonalInfo.FirstName + x.PersonalInfo.LastName;
            case "email":
                return x => x.Email;

        }
    }
}

4) Utilisez vos aides

Dbset.OrderBy(SortHelper.UserApp("firstname").ToStringForOrdering())

5) Vous pouvez l'utiliser avec la pagination ( PagedList )

public virtual  IPagedList<T> GetPage<TOrder>(Page page, Expression<Func<T, bool>> where, Expression<Func<T, TOrder>> order, bool isDesc = false,
      params Expression<Func<T, object>>[] includes)
    {
        var orderedQueryable = Dbset.OrderBy(order.ToStringForOrdering(isDesc));
        var query = orderedQueryable.Where(where).GetPage(page);
        query = AppendIncludes(query, includes);

        var results = query.ToList();
        var total =  Dbset.Count(where);

        return new StaticPagedList<T>(results, page.PageNumber, page.PageSize, total);
    }

Explication

System.Linq.Dynamic nous permet de définir une valeur de chaîne dans la méthode OrderBy. Mais à l'intérieur de cette extension, la chaîne sera analysée en Lambda. J'ai donc pensé que cela fonctionnerait si nous analysions Lambda en chaîne et la donnions à la méthode OrderBy. Et il fonctionne!

Igor Valikovsky
la source
6
   private Func<T, object> GetOrderByExpression<T>(string sortColumn)
    {
        Func<T, object> orderByExpr = null;
        if (!String.IsNullOrEmpty(sortColumn))
        {
            Type sponsorResultType = typeof(T);

            if (sponsorResultType.GetProperties().Any(prop => prop.Name == sortColumn))
            {
                System.Reflection.PropertyInfo pinfo = sponsorResultType.GetProperty(sortColumn);
                orderByExpr = (data => pinfo.GetValue(data, null));
            }
        }
        return orderByExpr;
    }

    public List<T> OrderByDir<T>(IEnumerable<T> source, string dir, Func<T, object> OrderByColumn)
    {
        return dir.ToUpper() == "ASC" ? source.OrderBy(OrderByColumn).ToList() : source.OrderByDescending(OrderByColumn).ToList();``
    }

 // Call the code like below
        var orderByExpression= GetOrderByExpression<SearchResultsType>(sort);

    var data = OrderByDir<SponsorSearchResults>(resultRecords, SortDirectionString, orderByExpression);    
Rakesh Suryawanshi
la source
Brillant! Exactement ce dont j'avais besoin.
Brandon Griffin
5

Voici quelque chose que j'ai proposé pour traiter une descente conditionnelle. Vous pouvez combiner cela avec d'autres méthodes de génération keySelectordynamique de la fonction.

    public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source,
            System.Linq.Expressions.Expression<Func<TSource, TKey>> keySelector,
            System.ComponentModel.ListSortDirection sortOrder
            )
    {
        if (sortOrder == System.ComponentModel.ListSortDirection.Ascending)
            return source.OrderBy(keySelector);
        else
            return source.OrderByDescending(keySelector);
    }

Usage:

//imagine this is some parameter
var direction = System.ComponentModel.ListSortDirection.Ascending;
query = query.OrderBy(ec => ec.MyColumnName, direction);

Notez que cela vous permet d'enchaîner cette .OrderByextension avec un nouveau paramètre sur n'importe quel IQueryable.

// perhaps passed in as a request of user to change sort order
// var direction = System.ComponentModel.ListSortDirection.Ascending;
query = context.Orders
        .Where(o => o.Status == OrderStatus.Paid)
        .OrderBy(ec => ec.OrderPaidUtc, direction);
AaronLS
la source
3

Cela ne vous permet pas de passer un string, comme vous l'avez demandé dans votre question, mais cela peut toujours fonctionner pour vous.

La OrderByDescendingméthode prend un Func<TSource, TKey>, donc vous pouvez réécrire votre fonction de cette façon:

List<Student> QueryStudents<TKey>(Func<Student, TKey> orderBy)
{
    return existingStudents.OrderByDescending(orderBy).ToList();
}

Il existe également d'autres surcharges OrderByDescendingqui prennent a Expression<Func<TSource, TKey>>, et / ou a IComparer<TKey>. Vous pouvez également les examiner et voir si elles vous fournissent quelque chose d'utile.

Merlyn Morgan-Graham
la source
Cela ne fonctionne pas car vous ne définissez pas le type de TKey. Vous devez changer votre <T> pour avoir <TKey> à la place.
Patrick Desjardins
C'était exactement ce qui fonctionnait pour moi! Je voulais une fonction qui ordonnerait une liste ascendante ou décroissante, en fonction d'une valeur booléenne passée. Votre code a très bien fonctionné avec quelques ajustements!
Joe Gayetty
LINQ en action: IEnumerable <Book> CustomSort <TKey> (Func <Book, TKey> selector, Boolean ascending) {IEnumerable <Book> books = SampleData.Books; retour ascendant? books.OrderBy (sélecteur): books.OrderByDescending (sélecteur); }
Leszek P
1

La seule solution qui a fonctionné pour moi a été publiée ici https://gist.github.com/neoGeneva/1878868 par neoGeneva.

Je republierai son code car il fonctionne bien et je ne voudrais pas qu'il soit perdu dans les interwebs!

    public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string sortExpression)
    {
        if (source == null)
            throw new ArgumentNullException("source", "source is null.");

        if (string.IsNullOrEmpty(sortExpression))
            throw new ArgumentException("sortExpression is null or empty.", "sortExpression");

        var parts = sortExpression.Split(' ');
        var isDescending = false;
        var propertyName = "";
        var tType = typeof(T);

        if (parts.Length > 0 && parts[0] != "")
        {
            propertyName = parts[0];

            if (parts.Length > 1)
            {
                isDescending = parts[1].ToLower().Contains("esc");
            }

            PropertyInfo prop = tType.GetProperty(propertyName);

            if (prop == null)
            {
                throw new ArgumentException(string.Format("No property '{0}' on type '{1}'", propertyName, tType.Name));
            }

            var funcType = typeof(Func<,>)
                .MakeGenericType(tType, prop.PropertyType);

            var lambdaBuilder = typeof(Expression)
                .GetMethods()
                .First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2)
                .MakeGenericMethod(funcType);

            var parameter = Expression.Parameter(tType);
            var propExpress = Expression.Property(parameter, prop);

            var sortLambda = lambdaBuilder
                .Invoke(null, new object[] { propExpress, new ParameterExpression[] { parameter } });

            var sorter = typeof(Queryable)
                .GetMethods()
                .FirstOrDefault(x => x.Name == (isDescending ? "OrderByDescending" : "OrderBy") && x.GetParameters().Length == 2)
                .MakeGenericMethod(new[] { tType, prop.PropertyType });

            return (IQueryable<T>)sorter
                .Invoke(null, new object[] { source, sortLambda });
        }

        return source;
    }
user1477388
la source
1
  • Ajoutez le package nugget Dynamite à votre code

  • Ajoutez l'espace de noms Dynamite.Extensions Ex: en utilisant Dynamite.Extensions;

  • Donner l'ordre par requête comme toute requête SQL Par exemple: étudiants.OrderBy ("Ville DESC, Adresse"). ToList ();

Sreeja SJ
la source
1

Pour étendre la réponse de @Icarus: si vous voulez trier par deux champs, je pourrais effectuer la fonction suivante (pour un champ la réponse d'Icarius fonctionne très bien).

public static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> q, string SortField1, string SortField2, bool Ascending)
        {
            var param = Expression.Parameter(typeof(T), "p");
            var body = GetBodyExp(SortField1, SortField2, param);
            var exp = Expression.Lambda(body, param);

            string method = Ascending ? "OrderBy" : "OrderByDescending";
            Type[] types = new Type[] { q.ElementType, exp.Body.Type };
            var mce = Expression.Call(typeof(Queryable), method, types, q.Expression, exp);
            return q.Provider.CreateQuery<T>(mce);
        }

C'est la fonction que le corps renvoie pour l'expression lambda, cela fonctionne avec string et int, mais il suffit d'ajouter plus de types pour le faire fonctionner selon les besoins de chaque programmeur

public static NewExpression GetBodyExp(string field1, string field2, ParameterExpression Parametro)
        {    
            // SE OBTIENE LOS NOMBRES DE LOS TIPOS DE VARIABLE 
            string TypeName1 = Expression.Property(Parametro, field1).Type.Name;
            string TypeName2 = Expression.Property(Parametro, field2).Type.Name;

            // SE DECLARA EL TIPO ANONIMO SEGUN LOS TIPOS DE VARIABLES
            Type TypeAnonymous = null;
            if (TypeName1 == "String")
            {
                string var1 = "0";
                if (TypeName2 == "Int32")
                {
                    int var2 = 0;
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }

                if (TypeName2 == "String")
                {
                    string var2 = "0";
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }    
            }    

            if (TypeName1 == "Int32")
            {
                int var1 = 0;
                if (TypeName2 == "Int32")
                {
                    string var2 = "0";
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }

                if (TypeName2 == "String")
                {
                    string var2 = "0";
                    var example = new { var1, var2 };
                    TypeAnonymous = example.GetType();
                }    
            }

            //se declaran los TIPOS NECESARIOS PARA GENERAR EL BODY DE LA EXPRESION LAMBDA
            MemberExpression[] args = new[] { Expression.PropertyOrField(Parametro, field1), Expression.PropertyOrField(Parametro, field2) };
            ConstructorInfo CInfo = TypeAnonymous.GetConstructors()[0];
            IEnumerable<MemberInfo> a = TypeAnonymous.GetMembers().Where(m => m.MemberType == MemberTypes.Property);

            //BODY 
            NewExpression body = Expression.New(CInfo, args, TypeAnonymous.GetMembers().Where(m => m.MemberType == MemberTypes.Property));

            return body;
        }

pour l'utiliser, c'est fait

IQueryable<MyClass> IqMyClass= context.MyClass.AsQueryable();
List<MyClass> ListMyClass= IqMyClass.OrderByDynamic("UserName", "IdMyClass", true).ToList();

s'il y a une meilleure façon de le faire, ce serait formidable qu'ils le partagent

J'ai réussi à le résoudre grâce à: Comment puis-je créer une expression lambda à propriétés multiples avec Linq

Ruben Hidalgo
la source
-2

Je suis bien en retard à la fête mais aucune de ces solutions n'a fonctionné pour moi. J'avais hâte d'essayer System.Linq.Dynamic, mais je n'ai pas trouvé cela sur Nuget, peut-être déprécié? D'une manière ou d'une autre...

Voici une solution que j'ai trouvée. J'avais besoin d'utiliser dynamiquement un mélange de OrderBy , OrderByDescending et OrderBy> ThenBy .

J'ai simplement créé une méthode d'extension pour mon objet de liste, un peu hacky je sais ... Je ne recommanderais pas cela si c'était quelque chose que je faisais beaucoup, mais c'est bon pour un seul.

List<Employee> Employees = GetAllEmployees();

foreach(Employee oEmployee in Employees.ApplyDynamicSort(eEmployeeSort))
{
    //do stuff
}

public static IOrderedEnumerable<Employee> ApplyDynamicSort(this List<Employee> lEmployees, Enums.EmployeeSort eEmployeeSort)
{
    switch (eEmployeeSort)
    {
        case Enums.EmployeeSort.Name_ASC:
            return lEmployees.OrderBy(x => x.Name);
        case Enums.EmployeeSort.Name_DESC:
            return lEmployees.OrderByDescending(x => x.Name);
        case Enums.EmployeeSort.Department_ASC_Salary_DESC:
            return lEmployees.OrderBy(x => x.Department).ThenByDescending(y => y.Salary);
        default:
            return lEmployees.OrderBy(x => x.Name);
    }
}
Nicolay
la source