4

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 Betweenthat 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 the query.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

    1. Whether I can implement this for a string (knowing that I can't extend the actual '>' operator for strings)
    2. 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());
    }
}
Community
  • 1
  • 1
scott-pascoe
  • 1,463
  • 1
  • 13
  • 31
  • Why not just modify the `Between` method to handle strings as a special case (and invoke `CompareTo` instead of the greater-than/less-than operators)? – Kirk Woll Feb 15 '13 at 18:15
  • I would have a special-case based on typeof(TKey)==typeof(string) and use CompareTo. Or frankly you could manually check for the operators, and then check for a CompareTo for a more generic solution – Marc Gravell Feb 15 '13 at 18:20

1 Answers1

3

You have to invoke the string.CompareTo method as part of the expression tree. You can then test its result. To see how that might look like, look at the following value in the debugger:

Expression<<Func<string, bool>> filter = str => str.CompareTo("abc") > 0;
Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
usr
  • 168,620
  • 35
  • 240
  • 369
  • I understand what your example statement is saying and thought I might be able to do something like `Expression<> lowCheck = str => str.CompareTo(low);` to get an expression that I could then run through the GreaterThan check, but that gave me an error that GreaterThan is not implemented for Expression<> -- obviously, but my brain is missing the link to success. Could you possibly elaborate on what you were suggesting? – scott-pascoe Feb 21 '13 at 15:44
  • You have to pick `lowCheck` apart and drill down to the `CompareTo` call. Right now it seems you try to use the entire `lowCheck` which is much more than just the method call - it is an entire function including parameters (and different parameters!). The best approach is to create the method call directly using `Expression.Call`. – usr Feb 21 '13 at 19:09