Hermann
Hermann

Reputation: 21

Entity Framework LinqKit dynamic where predicate over related data

The code is in Alpha version, don't have validations or error management ... It would be included later.

I have a very simple model with two related entities: Location and Country:

public class UNCountry
{
    public int UNCountryId { get; set; }
    
    [Required]
    [MaxLength(2, ErrorMessage = "The field {0} only can contains a maximum of {1} characters lenght.")]
    [RegularExpression("[A-Z][A-Z]", ErrorMessage = "The field {0}, This code is formed by Two(2) capital letters")]
    public string Code { get; set; }
    
    [Required]
    [MaxLength(255, ErrorMessage = "The field {0} only can contains a maximum of {1} characters lenght.")]
    public string Name { get; set; }
}

public class UNLocation
{
    public int UNLocationId { get; set; }
    
    [MaxLength(1, ErrorMessage = "The field {0} only can contains a maximum of {1} characters lenght.")]
    public string UNChangeType { get; set; }      

    [Required]
    [MaxLength(5, ErrorMessage = "The field {0} only can contains a maximum of {1} characters lenght.")]
    [RegularExpression("[A-Z]{5}", ErrorMessage = "The field {0}, This code is formed by 5 capital letters")]
    public string Code { get; set; }

    [Required]
    [MaxLength(255, ErrorMessage = "The field {0} only can contains a maximum of {1} characters lenght.")]
    public string Name { get; set; }

    public int UNCountryId { get; set; }
    public UNCountry UNCountry { get; set; }
}

I have an app that list all the locations and the user can filter by any column, this is a generic Component View in Angular and I want to have all the Column Filters be Dynamic. The filters are passed in JsonFilters Parameter as an array

[{name:"nameOfField1", value:"valueOfField1"},...], 

then I construct a predicate with LinqKit and PredicateBuilder, and use the predicate in the query .Where(predicate). It works perfectly.

Here is the initial code in the controller

       var validFilter = new PaginationFilter(
                pF.PageNumber, pF.PageSize, pF.JsonFilters
        );
        var p = validFilter.CreateDynamicSearch<UNLocation>();
        //var b = PredicateBuilder.New<UNLocation>(true);
        //b = b.And(c => c.UNCountry.Name.Contains("Col"));
        var pagedData = await _context.UNLocations
            .Include(c => c.UNCountry)
            .Where(p)
            //.Where(d => d.UNCountry.Name.Contains("Col"))
            .OrderBy(x => x.UNLocationId)
            .Skip((validFilter.PageNumber - 1) * validFilter.PageSize)
            .Take(validFilter.PageSize)
            .ToListAsync();
        var totalRecords = await _context.UNLocations.CountAsync();
        return Ok(new PagedResponse<List<UNLocation>>(pagedData, validFilter.PageNumber, validFilter.PageSize,totalRecords));

I pass the fieldname and the value in the filters parameter, and all works well, but when I have a filter over the name of the country (the field name is UNCountry.Name) the predicate fails using this name of field over UNLocations, it fails in the function that constructs the predicate, but if I use in the Where directly (The commented line //.Where(d => d.UNCountry.Name.Contains("Col")) in the previous code) It Works, I could be solve the problem like this, but it would not be dynamic for other Views with more related data fields in query.

Here is the predicate generation function:

public Expression<Func<T, bool>> CreateDynamicSearch<T>()
    {
        var predicate = PredicateBuilder.New<T>(true);
        foreach (ReqFilter filter in this.Filters)
        {
            var columnFilter = PredicateBuilder.New<T>(false);
            var param = Expression.Parameter(typeof(T), "a");

            string[] filterParts = filter.Name.Split(".");
            string filterName = filterParts[0];
            if (filterParts.Length == 1)
            {
                var prop = Expression.Property(param, filterName);
                var call = Expression.Call(prop, "Contains", new Type[0], Expression.Constant(filter.Value));
                columnFilter = columnFilter.Or(Expression.Lambda<Func<T, bool>>(call, param));
                predicate = predicate.And(columnFilter);
            }
            else
            {
                 var prop = Expression.Property(param, filter.Name);

                var call = Expression.Call(prop, "Contains", new Type[0], Expression.Constant(filter.Value));
                columnFilter = columnFilter.Or(Expression.Lambda<Func<T, bool>>(call, param));
                predicate = predicate.And(columnFilter);

            }
        }
        return predicate;
    }

This code works well if i pass the filters with the normal fields of the UNLocation, like code or Name byExample [{code:"MIA"},name:"MI"], but the predicate constructor fails if i send the column name of the related UNCountry Name byExample [{UNCountry.Name:"United States"}], I repeat It works if I write the Where in Design Time code, but i need to have mor flexybility, and generate in predicate. It is possible or how can achieve this ? Thanks in advance, excuse my English.

Upvotes: 2

Views: 326

Answers (0)

Related Questions