Using the System.Linq.Dynamic
namespace I am able to construct a generic column list to search based on the columns present in the current usercontrol (a searchable grid we use in various places). The process is simple, take the column list visible to the current user, append the columns into a dynamic query expression in a where clause, see if the entire concatenated sequence contains the specified string.
This achieves 2 things, letting the user search with a single search box (google style) that works across all grids the user sees in the same way, as well as offloading the search to the database.
Here's how it currently works (result = IQueryable<T
> or IEnumerable<T>
):
var se = GetGridSearchExpression(grid);
if (se != null) result = result.Where(se, grid.SearchText.ToLower());
private static string GetGridSearchExpression(Grid grid)
{
if (grid.SearchText.IsNullOrEmpty()) return null;
var sb = new StringBuilder();
foreach (var s in grid.ColumnNames)
{
sb.AppendFormat("{0} {1} ",
sb.Length == 0 ? string.Empty : "+\"|^|\"+", s);
}
return string.Format("({0}).ToLower().Contains(@0)", sb);
}
The printed "|^|" string is something random to prevent a search on a single column from matching across to the next, e.g. columns "Bo" "Bryant" from matching the search "Bob", the result searched against is "Bo |^| Bryant" preventing the match.
Nullables are where the problems come in, having a DateTime? or Nullable type for example results in the following error:
Expression of type 'System.Nullable`1[System.DateTime]' cannot be used for
parameter of type 'System.Object' of method
'System.String Concat(System.Object, System.Object)'
Here's the part of DynamicQueryable that's blowing up:
Expression GenerateStringConcat(Expression left, Expression right) {
return Expression.Call(null,
typeof (string).GetMethod("Concat", new[] {typeof (object), typeof (object)}),
new[] {left, right});
}
The only way I've found to eliminate this thus far is to replace the append in the expression builder to:
foreach (var s in grid.ColumnNames)
{
sb.AppendFormat("{0}({1} != null ? {1}.ToString() : string.Empty)",
sb.Length == 0 ? string.Empty : "+\"|^|\"+", s);
}
Since we're in LINQ to SQL, this results in a bloated case statement. Given the alternative of loading every object from the DB to then search against, a case statement for 8-10 columns at most is acceptable.
Is there a cleaner or easier way to do all or any part of this?
Edited: Thanks Marc...I never use GetEnumerator anywhere in my code, always foreach or .ForEach()...but for some reason at the time it made debugging this easier, though I can't remember now why. Cleared up the question to the current code.