Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assistance with refactoring a LINQ method

Tags:

c#

asp.net

linq

I am in need of some assistance, I have a method which is repeated 6 times in my class, and the only thing that changes in each of the methods is the LINQ property (in the example "first name", but I also have one for last name, firm name, user id, status). I would like some help refactoring this so that I can use just one method and make the property to be dynamic or passed in.

private static IQueryable<MyModel> FilterFirstName(IQueryable<MyModel> query, string searchText, string searchFilter)
    {
        switch (searchFilter.ToLower())
        {
            case "contains":
                query = query.Where(x => x.FirstName.ToLower().Contains(searchText.ToLower()));
                break;
            case "does not contain":
                query = query.Where(x => !x.FirstName.ToLower().Contains(searchText.ToLower()));
                break;
            case "starts with":
                query = query.Where(x => x.FirstName.StartsWith(searchText, StringComparison.InvariantCultureIgnoreCase));
                break;
            case "ends with":
                query = query.Where(x => x.FirstName.EndsWith(searchText, StringComparison.InvariantCultureIgnoreCase));
                break;
            case "equals":
                query = query.Where(x => x.FirstName.Equals(searchText, StringComparison.InvariantCultureIgnoreCase));
                break;
        }

        return query;
    }
like image 665
Madhatter5501 Avatar asked Nov 26 '22 20:11

Madhatter5501


1 Answers

What you can do is use a Compose method that can compose one expression with another:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

Which uses the following method to replace all instances of one expression with another:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

Now you can write:

private static IQueryable<MyModel> FilterFirstName(
    IQueryable<MyModel> query,
    Expression<Func<MyModel, string>> selector,
    string searchText,
    string searchFilter)
{
    switch (searchFilter.ToLower())
    {
        case "contains":
            query = query.Where(selector.Compose(
                text => text.ToLower().Contains(searchText.ToLower())));
            break;
        case "does not contain":
            query = query.Where(selector.Compose(
                text => !text.ToLower().Contains(searchText.ToLower())));
            break;
        case "starts with":
            query = query.Where(selector.Compose(
                text => text.StartsWith(searchText, StringComparison.InvariantCultureIgnoreCase)));
            break;
        case "ends with":
            query = query.Where(selector.Compose(
                text => text.EndsWith(searchText, StringComparison.InvariantCultureIgnoreCase)));
            break;
        case "equals":
            query = query.Where(selector.Compose(
                text => text.Equals(searchText, StringComparison.InvariantCultureIgnoreCase)));
            break;
    }

    return query;
}

On a side note, you should really use an enum to represent the different types of filters for searchFilter, rather than a string. It will make it much easier on the caller, as they won't need to type out an exact string without having any good way of knowing what the exact options are, or if the provided option is valid.

like image 97
Servy Avatar answered Dec 05 '22 00:12

Servy