How to search a database using properties of model - c#

I have an MVC4 program in which I have a dropdown with a list of all of the properties of a model. I want the user to be able to select which property then type in a string value to the textbox to search. The issue is I can't dynamically change the query with the value of the dropdown.
public ActionResult SearchIndex(string searchString, string searchFields)
{
var selectListItems = new List<SelectListItem>();
var first = db.BloodStored.First();
foreach(var item in first.GetType().GetProperties())
{
selectListItems.Add(new SelectListItem(){ Text = item.Name, Value = item.Name});
}
IEnumerable<SelectListItem> enumSelectList = selectListItems;
ViewBag.SearchFields = enumSelectList;
var bloodSearch = from m in db.BloodStored
select m;
if (!String.IsNullOrEmpty(searchString))
{
PropertyInfo selectedSearchField = getType(searchFields);
string fieldName = selectedSearchField.Name;
//Dynamic Linq query this is how it needs to be set up to pass through linq.dynamic without exception
bloodSearch = bloodSearch.Where("x => x." + fieldName + " == " + "#0", searchString).OrderBy("x => x." + fieldName);
return View(bloodSearch);
}
return View(bloodSearch);
}
Here is my getType method
public PropertyInfo getType(string searchFields)
{
var first = db.BloodStored.First();
foreach (var item in first.GetType().GetProperties())
{
if(searchFields == item.Name)
{
return item;
}
}
return null;
}
I have updated my code to reflect the working query encase it can help anyone else out.
Here is a link to the post that I found my answer from Dynamic query with LINQ won't work

you should use the Dynamic.cs library. you can find it here as well as examples. You can then create your where clauses dynamically.
dynamic.cs

you could build the expression yourself.
static Expression<Func<T, bool>> GetExpression<T>(string propertyName, string propertyValue)
{
var parameterExp = Expression.Parameter(typeof(T), "type");
var propertyExp = Expression.Property(parameterExp, propertyName);
MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var someValue = Expression.Constant(propertyValue, typeof(string));
var containsMethodExp = Expression.Call(propertyExp, method, someValue);
return Expression.Lambda<Func<T, bool>>(containsMethodExp, parameterExp);
}
stole this from a post by Marc Gravell.

Related

Linq WHERE EF.Functions.Like - Why direct properties work and reflection does not?

