TransWikia.com

C#, Linq, Filter a list whether its properties appear on another list

Stack Overflow Asked by HieDeptrai on December 9, 2021

I’m trying to filter a list if its element properties appear on another list.
Here is my example:

 public class Detail 
    {
        public int Id { get; set; }
        public string Material { get; set; }
        public string Title { get; set; }
        public string Duration { get; set; }
        public string FileName { get; set; }
        public string FileLocation{ get; set; }
        
    }

I have a dataList = List<Detail>() a displayList = ["Title", "Duration","FileName"] and a filterString = "test"
with a normal Linq we have:

dataList.Where(x x=> x.Title.Contains(filterString) || x.Duration.Contains(filterString) ||x.FileName.Contains(filterString))

but my task is to filter it in more programmatically way, I want to filter dataList if the Detail properties appear on displayList. Is there any way to do it

2 Answers

The following solution requires some more coding, but it can be reused for every class and for every type that you want to check.

We won't make a solutioin for class Detail only, nor for filterStrings only, I'll make this a generic extension methods of IEnumerable<...>, so you can intertwine it with your other LINQ methods. See extension methods demystified

First I'll give the signatures, then I write the code:

Just like most LINQ functions: you can provide a comparer, but you don't have to; in that case the default comparer is used:

public static IEnumerable<TSource> Filter<TSource, TKey>(
    this IEnumerable<TSource> source,
    IEnumerable<string> propertyNames,
    TKey filterValue)
{
    // only call the overload with a null comparer:
    return Filter(source, propertyNames, filterValue, null);
}

public static IEnumerable<TSource> Filter<TSource, TKey>(
    this IEnumerable<TSource> source,
    IEnumerable<string> propertyNames,
    TKey filterValue,
    IEqualityComparer comparer)
{
    // TODO: exceptions if input null

    if (comparer == null) comparer = EqualityComparer<TKey>.Default;
    
    // TODO: implement
}
  • First I'll translate the strings to PropertyInfos.
  • Convert every PropertyInfo into a PropertySelector, similar to the keySelector in GroupBy.
  • Convert every KeySelector and the filterValue into a predicate as used in Where

So from a sequence of strings, I have now created a sequence of predicates: put in an object of class Detail, and it will give you for every predicate whether the property equals filterValue.

I'll do this in smaller steps, so you understand what's happening:

// From the Type of TSource, get all public readable properties
// that have a name that is in propertyNames
// and that returns a type of TKey
Type type = typeof(TSource);
IEnumerable<PropertyInfo> propertyInfos = type.GetProperties()
    .Where(property => property.CanRead
                    && propertyNames.Contains(property.Name)
                    && property.PropertyType == typeof(TKey));

So if you mentioned a propertyName that does not return the correct type (in your Detail it had to be a string), those properties are not used. If you don't want that, consider throwing an exception.

Now that we have all PropertyInfos, we can get values and compare them. The easy way is a simple foreach:

foreach (TSource sourceElement in source)
{
    // does this source element match all predicates?
    bool matchesAllFilterValues = propertyInfos.All(propertyInfo =>
    {
        TKey propertyValue = (TKey)propertyInfo.GetValue(sourceElement);
        bool matchesFilterValue = comparer.Equals(propertyValue, filterValue);
        return matchesFilterValue;
    });

    // if all filter values are matched, then this sourceElement passes the filter
    if (matchesAllFilterValues)
       yield return sourceElement;
 }

Some people don't like to yield return, you can also continue the LINQ statements:

// From every PropertyInfo, make a keySelector, similar to GroupBy
IEnumerable<Func<TSource, TKey>> keySelectors = propertyInfos
    .Select<PropertyInfo, Func<TSource, TKey>> (propertyInfo =>
        x => (TKey)propertyInfo.GetValue(x));

So if we had a string "Title", it was converted into the PropertyInfo with name Title. If you put a use it to GetValue(detcal), you get the string value of detail.Title.

// From every KeySelector and the filterValue, create a Predicate, like in Where
IEnumerable<Func<TSource, bool>> predicates = keySelectors
    .Select<Func<TSource, TKey>, Func<TSource, bool>>(
        keySelector => sourceItem => comparer.Equals(keySelector(sourceItem), filterValue));

We had the keySelector to fetch detail.Title, this is converted to a predicate: true if detail.Title == filterValue (using the comparer)

Now that you've converted your sequence of propertyNames and your filterValue, you can return all source items that match all predicates:

IEnumerable<TSource> filteredValues = source.Where(sourceItem =>
    predicates.All(predicate => predicate(sourceItem);
return filteredValues;

Of course you can put this into one big LINQ statement, but I don't believe this makes it more readable. I even think that the yield method is more readable.

Finally a usage:

string filterValue = "Test";
IEnumerable<string> propertyNames = new string[] {"Title", "FileName"};
IEnumerable<Detail> details = ...

IEnumerable<Detail> detailsWithTitleFileNameEqualsTest = details.Filter(
    propertyNames,
    filterValue);

Or:

IEnumerable<Detail> detailsWithTitleFileNameEqualsTest = details.Filter(
    propertyNames,
    filterValue,
    StringComparer.OrdinalIgnoreCase);

You can even intertwine it with other LINQ statements:

var result = dbLibraryContext.Books
    .Where(book => ...)
    .Select(book => new Detail() {....})
    .Filter(propertyNames, filterValue)
    .GroupBy(detail => detail.Material);
    

Answered by Harald Coppoolse on December 9, 2021

I want to filter dataList if the Detail properties appear on displayList

Not very elegant, but straightforward:

dataList.Where(x =>
    displayList.Contains(nameof(Detail.Title)) && x.Title.Contains(filterString) ||
    displayList.Contains(nameof(Detail.Duration)) && x.Duration.Contains(filterString) ||
    ...
)
 

Answered by Sinatr on December 9, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP