Dynamic Expression Builder with EF Core



I was recently working on an ASP.NET Core MVC project where the end user wanted to have a search form with several fields with drop down boxes and a couple of free-form text fields. One of the requirements was to be able to put "*" at the front or the end of a search term in the text boxes to indicate a wildcard search. That was the way a previous application worked and they wanted to maintain the same functionality.
Since I was working with Entity Framework Core using a generic repository pattern, I wanted to be able to dynamically build the filter expression that I passed to the repository. I created a static Expression builder class with static methods for creating each type of expression needed.
public static class ExpressionBuilder
    {
        public static Expression GetStringExpression(string dataModelProperty, string dtoProperty, Expression combinedExpression, ParameterExpression parameterExpression)
        {
            if (dtoProperty != null)
                combinedExpression = combinedExpression == null
                    ? GetStringExpression(parameterExpression, dataModelProperty, dtoProperty)
                    : Expression.And(combinedExpression, GetStringExpression(parameterExpression, dataModelProperty, dtoProperty));

            return combinedExpression;
        }

        public static Expression GetEqualExpression(string dataModelProperty, string dtoProperty, Expression combinedExpression, ParameterExpression parameterExpression)
        {
            if (dtoProperty != null)
                combinedExpression = combinedExpression == null
                ? GetEqualExpression(parameterExpression, dataModelProperty, dtoProperty)
                : Expression.And(combinedExpression, GetEqualExpression(parameterExpression, dataModelProperty, dtoProperty));

            return combinedExpression;
        }
        public static Expression GetEqualExpression(ParameterExpression pe, string property, string dtoProperty)
        {
            //Expression for accessing Entity Framework property
            Expression entityFrameworkProperty = Expression.Property(pe, property);
            //the constant to match
            Expression compareConstant = Expression.Constant(dtoProperty);

            return Expression.Equal(entityFrameworkProperty, compareConstant);
        }

        public static Expression GetStringExpression(ParameterExpression pe, string property, string dtoProperty)
        {
            if (dtoProperty.StartsWith("*") && dtoProperty.EndsWith("*"))
                return GetContainsExpression(pe, property, dtoProperty.Replace("*", ""));
            if (dtoProperty.StartsWith("*"))
                return GetEndsWithExpression(pe, property, dtoProperty.Replace("*", ""));
            if (dtoProperty.EndsWith("*"))
                return GetStartsWithExpression(pe, property, dtoProperty.Replace("*", ""));

            return GetEqualExpression(pe, property, dtoProperty);
        }

        public static Expression GetContainsExpression(ParameterExpression pe, string property, string dtoProperty)
        {
            //define the contains method
            MethodInfo stringContainsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
            //Expression for accessing Entity Framework property
            Expression entityFrameworkProperty = Expression.Property(pe, property);
            //the constant to match
            Expression compareConstant = Expression.Constant(dtoProperty, typeof(string));

            return Expression.Call(entityFrameworkProperty, stringContainsMethod, compareConstant);
        }

        public static Expression GetStartsWithExpression(ParameterExpression pe, string property, string dtoProperty)
        {
            //define the starts with method
            MethodInfo stringStartsWithMethod = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
            //Expression for accessing Entity Framework property
            Expression entityFrameworkProperty = Expression.Property(pe, property);
            //the constant to match
            Expression compareConstant = Expression.Constant(dtoProperty, typeof(string));

            return Expression.Call(entityFrameworkProperty, stringStartsWithMethod, compareConstant);
        }

        public static Expression GetEndsWithExpression(ParameterExpression pe, string property, string dtoProperty)
        {
            //define the Ends With method
            MethodInfo stringEndsWithMethod = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });
            //Expression for accessing Entity Framework property
            Expression entityFrameworkProperty = Expression.Property(pe, property);
            //the constant to match
            Expression compareConstant = Expression.Constant(dtoProperty, typeof(string));

            return Expression.Call(entityFrameworkProperty, stringEndsWithMethod, compareConstant);
        }
    }
The GetStringExpression method checks for "*" in the string then calls the appropriate method. The equals method is really straightforward, but the contains, startwith, and endswith methods need to have the method defined than use the call method of the expression instead of using the method directly.
When the search form is submitted to the server, I use a DTO to send the data to a method that checks for null and calls the appropriate method and returns an expression that I can pass to my repository.
private Expression<Func<[DataModel], Boolean>> GetWherePredicate(SearchDto searchDto)
        {
            Expression combinedExpression = null;

            //the 'IN' parameter for expression ie [DataModel] => condition
            ParameterExpression parameterExpression = Expression.Parameter(typeof([DataModel]), "DataModel");

            combinedExpression = ExpressionBuilder.GetEqualExpression("ASSIGNED_TO", searchDto.AssignedTo,
                combinedExpression, parameterExpression);

            combinedExpression = ExpressionBuilder.GetStringExpression("CUSTOMER_NUM",
                searchDto.CustomerNum, combinedExpression, parameterExpression);
           
            combinedExpression = ExpressionBuilder.GetEqualExpression("INVOICE_NUM",
                searchDto.InvoiceNum, combinedExpression, parameterExpression);

            combinedExpression = ExpressionBuilder.GetStringExpression("CUSTOMER_NAME",
                searchDto.CustomerNAME, combinedExpression, parameterExpression);

            //create and return the predicate
            if (combinedExpression != null)
                return Expression.Lambda<Func<[DataModel], Boolean>>(combinedExpression, parameterExpression);

            return null;
        }
In the code above, you would substitute the [DataModel] with the class name for the data model you are filtering. The expression that is returned can then be passed to a method in your repository that takes in an expression as a filter. Here is an example of the Get method from my repository.
public virtual IEnumerable<TEntity> Get(
                   Expression<Func<TEntity, bool>> filter = null,
                   Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
                   string includeProperties = "")
        {
            IQueryable<TEntity> query = db.Set<TEntity>();

            if (filter != null)
            {
                query = query.Where(filter);
            }

            foreach (var includeProperty in includeProperties.Split
                (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
            {
                return orderBy(query).ToList();
            }
            else
            {
                return query.ToList();
            }
        }
This is a pretty basic implementation that meets my requirements. The expression that is built uses only "AND" to connect each expression. Additional logic could be added to handle "OR" and more sophisticated filters.

I think with a little bit of tweaking, I could use reflection and loop through my DTO to build my expression and make my calling code a little cleaner. Each time I work with this, it gets a little better. 

Comments

Popular posts from this blog

Asp.Net Core with Extended Identity and Jwt Auth Walkthrough

File Backups to Dropbox with PowerShell