I try to perform a simple LIKE action on the database site, while having query building services based on generic types. I found out while debugging however, that performing EF.Functions.Like() with reflection does not work as expected:
The LINQ expression 'where __Functions_0.Like([c].GetType().GetProperty("FirstName").GetValue([c], null).ToString(), "%Test%")' could not be translated and will be evaluated locally..
The code that makes the difference
That works:
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.FirstName, "%Test%"));
This throws the warning & tries to resolve in memory:
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.GetType().GetProperty("FirstName").GetValue(c, null).ToString(), "%Test%"));
Does the Linq query builder or the EF.Functions not support reflections?
Sorry if the questions seem basic, it's my first attempt with .NET Core :)
In EF the lambdas are ExpressionTrees and the expressions are translated to T-SQL so that the query can be executed in the database.
You can create an extension method like so:
public static IQueryable<T> Search<T>(this IQueryable<T> source, string propertyName, string searchTerm)
{
if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(searchTerm))
{
return source;
}
var property = typeof(T).GetProperty(propertyName);
if (property is null)
{
return source;
}
searchTerm = "%" + searchTerm + "%";
var itemParameter = Parameter(typeof(T), "item");
var functions = Property(null, typeof(EF).GetProperty(nameof(EF.Functions)));
var like = typeof(DbFunctionsExtensions).GetMethod(nameof(DbFunctionsExtensions.Like), new Type[] { functions.Type, typeof(string), typeof(string) });
Expression expressionProperty = Property(itemParameter, property.Name);
if (property.PropertyType != typeof(string))
{
expressionProperty = Call(expressionProperty, typeof(object).GetMethod(nameof(object.ToString), new Type[0]));
}
var selector = Call(
null,
like,
functions,
expressionProperty,
Constant(searchTerm));
return source.Where(Lambda<Func<T, bool>>(selector, itemParameter));
}
And use it like so:
var query = _context.Set<Customer>().Search("FirstName", "Test").ToList();
var query2 = _context.Set<Customer>().Search("Age", "2").ToList();
For reference this was the Customer I used:
public class Customer
{
[Key]
public Guid Id { get; set; }
public string FirstName { get; set; }
public int Age { get; set; }
}
Simple answer, no.
EntityFramework is trying to covert your where clause in to a SQL Query. There is no native support for reflection in this conversation.
You have 2 options here. You can construct your text outside of your query or directly use property itself. Is there any specific reason for not using something like following?
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.FirstName, "%Test%"));
Keep in mind that every ExpresionTree that you put in Where clause has to be translated into SQL query.
Because of that, ExpressionTrees that you can write are quite limited, you have to stick to some rules, thats why reflection is not supported.
Image that instead of :
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(c.GetType().GetProperty("FirstName").GetValue(c, null).ToString(), "%Test%"));
You write something like:
var query = _context.Set<Customer>().Where(c => EF.Functions.Like(SomeMethodThatReturnsString(c), "%Test%"));
It would mean that EF is able to translate any c# code to SQL query - it's obviously not true :)
I chucked together a version of the accepted answer for those using NpgSQL as their EF Core provider as you will need to use the ILike function instead if you want case-insensitivity, also added a second version which combines a bunch of properties into a single Where() clause:
public static IQueryable<T> WhereLike<T>(this IQueryable<T> source, string propertyName, string searchTerm)
{
// Check property name
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentNullException(nameof(propertyName));
}
// Check the search term
if(string.IsNullOrEmpty(searchTerm))
{
throw new ArgumentNullException(nameof(searchTerm));
}
// Check the property exists
var property = typeof(T).GetProperty(propertyName);
if (property == null)
{
throw new ArgumentException($"The property {typeof(T)}.{propertyName} was not found.", nameof(propertyName));
}
// Check the property type
if(property.PropertyType != typeof(string))
{
throw new ArgumentException($"The specified property must be of type {typeof(string)}.", nameof(propertyName));
}
// Get expression constants
var searchPattern = "%" + searchTerm + "%";
var itemParameter = Expression.Parameter(typeof(T), "item");
var functions = Expression.Property(null, typeof(EF).GetProperty(nameof(EF.Functions)));
var likeFunction = typeof(NpgsqlDbFunctionsExtensions).GetMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new Type[] { functions.Type, typeof(string), typeof(string) });
// Build the property expression and return it
Expression selectorExpression = Expression.Property(itemParameter, property.Name);
selectorExpression = Expression.Call(null, likeFunction, functions, selectorExpression, Expression.Constant(searchPattern));
return source.Where(Expression.Lambda<Func<T, bool>>(selectorExpression, itemParameter));
}
public static IQueryable<T> WhereLike<T>(this IQueryable<T> source, IEnumerable<string> propertyNames, string searchTerm)
{
// Check property name
if (!(propertyNames?.Any() ?? false))
{
throw new ArgumentNullException(nameof(propertyNames));
}
// Check the search term
if (string.IsNullOrEmpty(searchTerm))
{
throw new ArgumentNullException(nameof(searchTerm));
}
// Check the property exists
var properties = propertyNames.Select(p => typeof(T).GetProperty(p)).AsEnumerable();
if (properties.Any(p => p == null))
{
throw new ArgumentException($"One or more specified properties was not found on type {typeof(T)}: {string.Join(",", properties.Where(p => p == null).Select((p, i) => propertyNames.ElementAt(i)))}.", nameof(propertyNames));
}
// Check the property type
if (properties.Any(p => p.PropertyType != typeof(string)))
{
throw new ArgumentException($"The specified properties must be of type {typeof(string)}: {string.Join(",", properties.Where(p => p.PropertyType != typeof(string)).Select(p => p.Name))}.", nameof(propertyNames));
}
// Get the expression constants
var searchPattern = "%" + searchTerm + "%";
var itemParameter = Expression.Parameter(typeof(T), "item");
var functions = Expression.Property(null, typeof(EF).GetProperty(nameof(EF.Functions)));
var likeFunction = typeof(NpgsqlDbFunctionsExtensions).GetMethod(nameof(NpgsqlDbFunctionsExtensions.ILike), new Type[] { functions.Type, typeof(string), typeof(string) });
// Build the expression and return it
Expression selectorExpression = null;
foreach (var property in properties)
{
var previousSelectorExpression = selectorExpression;
selectorExpression = Expression.Property(itemParameter, property.Name);
selectorExpression = Expression.Call(null, likeFunction, functions, selectorExpression, Expression.Constant(searchPattern));
if(previousSelectorExpression != null)
{
selectorExpression = Expression.Or(previousSelectorExpression, selectorExpression);
}
}
return source.Where(Expression.Lambda<Func<T, bool>>(selectorExpression, itemParameter));
}

