Many many-to-many filters

Refresh

March 2019

Views

391 time

1

Today we faced a quite simple problem that were made even simpler by the dear predicates. We had a kind of event log and wanted to filter it client side (Windows Forms) using a list of criterias. We began by implementing to filter by a number of categories.

private List<Events> FilterEventsByCategory(List<Events> events,
                                        List<Category> categories) 
{
  return events.FindAll(ev => 
      categories.Exists(category => category.CategoryId==ev.CategoryId)); 
}

The next step is to implement a couple of other filters. Do you know of a nice way to generalize these to maybe somehow not having to write one method per filter? Or at least a clean way to have a dynamic list of filters that we want to apply simultaneously.

The clients are still on framework 3.0 so no LINQ.

Update: I had a hard time deciding who I should give credit for my solution. Marc had some nice ideas and is really good at explaining them. Most probaly I would have got my answer from him if I only had explained my problem a little better. Ultimately it was the generic Filter class that cmartin provided that got me on track. The Filter class used below can be found in cmartins' answer and the User class you get to dream up yourself.

var categoryFilter = new Filter<Event>(ev => categories.Exists(category => category.CategoryId == ev.CategoryId));
var userFilter = new Filter<Event>(ev => users.Exists(user => user.UserId == ev.UserId));

var filters = new List<Filter<Event>>();
filters.Add(categoryFilter);
filters.Add(userFilter);

var eventsFilteredByAny = events.FindAll(ev => filters.Any(filter => filter.IsSatisfied(ev)));
var eventsFilteredByAll = events.FindAll(ev => filters.All(filter => filter.IsSatisfied(ev)));

2 answers

2

re "so no LINQ" - have you looked at LINQBridge? Since you are using C# 3.0 this would be ideal...

I'm afraid for the main question, I don't fully understand what you are trying to do and what you are trying to avoid - can you clarify at all? But if you use the LINQBridge approach you can combine filters using successive .Where() calls.

One interpretation of the question is that you don't want lots of filter methods - so perhaps pass in one or more further predicates into the method - essentially as a Func<Event, Func<Category, bool>> - or in pure 2.0 terms, a Converter<Event, Predicate<Category>>:

private static List<Events> FilterEvents(
    List<Events> events,
    List<Category> categories,
    Converter<Events, Predicate<Category>> func)
{
    return events.FindAll(evt =>
        categories.Exists(func(evt)));
}

which is then used (to be as above) as:

var result = FilterEvents(events, categories,
      evt => category => category.CategoryId==evt.CategoryId);
1

Here's a very rudimentary sample of where I would start.

internal class Program
{
    private static void Main()
    {
        var ms = new Category(1, "Microsoft");
        var sun = new Category(2, "Sun");

        var events = new List<Event>
                         {
                             new Event(ms, "msdn event"),
                             new Event(ms, "mix"),
                             new Event(sun, "java event")
                         };

        var microsoftFilter = new Filter<Event>(e => e.CategoryId == ms.CategoryId);

        var microsoftEvents = FilterEvents(events, microsoftFilter);

        Console.Out.WriteLine(microsoftEvents.Count);
    }

    public static List<Event> FilterEvents(List<Event> events, Filter<Event> filter)
    {
        return events.FindAll(e => filter.IsSatisfied(e));
    }
}

public class Filter<T> where T: class
{
    private readonly Predicate<T> criteria;

    public Filter(Predicate<T> criteria)
    {
        this.criteria = criteria;
    }

    public bool IsSatisfied(T obj)
    {
        return criteria(obj);
    }
}

public class Event
{
    public Event(Category category, string name)
    {
        CategoryId = category.CategoryId;
        Name = name;
    }

    public int CategoryId { get; set; }
    public string Name { get; set; }
}

public class Category
{
    public Category(int categoryId, string name)
    {
        CategoryId = categoryId;
        Name = name;
    }

    public string Name { get; set; }
    public int CategoryId { get; set; }
}