Recently, I was working on a Web API application which is consumed by Angular UI for displaying multiple grids while loading the website itself. And my application is using a lot of grids and tables on the UI which has all the operations like Filtering, Sorting, Grouping, and Paging also. But to do this operation on the client side, I see a lot of code has to be written on the front-end side for each page or view.
Here, the problem is to bind the different models in different components and repeat the similar code for filtering, sorting, grouping, and paging for each model; but each model has different fields and different data types.
For example,
<,<=,>,>=,!=,== filters we can be applied on the numeric and date datatypes, but for string data type, we need to use different filters, like - starts with, ends with, contains, is empty, is not empty etc.
Then, for unit testing, I need to write a lot of code. Down the line, it will create a big headache for maintaining this business logic for each model because my ViewModelsare keeps on increasing.
Then, I thought of refactoring this by using Generics and Reflection to make it simple and we can use this code across the application. Please see below what I did.
Step 1
Create a FilterUtility class like below for applying the generic filter.
- public class FilterUtility
- {
- }
Step 2
Then, inside the FilterUtility class create an enum for different filter option. I have mentioned common filters. You can still add from your own if you need.
- /// <summary>
- /// Enums for filter options
- /// same sequence UI is following
- /// </summary>
- public enum FilterOptions
- {
- StartsWith = 1,
- EndsWith,
- Contains,
- DoesNotContain,
- IsEmpty,
- IsNotEmpty,
- IsGreaterThan,
- IsGreaterThanOrEqualTo,
- IsLessThan,
- IsLessThanOrEqualTo,
- IsEqualTo,
- IsNotEqualTo
- }
Step 3
Create another class called FilterParams for different filter parameters inside the FilterUtility class like below.
- /// <summary>
- /// Filter parameters Model Class
- /// </summary>
- public class FilterParams
- {
- public string ColumnName { get; set; } = string.Empty;
- public string FilterValue { get; set; } = string.Empty;
- public FilterOptions FilterOption { get; set; } = FilterOptions.Contains;
- }
Step 4
Create a generic Filter<T> class which will be responsible for filtering the data inside the FilterUtility class.
In the below class I have mentioned two methods i.e. FilteredData() and FilterData().
FilterData() method will actually filter the data based on FilterOptions using reflection. Some filter options are applicable to specific data types. Like for string data type we use starts with, ends with, contains etc.
The FilteredData() method is responsible for handling multiple filters in the complex type, because the above code can handle multiple filters at the same time. Since I have used generics here, I need to use the reflection to get the property name of any complex type.
- /// <summary>
- /// This is generic class
- /// responsible for filtering the data
- /// </summary>
- /// <typeparam name="T"></typeparam>
- public class Filter<T>
- {
- public static IEnumerable<T> FilteredData(IEnumerable<FilterParams> filterParams, IEnumerable<T> data)
- {
- IEnumerable<string> distinctColumns = filterParams.Where(x => !String.IsNullOrEmpty(x.ColumnName)).Select(x => x.ColumnName).Distinct();
- foreach (string colName in distinctColumns)
- {
- var filterColumn = typeof(T).GetProperty(colName, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
- if (filterColumn != null)
- {
- IEnumerable<FilterParams> filterValues = filterParams.Where(x => x.ColumnName.Equals(colName)).Distinct();
- if (filterValues.Count() > 1)
- {
- IEnumerable<T> sameColData = Enumerable.Empty<T>();
- foreach (var val in filterValues)
- {
- sameColData = sameColData.Concat(FilterData(val.FilterOption, data, filterColumn, val.FilterValue));
- }
- data = data.Intersect(sameColData);
- }
- else
- {
- data = FilterData(filterValues.FirstOrDefault().FilterOption, data, filterColumn, filterValues.FirstOrDefault().FilterValue);
- }
- }
- }
- return data;
- }
- private static IEnumerable<T> FilterData(FilterOptions filterOption, IEnumerable<T> data, PropertyInfo filterColumn, string filterValue)
- {
- int outValue;
- DateTime dateValue;
- switch (filterOption)
- {
- #region [StringDataType]
- case FilterOptions.StartsWith:
- data = data.Where(x => filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString().ToLower().StartsWith(filterValue.ToString().ToLower())).ToList();
- break;
- case FilterOptions.EndsWith:
- data = data.Where(x => filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString().ToLower().EndsWith(filterValue.ToString().ToLower())).ToList();
- break;
- case FilterOptions.Contains:
- data = data.Where(x => filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString().ToLower().Contains(filterValue.ToString().ToLower())).ToList();
- break;
- case FilterOptions.DoesNotContain:
- data = data.Where(x => filterColumn.GetValue(x, null) == null ||
- (filterColumn.GetValue(x, null) != null && !filterColumn.GetValue(x, null).ToString().ToLower().Contains(filterValue.ToString().ToLower()))).ToList();
- break;
- case FilterOptions.IsEmpty:
- data = data.Where(x => filterColumn.GetValue(x, null) == null ||
- (filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString() == string.Empty)).ToList();
- break;
- case FilterOptions.IsNotEmpty:
- data = data.Where(x => filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString() != string.Empty).ToList();
- break;
- #endregion
- #region [Custom]
- case FilterOptions.IsGreaterThan:
- if ((filterColumn.PropertyType == typeof(Int32) || filterColumn.PropertyType == typeof(Nullable<Int32>)) && Int32.TryParse(filterValue, out outValue))
- {
- data = data.Where(x => Convert.ToInt32(filterColumn.GetValue(x, null)) > outValue).ToList();
- }
- else if ((filterColumn.PropertyType == typeof(Nullable<DateTime>)) && DateTime.TryParse(filterValue, out dateValue))
- {
- data = data.Where(x => Convert.ToDateTime(filterColumn.GetValue(x, null)) > dateValue).ToList();
- }
- break;
- case FilterOptions.IsGreaterThanOrEqualTo:
- if ((filterColumn.PropertyType == typeof(Int32) || filterColumn.PropertyType == typeof(Nullable<Int32>)) && Int32.TryParse(filterValue, out outValue))
- {
- data = data.Where(x => Convert.ToInt32(filterColumn.GetValue(x, null)) >= outValue).ToList();
- }
- else if ((filterColumn.PropertyType == typeof(Nullable<DateTime>)) && DateTime.TryParse(filterValue, out dateValue))
- {
- data = data.Where(x => Convert.ToDateTime(filterColumn.GetValue(x, null)) >= dateValue).ToList();
- break;
- }
- break;
- case FilterOptions.IsLessThan:
- if ((filterColumn.PropertyType == typeof(Int32) || filterColumn.PropertyType == typeof(Nullable<Int32>)) && Int32.TryParse(filterValue, out outValue))
- {
- data = data.Where(x => Convert.ToInt32(filterColumn.GetValue(x, null)) < outValue).ToList();
- }
- else if ((filterColumn.PropertyType == typeof(Nullable<DateTime>)) && DateTime.TryParse(filterValue, out dateValue))
- {
- data = data.Where(x => Convert.ToDateTime(filterColumn.GetValue(x, null)) < dateValue).ToList();
- break;
- }
- break;
- case FilterOptions.IsLessThanOrEqualTo:
- if ((filterColumn.PropertyType == typeof(Int32) || filterColumn.PropertyType == typeof(Nullable<Int32>)) && Int32.TryParse(filterValue, out outValue))
- {
- data = data.Where(x => Convert.ToInt32(filterColumn.GetValue(x, null)) <= outValue).ToList();
- }
- else if ((filterColumn.PropertyType == typeof(Nullable<DateTime>)) && DateTime.TryParse(filterValue, out dateValue))
- {
- data = data.Where(x => Convert.ToDateTime(filterColumn.GetValue(x, null)) <= dateValue).ToList();
- break;
- }
- break;
- case FilterOptions.IsEqualTo:
- if (filterValue == string.Empty)
- {
- data = data.Where(x => filterColumn.GetValue(x, null) == null
- || (filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString().ToLower() == string.Empty)).ToList();
- }
- else
- {
- if ((filterColumn.PropertyType == typeof(Int32) || filterColumn.PropertyType == typeof(Nullable<Int32>)) && Int32.TryParse(filterValue, out outValue))
- {
- data = data.Where(x => Convert.ToInt32(filterColumn.GetValue(x, null)) == outValue).ToList();
- }
- else if ((filterColumn.PropertyType == typeof(Nullable<DateTime>)) && DateTime.TryParse(filterValue, out dateValue))
- {
- data = data.Where(x => Convert.ToDateTime(filterColumn.GetValue(x, null)) == dateValue).ToList();
- break;
- }
- else
- {
- data = data.Where(x => filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString().ToLower() == filterValue.ToLower()).ToList();
- }
- }
- break;
- case FilterOptions.IsNotEqualTo:
- if ((filterColumn.PropertyType == typeof(Int32) || filterColumn.PropertyType == typeof(Nullable<Int32>)) && Int32.TryParse(filterValue, out outValue))
- {
- data = data.Where(x => Convert.ToInt32(filterColumn.GetValue(x, null)) != outValue).ToList();
- }
- else if ((filterColumn.PropertyType == typeof(Nullable<DateTime>)) && DateTime.TryParse(filterValue, out dateValue))
- {
- data = data.Where(x => Convert.ToDateTime(filterColumn.GetValue(x, null)) != dateValue).ToList();
- break;
- }
- else
- {
- data = data.Where(x => filterColumn.GetValue(x, null) == null ||
- (filterColumn.GetValue(x, null) != null && filterColumn.GetValue(x, null).ToString().ToLower() != filterValue.ToLower())).ToList();
- }
- break;
- #endregion
- }
- return data;
- }
- }

chidambaram kPosted Jan 27, 2024, 11:01 AM
Grate work, but am getting some error, System.InvalidOperationException: Unknown enum type. at System.RuntimeType.IsEnumDefined(Object value) at Vehicle.Infrastructure.Repositories.GroupRepository.ListGroup(PaginatedInputModel pagingParams)
Elena DanisorPosted Oct 11, 2023, 9:43 AM
Why in pagination you are not using await ?
Sandeep Kumar GuptaPosted Nov 5, 2021, 4:46 PM
Yes sir I have passed in query string of .net core web api using swagger but didnt received the query value in PagingParams object in controller from [FromQuery] attribute please help sir
sedat akyolPosted Aug 8, 2020, 5:10 AM
Hi , can you share source code ?
Christian PonziPosted Dec 19, 2019, 4:47 AM
Great article, i try to pass Sorting and fileter parameters by iactionlink but it dosent work, what can i do? @Html.ActionLink("Denominazione", "Index", new { SortingParam = new SortingUtility.SortingParams{1,"Descrizione" }, FilterParam = ViewBag.FilterParam, GroupingColumns = ViewBag.GroupingColumns, pageNumber = 1 }) i tried in different ways.