How to create dynamic entity framework filter expression like Expression>

c# dynamic-linq entity-framework lambda linq

Question

I want to create a dynamic lambda expressions for filtering.

I searched little bit but could not find useful stuff for child collections. So how can I create an expression like that?

Expression<Func<ORDERS, bool>> filter1 = c => c.ORDER_DETAILS.Any(x => x.PRODUCTS.HEADING.Contains("foo"));

PS: I asked similar question like that but i didn't get correct answer. So I decided to go from this direction if I'm not wrong.

More information about my question: (How to filter child collection with linq dynamic)

I'm trying to filter results for user request. For instance you have orders and order details and products is child collection.

When user wants to filter by product I'm getting an error because of No property or field 'PRODUCTS' exists in type 'ICollection1'`

I'm writing my query like this.

var orders = _uow.Repository() .Query() .Where("PRODUCTS.HEADING.ToLower().Contains(\"foo\")") .Include("ORDER_DETAILS") .Include("ORDER_DETAILS.PRODUCTS") .ToList(); So it's not possible to filter child collection like this? Or any way to filter?

1
2
5/23/2017 11:44:10 AM

Accepted Answer

Yes, you can. One approach I've used uses an object which is the same as your return type as a search filter. So if you want to search for the customer name of "Bill", then you set Order.Customer.Name to Bill. Passing that object to a method then applies all applicable searches.

To do this, start by defining a list of searchable fields:

Field<Order>[] Fields;

Fill these by declaring new fields:

var newField = new Field<Order>(o => o.Customer.Name, true, "Customer Name");

The "true" parameter means that it will act as the sort field for the results.

The Field object contains enough information to generate expressions later. It will look like this:

public class Field<T>
{
    public Field(Expression<Func<T, object>> field, bool sortField = false, string displayName = null)
    {
        //get & validate member
        MemberExp = field.Body is UnaryExpression ? ((UnaryExpression)field.Body).Operand as MemberExpression
                                                  : (MemberExpression)field.Body;

        Field = MemberExp?.Member;
        if (Field == null) throw new ArgumentException("Field expression is not a member.");

        //set field type
        switch (Field.MemberType)
        {
            case MemberTypes.Property:
                PropertyInfo p = (PropertyInfo)Field;
                FieldType = p.PropertyType;
                break;
            case MemberTypes.Field:
                FieldInfo f = (FieldInfo)Field;
                FieldType = f.FieldType;
                break;
            default:
                throw new Exception("Unsupported member type detected.");
        }

        //store input values
        FieldExpression = field;
        SortField = sortField;
        DisplayName = displayName ?? Field.Name;
    }

    public bool SortField { get; set; }
    public string DisplayName { get; private set; }
    public MemberExpression MemberExp { get; private set; }
    public Expression<Func<T, object>> FieldExpression { get; private set; }
    public Func<T, object> GetValue => FieldExpression.Compile();
    public Type FieldType { get; set; }

    /// <summary>
    /// Gets the full field name, i.e o => o.Customer.CustomerName returns "Customer.CustomerName"
    /// </summary>
    public string UnqualifiedFieldName
    {
        get
        {
            var stringExp = MemberExp.ToString();
            var paramEnd = stringExp.IndexOf('.') + 1;
            return  stringExp.Substring(paramEnd);
        }
    }
}

Once you have defined all the searchable fields, you will call a method to fetch the results of your search based on the search filters (T) you've collected from the user:

//get the results in ascending order, 10 items per page, first page
var results = GetSearchResults(searchFilters, "ASC", 10, 1);

The method will require you to have a queryable collection of data. I'll assume that you have some method such as context.GetCollection() that retrieves your data. The GetSearchResults method will look like this:

//Returns a filtered dataset based on provided search filters
//searchFilters is an object T which contains the search filters entered.
private List<T> GetSearchResults(T searchFilters, string sortDir = "ASC", int pageSize, int currentPage)
{
    IQueryable<T> searchResults = context.GetCollection(); //get your data context here

    var filterExpressions = new List<Expression<Func<T, bool>>>();

    //Add filters
    foreach (var field in Fields)
    {
        //try to get the search value, ignoring null exceptions because it's much harder
        //to check for null objects at multiple levels. Instead the exception tells us there's
        //no search value
        string searchValue = null;
        try 
        {
            searchValue = field.GetValue(searchFilters)?.ToString(); 
        }
        catch (NullReferenceException) { }
        if (string.IsNullOrWhiteSpace(searchValue)) continue;

        //shared expression setup
        ParameterExpression param = field.FieldExpression.Parameters.First();
        Expression left = field.FieldExpression.Body;
        ConstantExpression right = Expression.Constant(searchValue);
        Expression body = null;

        //create expression for strings so we can use "contains" instead of "equals"           
        if (field.FieldType == typeof(string))
        {
            //build the expression body
            MethodInfo method = typeof(string).GetMethod("Contains", new[] { typeof(string) });                    
            body = Expression.Call(left, method, right);
        }
        else
        {   //handle expression for all other types      
            body = Expression.Equal(left, right);
        }

        //finish expression
        Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(body, param);
        filterExpressions.Add(lambda);
    }

    //apply the expressions
    searchResults = filterExpressions.Aggregate(searchResults, (current, expression) => current.Where(expression));

    //get sort field
    Field<T> sortField = Fields.FirstOrDefault(f => f.SortField);
    searchResults = searchResults.OrderBy($"{sortField.UnqualifiedFieldName} {sortDir}");                                                                         

    // Get the search results
    int count = searchResults.Count();
    int maxPage = count / pageSize;
    if (maxPage * pageSize < count) maxPage++;
    if (currentPage > maxPage) currentPage = maxPage;
    int skip = Math.Max(0, (filters.page - 1) * pageSize);
    int display = Math.Max(0, Math.Min(count - skip, pageSize));
    return searchResults.Skip(skip).Take(display).ToList();
}     

This method uses your Field[] array to build expressions for your criteria and apply them to the dataset.

I hope that helps! Let me know if you have any questions.

3
7/11/2016 6:57:56 PM


Related Questions





Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow