Is it possible to evaluate a select projection at some point to get a list of which properties are being selected?
For example, if I have the following class:
public class Example()
{
public string Aaa { get; set; }
public int Bbb { get; set; }
public string Ccc { get; set; }
}
and the following select projection:
Expression<Func<Example, Example>> select = x => new Example { Aaa= x.Aaa, Ccc = x.Ccc };
Would it be possible to interpret the select projection to get a result along the lines of var result = new List<string> { "Aaa", "Ccc" };?
The best way to do something like this is using an ExpressionVisitor.
Here is an example:
public class MyMemberExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
if (node.Member.MemberType == MemberTypes.Property // is a property
&& node.Expression.NodeType == ExpressionType.Parameter // is from a parameter expression
&& Members.All(s => s != node.Member.Name)) // avoids duplicates
{
Members.Add(node.Member.Name);
}
return base.VisitMember(node);
}
public List<string> Members { get; set; } = new List<string>();
}
Then you can use it like this:
// Complex expressions work too!
Example outsideExample = new Example();
Expression<Func<Example, Example>> expression = x => new Example(
x.Aaa + outsideExample.Bbb,
x.Ccc + x.Aaa.Length);
var myVisitor = new MemberExpressionVisitor();
myVisitor.Visit(expression);
Console.WriteLine(string.Join(", ", myVisitor.Members)); // This should print out "Aaa, Ccc"
You can visit How to: Implement an Expression Tree Visitor to learn more on how to implement one.
To find all of the property expressions within that expression you can use an ExpressionVisitor to inspect all of the Expression instances within a given expression and see which are property accesses, and what property is being accessed:
internal class PropertySearchVisitor : ExpressionVisitor
{
private List<MemberInfo> properties = new List<MemberInfo>();
public IEnumerable<MemberInfo> Properties => properties;
protected override Expression VisitMember(MemberExpression node)
{
if (node?.Member?.MemberType == MemberTypes.Property)
properties.Add(node.Member);
return base.VisitMember(node);
}
}
Once you have that you can write a method to visit a given expression and return the properties (or property names) of that expression:
public static IEnumerable<MemberInfo> GetProperties(this Expression expression)
{
var visitor = new PropertySearchVisitor();
visitor.Visit(expression);
return visitor.Properties;
}
public static IEnumerable<string> GetPropertyNames(this Expression expression)
{
var visitor = new PropertySearchVisitor();
visitor.Visit(expression);
return visitor.Properties.Select(property => property.Name);
}
Related
I'm trying to do a LinQ request with a group by where a parameter is parametrizable by an Expression ( (Expression<Func<CompanyModel,TKey>> myGroupingProperty) and the other one is hard coded. But even if my code compile I get an error that linq does not support lambdas. Would you have any idea how to do this request?
Here is the code sample:
public List<timelineResult> getTimelinebyCompany<TKey>(Expression<Func<CompanyModel,TKey>> myGroupingProperty, Filter item)
{
int cntToBeSureThatTheQueryExecuteAtLeastOneTime = 0;
List<timelineResult> toto = new List<timelineResult>();
using (var db = new fintechDbContext())
{
while (cntToBeSureThatTheQueryExecuteAtLeastOneTime == 0)
{
toto = (from p in db.companyDBSET
select p).GroupBy(p=> new {p.Founded_Year, myGroupingProperty})
.Select(o => new timelineResult{ year = o.Key.Founded_Year, cluster = o.myGroupingProperty.ToString(), count = o.Count() })
.OrderBy(o => o.year).ToList();
cntToBeSureThatTheQueryExecuteAtLeastOneTime++;
}
}
return toto;
}
What you are seeking for is doable, but not the way you tried because the passed lambda expression cannot be used directly inside another lambda expression.
You should start first by creating a generic class to hold the grouping key. It's similar to system provided Tuple, but has parameterless constructor and simple property get/setters to conform to the EF projection rules:
public class GroupKey<K1, K2>
{
public K1 Key1 { get; set; }
public K2 Key2 { get; set; }
}
Then you need to build dynamically lambda expression like this
Expression<Func<T, K1, K2>> keySelector = x =>
new GroupKey<K1, K2> { Key1 = x.Prop1, Key2 = x.Prop2 };
In order to do that, you'll need some Expression helpers:
public static class ExpressionUtils
{
public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
{
return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
}
class ParameterReplacer : ExpressionVisitor
{
public ParameterExpression Source;
public Expression Target;
protected override Expression VisitParameter(ParameterExpression node)
{
return node == Source ? Target : base.VisitParameter(node);
}
}
}
and you can encapsulate the grouping part in a custom extension method:
public static class QueryableExtensions
{
public static IQueryable<IGrouping<GroupKey<K1, K2>, T>> GroupByPair<T, K1, K2>(this IQueryable<T> source, Expression<Func<T, K1>> keySelector1, Expression<Func<T, K2>> keySelector2)
{
var parameter = keySelector1.Parameters[0];
var key1 = keySelector1.Body;
var key2 = keySelector2.Body.ReplaceParameter(keySelector2.Parameters[0], parameter);
var keyType = typeof(GroupKey<K1, K2>);
var keySelector = Expression.Lambda<Func<T, GroupKey<K1, K2>>>(
Expression.MemberInit(
Expression.New(keyType),
Expression.Bind(keyType.GetProperty("Key1"), key1),
Expression.Bind(keyType.GetProperty("Key2"), key2)),
parameter);
return source.GroupBy(keySelector);
}
}
Finally, the essential part of your method becomes like this:
toto = db.companyDBSET
.GroupByPair(p => p.Founded_Year, myGroupingProperty)
.Select(g => new timelineResult
{
year = g.Key.Key1,
cluster = g.Key.Key2.ToString(),
count = g.Count()
})
.OrderBy(o => o.year)
.ToList();
Let's say I have this expression:
e => e.Name.StartsWith(GetArgument())
Where GetArgument() is defined as follows:
public string GetArgument() { return "Lu"; }
I want this expression to be translated to the following string:
"begins_with(Name, Lu)"
I have developed an expression visitor that visits each sub expression in a recursive manner:
public class MyExpressionTranslator : ExpressionVisitor, IExpressionTranslator<string>
{
// ....
//Implementing IExpressionTranslator<string>
public string Translate(Expression expr)
{
//Begin visiting the expression and its sub expressions
base.Visit(expr);
// I need to return the string here
return null;
}
// Overrides method from base class ExpressionVisitor
protected override MethodCallExpression VisitMethodCall(MethodCallExpression expr)
{
//This method is called when a method call sub expression is visited
//For example, I can check if the method being called is "StartsWith"
if(expr.Method.Name == "StartsWith")
{
// I have no idea what to do here
}
//Proceeds to visit this expression's sub expressions
return base.VisitMethodCall(expr);
}
}
I would use this class as follows:
MyExpressionTranslator translator = // new MyExpressionTranslator(...)
Expression<Func<SomeClass, bool>> expr = e => e.Name.StartsWith(GetArgument());
string result = translator.Translate(expr);
// result should be "begins_with(Name, Lu)"
Providing my base class has a virtual visit method for each expression type (be it a constant, an argument, a method call, or any other), how can I build the expected string output?
What are you trying to accomplish? Something like the below might work. It won't be easy to fill out the other methods though.
class Program
{
static void Main(string[] args)
{
Expression<Func<TestObject, bool>> expr = e => e.FirstName.StartsWith(GetArgument());
var visitor = new MyExpressionTranslator();
var translation = visitor.Translate(expr); // = "begins_with(FirstName, Lu)"
}
static string GetArgument()
{
return "Lu";
}
}
class TestObject
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public interface IExpressionTranslator<T>
{
T Translate(Expression expr);
}
public class MyExpressionTranslator : ExpressionVisitor, IExpressionTranslator<string>
{
private StringBuilder _sb = null;
public string Translate(Expression expr)
{
_sb = new StringBuilder();
base.Visit(expr);
// I need to return the string here
return _sb.ToString();
}
protected override Expression VisitMethodCall(MethodCallExpression expr)
{
if (expr.Method.Name == "StartsWith")
{
var mainArg = expr.Arguments[0];
var lambda = Expression.Lambda<Func<string>>(mainArg);
var arg = lambda.Compile()();
var member = expr.Object as MemberExpression;
if (member != null)
{
_sb.AppendFormat("begins_with({0}, {1})", member.Member.Name, arg);
}
else
{
//Don't know what you want here.
_sb.AppendFormat("begins_with({0}, {1}))", "(obj)", arg);
}
}
return base.VisitMethodCall(expr);
}
}
note: it's a long post, please scroll to the bottom to see questions - hopefully that will make it easier to understand my problem. Thanks!
I have "Member" model which is defined as follows:
public class Member
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string ScreenName { get; set; }
[NotMapped]
public string RealName
{
get { return (FirstName + " " + LastName).TrimEnd(); }
}
[NotMapped]
public string DisplayName
{
get
{
return string.IsNullOrEmpty(ScreenName) ? RealName : ScreenName;
}
}
}
This is existing project, and model, and I don't want to change this. Now we got a request to enable profile retrieval by DisplayName:
public Member GetMemberByDisplayName(string displayName)
{
var member = this.memberRepository
.FirstOrDefault(m => m.DisplayName == displayName);
return member;
}
This code does not work because DisplayName is not mapped to a field in database. Okay, I will make an expression then:
public Member GetMemberByDisplayName(string displayName)
{
Expression<Func<Member, bool>> displayNameSearchExpr = m => (
string.IsNullOrEmpty(m.ScreenName)
? (m.Name + " " + m.LastName).TrimEnd()
: m.ScreenName
) == displayName;
var member = this.memberRepository
.FirstOrDefault(displayNameSearchExpr);
return member;
}
this works. The only problem is that business logic to generate display name is copy/pasted in 2 different places. I want to avoid this. But I do not understand how to do this. The best I came with is the following:
public class Member
{
public static Expression<Func<Member, string>> GetDisplayNameExpression()
{
return m => (
string.IsNullOrEmpty(m.ScreenName)
? (m.Name + " " + m.LastName).TrimEnd()
: m.ScreenName
);
}
public static Expression<Func<Member, bool>> FilterMemberByDisplayNameExpression(string displayName)
{
return m => (
string.IsNullOrEmpty(m.ScreenName)
? (m.Name + " " + m.LastName).TrimEnd()
: m.ScreenName
) == displayName;
}
private static readonly Func<Member, string> GetDisplayNameExpressionCompiled = GetDisplayNameExpression().Compile();
[NotMapped]
public string DisplayName
{
get
{
return GetDisplayNameExpressionCompiled(this);
}
}
[NotMapped]
public string RealName
{
get { return (FirstName + " " + LastName).TrimEnd(); }
}
}
Questions:
(1) How to reuse GetDisplayNameExpression() inside FilterMemberByDisplayNameExpression()? I tried Expression.Invoke:
public static Expression<Func<Member, bool>> FilterMemberByDisplayNameExpression(string displayName)
{
Expression<Func<string, bool>> e0 = s => s == displayName;
var e1 = GetDisplayNameExpression();
var combinedExpression = Expression.Lambda<Func<Member, bool>>(
Expression.Invoke(e0, e1.Body), e1.Parameters);
return combinedExpression;
}
but I get the following error from the provider:
The LINQ expression node type 'Invoke' is not supported in LINQ to
Entities.
(2) Is it a good approach to use Expression.Compile() inside DisplayName property? Any issues with it?
(3) How to move RealName logic inside GetDisplayNameExpression()? I think I have to create another expression and another compiled expression, but I do not understand how to CALL RealNameExpression from inside GetDisplayNameExpression().
Thank you.
I can fix your expression generator, and I can compose your GetDisplayNameExpression (so 1 and 3)
public class Member
{
public string ScreenName { get; set; }
public string Name { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public static Expression<Func<Member, string>> GetRealNameExpression()
{
return m => (m.Name + " " + m.LastName).TrimEnd();
}
public static Expression<Func<Member, string>> GetDisplayNameExpression()
{
var isNullOrEmpty = typeof(string).GetMethod("IsNullOrEmpty", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(string) }, null);
var e0 = GetRealNameExpression();
var par1 = e0.Parameters[0];
// Done in this way, refactoring will correctly rename m.ScreenName
// We could have used a similar trick for string.IsNullOrEmpty,
// but it would have been useless, because its name and signature won't
// ever change.
Expression<Func<Member, string>> e1 = m => m.ScreenName;
var screenName = (MemberExpression)e1.Body;
var prop = Expression.Property(par1, (PropertyInfo)screenName.Member);
var condition = Expression.Condition(Expression.Call(null, isNullOrEmpty, prop), e0.Body, prop);
var combinedExpression = Expression.Lambda<Func<Member, string>>(condition, par1);
return combinedExpression;
}
private static readonly Func<Member, string> GetDisplayNameExpressionCompiled = GetDisplayNameExpression().Compile();
private static readonly Func<Member, string> GetRealNameExpressionCompiled = GetRealNameExpression().Compile();
public string DisplayName
{
get
{
return GetDisplayNameExpressionCompiled(this);
}
}
public string RealName
{
get
{
return GetRealNameExpressionCompiled(this);
}
}
public static Expression<Func<Member, bool>> FilterMemberByDisplayNameExpression(string displayName)
{
var e0 = GetDisplayNameExpression();
var par1 = e0.Parameters[0];
var combinedExpression = Expression.Lambda<Func<Member, bool>>(
Expression.Equal(e0.Body, Expression.Constant(displayName)), par1);
return combinedExpression;
}
Note how I reuse the same parameter of the GetDisplayNameExpression expression e1.Parameters[0] (put in par1) so that I don't have to rewrite the expression (otherwise I would have needed to use an expression rewriter).
We could use this trick because we had only a single expression to handle, to which we had to attach some new code. Totally different (we would have needed an expression rewriter) would be the case of trying to combine two expressions (for example to do a GetRealNameExpression() + " " + GetDisplayNameExpression(), both require as a parameter a Member, but their parameters are separate... Probably this https://stackoverflow.com/a/5431309/613130 would work...
For the 2, I don't see any problem. You are correctly using static readonly. But please, look at GetDisplayNameExpression and think "is it better some business code duplication pay or that?"
Generic solution
Now... I was quite sure it was doable... and in fact it is doable: an expression "expander" that "expands" "special properties" to their Expression(s) "automagically".
public static class QueryableEx
{
private static readonly ConcurrentDictionary<Type, Dictionary<PropertyInfo, LambdaExpression>> expressions = new ConcurrentDictionary<Type, Dictionary<PropertyInfo, LambdaExpression>>();
public static IQueryable<T> Expand<T>(this IQueryable<T> query)
{
var visitor = new QueryableVisitor();
Expression expression2 = visitor.Visit(query.Expression);
return query.Expression != expression2 ? query.Provider.CreateQuery<T>(expression2) : query;
}
private static Dictionary<PropertyInfo, LambdaExpression> Get(Type type)
{
Dictionary<PropertyInfo, LambdaExpression> dict;
if (expressions.TryGetValue(type, out dict))
{
return dict;
}
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
dict = new Dictionary<PropertyInfo, LambdaExpression>();
foreach (var prop in props)
{
var exp = type.GetMember(prop.Name + "Expression", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static).Where(p => p.MemberType == MemberTypes.Field || p.MemberType == MemberTypes.Property).SingleOrDefault();
if (exp == null)
{
continue;
}
if (!typeof(LambdaExpression).IsAssignableFrom(exp.MemberType == MemberTypes.Field ? ((FieldInfo)exp).FieldType : ((PropertyInfo)exp).PropertyType))
{
continue;
}
var lambda = (LambdaExpression)(exp.MemberType == MemberTypes.Field ? ((FieldInfo)exp).GetValue(null) : ((PropertyInfo)exp).GetValue(null, null));
if (prop.PropertyType != lambda.ReturnType)
{
throw new Exception(string.Format("Mismatched return type of Expression of {0}.{1}, {0}.{2}", type.Name, prop.Name, exp.Name));
}
dict[prop] = lambda;
}
// We try to save some memory, removing empty dictionaries
if (dict.Count == 0)
{
dict = null;
}
// There is no problem if multiple threads generate their "versions"
// of the dict at the same time. They are all equivalent, so the worst
// case is that some CPU cycles are wasted.
dict = expressions.GetOrAdd(type, dict);
return dict;
}
private class SingleParameterReplacer : ExpressionVisitor
{
public readonly ParameterExpression From;
public readonly Expression To;
public SingleParameterReplacer(ParameterExpression from, Expression to)
{
this.From = from;
this.To = to;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node != this.From ? base.VisitParameter(node) : this.Visit(this.To);
}
}
private class QueryableVisitor : ExpressionVisitor
{
protected static readonly Assembly MsCorLib = typeof(int).Assembly;
protected static readonly Assembly Core = typeof(IQueryable).Assembly;
// Used to check for recursion
protected readonly List<MemberInfo> MembersBeingVisited = new List<MemberInfo>();
protected override Expression VisitMember(MemberExpression node)
{
var declaringType = node.Member.DeclaringType;
var assembly = declaringType.Assembly;
if (assembly != MsCorLib && assembly != Core && node.Member.MemberType == MemberTypes.Property)
{
var dict = QueryableEx.Get(declaringType);
LambdaExpression lambda;
if (dict != null && dict.TryGetValue((PropertyInfo)node.Member, out lambda))
{
// Anti recursion check
if (this.MembersBeingVisited.Contains(node.Member))
{
throw new Exception(string.Format("Recursively visited member. Chain: {0}", string.Join("->", this.MembersBeingVisited.Concat(new[] { node.Member }).Select(p => p.DeclaringType.Name + "." + p.Name))));
}
this.MembersBeingVisited.Add(node.Member);
// Replace the parameters of the expression with "our" reference
var body = new SingleParameterReplacer(lambda.Parameters[0], node.Expression).Visit(lambda.Body);
Expression exp = this.Visit(body);
this.MembersBeingVisited.RemoveAt(this.MembersBeingVisited.Count - 1);
return exp;
}
}
return base.VisitMember(node);
}
}
}
How does it work? Magic, reflection, fairy dust...
Does it support properties referencing other properties? Yes
What does it need?
It needs that every "special" property of name Foo has a corresponding static field/static property named FooExpression that returns an Expression<Func<Class, something>>
It needs that the query is "transformed" through the extension method Expand() at some point before the materialization/enumeration. So:
public class Member
{
// can be private/protected/internal
public static readonly Expression<Func<Member, string>> RealNameExpression =
m => (m.Name + " " + m.LastName).TrimEnd();
// Here we are referencing another "special" property, and it just works!
public static readonly Expression<Func<Member, string>> DisplayNameExpression =
m => string.IsNullOrEmpty(m.ScreenName) ? m.RealName : m.ScreenName;
public string RealName
{
get
{
// return the real name however you want, probably reusing
// the expression through a compiled readonly
// RealNameExpressionCompiled as you had done
}
}
public string DisplayName
{
get
{
}
}
}
// Note the use of .Expand();
var res = (from p in ctx.Member
where p.RealName == "Something" || p.RealName.Contains("Anything") ||
p.DisplayName == "Foo"
select new { p.RealName, p.DisplayName, p.Name }).Expand();
// now you can use res normally.
Limits 1: one problem is with methods like Single(Expression), First(Expression), Any(Expression) and similar, that don't return an IQueryable. Change by using first a Where(Expression).Expand().Single()
Limit 2: "special" properties can't reference themselves in cycles. So if A uses B, B can't use A, and tricks like using ternary expressions won't make it work.
Recently I was faced with the need to keep some business logic in expressions which allow to use it in SQL queries and in .net code. I've moved some code which help with this to github repo. I've implemented the easy way to combine and reuse expressions. See my example:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public Company Company { get; set; }
public static Expression<Func<Person, string>> FirstNameExpression
{
get { return x => x.FirstName; }
}
public static Expression<Func<Person, string>> LastNameExpression
{
get { return x => x.LastName; }
}
public static Expression<Func<Person, string>> FullNameExpression
{
//get { return FirstNameExpression.Plus(" ").Plus(LastNameExpression); }
// or
get { return x => FirstNameExpression.Wrap(x) + " " + LastNameExpression.Wrap(x); }
}
public static Expression<Func<Person, string>> SearchFieldExpression
{
get
{
return
p => string.IsNullOrEmpty(FirstNameExpression.Wrap(p)) ? LastNameExpression.Wrap(p) : FullNameExpression.Wrap(p);
}
}
public static Expression<Func<Person, bool>> GetFilterExpression(string q)
{
return p => SearchFieldExpression.Wrap(p) == q;
}
}
Extension method .Wrap() is the marker only:
public static TDest Wrap<TSource, TDest>(this Expression<Func<TSource, TDest>> expr, TSource val)
{
throw new NotImplementedException("Used only as expression transform marker");
}
What is FullName? It's FirstName + " " + LastName where FirstName and LastName - strings. But we have expressions, it's not a real value and we need to combine these expressions. Method .Wrap(val) help us to move to a simple code. We don't need to write any composers or other visitors for expressions. All this magic is already done by method .Wrap(val) where val - parameter which will be passed to called lambda expression.
So we describe expressions using other expressions. To get the full expression need to expand all usages of Wrap method, so you need to call method Unwrap on Expression (or IQueryable).
See sample:
using (var context = new Entities())
{
var originalExpr = Person.GetFilterExpression("ivan");
Console.WriteLine("Original: " + originalExpr);
Console.WriteLine();
var expr = Person.GetFilterExpression("ivan").Unwrap();
Console.WriteLine("Unwrapped: " + expr);
Console.WriteLine();
var persons = context.Persons.Where(Person.GetFilterExpression("ivan").Unwrap());
Console.WriteLine("SQL Query 1: " + persons);
Console.WriteLine();
var companies = context.Companies.Where(x => x.Persons.Any(Person.GetFilterExpression("abc").Wrap())).Unwrap(); // here we use .Wrap method without parameters, because .Persons is the ICollection (not IQueryable) and we can't pass Expression<Func<T, bool>> as Func<T, bool>, so we need it for successful compilation. Unwrap method expand Wrap method usage and convert Expression to lambda function.
Console.WriteLine("SQL Query 2: " + companies);
Console.WriteLine();
var traceSql = persons.ToString();
}
Console output:
Original: p => (Person.SearchFieldExpression.Wrap(p) ==
value(QueryMapper.Exampl es.Person+<>c__DisplayClass0).q)
Unwrapped: p => (IIF(IsNullOrEmpty(p.FirstName), p.LastName,
((p.FirstName + " " ) + p.LastName)) ==
value(QueryMapper.Examples.Person+<>c__DisplayClass0).q)
SQL Query 1: SELECT [Extent1].[Id] AS [Id], [Extent1].[FirstName] AS
[FirstName], [Extent1].[LastName] AS [LastName], [Extent1].[Age] AS
[Age], [Extent1].[Company_Id] AS [Company_Id] FROM [dbo].[People] AS
[Extent1] WHERE (CASE WHEN (([Extent1].[FirstName] IS NULL) OR ((
CAST(LEN([Extent1].[Firs tName]) AS int)) = 0)) THEN
[Extent1].[LastName] ELSE [Extent1].[FirstName] + N' ' +
[Extent1].[LastName] END) = #p_linq_0
SQL Query 2: SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name]
FROM [dbo].[Companies] AS [Extent1] WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[People] AS [Extent2]
WHERE ([Extent1].[Id] = [Extent2].[Company_Id]) AND ((CASE WHEN (([Exten t2].[FirstName] IS NULL) OR ((
CAST(LEN([Extent2].[FirstName]) AS int)) = 0)) TH EN
[Extent2].[LastName] ELSE [Extent2].[FirstName] + N' ' +
[Extent2].[LastName] END) = #p_linq_0) )
So the main idea to use .Wrap() method to convert from Expression world to non-expression which provide easy way to reuse expressions.
Let me know if you need more explanations.
I have created a generic search extension method for IQueryable that enables you to search for a single property to see if a search term is contained within it.
http://jnye.co/Posts/6/c%23-generic-search-extension-method-for-iqueryable
I now want to enable the user to select multiple properties to search within each, matching if any property contains the text.
The code:
The user enters the following code to perform this search:
string searchTerm = "Essex";
context.Clubs.Search(searchTerm, club => club.Name, club => club.County)
//Note: If possible I would rather something closer to the following syntax...
context.Clubs.Search(club => new[]{ club.Name, club.County}, searchTerm);
// ... or, even better, something similar to this...
context.Clubs.Search(club => new { club.Name, club.County}, searchTerm);
This will return any golf club with 'Essex' in the Name or as the County.
public static IQueryable<TSource> Search<TSource>(this IQueryable<TSource> source, string searchTerm, params Expression<Func<TSource, string>>[] stringProperties)
{
if (String.IsNullOrEmpty(searchTerm))
{
return source;
}
// The lamda I would like to reproduce:
// source.Where(x => x.[property1].Contains(searchTerm)
// || x.[property2].Contains(searchTerm)
// || x.[property3].Contains(searchTerm)...)
//Create expression to represent x.[property1].Contains(searchTerm)
var searchTermExpression = Expression.Constant(searchTerm);
//Build parameters
var parameters = stringProperties.SelectMany(prop => prop.Parameters);
Expression orExpression = null;
//Build a contains expression for each property
foreach (var stringProperty in stringProperties)
{
var checkContainsExpression = Expression.Call(stringProperty.Body, typeof(string).GetMethod("Contains"), searchTermExpression);
if (orExpression == null)
{
orExpression = checkContainsExpression;
}
//Build or expression for each property
orExpression = Expression.OrElse(orExpression, checkContainsExpression);
}
var methodCallExpression = Expression.Call(typeof(Queryable),
"Where",
new Type[] { source.ElementType },
source.Expression,
Expression.Lambda<Func<TSource, bool>>(orExpression, parameters));
return source.Provider.CreateQuery<TSource>(methodCallExpression);
}
The error
If I change the number of parameters supplied to 1:
Expression.Lambda<Func<TSource, bool>>(orExpression, parameters.First()));
I get a new error:
UPDATE
I have written a post on the work discussed in this question. Check it out on GitHub too.
Here we go; you were pretty close - as I noted in comments, the key piece here is to use ExpressionVisitor to re-write the trees in terms of the single parameter you want to keep:
using System;
using System.Linq;
using System.Linq.Expressions;
static class Program
{
static void Main()
{
var data = new[] { new Foo { A = "x1", B = "y1", C = "y1" }, new Foo { A = "y2", B = "y2", C = "y2" },
new Foo { A = "y3", B = "y3", C = "x3" } }.AsQueryable();
var result = data.Search("x", x => x.A, x => x.B, x => x.C);
foreach (var row in result)
{
Console.WriteLine("{0}, {1}, {2}", row.A, row.B, row.C);
}
}
class Foo
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
public class SwapVisitor : ExpressionVisitor
{
private readonly Expression from, to;
public SwapVisitor(Expression from, Expression to)
{
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node)
{
return node == from ? to : base.Visit(node);
}
public static Expression Swap(Expression body, Expression from, Expression to)
{
return new SwapVisitor(from, to).Visit(body);
}
}
public static IQueryable<TSource> Search<TSource>(this IQueryable<TSource> source, string searchTerm, params Expression<Func<TSource, string>>[] stringProperties)
{
if (String.IsNullOrEmpty(searchTerm))
{
return source;
}
if (stringProperties.Length == 0) return source.Where(x => false);
// The lamda I would like to reproduce:
// source.Where(x => x.[property1].Contains(searchTerm)
// || x.[property2].Contains(searchTerm)
// || x.[property3].Contains(searchTerm)...)
//Create expression to represent x.[property1].Contains(searchTerm)
var searchTermExpression = Expression.Constant(searchTerm);
var param = stringProperties[0].Parameters.Single();
Expression orExpression = null;
//Build a contains expression for each property
foreach (var stringProperty in stringProperties)
{
// re-write the property using the param we want to keep
var body = SwapVisitor.Swap(stringProperty.Body, stringProperty.Parameters.Single(), param);
var checkContainsExpression = Expression.Call(
body, typeof(string).GetMethod("Contains"), searchTermExpression);
if (orExpression == null)
{
orExpression = checkContainsExpression;
}
else
{ // compose
orExpression = Expression.OrElse(orExpression, checkContainsExpression);
}
}
var lambda = Expression.Lambda<Func<TSource, bool>>(orExpression, param);
return source.Where(lambda);
}
}
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
LINQ To SQL exception: Local sequence cannot be used in LINQ to SQL implementation of query operators except the Contains operator
I am trying the following query:
var data = (from bk in DataContext.Book
where ((searchArray.Count() == 0 || searchArray.ToList().Any(x => bk.Name.Contains(x))) ||
(searchArray.Count() == 0 || searchArray.ToList().Any(x => bk.Genre.Contains(x)))))
where searchArray is a Array containing the individual words that I want to search for, I split the string the user entered and put the results in this array. Whenever I try to run this I get the following error:
"Local sequence cannot be used in LINQ to SQL implementations of query operators except the Contains operator."
Can anyone tell me what I am doing wrong and what is the correct way to perform this search?
In a nutshell, I am trying to allow a user to enter a string like "Hello World" and for a query to be generated that will look for either hello or world or both. But, a user can enter any number of words.
The simplest option is probably to build the lambda expression by hand:
static class ContainsAny
{
private static readonly MethodInfo StringContains
= typeof(string).GetMethod("Contains", new[] { typeof(string) });
public static Builder<T> Words<T>(IEnumerable<string> words)
{
return new Builder<T>(words);
}
public static Builder<T> Words<T>(params string[] words)
{
return new Builder<T>(words);
}
public sealed class Builder<T>
{
private static readonly ParameterExpression Parameter
= Expression.Parameter(typeof(T), "obj");
private readonly List<Expression> _properties = new List<Expression>();
private readonly List<ConstantExpression> _words;
internal Builder(IEnumerable<string> words)
{
_words = words
.Where(word => !string.IsNullOrEmpty(word))
.Select(word => Expression.Constant(word))
.ToList();
}
public Builder<T> WithProperty(Expression<Func<T, string>> property)
{
if (_words.Count != 0)
{
_properties.Add(ReplacementVisitor.Transform(
property, property.Parameters[0], Parameter));
}
return this;
}
private Expression BuildProperty(Expression prop)
{
return _words
.Select(w => (Expression)Expression.Call(prop, StringContains, w))
.Aggregate(Expression.OrElse);
}
public Expression<Func<T, bool>> Build()
{
if (_words.Count == 0) return (T obj) => true;
var body = _properties
.Select(BuildProperty)
.Aggregate(Expression.OrElse);
return Expression.Lambda<Func<T, bool>>(body, Parameter);
}
}
private sealed class ReplacementVisitor : ExpressionVisitor
{
private ICollection<ParameterExpression> Parameters { get; set; }
private Expression Find { get; set; }
private Expression Replace { get; set; }
public static Expression Transform(
LambdaExpression source,
Expression find,
Expression replace)
{
var visitor = new ReplacementVisitor
{
Parameters = source.Parameters,
Find = find,
Replace = replace,
};
return visitor.Visit(source.Body);
}
private Expression ReplaceNode(Expression node)
{
return (node == Find) ? Replace : node;
}
protected override Expression VisitConstant(ConstantExpression node)
{
return ReplaceNode(node);
}
protected override Expression VisitBinary(BinaryExpression node)
{
var result = ReplaceNode(node);
if (result == node) result = base.VisitBinary(node);
return result;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (Parameters.Contains(node)) return ReplaceNode(node);
return Parameters.FirstOrDefault(p => p.Name == node.Name) ?? node;
}
}
}
With this code in place, you can call:
Expression<Func<Book, bool>> filter = ContainsAny
.Words<Book>(searchArray)
.WithProperty(book => book.Name)
.WithProperty(book => book.Genre)
.Build();
var data = DataContext.Book.Where(filter);
For example, if the searchArray contains { "Hello", "World" }, the generated lambda will be:
obj => (obj.Name.Contains("Hello") || obj.Name.Contains("World"))
|| (obj.Genre.Contains("Hello") || obj.Genre.Contains("World")))
If I understand what you're trying to do correctly, you should be able to condense your query down to:
from bk in DataContext.Book
where searchArray.Contains(bk.Name) || searchArray.Contains(bk.Genre)
select bk
This is basically equivalent to the SQL:
select bk.*
from Book bk
where bk.Name in (...) or bk.Genre in (...)
In your case you must combine interpreted and local queries which can hurt performance or use SQL CLR integration by creating CLR function on database.