congrats on your first question.
Let's begin by looking at an approach for filtering a collection of data based on some custom filters. I will assume that the NameValueCollection
Type
you prefer to pass in your filters, holds PropertyNames as Keys and PropertyValues as Value.
Before we go forth filtering an entire collection, let's first figure out how to determine whether one object has properties that match our filters. And since we do not know the Type
of our object till runtime, we will need to use Generics in C# to accomplish this.
Step 1
- Get All Class Properties
We will need to get all properties of our generic class, e.g <TClass>
. Doing this using Reflection is deemed as slow and Matt Warren explains Why Reflection is slow in .NET and how to work around it. We shall therefore implement caching of class component model to get its PropertyDescriptorCollection
which exists in the namespace System.ComponentModel.PropertyDescriptorCollection.
Components Cache
private static IDictionary<string, PropertyDescriptorCollection> _componentsCache
= new Dictionary<string, PropertyDescriptorCollection>();
The key of our Dictionary
represents the name of the generic class and the value holds the PropertyDescriptorCollection
of that given class.
internal static bool InnerFilter<T>(T obj, NameValueCollection filters)
where T : class
{
Type type = typeof(T);
PropertyDescriptorCollection typeDescriptor = null;
if (_componentsCache.ContainsKey(type.Name))
typeDescriptor = _componentsCache[type.Name];
else
{
typeDescriptor = TypeDescriptor.GetProperties(type);
_componentsCache.Add(type.Name, typeDescriptor);
}
}
Step 2
- Loop through filters
After we have gotten the PropertyDescriptorCollection
for the generic class T
in the variable typeDescriptor
as shown above, now let's loop through our filters and see if any of its property names match any of our filter keys. If T
has a property name that matches any of our filter keys, now we inspect if the actual value of the property matches our filter value. To improve the quality of our search/filter function, we are going to use Regular Expressions in C# to determine whether a comparison is a hit or a miss.
for (int i = 0; i < filters.Count; i++)
{
string filterName = filters.GetKey(i);
string filterValue = filters[i];
PropertyDescriptor propDescriptor = typeDescriptor[filterName];
if (propDescriptor == null)
continue;
else
{
string propValue = propDescriptor.GetValue(obj).ToString();
bool isMatch = Regex.IsMatch(propValue, $"({filterValue})");
if (isMatch)
return true;
else
continue;
}
}
Step 3
- Implement Extension Methods.
To make the code that we've written easy to use and re-use, we are going to implement Extension Methods in C# so that we can better re-use our functions anywhere within our project.
- Generic Collection Filter Function that Uses the above Function.
Since an IQueryable<T>
can be converted to an IEnumerable<T>
by the .Where()
function in System.Linq
, we are going to utilize that in our function call as shown below.
public static IEnumerable<T> Filter<T>(this IEnumerable<T> collection, NameValueCollection filters)
where T : class
{
if (filters.Count < 1)
return collection;
return collection.Where(x => x.InnerFilter(filters));
}
Step 4
Put everything together.
Now that we have everything we need, let's look at how the final/full code looks as one block of code in a single static
class.
public static class Question54484908
{
private static IDictionary<string, PropertyDescriptorCollection> _componentsCache = new Dictionary<string, PropertyDescriptorCollection> ();
public static IEnumerable<T> Filter<T> (this IEnumerable<T> collection, NameValueCollection filters)
where T : class
{
if (filters.Count < 1)
return collection;
return collection.Where (x => x.InnerFilter (filters));
}
internal static bool InnerFilter<T> (this T obj, NameValueCollection filters)
where T : class
{
Type type = typeof (T);
PropertyDescriptorCollection typeDescriptor = null;
if (_componentsCache.ContainsKey (type.Name))
typeDescriptor = _componentsCache[type.Name];
else {
typeDescriptor = TypeDescriptor.GetProperties (type);
_componentsCache.Add (type.Name, typeDescriptor);
}
for (int i = 0; i < filters.Count; i++) {
string filterName = filters.GetKey (i);
string filterValue = filters[i];
PropertyDescriptor propDescriptor = typeDescriptor[filterName];
if (propDescriptor == null)
continue;
else {
string propValue = propDescriptor.GetValue (obj).ToString ();
bool isMatch = Regex.IsMatch (propValue, $"({filterValue})");
if (isMatch)
return true;
else
continue;
}
}
return false;
}
}
FINALLY
Filtering IEnumerable<T>
, List<T>
, Arrays
This is how you are going to use the above code anywhere in your project.
private IEnumerable<Question> _questions;
_questions = new List<Question>()
{
new Question("Question 1","How do i work with tuples"),
new Question("Question 2","How to use Queryable.Where when type is set at runtime?")
};
var filters = new NameValueCollection
{
{ "Description", "work" }
};
var results = _questions.Filter(filters);
Filtering DbSet<T>
Every DbContext
has a function .Set<T>
that returns a DbSet<T>
that can be used as an IQueryable<T>
and thus our function can be used as well as shown below.
Example
_dbContext.Set<Question>().Filter(filters);
Hope this answers your question or rather points you in the right direction.