Filtering by flags enums in Episerver Search & Navigation

Whilst Episerver Search & Navigation (Find) supports filtering by enums and nullable enums it doesn't come with any out-of-the-box filtering support for flags enums, something which is called out unambiguously in the documentation:

Note: Currently, there is support only for filtering by exact matching of values for enums. That is, bitwise comparison for enum types that have the Flags attribute is not baked into the Match method but must be done explicitly.

That means, if you want to filter by a flags enum you need to compare it to all possible combinations that include the bit flags you're filtering against—which perhaps sounds more complicated than it is!

As an example let's consider a DayOfWeek enum (essentially the same as the one Grzegorz uses in his SelectMany post):

[Flags]
public enum DayOfWeek
{
    Monday    = 1 << 0,
    Tuesday   = 1 << 1,
    Wednesday = 1 << 2,
    Thursday  = 1 << 3,
    Friday    = 1 << 4,
    Saturday  = 1 << 5,
    Sunday    = 1 << 6
}

We can then create a simple method to resolve all the combinations that we need to compare against:

/// <summary>
/// Gets all combinations of <see cref="DayOfWeek"/> that have the same bit flags set
/// </summary>
private static IEnumerable<DayOfWeek> GetAllCombinations(DayOfWeek dayOfWeek)
{
    var max = Enum.GetValues(typeof(DayOfWeek)).Cast<int>().Sum();

    ICollection<DayOfWeek> daysOfWeek = new List<DayOfWeek>();

    for (var i = 0; i <= max; i++)
    {
        var value = (DayOfWeek)i;

        if (value.HasFlag(dayOfWeek))
        {
            daysOfWeek.Add(value);
        }
    }

    return daysOfWeek;
}

Finally, if was imagine we have EventPage with a EventDays property using this enum we can easily utilize our method to build a filter (using the aptly named FilterBuilder):

var flags = DayOfWeek.Saturday | DayOfWeek.Sunday;

var search = _client.Search<EventPage>();

// Get all combinations that contain our bit flags
var dayOfWeeks = GetAllCombinations(DayOfWeek.Saturday | DayOfWeek.Sunday);

var dayOfWeekFilter = _client.BuildFilter<EventPage>();

// Create our "or" fiter
foreach (var dayOfWeek in dayOfWeeks)
{
    dayOfWeekFilter = dayOfWeekFilter.Or(x => x.EventDays.Match(dayOfWeek));
}

search = search.Filter(dayOfWeekFilter);

var result = search.GetContentResult();

That's all there is to it.

But...what about if we had a whole load of flags enums we wanted to search on? It'd get a bit tedious (and repetitive) to keep duplicating the method above. Well, how about a convenient little extension method that genericizes the whole thing:

public static class EnumFilterExtensions
{
    /// <summary>
    /// Applies a flags enum filter to the search
    /// </summary>
    /// <typeparam name="TSource">The search type</typeparam>
    /// <typeparam name="TEnum">The type of the enum</typeparam>
    /// <param name="search">The search</param>
    /// <param name="value">The enum value</param>
    /// <param name="fieldSelector">The field selector</param>
    /// <returns>A <see cref="ITypeSearch{TSource}" /> with the enum filter applied</returns>
    public static ITypeSearch<TSource> FilterFlagsEnum<TSource, TEnum>(this ITypeSearch<TSource> search, TEnum value,
        Expression<Func<TSource, TEnum>> fieldSelector)
    {
        if (Convert.ToInt32(value) == 0)
        {
            return search;
        }

        var fieldName = search.Client.Conventions.FieldNameConvention.GetFieldName(fieldSelector);

        IList<Filter> filters = GetAllCombinations(value)
            .Select(enumCombo => new TermFilter(fieldName, FieldFilterValue.Create(Convert.ToInt32(enumCombo))))
            .Cast<Filter>().ToList();

        search = search.Filter(new OrFilter(filters));

        return search;
    }

    /// <summary>
    /// Gets all combinations of a flags enum that have the same bit flags set
    /// </summary>
    /// <typeparam name="TEnum">The type of the enum</typeparam>
    /// <param name="value">The enum</param>
    /// <returns>A <see cref="IEnumerable{TEnum}" /> of all possible combinations</returns>
    private static IEnumerable<TEnum> GetAllCombinations<TEnum>(TEnum value)
    {
        // The max value of the enum with all flags
        var max = Enum.GetValues(typeof(TEnum)).Cast<int>().Sum();

        ICollection<TEnum> enums = new List<TEnum>();

        var num = Convert.ToUInt64(value);

        for (var i = 0; i <= max; i++)
        {
            // If the result contains our enum value we add to the list
            if ((Convert.ToUInt64(i) & num) == num)
            {
                enums.Add((TEnum)Enum.Parse(typeof(TEnum), i.ToString()));
            }
        }

        return enums;
    }
}

This makes filtering for flags enums as easy as one line (the other 2 are for context, honestly!):

var search = _client.Search<EventPage>();

search = search.FilterFlagsEnum(DayOfWeek.Saturday | DayOfWeek.Sunday, x => x.EventDays);

var result = search.GetContentResult();

Comments

That's really cool! Thanks for shairing

Valina