Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/pages/guide/gridifyMapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -351,6 +352,14 @@ If true, string comparison operations are case insensitive by default.
var mapper = new GridifyMapper<Person>(q => q.CaseInsensitiveFiltering = true);
```

You can also override this behavior per map:

```csharp
var mapper = new GridifyMapper<Person>(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.
Expand Down
14 changes: 9 additions & 5 deletions src/Gridify/Builder/BaseQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -264,7 +264,8 @@ private object AddIndexerNullCheck(LambdaExpression mapTarget, object query)
ValueExpressionSyntax valueExpression,
ISyntaxNode op,
Func<string, object>? convertor,
bool isNested)
bool isNested,
bool? mapCaseInsensitive)
{
// Remove the boxing for value types
if (body.NodeType == ExpressionType.Convert) body = ((UnaryExpression)body).Operand;
Expand Down Expand Up @@ -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);
Comment thread
alirezanet marked this conversation as resolved.
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
Expand Down
5 changes: 3 additions & 2 deletions src/Gridify/Builder/LinqQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public LinqQueryBuilder(IGridifyMapper<T> mapper) : base(mapper)
value,
op,
gMap.Convertor,
true);
true,
gMap.CaseInsensitive);
Comment thread
moxplod marked this conversation as resolved.

if (conditionExp is not LambdaExpression lambdaExp) return null;

Expand Down Expand Up @@ -272,7 +273,7 @@ protected override Expression<Func<T, bool>> CombineWithOrOperator(Expression<Fu
case MethodCallExpression { Method.Name: "Select" } selectExp:
{
var targetExp = selectExp.Arguments.Single(a => 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;

Expand Down
1 change: 1 addition & 0 deletions src/Gridify/CompositeGMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class CompositeGMap<T> : IGMap<T>
public string From { get; set; }
public LambdaExpression To { get; set; }
public Func<string, object>? Convertor { get; set; }
public bool? CaseInsensitive { get; set; }

/// <summary>
/// Collection of expressions that will be combined with OR logic
Expand Down
1 change: 1 addition & 0 deletions src/Gridify/GMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public partial class GMap<T> : IGMap<T>
public string From { get; set; }
public LambdaExpression To { get; set; }
public Func<string, object>? Convertor { get; set; }
public bool? CaseInsensitive { get; set; }

public GMap(string from, Expression<Func<T, object?>> to, Func<string, object>? convertor = null)
{
Expand Down
8 changes: 4 additions & 4 deletions src/Gridify/GridifyMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public GridifyMapper(Action<GridifyMapperConfiguration> configuration, bool auto
GenerateMappings();
}

public IGridifyMapper<T> AddMap(string from, Func<string, object>? convertor = null!, bool overrideIfExists = true)
public IGridifyMapper<T> AddMap(string from, Func<string, object>? convertor = null!, bool overrideIfExists = true, bool? caseInsensitive = null)
{
if (!overrideIfExists && HasMap(from))
throw new GridifyMapperException($"Duplicate Key. the '{from}' key already exists");
Expand All @@ -57,7 +57,7 @@ public IGridifyMapper<T> AddMap(string from, Func<string, object>? convertor = n
}

RemoveMap(from);
_mappings.Add(new GMap<T>(from, to!, convertor));
_mappings.Add(new GMap<T>(from, to!, convertor) { CaseInsensitive = caseInsensitive });
return this;
}

Expand Down Expand Up @@ -105,13 +105,13 @@ private void GenerateMappingsRecursive(Type type, string prefix, ushort maxNesti
}

public IGridifyMapper<T> AddMap(string from, Expression<Func<T, object?>> to, Func<string, object>? 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<T>(from, to, convertor));
_mappings.Add(new GMap<T>(from, to, convertor) { CaseInsensitive = caseInsensitive });
return this;
}

Expand Down
4 changes: 3 additions & 1 deletion src/Gridify/IGMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ public interface IGMap<T>
string From { get; set; }
LambdaExpression To { get; set; }
Func<string, object>? Convertor { get; set; }
Comment thread
alirezanet marked this conversation as resolved.
}
// null = use global config, true = force case-insensitive, false = force case-sensitive
bool? CaseInsensitive { get; set; }
}
4 changes: 2 additions & 2 deletions src/Gridify/IGridifyMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Gridify;

public interface IGridifyMapper<T>
{
IGridifyMapper<T> AddMap(string from, Expression<Func<T, object?>> to, Func<string, object>? convertor = null, bool overrideIfExists = true);
IGridifyMapper<T> AddMap(string from, Expression<Func<T, object?>> to, Func<string, object>? convertor = null, bool overrideIfExists = true, bool? caseInsensitive = null);

IGridifyMapper<T> AddMap(string from, Expression<Func<T, int, object?>> to, Func<string, object>? convertor = null!,
bool overrideIfExists = true);
Expand All @@ -16,7 +16,7 @@ IGridifyMapper<T> AddMap<TSubKey>(string from, Expression<Func<T, TSubKey, objec
bool overrideIfExists = true);

IGridifyMapper<T> AddMap(IGMap<T> gMap, bool overrideIfExists = true);
IGridifyMapper<T> AddMap(string from, Func<string, object>? convertor = null!, bool overrideIfExists = true);
IGridifyMapper<T> AddMap(string from, Func<string, object>? convertor = null!, bool overrideIfExists = true, bool? caseInsensitive = null);

Comment thread
alirezanet marked this conversation as resolved.
/// <summary>
/// Adds a composite mapping that combines multiple property expressions with OR logic.
Expand Down
81 changes: 81 additions & 0 deletions test/Gridify.Tests/IssueTests/CaseInsensitivePerMapTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace Gridify.Tests.IssueTests;

public class CaseInsensitivePerMapTests
{
private List<TestClass> 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<TestClass>(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<TestClass>(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 ---
Comment thread
alirezanet marked this conversation as resolved.
Outdated

[Fact]
public void AddMap_CaseSensitiveFalse_OnStringField_ShouldMatchWrongCaseWhenGlobalIsOff()
{
var mapper = new GridifyMapper<TestClass>(c => c.CaseInsensitiveFiltering = false)
.AddMap("Name", x => x.Name, caseInsensitive: true);

Assert.Single(DataSource.AsQueryable().ApplyFiltering("Name=alice", mapper).ToList());
}

// --- List<string> 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<TestClass>(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<TestClass>(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<TestClass>(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<string> Tags { get; set; } = [];
}
}