8

I want to accept a string array of where conditions from the client like field == value. It would be really nice to create a specification object that could accept the string in the constructor and output a lambda expression to represent the Where clause. For example, I could do the following:

var myCondition = new Specification<Product>( myStringArrayOfConditions); 
var myProducts = DB.Products.Where( myCondition);

How could you turn "name == Jujyfruits" into
DB.Products.Where(p => p.name == "JujyFruits")?

Zachary Scott
  • 20,968
  • 35
  • 123
  • 205
  • Why can't you pass a lambda into the method, so instead of passing the string .Where("name == Jujyfruits") you pass in .Where(x => x.name == "Jujyfruits")? I don't really know what you're trying to do. – Phill May 02 '11 at 05:38
  • If the "specification" (where clause) comes from the client, it will be in the form of a string. I would need to convert it in to something EF could understand, which is normally a lambda expression. – Zachary Scott May 03 '11 at 01:02

3 Answers3

14

You can use

  • Reflection to get the Property Product.name from the string name and
  • the LINQ Expression class to manually create a lambda expression.

Note that the following code example will only work for Equals (==) operations. However, it is easy to generalize to other operations as well (split on whitespace, parse the operator and choose the appropriate Expression instead of Expression.Equal).

    var condition = "name == Jujyfruits";

    // Parse the condition
    var c = condition.Split(new string[] { "==" }, StringSplitOptions.None);
    var propertyName = c[0].Trim();
    var value = c[1].Trim();

    // Create the lambda
    var arg = Expression.Parameter(typeof(Product), "p");
    var property = typeof(Product).GetProperty(propertyName);
    var comparison = Expression.Equal(
        Expression.MakeMemberAccess(arg, property),
        Expression.Constant(value));
    var lambda = Expression.Lambda<Func<Product, bool>>(comparison, arg).Compile();

    // Test
    var prod1 = new Product() { name = "Test" };
    var prod2 = new Product() { name = "Jujyfruits" };
    Console.WriteLine(lambda(prod1));  // outputs False
    Console.WriteLine(lambda(prod2));  // outputs True

About the constructor thing: Since Func<T, TResult> is sealed, you cannot derive from it. However, you could create an implicit conversion operator that translates Specification<T> into Func<T, bool>.

Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • So you could do something like Products.Where( p => lambda(p))? – Zachary Scott May 02 '11 at 05:39
  • 1
    @Dr. Zim: Actually, `Products.Where(lambda)` should be sufficient. – Heinzi May 02 '11 at 05:50
  • 1
    If you're going to go down this route, consider caching the delegate (var lambda). There is a reasonable performance hit from compiling expression trees and you definitely wouldn't want to do it prior to each search query. – AnteSim May 02 '11 at 06:36
  • 1
    @AnteSim: That depends on how you define "reasonable". Compiling the lambda in my example above takes ~5ms on my machine. I guess the database query part of the search takes significantly longer... – Heinzi May 02 '11 at 08:33
  • 1
    @Heinzi: What happens when he adapts this code to filter the search based on multiple properties? In isolation, your code is fine. But you just need to be aware that this is a point of optimisation should the need arise. – AnteSim May 02 '11 at 21:40
  • Just found http://msdn.microsoft.com/en-us/library/z5z9kes2%28VS.71%29.aspx, which should allow me to "convert" the where string in the specification object to the complied lambda expression implicitly. – Zachary Scott May 03 '11 at 04:10
  • @Dr. Zim: Thanks for the link, I've added it to my answer. – Heinzi May 03 '11 at 06:43
  • I also found http://linqspecs.codeplex.com/ which is similar to what I am trying to do here. ( I think this code is much better.) – Zachary Scott May 06 '11 at 14:42
  • Instead of Expression.Equal, how could I figure out how to translate `p => p.Contains("something")`? Is there a way to create a lambda delegate, then analyze how it is created? – Zachary Scott May 16 '11 at 04:51
  • I guess you mean `p.name.Contains("something")`. That's two member accesses (`MakeMemberAccess`): First, the `name` property of the class `Product`, then the `Contains` method of the class `String`. – Heinzi May 16 '11 at 08:03
3

We've recently discovered the Dynamic LINQ library from the VS2008 sample projects. Works perfectly to turn string based "Where" clauses into expressions.

This link will get you there.

Jim Ross
  • 126
  • 4
  • It definitely uses a string in the where clause. It's a good answer. However, it produces a side effect where it changes the data type. – Zachary Scott May 03 '11 at 02:06
0

You need to turn your search term into a predicate. Try something like the following:

string searchString = "JujyFruits";
Func<Product, bool> search = new Func<Product,bool>(p => p.name == searchString);

return DB.Products.Where(search);
Paul Suart
  • 6,505
  • 7
  • 44
  • 65
  • Would there be a way around the big switch statement testing the name of every property of Product? Could you get a list of the "primitive" properties of Product, test against the field magic string, and construct a lambda expression where p.name is expressed as a magic string? – Zachary Scott May 02 '11 at 05:17
  • 1
    Hmm, I'm not sure this is trivial. See this question: http://stackoverflow.com/q/714799/68432 – Paul Suart May 02 '11 at 05:26
  • http://linqspecs.codeplex.com/ is very much like your answer, which reinforces that your method is probably the generally accepted approach of making specifications first class citizens. – Zachary Scott May 06 '11 at 14:46