C# using ExpressionTree to map DataTable to List<T>

I have written a ToList(); extension Method to convert a DataTable to List. This just works under some circumstances but we have much old code which uses DataTables and sometimes it's needed. My Problem is that this method works with reflection what is ok but not that performant. I need about 1,2sek for 100.000 DataRows.
So i decided to build this with Expression Trees. At first i want to replace the Setter Call of Properties. Up to this time i could easily get the value:
var exactType = Nullable.GetUnderlyingType(propType) ?? propType;
var wert = Convert.ChangeType(zeile[spaltenname], exactType);
and set it:
propertyInfo.SetValue(tempObjekt, wert, null);
Now i searched StackOverflow and found this:
var zielExp = Expression.Parameter(typeof(T));
var wertExp = Expression.Parameter(propType);
var propertyExp = Expression.Property(zielExp, matchProp);
var zuweisungExp = Expression.Assign(propertyExp, wertExp);
var setter = Expression.Lambda<Action<T, int>>(zuweisungExp, zielExp, wertExp).Compile();
setter(tempObjekt, wert);
My big Problem is that the Lambda Action expects an integer. But i need this expecting the type of my Property. I have the Type of my Property via PropertyInfo. But can't get this to work. Thought i can easily make:
Action<T, object>
but this results in following excepion:
ArgumentException The ParameterExpression from Type "System.Int32"
cannot be used as Delegateparameter from Type "System.Object".
Someone out there knows a possible solution?
Instead of the generic Expression.Lambda method you can use this overload which takes a type:
public static LambdaExpression Lambda(
Type delegateType,
Expression body,
params ParameterExpression[] parameters
)
Then you can use the Type.MakeGenericType method to create the type for your action:
var actionType = typeof(Action<,>).MakeGenericType(typeof(T), proptype);
var setter = Expression.Lambda(actionType, zuweisungExp, zielExp, wertExp).Compile();
Edit following the comments regarding performance:
You can also just build the expression runtime to map the DataTable to your class of type T with a select, so there's only need to use reflection once, which should greatly improve performance. I wrote the following extension method to convert a DataTable to List<T> (note that this method will throw a runtime exception if you don't plan to map all datacolumns to a property in the class, so be sure to take care of that if that might happen):
public static class LocalExtensions
{
public static List<T> DataTableToList<T>(this DataTable table) where T : class
{
//Map the properties in a dictionary by name for easy access
var propertiesByName = typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(p => p.Name);
var columnNames = table.Columns.Cast<DataColumn>().Select(dc => dc.ColumnName);
//The indexer property to access DataRow["columnName"] is called "Item"
var property = typeof(DataRow).GetProperties().First(p => p.Name == "Item"
&& p.GetIndexParameters().Length == 1
&& p.GetIndexParameters()[0].ParameterType == typeof(string));
var paramExpr = Expression.Parameter(typeof(DataRow), "r");
var newExpr = Expression.New(typeof(T));
//Create the expressions to map properties from your class to the corresponding
//value in the datarow. This will throw a runtime exception if your class
//doesn't contain properties for all columnnames!
var memberBindings = columnNames.Select(columnName =>
{
var pi = propertiesByName[columnName];
var indexExpr = Expression.MakeIndex(paramExpr, property,
new[] { Expression.Constant(columnName) });
//Datarow["columnName"] is of type object, cast to the right type
var convert = Expression.Convert(indexExpr, pi.PropertyType);
return Expression.Bind(pi, convert);
});
var initExpr = Expression.MemberInit(newExpr, memberBindings);
var func = Expression.Lambda<Func<DataRow, T>>(initExpr,paramExpr).Compile();
return table.Rows.Cast<DataRow>().Select(func).ToList();
}
}
Then I wrote a small testclass and some code which creates a datatable of 1,000,000 rows that get mapped to a list. Building the expression + converting to a list now only takes 486ms on my pc (granted it is a very small class of course):
class Test
{
public string TestString { get; set; }
public int TestInt { get; set; }
}
class Program
{
static void Main()
{
DataTable table = new DataTable();
table.Columns.Add(new DataColumn("TestString", typeof(string)));
table.Columns.Add(new DataColumn("TestInt", typeof(int)));
for(int i = 0; i < 1000000; i++)
{
var row = table.NewRow();
row["TestString"] = $"String number: {i}";
row["TestInt"] = i;
table.Rows.Add(row);
}
var stopwatch = Stopwatch.StartNew();
var myList = table.DataTableToList<Test>();
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed.ToString());
}
}
I think I've understood you correctly. I cannot translate your variables so I'm taking my best guess here based on what I'm seeing in your question:
For an Action<object,object> where the first parameter is the Entity itself and the second is the type of the property you can use
var instance = Expression.Parameter(typeof(object), "i");
var argument = Expression.Parameter(typeof(object), "a");
var convertObj = Expression.TypeAs(instance, propertyInfo.DeclaringType);
var convert = Expression.Convert(argument, propertyInfo.PropertyType);
var setterCall = Expression.Call(convertObj, propertyInfo.GetSetMethod(), convert);
var compiled = ((Expression<Action<object, object>>) Expression.Lambda(setterCall, instance, argument)).Compile();
If you know T (ie, the type of the Entity), you can do this instead:
var instance = Expression.Parameter(typeof(T), "i");
var argument = Expression.Parameter(typeof(object), "a");
var convert = Expression.Convert(argument, propertyInfo.PropertyType);
var setterCall = Expression.Call(instance , propertyInfo.GetSetMethod(), convert);
var compiled = ((Expression<Action<T, object>>) Expression.Lambda(setterCall, instance, argument)).Compile();
I comment here because I do not have the necessary reputation to comment on the response of #Alexander Derek
var memberBindings = columnNames.Select(columnName =>
{
var pi = propertiesByName[columnName];
var indexExpr = Expression.MakeIndex(paramExpr, property,
new[] { Expression.Constant(columnName) });
//Datarow["columnName"] is of type object, cast to the right type
var convert = Expression.Convert(indexExpr, pi.PropertyType);
return Expression.Bind(pi, convert);
});
in order to avoid runtime exception i added a try-catch and .where()
var memberBindings = columnNames.Select(columnName =>
{
try
{
var pi = propertiesByName[columnName];
var indexExpr = Expression.MakeIndex(paramExpr, property,
new[] { Expression.Constant(columnName) });
var convert = Expression.Convert(indexExpr, pi.PropertyType);
return Expression.Bind(pi, convert);
}
catch(Exception e)
{
return null;
}
});
var initExpr = Expression.MemberInit(newExpr, memberBindings.Where(obj => obj != null));

