Here's a generic solution I created to solve these sorts of issues, and the specifics for this particular one. It uses an Attribute
class to mark methods (normally extension methods) as needing special processing for LINQ to SQL/EF and an ExpressionVisitor
to re-write the queries for each marked method.
First, the Attribute
class:
[AttributeUsage(AttributeTargets.Method)]
public class ExpandMethodAttribute : Attribute {
private string methodName;
public ExpandMethodAttribute(string aMethodName = null) => methodName = aMethodName;
public MethodInfo ExpandingMethod(MethodInfo mi) {
var methodType = mi.DeclaringType;
var origMethodName = mi.Name;
var argTypes = new[] { typeof(Expression) }.Concat(mi.GetParameters().Skip(1).Select(pi => pi.ParameterType)).ToArray();
var bf = BindingFlags.Public | BindingFlags.NonPublic | (mi.IsStatic ? BindingFlags.Static : BindingFlags.Instance);
var expandMethodName = methodName ?? $"{origMethodName}Expander";
var em = methodType.GetMethod(expandMethodName, bf, null, argTypes, null);
if (em == null)
throw new NullReferenceException($"Unable to find MethodInfo for {methodType.Name}.{expandMethodName}");
else
return em;
}
}
Now, an IQueryable
extension to trigger the expansion:
public static class IQueryableExt {
private static object Evaluate(this Expression e) => (e is ConstantExpression c) ? c.Value : Expression.Lambda(e).Compile().DynamicInvoke();
/// <summary>
/// ExpressionVisitor to replace x.method("x..z") to methodexpander(x, "x..z")
/// </summary>
private class ExpandableMethodVisitor : ExpressionVisitor {
public override Expression Visit(Expression node) {
if (node?.NodeType == ExpressionType.Call) {
var callnode = node as MethodCallExpression;
var ema = callnode.Method.GetCustomAttribute<ExpandMethodAttribute>();
if (ema != null)
return (Expression)ema.ExpandingMethod(callnode.Method).Invoke(callnode.Object, callnode.Arguments.Select((ae, n) => n == 0 ? ae : ae.Evaluate()).ToArray());
}
return base.Visit(node);
}
}
private static T ExpandMethods<T>(this T orig) where T : Expression => (T)(new ExpandableMethodVisitor().Visit(orig));
public static IQueryable<T> Expand<T>(this IQueryable<T> q) => q.Provider.CreateQuery<T>(q.Expression.ExpandMethods());
}
Finally, the specific extension needed to filter characters from a field expression:
public static class LINQExt {
// body only for LINQ to Objects use
[ExpandMethod("CleanUp")]
public static string RemoveAll(this string src, string removeChars) => removeChars.Aggregate(src, (ans, ch) => ans.Replace(ch.ToString(), ""));
private static Expression CleanUp(this Expression dbFn, string charsToRemove) {
var toCharE = Expression.Constant(String.Empty);
var replaceMI = typeof(string).GetMethod("Replace", new[] { typeof(string), typeof(string) });
var methodBody = dbFn;
foreach (var ch in charsToRemove)
methodBody = Expression.Call(methodBody, replaceMI, Expression.Constant(ch.ToString()), toCharE);
return methodBody;
}
}
Now you can use the RemoveAll
extension in a query, and process the query with Expand
before instantiating it.
So, for the example:
var filter = "filter";
var removals = "'\"#/-";
query = query.Where(x =>
x.Name.RemoveAll(removals).Contains(filter) ||
x.Full.RemoveAll(removals).Contains(filter))
.Expand();
This could probably be added to LINQKit to be handled with their IQueryable
/IProvider
wrappers.