Hamed F
Hamed F

Reputation: 840

Expression.Like in C#

eg: x=> x.Name = "g"

I have code block like this

public Expression<Func<TEntity, bool>> SearchExpression()
{
    var c = new ConstantExpression[_paramList.Count];
    var b = new BinaryExpression[_paramList.Count];
    BinaryExpression comparisonExpression = null;

    var entity = Expression.Parameter(typeof(TEntity));

    for (int i = 0; i < _paramList.Count; i++)
    {
        var value = Convert.ChangeType(_paramList[i].Item2 /*"g"*/, _paramList[i].Item3 /*System.String*/);
        c[i] = Expression.Constant(value); //"g"

        // PROBLEM IS HERE
        b[i] = Expression.Equal(Expression.Property(entity, _paramList[i].Item1 /*Name*/, c[i]);
        // PROBLEM IS HERE



    }
    _paramList.Clear();
    comparisonExpression = b.Aggregate(Expression.And);
    return Expression.Lambda<Func<TEntity, bool>>(comparisonExpression, entity);
}

works like charm but I need Expression.Like (Like "g" not Equal "g")

Expression.Like(Expression.Property(entity, _paramList[i].Item1), c[i])

but C# expression tree does not support Like method

UPDATE :

I wrote something like this :

Expression.Call(Expression.Property(entity, _paramList[i].Item1),
                typeof(String).GetMethod("Contains"), new Expression[] { c[i] });  

but I need BinaryExpression not MethodCallExpression

Upvotes: 11

Views: 6347

Answers (3)

Sean
Sean

Reputation: 62522

I've done this in a scripting language I wrote, which allows you to say things like name like 'bob%'. The trick is that you need to map it to a method call which takes the value and regular expression and call this from within the Expression.

If you take a look at the LikeEvaluator class in my Wire scripting language you'll see how I did it:

static class LikeEvaluator
{
    private static readonly MethodInfo ApplyLikeMethodInfo=typeof(LikeEvaluator).GetMethod("ApplyLike");
    private static readonly MethodInfo ApplyLikeNoCaseMethodInfo=typeof(LikeEvaluator).GetMethod("ApplyLikeNoCase");

    public static Expression Like(CaseMode caseMode, Expression lhs, Expression pattern)
    {
        Expression x=null;

        if(caseMode==CaseMode.Sensitive)
        {
            x=Expression.Call(ApplyLikeMethodInfo,lhs,pattern);
        }
        else
        {
            x=Expression.Call(ApplyLikeNoCaseMethodInfo,lhs,pattern);
        }

        return x;
    }

    public static bool ApplyLike(string text, string likePattern)
    {
        string pattern=PatternToRegex(likePattern);
        return Regex.IsMatch(text,pattern,RegexOptions.None);
    }

    public static bool ApplyLikeNoCase(string text, string likePattern)
    {
        string pattern=PatternToRegex(likePattern);
        return Regex.IsMatch(text,pattern,RegexOptions.IgnoreCase);
    }

    public static string PatternToRegex(string pattern)
    {
        pattern=Regex.Escape(pattern);
        pattern=pattern.Replace("%",@".*");
        pattern=string.Format("^{0}$",pattern);

        return pattern;
    }
}

Upvotes: 2

Roger Johansson
Roger Johansson

Reputation: 23214

You can make your code work by adding an equals expression over the method call, like so:

    b[i] = Expression.Equal(
        Expression.Call(Expression.Property(entity, _paramList[i].Item1),
        typeof (String).GetMethod("Contains"), 
          new Expression[] {c[i]}), Expression.Constant(true));

In pseudo code this reads as:

b[i] = entity => entity.someProperty.Contains(c[i]) == true;

Which will return a binary expression for you.

Upvotes: 10

Bas
Bas

Reputation: 27105

This answer does not consider your array and the 'and' aggregation, but this should be considered as a separate issue.

Consider this class:

class MyEntity { string Name { get; set; } }

We want to query:

select ... from MyEntity where Name like '%query%';

The following method is a general implementation of the above query pattern:

static Expression<Func<TEntity, bool>> Like<TEntity>(string propertyName, string queryText)
{
    var parameter = Expression.Parameter(typeof (TEntity), "entity");
    var getter = Expression.Property(parameter, propertyName);
    //ToString is not supported in Linq-To-Entities, throw an exception if the property is not a string.
    if (getter.Type != typeof (string))
        throw new ArgumentException("Property must be a string");
    //string.Contains with string parameter.
    var stringContainsMethod = typeof (string).GetMethod("Contains", new[] {typeof (string)});
    var containsCall = Expression.Call(getter, stringContainsMethod,
        Expression.Constant(queryText, typeof (string)));

    return Expression.Lambda<Func<TEntity, bool>>(containsCall, parameter);
}

If you want to have a pattern of query% or %query you can use string.StartsWith and string.EndsWith instead of Contains.

Also, you can share the parameter across multiple calls if you adjust the signature.

The current implementation throws an exception if the data type of the property is not a string. Look at this answer https://stackoverflow.com/a/3292773/668272 for converting numbers to strings.

Upvotes: 6

Related Questions