diff --git a/Directory.Packages.props b/Directory.Packages.props
index 511cf4e..bd03989 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -12,12 +12,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/README.md b/README.md
index a4355fe..8ea135c 100644
--- a/README.md
+++ b/README.md
@@ -244,4 +244,17 @@ Not all MUI operators are supported for all default handlers.
getGridSingleSelectOperators().filter(({ value }) =>
['equals', 'isEmpty', 'isNotEmpty', 'isAnyOf'].includes(value)
);
-```
\ No newline at end of file
+```
+
+### Enum collection (`DefaultEnumMultiSelectHandler`)
+
+`isAnyOf` — matches rows where the entity's enum collection contains **any** of the filter values.
+
+Register explicitly on the property; auto-detection is not performed:
+
+```csharp
+// Configure once for IEnumerable; this also covers IList and ICollection
+builder.Property(u => u.Roles).SetEnumMultiSelectHandler();
+```
+
+Filter values are enum name strings normalised via Humanizer, e.g. `SUPER_USER`, `Super User`, and `SuperUser` all resolve to the same member. Unknown values throw `ArgumentException` immediately.
diff --git a/src/Hotchocolate.MudDataGrid/README.md b/src/Hotchocolate.MudDataGrid/README.md
index fe52740..ca00c51 100644
--- a/src/Hotchocolate.MudDataGrid/README.md
+++ b/src/Hotchocolate.MudDataGrid/README.md
@@ -105,7 +105,31 @@ Operators are matched **case-insensitively** after trimming. The table below sho
| `is not` / `not` | `not` | DateTime, SingleSelect |
| `empty` | `isEmpty` | String, Number, DateTime, Guid |
| `not empty` | `isNotEmpty` | String, Number, DateTime, Guid |
-| `any of` / `is any of` | `isAnyOf` | String, Number, Guid, SingleSelect |
+| `any of` / `is any of` | `isAnyOf` | String, Number, Guid, SingleSelect, **EnumMultiSelect** |
+
+### Enum collection filtering (isAnyOf)
+
+Use `SetEnumMultiSelectHandler` to filter entity properties that hold a collection of enum values. Configure it for `IEnumerable` and it will also apply to `IList` and `ICollection` because they implement `IEnumerable`. A row is returned when its collection contains **at least one** of the selected values.
+
+**Data type configuration:**
+
+```csharp
+builder.Property(u => u.Roles).SetEnumMultiSelectHandler();
+```
+
+**Mud / MUI filter payload:**
+
+```graphql
+filterDefinitions: [{
+ field: "roles",
+ operator: "any of", // or "is any of"
+ value: ["ADMIN", "SUPER_USER"]
+}]
+```
+
+Enum string values are normalised with [Humanizer](https://github.com/Humanizr/Humanizer) before parsing, so `SUPER_USER`, `Super User`, and `SuperUser` all resolve to the same enum member. Unknown enum strings throw an `ArgumentException` immediately (fail-fast, consistent with `DefaultEnumSingleSelectHandler`).
+
+`SetEnumMultiSelectHandler` is defined for `IEnumerable`. This also covers `IList`, `ICollection`, and `List` implementations. It **does not auto-register** — you must call it explicitly on the relevant property.
Any operator not listed above throws a `GraphQLException` immediately (fail-fast).
diff --git a/src/Hotchocolate.MuiDataGrid/DataTypePropertyBuilderExtensions.cs b/src/Hotchocolate.MuiDataGrid/DataTypePropertyBuilderExtensions.cs
index 9c68364..8d51ee9 100644
--- a/src/Hotchocolate.MuiDataGrid/DataTypePropertyBuilderExtensions.cs
+++ b/src/Hotchocolate.MuiDataGrid/DataTypePropertyBuilderExtensions.cs
@@ -11,6 +11,74 @@ public static DataTypePropertyBuilder SetEnumHandler(
return builder.SetHandler(new DefaultEnumSingleSelectHandler());
}
+ ///
+ /// Registers for an
+ /// property. Only the isAnyOf
+ /// operator is supported: rows are returned when the collection contains any
+ /// of the selected enum values.
+ ///
+ /// The entity type.
+ /// The enum type stored in the collection.
+ /// The property builder to configure.
+ /// The same builder instance for chaining.
+ public static DataTypePropertyBuilder> SetEnumMultiSelectHandler(
+ this DataTypePropertyBuilder> builder)
+ where TEnum : struct, Enum
+ {
+ return builder.SetHandler(new DefaultEnumMultiSelectHandler());
+ }
+
+ ///
+ /// Registers for an
+ /// property. Only the isAnyOf
+ /// operator is supported: rows are returned when the collection contains any
+ /// of the selected enum values.
+ ///
+ /// The entity type.
+ /// The enum type stored in the collection.
+ /// The property builder to configure.
+ /// The same builder instance for chaining.
+ public static DataTypePropertyBuilder> SetEnumMultiSelectHandler(
+ this DataTypePropertyBuilder> builder)
+ where TEnum : struct, Enum
+ {
+ return builder.SetHandler(new DefaultEnumMultiSelectHandler());
+ }
+
+ ///
+ /// Registers for an
+ /// property. Only the isAnyOf
+ /// operator is supported: rows are returned when the collection contains any
+ /// of the selected enum values.
+ ///
+ /// The entity type.
+ /// The enum type stored in the collection.
+ /// The property builder to configure.
+ /// The same builder instance for chaining.
+ public static DataTypePropertyBuilder> SetEnumMultiSelectHandler(
+ this DataTypePropertyBuilder> builder)
+ where TEnum : struct, Enum
+ {
+ return builder.SetHandler(new DefaultEnumMultiSelectHandler());
+ }
+
+ ///
+ /// Registers for a
+ /// property. Only the isAnyOf
+ /// operator is supported: rows are returned when the collection contains any
+ /// of the selected enum values.
+ ///
+ /// The entity type.
+ /// The enum type stored in the collection.
+ /// The property builder to configure.
+ /// The same builder instance for chaining.
+ public static DataTypePropertyBuilder> SetEnumMultiSelectHandler(
+ this DataTypePropertyBuilder> builder)
+ where TEnum : struct, Enum
+ {
+ return builder.SetHandler(new DefaultEnumMultiSelectHandler());
+ }
+
public static DataTypePropertyBuilder SetNodeIdHandler(
this DataTypePropertyBuilder builder,
INodeIdSerializer idSerializer,
diff --git a/src/Hotchocolate.MuiDataGrid/DefaultEnumMultiSelectHandler.cs b/src/Hotchocolate.MuiDataGrid/DefaultEnumMultiSelectHandler.cs
new file mode 100644
index 0000000..e40d7b3
--- /dev/null
+++ b/src/Hotchocolate.MuiDataGrid/DefaultEnumMultiSelectHandler.cs
@@ -0,0 +1,60 @@
+namespace Stackworx.Hotchocolate.MuiDataGrid;
+
+using Humanizer;
+
+///
+/// Handles filtering for entity properties that are collections of enum values.
+/// Only the isAnyOf operator is supported: a row matches when the entity's
+/// enum collection contains at least one of the selected filter values.
+///
+/// The entity type.
+/// The enum type stored in the collection.
+public class DefaultEnumMultiSelectHandler : ExpressionBuilderHandler
+ where TEnum : struct, Enum
+{
+ protected override Expression InternalHandle(ColumnLookupMember member, ExpressionBuilderFlavour flavour, MuiDataGridFilterItemInput filter)
+ {
+ return filter.Operator switch
+ {
+ "isAnyOf" => this.BuildIsAnyOf(member, filter),
+ _ => throw new ArgumentException($"Unknown operator: {filter.Operator}"),
+ };
+ }
+
+ protected override dynamic ParseValue(ColumnLookupMember member, MuiValue value)
+ {
+ var v = value.AsString();
+ v = v.Humanize(LetterCasing.Title).Transform(To.LowerCase, To.TitleCase).Dehumanize();
+ if (Enum.TryParse(v, out var result))
+ {
+ return result;
+ }
+
+ throw new ArgumentException($"Failed to Parse {typeof(TEnum).Name}: {v}");
+ }
+
+ // Builds: entity.Collection.Any(e => filterValues.Contains(e))
+ private Expression BuildIsAnyOf(ColumnLookupMember member, MuiDataGridFilterItemInput filter)
+ {
+ filter.Value.AssertNotNull(filter.Operator);
+
+ var enumValues = filter.Value
+ .AsArray()
+ .Select(v => (TEnum)this.ParseValue(member, v))
+ .ToList();
+
+ var filterConstant = Expression.Constant(enumValues, typeof(List));
+
+ var enumParam = Expression.Parameter(typeof(TEnum), "e");
+ var containsMethod = typeof(List).GetMethod("Contains", new[] { typeof(TEnum) })!;
+ var containsCall = Expression.Call(filterConstant, containsMethod, enumParam);
+ var predicate = Expression.Lambda>(containsCall, enumParam);
+
+ var anyMethod = typeof(Enumerable)
+ .GetMethods()
+ .First(m => m.Name == "Any" && m.GetParameters().Length == 2)
+ .MakeGenericMethod(typeof(TEnum));
+
+ return Expression.Call(anyMethod, member.Expression, predicate);
+ }
+}
\ No newline at end of file
diff --git a/tests/Hotchocolate.MuiDataGrid.Test/MudDataGridAdapterTests.cs b/tests/Hotchocolate.MuiDataGrid.Test/MudDataGridAdapterTests.cs
index cae5292..d130709 100644
--- a/tests/Hotchocolate.MuiDataGrid.Test/MudDataGridAdapterTests.cs
+++ b/tests/Hotchocolate.MuiDataGrid.Test/MudDataGridAdapterTests.cs
@@ -58,6 +58,77 @@ public void Map_ShouldPreserveSortDefinitionOrder()
result.Sorting!.Select(x => x.Field).Should().Equal("first", "second", "third");
}
+ [Fact]
+ public void Map_ShouldMapEnumListWithAnyOf_ToIsAnyOf()
+ {
+ // Verifies that a MudDataGrid "any of" filter carrying a list of enum string
+ // values is forwarded untouched as an isAnyOf MUI filter item.
+ var enumValues = new MuiValue(new List { "ADMIN", "SUPER_USER" });
+
+ var input = new MudDataGridFilterInput
+ {
+ FilterDefinitions =
+ [
+ new MudDataGridFilterDefinitionInput("roles", "any of", enumValues),
+ ],
+ };
+
+ var result = MudToMuiDataGridAdapter.Map(input);
+
+ result.Filters.Should().NotBeNull();
+ result.Filters!.Items.Should().HaveCount(1);
+
+ var item = result.Filters.Items[0];
+ item.Field.Should().Be("roles");
+ item.Operator.Should().Be("isAnyOf");
+ item.Value.Should().Be(enumValues);
+ item.FromInput.Should().Be("mud");
+ }
+
+ [Fact]
+ public void Map_ShouldMapEnumListWithIsAnyOf_ToIsAnyOf()
+ {
+ var enumValues = new MuiValue(new List { "READ_ONLY" });
+
+ var input = new MudDataGridFilterInput
+ {
+ FilterDefinitions =
+ [
+ new MudDataGridFilterDefinitionInput("roles", "is any of", enumValues),
+ ],
+ };
+
+ var result = MudToMuiDataGridAdapter.Map(input);
+
+ result.Filters!.Items.Single().Operator.Should().Be("isAnyOf");
+ result.Filters.Items.Single().Value.Should().Be(enumValues);
+ }
+
+ [Fact]
+ public void Map_ShouldPreserveEnumArrayValuesAcrossMapping()
+ {
+ // Ensures the MuiValue array holding the enum strings is identical by ref
+ // so no data is lost during operator normalisation.
+ var enumValues = new MuiValue(new List { "ADMIN", "SUPER_USER", "READ_ONLY" });
+
+ var input = new MudDataGridFilterInput
+ {
+ FilterDefinitions =
+ [
+ new MudDataGridFilterDefinitionInput("permissions", "any of", enumValues),
+ ],
+ };
+
+ var result = MudToMuiDataGridAdapter.Map(input);
+
+ var mappedItem = result.Filters!.Items.Single();
+ mappedItem.Value.Should().BeSameAs(enumValues, "the original MuiValue array must not be cloned or altered");
+
+ // Verify the string values are accessible and intact
+ var arrayValues = mappedItem.Value!.AsArray().Select(v => v.AsString()).ToList();
+ arrayValues.Should().Equal("ADMIN", "SUPER_USER", "READ_ONLY");
+ }
+
[Fact]
public void Map_ShouldFailFastWithGraphqlError_WhenOperatorIsUnsupported()
{
diff --git a/tests/Hotchocolate.MuiDataGrid.Test/MuiDataGridEnumMultiSelectTests.cs b/tests/Hotchocolate.MuiDataGrid.Test/MuiDataGridEnumMultiSelectTests.cs
new file mode 100644
index 0000000..419072d
--- /dev/null
+++ b/tests/Hotchocolate.MuiDataGrid.Test/MuiDataGridEnumMultiSelectTests.cs
@@ -0,0 +1,253 @@
+namespace Stackworx.Hotchocolate.MuiDataGrid;
+
+using FluentAssertions;
+
+///
+/// Tests for which handles
+/// filtering on entity properties that are collections of enum values.
+///
+public class MuiDataGridEnumMultiSelectTests
+{
+ private enum Role
+ {
+ Admin,
+ SuperUser,
+ ReadOnly,
+ Viewer,
+ }
+
+ private enum Permission
+ {
+ Read,
+ Write,
+ }
+
+ private static List TestUsers =>
+ [
+ new() { Id = 1, Name = "Alice", Roles = [Role.Admin] },
+ new() { Id = 2, Name = "Bob", Roles = [Role.SuperUser] },
+ new() { Id = 3, Name = "Carol", Roles = [Role.Admin, Role.SuperUser] },
+ new() { Id = 4, Name = "Dave", Roles = [Role.ReadOnly] },
+ new() { Id = 5, Name = "Eve", Roles = [] },
+ ];
+
+ [Fact]
+ public void IsAnyOf_SingleFilter_ReturnsUsersWithMatchingRole()
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { "ADMIN" }),
+ Operator: "isAnyOf"),
+ ],
+ };
+
+ var result = TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ result.Should().HaveCount(2);
+ result.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Carol");
+ }
+
+ [Fact]
+ public void IsAnyOf_MultipleFilterValues_ReturnsUsersWithAnyMatchingRole()
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { "ADMIN", "SUPER_USER" }),
+ Operator: "isAnyOf"),
+ ],
+ };
+
+ var result = TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ // Alice (Admin), Bob (SuperUser), Carol (Admin + SuperUser) — Dave (ReadOnly) and Eve (empty) excluded
+ result.Should().HaveCount(3);
+ result.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Bob", "Carol");
+ }
+
+ [Fact]
+ public void IsAnyOf_NoOverlap_ReturnsEmptyList()
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { "VIEWER" }),
+ Operator: "isAnyOf"),
+ ],
+ };
+
+ var result = TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ result.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void IsAnyOf_AllRolesInFilter_ReturnsAllUsersWithAtLeastOneRole()
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { "ADMIN", "SUPER_USER", "READ_ONLY", "VIEWER" }),
+ Operator: "isAnyOf"),
+ ],
+ };
+
+ var result = TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ // Eve has no roles and should be excluded
+ result.Should().HaveCount(4);
+ result.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Bob", "Carol", "Dave");
+ }
+
+ [Fact]
+ public void IsAnyOf_UserWithMultipleRoles_OnlyMatchesWhenFilterRolePresent()
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { "READ_ONLY" }),
+ Operator: "isAnyOf"),
+ ],
+ };
+
+ var result = TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ result.Should().HaveCount(1);
+ result.Single().Name.Should().Be("Dave");
+ }
+
+ [Theory]
+ [InlineData("ADMIN")]
+ [InlineData("Admin")]
+ [InlineData("admin")]
+ public void IsAnyOf_EnumNameCaseVariants_ParsedCorrectly(string enumName)
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { enumName }),
+ Operator: "isAnyOf"),
+ ],
+ };
+
+ var result = TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ result.Should().HaveCount(2, $"'{enumName}' should resolve to Role.Admin");
+ result.Select(u => u.Name).Should().BeEquivalentTo("Alice", "Carol");
+ }
+
+ [Fact]
+ public void IsAnyOf_UnknownEnumValue_ThrowsArgumentException()
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { "GOD_MODE" }),
+ Operator: "isAnyOf"),
+ ],
+ };
+
+ var act = () => TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ act.Should().Throw()
+ .WithMessage("*Failed to Parse*");
+ }
+
+ [Fact]
+ public void UnsupportedOperator_ThrowsArgumentException()
+ {
+ var dataType = new UserWithRolesDataType();
+ var filter = new MuiDataGridFilterInput
+ {
+ Items =
+ [
+ new(
+ Field: "roles",
+ Value: new MuiValue(new List { "ADMIN" }),
+ Operator: "is"), // only isAnyOf is supported
+ ],
+ };
+
+ var act = () => TestUsers.AsQueryable().Where(dataType.Filter(filter)).ToList();
+
+ act.Should().Throw()
+ .WithMessage("*Unknown operator*");
+ }
+
+ [Fact]
+ public void AllCollectionInterfaceOverloads_RegisterWithoutError()
+ {
+ // Ensures all four overloads compile and register without exceptions at runtime.
+ var act = () => new TestAllInterfacesDataType();
+
+ act.Should().NotThrow();
+ }
+
+ private sealed class UserWithRolesDataType : DataType
+ {
+ protected override void Configure(DataTypeBuilder builder)
+ {
+ builder.Property(u => u.Name);
+ builder.Property(u => u.Roles).SetEnumMultiSelectHandler();
+ }
+ }
+
+ private sealed class TestAllInterfacesDataType : DataType
+ {
+ protected override void Configure(DataTypeBuilder builder)
+ {
+ builder.Property(r => r.RequiredPermissions).SetEnumMultiSelectHandler(); // ICollection
+ builder.Property(r => r.GrantedPermissions).SetEnumMultiSelectHandler(); // IEnumerable
+ builder.Property(r => r.DeniedPermissions).SetEnumMultiSelectHandler(); // List
+ }
+ }
+
+ private class UserWithRoles
+ {
+#pragma warning disable CA1822 // Mark members as static
+ public int Id { get; set; }
+
+ public string Name { get; set; } = string.Empty;
+
+ public IList Roles { get; set; } = new List();
+#pragma warning restore CA1822 // Mark members as static
+ }
+
+ private class ResourceWithPermissions
+ {
+ public ICollection RequiredPermissions { get; set; } = new List();
+
+ public IEnumerable GrantedPermissions { get; set; } = new List();
+
+ public List DeniedPermissions { get; set; } = [];
+ }
+}
\ No newline at end of file