Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Runtime Configuration of Where Condition LINQ for Objects

3.59/5 (3 votes)
6 Feb 2012CPOL3 min read 26.3K   119  
Allow runtime alteration of where conditions for LINQ for Objects queries in response to user input

Update April 2015

There's a much better solution to the problem of flexible where conditions in LINQ queries than the one outlined in this article. Have a look here:

Introduction

A problem I've encountered when using LINQ for Objects is creating queries that can deal easily with varying numbers of parameters obtained from a search dialogue. This short article outlines a partial solution to the problem.

Background

The solution is based on the simple replacement of a hard coded condition with a function. A basic LINQ query looks much as below:

C#
var query = from TestClass t in TestData
where t.Alpha == 10
select {t.Name}

This is ideal if all you want to do is test the Alpha property of TestClass objects. We can get a bit more flexibility using one or more Lambda functions and referencing the function that meets our needs.

C#
Func<TestClass, bool> filter = delegate(TestClass t)
                                     { return (t.Alpha >= 10 && t.Gamma >300); };

var query = from TestClass t in TestData
where (filter)
select {t.Name};

However, each function we define still hard codes the name and number of properties tested. This is not what I wanted.

Using the Code

There are only three classes.

325787/ClassDiagrams.png

Class Comment
Condition<T> Defines a where condition instance with zero or more Condition.Test definitions for one or more named properties for an object of type T.
Condition.Test Associates a string (text) or numeric test with a named property.
Comparison Provides the methods to apply the tests defined for an instance of class [Condition]

Class: [Condition]

Allows the programmer to group together a number of property tests either as an AND group or as an OR group. This collection of tests can be added to or completely re-initialised without affecting the LINQ query. The query can be re-run with different configurations of the Condition objects that were referenced in the query's definition.

Property Type Comment
Logic enumeration: TestLogic Determines how the tests within a condition are applied. AND or OR
Test List<Condition.Test> The tests to be applied

 

 

Method Returns Comment
C#
Evaluate<T> (T Instance)
bool Evaluates all defined Condition.Tests in the Condition instance using the default TestLogic. Called from within the LINQ object query.
C#
Evaluate<T> (T Instance, TestLogic)
bool Evaluates all defined Condition.Tests using the specified TestLogic. Called from within the LINQ object query.

 

 

Class: [Condition.Test]

This allows the programmer to associate a property name with a test appropriate to the property's type and a value to test against.

Property Type Comment
Compare object - enumeration
  • Comparison.Number
  • Comparison.Text
The test to be applied to the property name: >, = < etc.
CompareTo numeric object or string The numeric or string value to test the property against. Numeric values are all cast to double when tested.

Class: [Comparison]

Uses reflection and some simple switch{} statements to carry out a test defined by a Condition.Test or an instance of an object.

Method Returns Comment
C#
Test<T>
			(T Instance,
			string propertyName,
			Comparison.Text comparison,
			string compareTo)
bool Applies a string test to a named property of an instance.
C#
Test<T>
			(T Instance,
			string propertyName,
			Comparison.Number comparison,
			object compareTo)
bool Applies a numeric test to a named property of an instance.

Setting up a Condition

Although the example has the property names and test values hard coded, I hope it is apparent that the tests added to each condition can be defined in response to user input taken from a search dialogue or the result of some other change in program state.

Tests can either be applied as an OR group or as an AND group. If not specified, the default behaviour is AND.

C#
// Create a condition containing multiple tests that apply to an object
// of type TestClass
// Tests within the condition will be applied using logical OR.
Condition<TestClass> whereCondition = new Condition<TestClass>() {Logic = TestLogic.Or} ;

whereCondition.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Alpha",
                                                       Compare = Comparison.Number.GreaterEqual,
                                                       CompareTo = 10});
whereCondition.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Gamma",
                                                       Compare = Comparison.Number.Greater,
                                                       CompareTo = 300});
whereCondition.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Name",
                                                       Compare = Comparison.Text.Contains,
                                                       CompareTo = "D"});

Define the Query...

C#
var query = from TestClass t in TestClass.testdata()
where (whereCondition.Evaluate<TestClass>(t))
select new {t.Name};

...and Run It

C#
// Run the query with the where Condition in its initial state.
foreach (var t in query)
Console.WriteLine(t.Name);

Having separated the where logic setup from the query definition, we can now re-run the query as many times as we like altering the where condition as the fancy takes us...

Swapping to logical AND...

C#
whereCondition.Logic = TestLogic.And;
foreach (var t in query)
  Console.WriteLine(t.Name);

Completely Changing the Condition...

C#
whereCondition.Tests.Clear();
whereCondition.Logic = TestLogic.Or;
whereCondition.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Alpha",
                                                       Compare = Comparison.Number.GreaterEqual,
                                                       CompareTo = 100});
whereCondition.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Beta",
                                                       Compare = Comparison.Number.LessEqual,
                                                       CompareTo = 20});

foreach (var t in query)
  Console.WriteLine(t.Name);

Or have no tests at all...

C#
whereCondition.Tests.Clear();

foreach (var t in query)
  Console.WriteLine(t.Name);

Combining Multiple Conditions

C#
// Set up  (t.Alpha >= 100 || t.Beta <= 20) && t.Gamma >= 30

whereCondition.Logic = TestLogic.Or;
whereCondition.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Alpha",
                                                       Compare = Comparison.Number.GreaterEqual,
                                                       CompareTo = 100});
whereCondition.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Beta",
                                                       Compare = Comparison.Number.LessEqual,
                                                       CompareTo = 20});

// ..define an additional condition.
Condition<TestClass> supplementary = new Condition<TestClass>();
supplementary.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Gamma",
                                                       Compare = Comparison.Number.GreaterEqual,
                                                       CompareTo = 30});


// ...and redefine the query to use both conditions.

query = from TestClass t in TestClass.testdata()
      where (whereCondition.Evaluate<TestClass>(t) &&
             supplementary.Evaluate<TestClass>(t))
      select new {t.Name};

foreach (var t in query)
  Console.WriteLine(t.Name);

Pros & Cons

Pros

  • Where condition definition independent of query definition
  • We open up the possibility of persisting and retrieving where condition definitions

Cons

  • Quite a bit more typing required to set up where conditions.
  • As written only suitable for properties representing string or numeric types. Date comparisons might be nice.
  • Code not so transparent and therefore may be more difficult to maintain.
  • There _must_ be a performance penalty.
  • There's probably a more elegant solution out there, I just haven't come across it. Yet.

History

  • February 2012 - First (and probably final) cut

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)