From 575abfc75b205ccb85861f20b99d87d58fece2d2 Mon Sep 17 00:00:00 2001 From: mohit Date: Tue, 5 May 2026 15:18:06 -0400 Subject: [PATCH 1/4] feat(mapper): add case-insensitive per-map override --- docs/pages/guide/gridifyMapper.md | 9 +++ src/Gridify/Builder/BaseQueryBuilder.cs | 14 ++-- src/Gridify/Builder/LinqQueryBuilder.cs | 5 +- src/Gridify/CompositeGMap.cs | 1 + src/Gridify/GMap.cs | 1 + src/Gridify/GridifyMapper.cs | 8 +- src/Gridify/IGMap.cs | 4 +- src/Gridify/IGridifyMapper.cs | 4 +- .../IssueTests/CaseInsensitivePerMapTests.cs | 81 +++++++++++++++++++ 9 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs diff --git a/docs/pages/guide/gridifyMapper.md b/docs/pages/guide/gridifyMapper.md index c08a3226..3c01b7c6 100644 --- a/docs/pages/guide/gridifyMapper.md +++ b/docs/pages/guide/gridifyMapper.md @@ -80,6 +80,7 @@ This method adds a mapping to the mapper. - the first parameter is the name of the field you want to use in the string query - the second parameter is a property selector expression - the third parameter is an optional [value convertor](#value-convertor) expression that you can use to convert user inputs to anything you want +- the optional `caseInsensitive` parameter lets you override `CaseInsensitiveFiltering` for a specific map (`true` = force case-insensitive, `false` = force case-sensitive) ### Value Convertor @@ -351,6 +352,14 @@ If true, string comparison operations are case insensitive by default. var mapper = new GridifyMapper(q => q.CaseInsensitiveFiltering = true); ``` +You can also override this behavior per map: + +```csharp +var mapper = new GridifyMapper(q => q.CaseInsensitiveFiltering = true) + .AddMap("userName", p => p.UserName, caseInsensitive: false) + .AddMap("email", p => p.Email, caseInsensitive: true); +``` + ### DefaultDateTimeKind By setting this property to a `DateTimeKind` value, you can change the default `DateTimeKind` used when parsing dates. diff --git a/src/Gridify/Builder/BaseQueryBuilder.cs b/src/Gridify/Builder/BaseQueryBuilder.cs index 349bfb4d..db70ebb5 100644 --- a/src/Gridify/Builder/BaseQueryBuilder.cs +++ b/src/Gridify/Builder/BaseQueryBuilder.cs @@ -148,7 +148,7 @@ public TQuery Build(ExpressionSyntax expression) } else { - var exprQuery = BuildQuery(exprMapTarget.Body, exprMapTarget.Parameters[0], right, op, gMap.Convertor, false); + var exprQuery = BuildQuery(exprMapTarget.Body, exprMapTarget.Parameters[0], right, op, gMap.Convertor, false, gMap.CaseInsensitive); if (exprQuery == null) continue; if (exprHasIndexer) @@ -193,7 +193,7 @@ public TQuery Build(ExpressionSyntax expression) return (result, isNested); } - var query = BuildQuery(mapTarget.Body, mapTarget.Parameters[0], right, op, gMap.Convertor, false); + var query = BuildQuery(mapTarget.Body, mapTarget.Parameters[0], right, op, gMap.Convertor, false, gMap.CaseInsensitive); if (query == null) return null; if (hasIndexer) @@ -264,7 +264,8 @@ private object AddIndexerNullCheck(LambdaExpression mapTarget, object query) ValueExpressionSyntax valueExpression, ISyntaxNode op, Func? convertor, - bool isNested) + bool isNested, + bool? mapCaseInsensitive) { // Remove the boxing for value types if (body.NodeType == ExpressionType.Convert) body = ((UnaryExpression)body).Operand; @@ -320,8 +321,11 @@ private object AddIndexerNullCheck(LambdaExpression mapTarget, object query) } // handle case-Insensitive search - if (value is not null && body.Type == typeof(string) && (valueExpression.IsCaseInsensitive - || mapper.Configuration.CaseInsensitiveFiltering) + // mapCaseInsensitive overrides the global mapper config per-map: true=force insensitive, false=force sensitive, null=use config + var isCaseInsensitive = valueExpression.IsCaseInsensitive + || mapCaseInsensitive == true + || (mapCaseInsensitive == null && mapper.Configuration.CaseInsensitiveFiltering); + if (value is not null && body.Type == typeof(string) && isCaseInsensitive && op.Kind is not SyntaxKind.GreaterThan && op.Kind is not SyntaxKind.LessThan && op.Kind is not SyntaxKind.GreaterOrEqualThan diff --git a/src/Gridify/Builder/LinqQueryBuilder.cs b/src/Gridify/Builder/LinqQueryBuilder.cs index 16116720..01e22fc6 100644 --- a/src/Gridify/Builder/LinqQueryBuilder.cs +++ b/src/Gridify/Builder/LinqQueryBuilder.cs @@ -30,7 +30,8 @@ public LinqQueryBuilder(IGridifyMapper mapper) : base(mapper) value, op, gMap.Convertor, - true); + true, + gMap.CaseInsensitive); if (conditionExp is not LambdaExpression lambdaExp) return null; @@ -272,7 +273,7 @@ protected override Expression> CombineWithOrOperator(Expression a.NodeType == ExpressionType.Lambda) as LambdaExpression; - var conditionExp = BuildQuery(targetExp!.Body, targetExp.Parameters[0], value, op, gMap.Convertor, true); + var conditionExp = BuildQuery(targetExp!.Body, targetExp.Parameters[0], value, op, gMap.Convertor, true, gMap.CaseInsensitive); if (conditionExp is not LambdaExpression lambdaExp) return null; diff --git a/src/Gridify/CompositeGMap.cs b/src/Gridify/CompositeGMap.cs index 20894697..0790c8b1 100644 --- a/src/Gridify/CompositeGMap.cs +++ b/src/Gridify/CompositeGMap.cs @@ -14,6 +14,7 @@ public class CompositeGMap : IGMap public string From { get; set; } public LambdaExpression To { get; set; } public Func? Convertor { get; set; } + public bool? CaseInsensitive { get; set; } /// /// Collection of expressions that will be combined with OR logic diff --git a/src/Gridify/GMap.cs b/src/Gridify/GMap.cs index 04477845..5ce1df29 100644 --- a/src/Gridify/GMap.cs +++ b/src/Gridify/GMap.cs @@ -9,6 +9,7 @@ public partial class GMap : IGMap public string From { get; set; } public LambdaExpression To { get; set; } public Func? Convertor { get; set; } + public bool? CaseInsensitive { get; set; } public GMap(string from, Expression> to, Func? convertor = null) { diff --git a/src/Gridify/GridifyMapper.cs b/src/Gridify/GridifyMapper.cs index 027e7210..518ac7dd 100644 --- a/src/Gridify/GridifyMapper.cs +++ b/src/Gridify/GridifyMapper.cs @@ -41,7 +41,7 @@ public GridifyMapper(Action configuration, bool auto GenerateMappings(); } - public IGridifyMapper AddMap(string from, Func? convertor = null!, bool overrideIfExists = true) + public IGridifyMapper AddMap(string from, Func? convertor = null!, bool overrideIfExists = true, bool? caseInsensitive = null) { if (!overrideIfExists && HasMap(from)) throw new GridifyMapperException($"Duplicate Key. the '{from}' key already exists"); @@ -57,7 +57,7 @@ public IGridifyMapper AddMap(string from, Func? convertor = n } RemoveMap(from); - _mappings.Add(new GMap(from, to!, convertor)); + _mappings.Add(new GMap(from, to!, convertor) { CaseInsensitive = caseInsensitive }); return this; } @@ -105,13 +105,13 @@ private void GenerateMappingsRecursive(Type type, string prefix, ushort maxNesti } public IGridifyMapper AddMap(string from, Expression> to, Func? convertor = null!, - bool overrideIfExists = true) + bool overrideIfExists = true, bool? caseInsensitive = null) { if (!overrideIfExists && HasMap(from)) throw new GridifyMapperException($"Duplicate Key. the '{from}' key already exists"); RemoveMap(from); - _mappings.Add(new GMap(from, to, convertor)); + _mappings.Add(new GMap(from, to, convertor) { CaseInsensitive = caseInsensitive }); return this; } diff --git a/src/Gridify/IGMap.cs b/src/Gridify/IGMap.cs index 90c4d169..972bb6f9 100644 --- a/src/Gridify/IGMap.cs +++ b/src/Gridify/IGMap.cs @@ -8,4 +8,6 @@ public interface IGMap string From { get; set; } LambdaExpression To { get; set; } Func? Convertor { get; set; } -} \ No newline at end of file + // null = use global config, true = force case-insensitive, false = force case-sensitive + bool? CaseInsensitive { get; set; } +} diff --git a/src/Gridify/IGridifyMapper.cs b/src/Gridify/IGridifyMapper.cs index 31bf7e9d..fe7aaab9 100644 --- a/src/Gridify/IGridifyMapper.cs +++ b/src/Gridify/IGridifyMapper.cs @@ -6,7 +6,7 @@ namespace Gridify; public interface IGridifyMapper { - IGridifyMapper AddMap(string from, Expression> to, Func? convertor = null, bool overrideIfExists = true); + IGridifyMapper AddMap(string from, Expression> to, Func? convertor = null, bool overrideIfExists = true, bool? caseInsensitive = null); IGridifyMapper AddMap(string from, Expression> to, Func? convertor = null!, bool overrideIfExists = true); @@ -16,7 +16,7 @@ IGridifyMapper AddMap(string from, Expression AddMap(IGMap gMap, bool overrideIfExists = true); - IGridifyMapper AddMap(string from, Func? convertor = null!, bool overrideIfExists = true); + IGridifyMapper AddMap(string from, Func? convertor = null!, bool overrideIfExists = true, bool? caseInsensitive = null); /// /// Adds a composite mapping that combines multiple property expressions with OR logic. diff --git a/test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs b/test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs new file mode 100644 index 00000000..cc826779 --- /dev/null +++ b/test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Gridify.Tests.IssueTests; + +public class CaseInsensitivePerMapTests +{ + private List DataSource => new() + { + new TestClass { Name = "Alice", Tags = ["CSharp", "DotNet"] }, + new TestClass { Name = "Bob", Tags = ["Python", "Django"] }, + }; + + // --- string field: caseSensitive=true overrides global CaseInsensitiveFiltering=true --- + + [Fact] + public void AddMap_CaseSensitiveTrue_OnStringField_ShouldNotMatchWrongCase() + { + var mapper = new GridifyMapper(c => c.CaseInsensitiveFiltering = true) + .AddMap("Name", x => x.Name, caseInsensitive: false); + + Assert.Empty(DataSource.AsQueryable().ApplyFiltering("Name=alice", mapper).ToList()); + } + + [Fact] + public void AddMap_CaseSensitiveTrue_OnStringField_ShouldMatchCorrectCase() + { + var mapper = new GridifyMapper(c => c.CaseInsensitiveFiltering = true) + .AddMap("Name", x => x.Name, caseInsensitive: false); + + Assert.Single(DataSource.AsQueryable().ApplyFiltering("Name=Alice", mapper).ToList()); + } + + // --- string field: caseSensitive=false overrides global CaseInsensitiveFiltering=false --- + + [Fact] + public void AddMap_CaseSensitiveFalse_OnStringField_ShouldMatchWrongCaseWhenGlobalIsOff() + { + var mapper = new GridifyMapper(c => c.CaseInsensitiveFiltering = false) + .AddMap("Name", x => x.Name, caseInsensitive: true); + + Assert.Single(DataSource.AsQueryable().ApplyFiltering("Name=alice", mapper).ToList()); + } + + // --- List field: use the (string, convertor, caseSensitive) overload so CreateExpression + // auto-wraps the collection with .Select(fc => fc), making IsNestedCollection() true --- + + [Fact] + public void AddMap_CaseSensitiveTrue_OnListOfStrings_ShouldNotMatchWrongCase() + { + var mapper = new GridifyMapper(c => c.CaseInsensitiveFiltering = true) + .AddMap("Tags", caseInsensitive: false); + + Assert.Empty(DataSource.AsQueryable().ApplyFiltering("Tags=csharp", mapper).ToList()); + } + + [Fact] + public void AddMap_CaseSensitiveTrue_OnListOfStrings_ShouldMatchCorrectCase() + { + var mapper = new GridifyMapper(c => c.CaseInsensitiveFiltering = true) + .AddMap("Tags", caseInsensitive: false); + + Assert.Single(DataSource.AsQueryable().ApplyFiltering("Tags=CSharp", mapper).ToList()); + } + + [Fact] + public void AddMap_CaseSensitiveFalse_OnListOfStrings_ShouldMatchWrongCaseWhenGlobalIsOff() + { + var mapper = new GridifyMapper(c => c.CaseInsensitiveFiltering = false) + .AddMap("Tags", caseInsensitive: true); + + Assert.Single(DataSource.AsQueryable().ApplyFiltering("Tags=csharp", mapper).ToList()); + } + + private class TestClass + { + public string Name { get; set; } = ""; + public List Tags { get; set; } = []; + } +} From 7e8abb4215aca715af5821871176bb55d34df410 Mon Sep 17 00:00:00 2001 From: Mohit Kukreja Date: Thu, 7 May 2026 14:54:10 -0400 Subject: [PATCH 2/4] Fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Gridify/Builder/LinqQueryBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gridify/Builder/LinqQueryBuilder.cs b/src/Gridify/Builder/LinqQueryBuilder.cs index 01e22fc6..40d0ca69 100644 --- a/src/Gridify/Builder/LinqQueryBuilder.cs +++ b/src/Gridify/Builder/LinqQueryBuilder.cs @@ -31,7 +31,7 @@ public LinqQueryBuilder(IGridifyMapper mapper) : base(mapper) op, gMap.Convertor, true, - gMap.CaseInsensitive); + gMap.CaseInsensitive ?? value.IsCaseInsensitive); if (conditionExp is not LambdaExpression lambdaExp) return null; From 25d10ff117aabcf5b84c7421880453f8854125bb Mon Sep 17 00:00:00 2001 From: mohit Date: Thu, 7 May 2026 14:56:45 -0400 Subject: [PATCH 3/4] feat(mapper): add case-insensitive per-map override --- test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs b/test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs index cc826779..88900f45 100644 --- a/test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs +++ b/test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs @@ -12,7 +12,7 @@ public class CaseInsensitivePerMapTests new TestClass { Name = "Bob", Tags = ["Python", "Django"] }, }; - // --- string field: caseSensitive=true overrides global CaseInsensitiveFiltering=true --- + // --- string field: caseInsensitive=false overrides global CaseInsensitiveFiltering=true --- [Fact] public void AddMap_CaseSensitiveTrue_OnStringField_ShouldNotMatchWrongCase() @@ -32,7 +32,7 @@ public void AddMap_CaseSensitiveTrue_OnStringField_ShouldMatchCorrectCase() Assert.Single(DataSource.AsQueryable().ApplyFiltering("Name=Alice", mapper).ToList()); } - // --- string field: caseSensitive=false overrides global CaseInsensitiveFiltering=false --- + // --- string field: caseInsensitive=true overrides global CaseInsensitiveFiltering=false --- [Fact] public void AddMap_CaseSensitiveFalse_OnStringField_ShouldMatchWrongCaseWhenGlobalIsOff() @@ -43,7 +43,7 @@ public void AddMap_CaseSensitiveFalse_OnStringField_ShouldMatchWrongCaseWhenGlob Assert.Single(DataSource.AsQueryable().ApplyFiltering("Name=alice", mapper).ToList()); } - // --- List field: use the (string, convertor, caseSensitive) overload so CreateExpression + // --- List field: use the (string, convertor, caseInsensitive) overload so CreateExpression // auto-wraps the collection with .Select(fc => fc), making IsNestedCollection() true --- [Fact] From e7bc48a44c66bbd72fbb755a588e5b250be42828 Mon Sep 17 00:00:00 2001 From: mohit Date: Thu, 7 May 2026 15:08:12 -0400 Subject: [PATCH 4/4] fix: restore global case-insensitive filtering for nested collections --- src/Gridify/Builder/LinqQueryBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gridify/Builder/LinqQueryBuilder.cs b/src/Gridify/Builder/LinqQueryBuilder.cs index 40d0ca69..01e22fc6 100644 --- a/src/Gridify/Builder/LinqQueryBuilder.cs +++ b/src/Gridify/Builder/LinqQueryBuilder.cs @@ -31,7 +31,7 @@ public LinqQueryBuilder(IGridifyMapper mapper) : base(mapper) op, gMap.Convertor, true, - gMap.CaseInsensitive ?? value.IsCaseInsensitive); + gMap.CaseInsensitive); if (conditionExp is not LambdaExpression lambdaExp) return null;