From 052ee189c47c30d407d77ba4b7645162cea62941 Mon Sep 17 00:00:00 2001 From: Waqas Ahmed Date: Wed, 9 Jul 2025 15:01:53 +0500 Subject: [PATCH 1/2] Case insensitive comparison for Equals just like Contains --- .../ToLowerEqualsComparisonAttribute.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/AutoFilterer/Attributes/ToLowerEqualsComparisonAttribute.cs diff --git a/src/AutoFilterer/Attributes/ToLowerEqualsComparisonAttribute.cs b/src/AutoFilterer/Attributes/ToLowerEqualsComparisonAttribute.cs new file mode 100644 index 0000000..47e9005 --- /dev/null +++ b/src/AutoFilterer/Attributes/ToLowerEqualsComparisonAttribute.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace AutoFilterer.Attributes; + +/// +/// This class generates equal query, for string fields. +/// +public class ToLowerEqualsComparisonAttribute : FilteringOptionsBaseAttribute +{ + public override Expression BuildExpression(ExpressionBuildContext context) + { + var equalsMethod = typeof(string).GetMethod(nameof(string.Equals), types: new[] { typeof(string) }); + + var toLowerMethod = typeof(string).GetMethod(nameof(string.ToLower), types: new Type[0]); + + var comparison = Expression.Equal( + Expression.Call( + method: equalsMethod, + instance: Expression.Call(method: toLowerMethod, instance: Expression.Property(context.ExpressionBody, context.TargetProperty.Name) + ), + arguments: new[] { Expression.Call(method: toLowerMethod, instance: context.FilterPropertyExpression) }), + Expression.Constant(true)); + + return comparison; + } +} From 831780ad2c15bb4640ab9534b245f52197aa1b7e Mon Sep 17 00:00:00 2001 From: enisn Date: Sat, 12 Jul 2025 11:58:52 +0300 Subject: [PATCH 2/2] Add tests for ToLowerEqualsComparisonAttribute Introduces unit tests for the ToLowerEqualsComparisonAttribute, covering case insensitivity, empty and null values, special and unicode characters, whitespace, no matches, and exact match scenarios for the Book.Title property. --- .../ToLowerEqualsComparisonAttributeTests.cs | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 tests/AutoFilterer.Tests/Attributes/ToLowerEqualsComparisonAttributeTests.cs diff --git a/tests/AutoFilterer.Tests/Attributes/ToLowerEqualsComparisonAttributeTests.cs b/tests/AutoFilterer.Tests/Attributes/ToLowerEqualsComparisonAttributeTests.cs new file mode 100644 index 0000000..f35d186 --- /dev/null +++ b/tests/AutoFilterer.Tests/Attributes/ToLowerEqualsComparisonAttributeTests.cs @@ -0,0 +1,208 @@ +using AutoFilterer.Extensions; +using AutoFilterer.Tests.Core; +using AutoFilterer.Tests.Environment.Dtos; +using AutoFilterer.Tests.Environment.Models; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using AutoFilterer.Attributes; +using AutoFilterer.Types; +using System; + +namespace AutoFilterer.Tests.Attributes; + +public class ToLowerEqualsComparisonAttributeTests +{ + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldGenerateQueryCorrect_WithAttribute(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = "Test Book" + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldBeCaseInsensitive(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = "TEST BOOK" + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldHandleEmptyString(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = "" + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldHandleNullValue(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = null + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + // When filter value is null, the framework should skip the filter entirely + // and return all results (no filtering applied) + Assert.Equal(dummyData.Count, result.Count); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldHandleSpecialCharacters(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = "Test-Book_123" + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldHandleWhitespace(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = " Test Book " + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldHandleUnicodeCharacters(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = "Café Book" + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldReturnEmptyResult_WhenNoMatch(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = "NonExistentBook" + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + Assert.Empty(result); + } + + [Theory, AutoMoqData(count: 16)] + public void BuildExpression_ShouldReturnExactMatchesOnly(List dummyData) + { + // Arrange + var filter = new BookFilter_LowerEquals + { + Title = "Test" + }; + IQueryable query = dummyData.AsQueryable(); + + // Act + var filteredQuery = query.ApplyFilter(filter); + var result = filteredQuery.ToList(); + + // Assert + var actualResult = query.Where(x => x.Title.ToLower().Equals(filter.Title.ToLower())).ToList(); + + Assert.Equal(result.Count, actualResult.Count); + + // Verify that all results are exact matches (not partial matches) + foreach (var book in result) + { + Assert.Equal(filter.Title.ToLower(), book.Title.ToLower()); + } + } +} + +public class BookFilter_LowerEquals : FilterBase +{ + [ToLowerEqualsComparison] + public string Title { get; set; } +} \ No newline at end of file