Search code examples
c#linq-to-sql

How do I overload LINQ string.Contains()?


For a generic filter I am implementing, I need to modify the following to accept the column name which will be searched in the Where method, something like:

        public IQueryable<TEntity> GetEntities(string val)
        {
            TEntity entity = _DbContext.Set<TEntity>()
                 .Where(e => e.Col1.Contains(val));
            return entities;
        }

to be changed to

        public IQueryable<TEntity> GetEntities(string val, string colName)
        {
            TEntity entity = _DbContext.Set<TEntity>()
                 .WhereContains(val, colName);
            return entities;
        }

colName is the name of a string column.

I looked at https://blog.jeremylikness.com/blog/dynamically-build-linq-expressions/ but could not modify the example there for my needs. The answer should be in the form of

public static IQueryable<TEntity> WhereContains<TEntity>(this IQueryable<TEntity> query, string value, string colName)
                    where TEntity : class
{
...
...
}

But I cant make it work...


Solution

  • OK, found a good reference and was able to modify it:

    public static IQueryable<T> TextFilter<T>(IQueryable<T> source, string[] colNames, string[] terms)
    {
        if (colNames.Length == 0) return source;
    
        // T is a compile-time placeholder for the element type of the query.
        Type elementType = typeof(T);
    
        // Get all the properties on this specific type for colNames.
        List<PropertyInfo> props = new List<PropertyInfo>();
        for (int i = 0; i < colNames.Length; i++)
        {
            PropertyInfo prop = elementType.GetProperties().Where(x => x.PropertyType == typeof(string) && x.Name.ToLower() == colNames[i].ToLower()).FirstOrDefault();
            if (prop == null) { return source; }
            props.Add(prop);
        }
    
        // Get the right overload of String.Contains.  Can be replaced e.g. with "Contains"
        MethodInfo containsMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!;
    
        // Create a parameter for the expression tree:
        // the 'x' in 'x => x.PropertyName.Contains("term")'
        // The type of this parameter is the query's element type
        ParameterExpression prm = Expression.Parameter(elementType);
    
        // Map each property to an expression tree node
        List<Expression> expressions = new List<Expression>();
        for (int i = 0; i < colNames.Length; i++)
        {
            expressions.Add(
                Expression.Call(
                    Expression.Property(
                        prm,
                        props.ElementAt(i)
                    ),
                    containsMethod,
                    Expression.Constant(terms[i])
                )
            );
        }
    
        // Combine all the resultant expression nodes using ||
        Expression body = expressions
            .Aggregate(
                (prev, current) => Expression.And(prev, current)
            );
    
        // Wrap the expression body in a compile-time-typed lambda expression
        Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(body, prm);
    
        // Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
        return source.Where(lambda);
    }
    

    I was not able to make this work as an extension, so calling it is done with:

    var qry= QueryableExtensions.TextFilter(_crudApiDbContext.Set<TEntity>()
        .Where(entity => entity.someColumn==someValue),
        filters.Keys.ToArray(), filters.Values.ToArray());
    List<TEntity> entities = await qry
                .Skip(pageSize * (page - 1)).Take(pageSize).ToListAsync();