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:
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.
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.
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 |
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. |
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 |
Test<T>
(T Instance,
string propertyName,
Comparison.Text comparison,
string compareTo)
| bool | Applies a string test to a named property of an instance. |
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
.
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...
var query = from TestClass t in TestClass.testdata()
where (whereCondition.Evaluate<TestClass>(t))
select new {t.Name};
...and Run It
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...
whereCondition.Logic = TestLogic.And;
foreach (var t in query)
Console.WriteLine(t.Name);
Completely Changing the Condition...
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...
whereCondition.Tests.Clear();
foreach (var t in query)
Console.WriteLine(t.Name);
Combining Multiple Conditions
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});
Condition<TestClass> supplementary = new Condition<TestClass>();
supplementary.Tests.Add(new Condition<TestClass>.Test() {PropertyName="Gamma",
Compare = Comparison.Number.GreaterEqual,
CompareTo = 30});
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