Concatenate string members using Linq PredicateBuilder for text search

I have a REST WebAPI using EntityFramework database first. All code is generated off the EDMX file, entities, repository classes and API controllers etc.
I have added some filtering functionality which allows users to add conditions via the query string that translate to LinqKit PredicateBuilder / Linq expressions that filter results when hitting the db.
e.g. /api/Users?FirstName_contains=Rog
This will return all users with 'Rog' in the User.FirstName member. This uses PredicateBuilder to dynamically build an appropriate Linq expression to then use as a Where clause against the DbSet.
For example:
var fieldName = "FirstName";
var value = "Rog";
var stringContainsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var parameter = Expression.Parameter(typeof(User), "m");
var fieldAccess = Expression.PropertyOrField(parameter, fieldName);
var fieldType = typeof(User).GetProperty(fieldName, BindingFlags.IgnoreCase | BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public).PropertyType;
var expression = Expression.Lambda<Func<User, bool>>(Expression.Call(fieldAccess, stringContainsMethod, Expression.Constant(value, fieldType))
, parameter)
var andPredicate = PredicateBuilder.True<User>();
andPredicate = andPredicate.And(expression);
var query = Db.Users
.AsQueryable()
.AsExpandable()
.Where(andPredicate);
Now the problem. I want the client to be able to match results based on a composition of members.
e.g. /api/Users?api_search[FirstName,LastName]=Rog
i.e. search first name + last name for matches of 'Rog', so I could search for 'Roger Sm' and get a result for first name = Roger and last name = Smith.
If I was to query the DbSet using fluent it would look like:
users.Where(u => (u.FirstName + " " + u.LastName).Contains("Rog"));
What I am struggling with is creating a predicate / linq expression that will handle the concatenation of string members FirstName + " " + LastName dynamically.
PredicateBuilder is not really needed here.
The string concatenation expression can be generated using string.Concat method call which is supported by EF:
static Expression<Func<T, string>> GenerateConcat<T>(IEnumerable<string> propertyNames)
{
var parameter = Expression.Parameter(typeof(T), "e");
// string.Concat(params string[] values)
var separator = Expression.Constant(" ");
var concatArgs = Expression.NewArrayInit(typeof(string), propertyNames
.SelectMany(name => new Expression[] { separator, Expression.PropertyOrField(parameter, name) })
.Skip(1));
var concatCall = Expression.Call(typeof(string).GetMethod("Concat", new[] { typeof(string[]) }), concatArgs);
return Expression.Lambda<Func<T, string>>(concatCall, parameter);
}
The string contains predicate can be generated by simple string.Contains method call:
static Expression<Func<T, bool>> GenerateContains<T>(Expression<Func<T, string>> member, string value)
{
var containsCall = Expression.Call(member.Body, "Contains", Type.EmptyTypes, Expression.Constant(value));
return Expression.Lambda<Func<T, bool>>(containsCall, member.Parameters);
}
Combining them together with your example:
var predicate = GenerateContains(GenerateConcat<User>(new[] { "FirstName", "LastName" }), "Rog");
Try the following (I haven't tested it against a database):
public class User
{
public string FirstName { get; set; }
public string LastName { get; set;}
}
void Main()
{
List<User> users = new List<User> {
new User { FirstName = "john", LastName = "smith" },
new User { FirstName = "siler", LastName = "johnston" } };
string searchName = "ja smi";
String[] terms = searchName.Split(' ');
var items = users.Where(x => terms.Any(y => x.FirstName.Contains(y))
|| terms.Any(y => x.LastName.Contains(y)));
}

Generic Method of Searching all Fields within an IEnumerable for any match in c#

I code below that works well with an API for the DataTables plugin; for each column that the DataTables searches in, regardless of type, the filter works as expected based on what is supplied.
DataTables also has a "Global" search feature where you can search in one field and if there is a match in ANY of the rows for said data then a match is returned.
What I am Hoping For:
A way to perform a search on an IEnumerable such that if any of the fields match the search the result is returned.
NameValueCollection nvc = HttpUtility.ParseQueryString(Request.RequestUri.Query);
var generalSearch = nvc["sSearch"];
if (!string.IsNullOrWhiteSpace(generalSearch))
{
var generalSearchProperties = typeof(T).GetProperties();
foreach (var currentProperty in generalSearchProperties)
{
Type propType = currentProperty.PropertyType;
set = set.Where(StaticUtility.PropertyEquals<T>(currentProperty, generalSearch, propType));
/* ^^^^^ */
/*
Instead of the "Where" here I am looking for something like "where or" which can be added to the IEnumerable.
*/
}
}
Original Code:
protected virtual IQueryable<T> FilterEntitiesBySearchParameters(IQueryable<T> set)
{
try
{
NameValueCollection nvc = HttpUtility.ParseQueryString(Request.RequestUri.Query);
var props = typeof(T).GetProperties();
foreach (var prop in props)
{
var name = prop.Name;
var val = nvc[name];
Type propType = prop.PropertyType;
if (val != null)
{
set = set.Where(StaticUtility.PropertyEquals<T>(prop, val, propType));
}
if (nvc.GetPairs().Where(p => p.Value == name).Where(p => p.Key.StartsWith("mDataProp")).Any())
{
var key = nvc.GetPairs().Where(p => p.Value == name).Where(p => p.Key.StartsWith("mDataProp")).FirstOrDefault().Key;
key = key.Replace("mDataProp", "sSearch");
val = nvc[key];
if (!String.IsNullOrEmpty(val))
set = set.Where(StaticUtility.PropertyEquals<T>(prop, val, propType));
}
}
return set;
} catch (Exception exc)
{
return set;
}
}
If I understand your request correctly, you basically want to search through your data and match any field for equality? If that's true, then simply add your matching data to a new set, and filter it by .Distinct() after the fact, to make sure you get one record of each. Something like so...
NameValueCollection nvc = HttpUtility.ParseQueryString(Request.RequestUri.Query);
var results = new IEnumerable<T>();
var generalSearch = nvc["sSearch"];
if (!string.IsNullOrWhiteSpace(generalSearch))
{
var generalSearchProperties = typeof(T).GetProperties();
foreach (var currentProperty in generalSearchProperties)
{
Type propType = currentProperty.PropertyType;
results.AddRange(set.Where(StaticUtility.PropertyEquals<T>(currentProperty, generalSearch, propType)));
}
}
return results.Distinct();
You could try to first create a BinaryExpression consisting of all the different options, then pass that expression to the Where() method of the query.
Assuming your StaticUtility class is used to create expressions, you might try something like the following:
NameValueCollection nvc = HttpUtility.ParseQueryString(Request.RequestUri.Query);
// Container for filter expression
BinaryExpression filter = null;
var generalSearch = nvc["sSearch"];
if (!string.IsNullOrWhiteSpace(generalSearch)) {
var generalSearchProperties = typeof(T).GetProperties();
foreach (var currentProperty in generalSearchProperties) {
Type propType = currentProperty.PropertyType;
if (filter == null) {
// Start with first filter expression
filter = StaticUtility.PropertyEquals<T>(currentProperty, generalSearch, propType);
} else {
// Add another filter using OR
BinaryExpression other = StaticUtility.PropertyEquals<T>(currentProperty, generalSearch, propType);
filter = BinaryExpression.OrElse(filter, other);
}
}
}
// Add actual filter to query
set = set.Where(filter);

Building an OrderBy Lambda expression based on child entity's property

I'm trying to generate a LINQ OrderBy clause using lambda expressions with an input of the column name of an entity as a string (in the "sortOn" variable below).
The code below works fine for a sortOn value like "Code" generating the lambda
p => p.Code
But I would also like to sort on a child entity, where the lambda might be
p => p.Category.Description
So in this instance, I would just like to set sortOn = "Category.Description" and have the correct lamdba expression generated.
Is this possible? Any suggestions about the best way to do this would be welcomed.
This code works fine for the simple case:
var param = Expression.Parameter(typeof (Product), "p");
var sortExpression = Expression.Lambda<Func<Product, object>>(
Expression.Property(param, sortOn), param);
if (sortAscending ?? true)
{
products = products.OrderBy(sortExpression);
}
else
{
products = products.OrderByDescending(sortExpression);
}
The use-case for this problem is displaying a grid of data and being able to sort the data, simply by passing the column name to be sorted on back to the server. I'd like to make the solution generic, but have started using a particular type (Product in the example) for now.
This will generate proper lambda expression:
var sortOn = "Category.Description";
var param = Expression.Parameter(typeof(Product), "p");
var parts = sortOn.Split('.');
Expression parent = param;
foreach (var part in parts)
{
parent = Expression.Property(parent, part);
}
var sortExpression = Expression.Lambda<Func<Product, object>>(parent, param);
Here is an extension OrderBy method which works for any number of nested parameters.
public static IQueryable<T> OrderBy<T>(this IQueryable<T> query, string key, bool asc = true)
{
try
{
string orderMethodName = asc ? "OrderBy" : "OrderByDescending";
Type type = typeof(T);
Type propertyType = type.GetProperty(key)?.PropertyType; ;
var param = Expression.Parameter(type, "x");
Expression parent = param;
var keyParts = key.Split('.');
for (int i = 0; i < keyParts.Length; i++)
{
var keyPart = keyParts[i];
parent = Expression.Property(parent, keyPart);
if (keyParts.Length > 1)
{
if (i == 0)
{
propertyType = type.GetProperty(keyPart).PropertyType;
}
else
{
propertyType = propertyType.GetProperty(keyPart).PropertyType;
}
}
}
MethodCallExpression orderByExpression = Expression.Call(
typeof(Queryable),
orderMethodName,
new Type[] { type, propertyType },
query.Expression,
CreateExpression(type, key)
);
return query.Provider.CreateQuery<T>(orderByExpression);
}
catch (Exception e)
{
return query;
}
}
The CreateExpression method which is used in my solution is defined in this post.
The usage of the OrderBy extension method is as follows.
IQueryable<Foo> q = [Your database context].Foos.AsQueryable();
IQueryable<Foo> p = null;
p = q.OrderBy("myBar.name"); // Ascending sort
// Or
p = q.OrderBy("myBar.name", false); // Descending sort
// Materialize
var result = p.ToList();
The type Foo and its properties are also taken from the same post as method CreateExpression.
Hope you find this post helpful.
You can use the Dynamic LINQ Query Library to do this easily. Assuming you have an IQueryable<T> implementation of Product, you can easily do:
IQueryable<Product> products = ...;
// Order by dynamically.
products = products.OrderBy("Category.Description");
The blog post has a link to the libary, and you'll have to build/include the project in your solution yourself, but it works very well, and the parsing is very robust. It prevents you from having to write the parsing code yourself; even for something so simple, if the requirements expand, the library has you covered, whereas a homegrown solution does not.
It also has a number of other dynamic operators (Select, Where, etc.) so you can perform other dynamic operations.
There's no magic under the hood, it just parses the strings you pass it and then creates the lambda expressions based on the parsing results.
If you don't need expressions, how about:
products = products.Orderby(p1 => p1.Code).ThenBy(p2 => p2.Category.Description)
Hi you can also create an extension method like which can sort to any depth not only just child
public static IEnumerable<TSource> CustomOrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
List<string> list=new List<string>();
List<TSource> returnList=new List<TSource>();
List<int> indexList = new List<int>();
if (source == null)
return null;
if (source.Count() <= 0)
return source;
source.ToList().ForEach(sc=>list.Add(keySelector(sc).ToString())); //Extract the strings of property to be ordered
list.Sort(); //sort the list of strings
foreach (string l in list) // extract the list of indexes of source according to the order
{
int i=0;
//list.ForEach(l =>
foreach (var s in source.ToList())
{
if (keySelector(s).ToString() == l)
break;
i++;
}
indexList.Add(i);
}
indexList.ForEach(i=>returnList.Add(source.ElementAt(i))); //rearrange the source according to the above extracted indexes
return returnList;
}
}
public class Name
{
public string FName { get; set; }
public string LName { get; set; }
}
public class Category
{
public Name Name { get; set; }
}
public class SortChild
{
public void SortOn()
{
List<Category> category = new List<Category>{new Category(){Name=new Name(){FName="sahil",LName="chauhan"}},
new Category(){Name=new Name(){FName="pankaj",LName="chauhan"}},
new Category(){Name=new Name(){FName="harish",LName="thakur"}},
new Category(){Name=new Name(){FName="deepak",LName="bakseth"}},
new Category(){Name=new Name(){FName="manish",LName="dhamaka"}},
new Category(){Name=new Name(){FName="arev",LName="raghaka"}}
};
var a = category.CustomOrderBy(s => s.Name.FName);
}
}
Its custom method and right now it works only for string property only however it can be reactified using generics to work for any primitive type. I hope this will help.

Categories

Resources