I saw a question this morning (Query my model on a range of values) that seems to be answered by (https://stackoverflow.com/a/1447926/195550), but the whole situation there peeked my interest in a more generalized solution.
I was hoping to be able to use Jon Skeet's answer to implement a Between
that would work with string keys in a non-SQL generated environment, but it appears that the fact that string does not implement the GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual operators gets in the way of Linq being able to build the Expression trees necessary to do this.
I realize that it is possible to just do query using the the
CompareTo
methods to accomplish this task, but I really like the elegance of thequery.Between(v=>v.StringKey, "abc", "hjk")
expression.I've looked through the System.Linq.Expression assembly, and saw that it is looking for a method named 'op_GreaterThan', for example for the GreaterThan operation, but I don't know
- Whether I can implement this for a string (knowing that I can't extend the actual '>' operator for strings)
- How to get the correct method signature built.
I created the following example and tests that shows where the Between extension method doesn't work on a string key.
It would be quite elegant if that could be implemented for string keys. Anybody have any suggestions, or insights on how to accomplish this?
Between operator from Jon Skeet, with inclusive flag added
public static class BetweenExtension
{
public static IQueryable<TSource> Between<TSource, TKey>(
this IQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
TKey low,
TKey high,
bool inclusive = true) where TKey : IComparable<TKey>
{
var key = Expression.Invoke(keySelector, keySelector.Parameters.ToArray());
var lowerBound = (inclusive)
? Expression.GreaterThanOrEqual(key, Expression.Constant(low))
: Expression.GreaterThan(key, Expression.Constant(low));
var upperBound = (inclusive)
? Expression.LessThanOrEqual(key, Expression.Constant(high))
: Expression.LessThan(key, Expression.Constant(high));
var and = Expression.AndAlso(lowerBound, upperBound);
var lambda = Expression.Lambda<Func<TSource, bool>>(
and, keySelector.Parameters);
return source.Where(lambda);
}
}
Working Test of the above using an "int" key
[TestFixture]
public class BetweenIntTests
{
public class SampleEntityInt
{
public int SampleSearchKey { get; set; }
}
private IQueryable<SampleEntityInt> BuildSampleEntityInt(params int[] values)
{
return values.Select(
value =>
new SampleEntityInt() { SampleSearchKey = value }).AsQueryable();
}
[Test]
public void BetweenIntInclusive()
{
var sampleData = BuildSampleEntityInt(1, 3, 10, 11, 12, 15);
var query = sampleData.Between(s => s.SampleSearchKey, 3, 10);
Assert.AreEqual(2, query.Count());
}
[Test]
public void BetweenIntNotInclusive()
{
var sampleData = BuildSampleEntityInt(1, 3, 10, 11, 12, 15);
var query = sampleData.Between(s => s.SampleSearchKey, 2, 11, false);
Assert.AreEqual(2, query.Count());
}
}
Non Working Test of the above using a "string" key
[TestFixture]
public class BetweenStringsTests
{
public class SampleEntityString
{
public string SampleSearchKey { get; set; }
}
private IQueryable<SampleEntityString> BuildSampleEntityString(params int[] values)
{
return values.Select(
value =>
new SampleEntityString() {SampleSearchKey = value.ToString() }).AsQueryable();
}
[Test]
public void BetweenStringInclusive()
{
var sampleData = BuildSampleEntityString(1, 3, 10, 11, 12, 15);
var query = sampleData.Between(s => s.SampleSearchKey, "3", "10");
Assert.AreEqual(2, query.Count());
}
[Test]
public void BetweenStringNotInclusive()
{
var sampleData = BuildSampleEntityString(1, 3, 10, 11, 12, 15);
var query = sampleData.Between(s => s.SampleSearchKey, "2", "11", false);
Assert.AreEqual(2, query.Count());
}
}