From 799c5e9e4e30e452d77598a1f367bbe3bb1bea5f Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Tue, 4 Nov 2025 10:27:11 +0200 Subject: [PATCH] DataGrid column CollectionFilterMode property added Close #2313 --- .../QueryableExtensionTests.cs | 832 ++++++++++++++++++ Radzen.Blazor/Common.cs | 24 + Radzen.Blazor/QueryableExtension.cs | 8 +- Radzen.Blazor/RadzenDataGridColumn.razor.cs | 9 + 4 files changed, 870 insertions(+), 3 deletions(-) diff --git a/Radzen.Blazor.Tests/QueryableExtensionTests.cs b/Radzen.Blazor.Tests/QueryableExtensionTests.cs index 343ec47d..4ee801bf 100644 --- a/Radzen.Blazor.Tests/QueryableExtensionTests.cs +++ b/Radzen.Blazor.Tests/QueryableExtensionTests.cs @@ -1689,6 +1689,838 @@ namespace Radzen.Blazor.Tests Assert.Single(result); Assert.Equal(2, result[0].Id); } + + // CollectionFilterMode tests + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag1", Value = 50 }, new { Name = "tag5", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "tag1", + FilterOperator = FilterOperator.Equals, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has Name == "tag1" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "active", Value = 10 }, new { Name = "active", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "active", Value = 30 }, new { Name = "inactive", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "active", Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "active", + FilterOperator = FilterOperator.Equals, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have Name == "active" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithGreaterThan() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 60 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 5 }, new { Name = "tag6", Value = 8 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 50, + FilterOperator = FilterOperator.GreaterThan, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has Value > 50 + Assert.Single(result); + Assert.Equal(2, result[0].Id); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithGreaterThan() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 60 }, new { Name = "tag2", Value = 70 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 60 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 55 }, new { Name = "tag6", Value = 80 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 50, + FilterOperator = FilterOperator.GreaterThan, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have Value > 50 + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithContains() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "important-task", Value = 10 }, new { Name = "review", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "normal", Value = 30 }, new { Name = "basic", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "important-meeting", Value = 50 }, new { Name = "urgent", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "important", + FilterOperator = FilterOperator.Contains, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag contains "important" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithContains() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "important-task", Value = 10 }, new { Name = "important-note", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "important-meeting", Value = 30 }, new { Name = "basic", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "important-reminder", Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "important", + FilterOperator = FilterOperator.Contains, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags contain "important" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithStartsWith() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "prefix_alpha", Value = 10 }, new { Name = "other_beta", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "prefix_gamma", Value = 30 }, new { Name = "prefix_delta", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "other_epsilon", Value = 50 }, new { Name = "other_zeta", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "prefix_", + FilterOperator = FilterOperator.StartsWith, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag starts with "prefix_" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 2); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithStartsWith() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "prefix_alpha", Value = 10 }, new { Name = "prefix_beta", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "prefix_gamma", Value = 30 }, new { Name = "other_delta", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "prefix_epsilon", Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "prefix_", + FilterOperator = FilterOperator.StartsWith, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags start with "prefix_" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithEndsWith() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "file.txt", Value = 10 }, new { Name = "doc.pdf", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "image.png", Value = 30 }, new { Name = "photo.txt", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "data.csv", Value = 50 }, new { Name = "info.doc", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = ".txt", + FilterOperator = FilterOperator.EndsWith, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag ends with ".txt" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 2); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithEndsWith() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "file.txt", Value = 10 }, new { Name = "doc.txt", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "image.txt", Value = 30 }, new { Name = "photo.png", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "data.txt", Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = ".txt", + FilterOperator = FilterOperator.EndsWith, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags end with ".txt" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithNotEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "active", Value = 10 }, new { Name = "inactive", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "inactive", Value = 30 }, new { Name = "inactive", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "pending", Value = 50 }, new { Name = "active", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "inactive", + FilterOperator = FilterOperator.NotEquals, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag is not equal to "inactive" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithNotEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "active", Value = 10 }, new { Name = "pending", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "active", Value = 30 }, new { Name = "inactive", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "pending", Value = 50 }, new { Name = "active", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "inactive", + FilterOperator = FilterOperator.NotEquals, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags are not equal to "inactive" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithDoesNotContain() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "test-alpha", Value = 10 }, new { Name = "beta", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "test-gamma", Value = 30 }, new { Name = "test-delta", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "epsilon", Value = 50 }, new { Name = "zeta", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "test", + FilterOperator = FilterOperator.DoesNotContain, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag does not contain "test" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithDoesNotContain() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "alpha", Value = 10 }, new { Name = "beta", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "test-gamma", Value = 30 }, new { Name = "delta", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "epsilon", Value = 50 }, new { Name = "zeta", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterValue = "test", + FilterOperator = FilterOperator.DoesNotContain, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags do not contain "test" + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithLessThan() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 50 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 60 }, new { Name = "tag4", Value = 70 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 15 }, new { Name = "tag6", Value = 80 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 20, + FilterOperator = FilterOperator.LessThan, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has Value < 20 + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithLessThan() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 15 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 18 }, new { Name = "tag4", Value = 70 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 5 }, new { Name = "tag6", Value = 12 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 20, + FilterOperator = FilterOperator.LessThan, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have Value < 20 + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithLessThanOrEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 20 }, new { Name = "tag2", Value = 50 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 60 }, new { Name = "tag4", Value = 70 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 15 }, new { Name = "tag6", Value = 80 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 20, + FilterOperator = FilterOperator.LessThanOrEquals, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has Value <= 20 + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithLessThanOrEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 18 }, new { Name = "tag4", Value = 70 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 5 }, new { Name = "tag6", Value = 12 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 20, + FilterOperator = FilterOperator.LessThanOrEquals, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have Value <= 20 + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithGreaterThanOrEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 50 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 10 }, new { Name = "tag4", Value = 15 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 60 }, new { Name = "tag6", Value = 30 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 50, + FilterOperator = FilterOperator.GreaterThanOrEquals, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has Value >= 50 + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithGreaterThanOrEquals() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 50 }, new { Name = "tag2", Value = 60 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 55 }, new { Name = "tag4", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 70 }, new { Name = "tag6", Value = 80 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Value", + FilterValue = 50, + FilterOperator = FilterOperator.GreaterThanOrEquals, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have Value >= 50 + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsNull() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = (string)null, Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = (string)null, Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsNull, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has null Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsNull() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = (string)null, Value = 10 }, new { Name = (string)null, Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = (string)null, Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = (string)null, Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsNull, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have null Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsNotNull() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = (string)null, Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = (string)null, Value = 30 }, new { Name = (string)null, Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsNotNull, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has non-null Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsNotNull() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = (string)null, Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsNotNull, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have non-null Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsEmpty() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "", Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsEmpty, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has empty Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsEmpty() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "", Value = 10 }, new { Name = "", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "", Value = 30 }, new { Name = "tag4", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "", Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsEmpty, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have empty Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_Any_WithIsNotEmpty() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "", Value = 30 }, new { Name = "", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 }, new { Name = "tag6", Value = 60 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsNotEmpty, + CollectionFilterMode = CollectionFilterMode.Any + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where at least one tag has non-empty Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } + + [Fact] + public void Where_FiltersCollectionItemProperty_WithCollectionFilterMode_All_WithIsNotEmpty() + { + var testData = new[] + { + new { Id = 1, Tags = new[] { new { Name = "tag1", Value = 10 }, new { Name = "tag2", Value = 20 } }.ToList() }, + new { Id = 2, Tags = new[] { new { Name = "tag3", Value = 30 }, new { Name = "", Value = 40 } }.ToList() }, + new { Id = 3, Tags = new[] { new { Name = "tag5", Value = 50 } }.ToList() } + }.AsQueryable(); + + var filters = new List + { + new FilterDescriptor + { + Property = "Tags", + FilterProperty = "Name", + FilterOperator = FilterOperator.IsNotEmpty, + CollectionFilterMode = CollectionFilterMode.All + } + }; + + var result = testData.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.Default).ToList(); + + // Should return items where all tags have non-empty Name + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Id == 1); + Assert.Contains(result, r => r.Id == 3); + } } } diff --git a/Radzen.Blazor/Common.cs b/Radzen.Blazor/Common.cs index a4536a47..69f6c53a 100644 --- a/Radzen.Blazor/Common.cs +++ b/Radzen.Blazor/Common.cs @@ -2106,6 +2106,22 @@ namespace Radzen Or } + /// + /// Specifies how the filter should be applied to a collection of items. + /// + public enum CollectionFilterMode + { + /// + /// The filter condition is satisfied if at least one item in the collection matches. + /// + Any, + + /// + /// The filter condition is satisfied only if all items in the collection match. + /// + All + } + /// /// Specifies the string comparison operator of a filter. /// @@ -2526,6 +2542,14 @@ namespace Radzen /// /// The logical filter operator. public LogicalFilterOperator LogicalFilterOperator { get; set; } + + /// + /// Gets or sets the mode that determines whether the filter applies to any or all items in a collection. + /// + /// + /// A value indicating whether the filter is satisfied by any or all items. + /// + public CollectionFilterMode CollectionFilterMode { get; set; } } /// diff --git a/Radzen.Blazor/QueryableExtension.cs b/Radzen.Blazor/QueryableExtension.cs index 7f2004c5..b56f40dd 100644 --- a/Radzen.Blazor/QueryableExtension.cs +++ b/Radzen.Blazor/QueryableExtension.cs @@ -481,7 +481,7 @@ namespace Radzen if (collectionItemType != null && primaryExpression != null && !(filter.FilterOperator == FilterOperator.In || filter.FilterOperator == FilterOperator.NotIn)) { - primaryExpression = Expression.Call(typeof(Enumerable), nameof(Enumerable.Any), new Type[] { collectionItemType }, + primaryExpression = Expression.Call(typeof(Enumerable), filter.CollectionFilterMode == CollectionFilterMode.Any ? nameof(Enumerable.Any) : nameof(Enumerable.All), new Type[] { collectionItemType }, GetNestedPropertyExpression(parameter, filter.Property), Expression.Lambda(primaryExpression, collectionItemTypeParameter)); } @@ -627,7 +627,8 @@ namespace Radzen FilterOperator = c.GetFilterOperator(), SecondFilterValue = c.GetSecondFilterValue(), SecondFilterOperator = c.GetSecondFilterOperator(), - LogicalFilterOperator = c.GetLogicalFilterOperator() + LogicalFilterOperator = c.GetLogicalFilterOperator(), + CollectionFilterMode = c.CollectionFilterMode }); if (filters.Any()) @@ -1029,7 +1030,8 @@ namespace Radzen FilterOperator = c.GetFilterOperator(), SecondFilterValue = c.GetSecondFilterValue(), SecondFilterOperator = c.GetSecondFilterOperator(), - LogicalFilterOperator = c.GetLogicalFilterOperator() + LogicalFilterOperator = c.GetLogicalFilterOperator(), + CollectionFilterMode = c.CollectionFilterMode }), gridLogicalFilterOperator, gridFilterCaseSensitivity); } diff --git a/Radzen.Blazor/RadzenDataGridColumn.razor.cs b/Radzen.Blazor/RadzenDataGridColumn.razor.cs index 5f5b870a..fcf9c031 100644 --- a/Radzen.Blazor/RadzenDataGridColumn.razor.cs +++ b/Radzen.Blazor/RadzenDataGridColumn.razor.cs @@ -652,6 +652,15 @@ namespace Radzen.Blazor [Parameter] public LogicalFilterOperator LogicalFilterOperator { get; set; } = LogicalFilterOperator.And; + /// + /// Gets or sets the mode that determines whether the filter applies to any or all items in a collection. + /// + /// + /// A value indicating whether the filter is satisfied by any or all items. + /// + [Parameter] + public CollectionFilterMode CollectionFilterMode { get; set; } + /// /// Gets or sets the data type. ///