Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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);

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: caseInsensitive=false 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: caseInsensitive=true overrides global CaseInsensitiveFiltering=false ---

[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, caseInsensitive) 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; } = [];
}
}