Skip to content

Commit bbf1066

Browse files
committed
feat: add not in operator
fixes #17
1 parent f541829 commit bbf1066

5 files changed

Lines changed: 130 additions & 0 deletions

File tree

QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,73 @@ public async Task can_handle_case_sensitive_in_for_string()
696696
people.FirstOrDefault(x => x.Id == fakePersonOne.Id).Should().BeNull();
697697
}
698698

699+
[Fact]
700+
public async Task can_handle_not_in_for_int()
701+
{
702+
// Arrange
703+
var testingServiceScope = new TestingServiceScope();
704+
var fakePersonOne = new FakeTestingPersonBuilder()
705+
.WithAge(77435)
706+
.Build();
707+
var fakePersonTwo = new FakeTestingPersonBuilder()
708+
.WithAge(33451)
709+
.Build();
710+
await testingServiceScope.InsertAsync(fakePersonOne, fakePersonTwo);
711+
712+
var input = """Age !^^ [77435]""";
713+
714+
// Act
715+
var queryablePeople = testingServiceScope.DbContext().People;
716+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input);
717+
var people = await appliedQueryable.ToListAsync();
718+
719+
// Assert
720+
people.Count.Should().BeGreaterOrEqualTo(1);
721+
people.FirstOrDefault(x => x.Id == fakePersonOne.Id).Should().BeNull();
722+
people.FirstOrDefault(x => x.Id == fakePersonTwo.Id).Should().NotBeNull();
723+
}
724+
725+
[Fact]
726+
public async Task can_handle_case_insensitive_not_in_for_string()
727+
{
728+
// Arrange
729+
var testingServiceScope = new TestingServiceScope();
730+
var fakePersonOne = new FakeTestingPersonBuilder().Build();
731+
var fakePersonTwo = new FakeTestingPersonBuilder().Build();
732+
await testingServiceScope.InsertAsync(fakePersonOne, fakePersonTwo);
733+
734+
var input = $"""Title !^^* ["{fakePersonOne.Title.ToUpper()}"]""";
735+
736+
// Act
737+
var queryablePeople = testingServiceScope.DbContext().People;
738+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input);
739+
var people = await appliedQueryable.ToListAsync();
740+
741+
// Assert
742+
people.Count.Should().BeGreaterOrEqualTo(1);
743+
people.FirstOrDefault(x => x.Id == fakePersonOne.Id).Should().BeNull();
744+
people.FirstOrDefault(x => x.Id == fakePersonTwo.Id).Should().NotBeNull();
745+
}
746+
747+
[Fact]
748+
public async Task can_handle_case_sensitive_not_in_for_string()
749+
{
750+
// Arrange
751+
var testingServiceScope = new TestingServiceScope();
752+
var fakePersonOne = new FakeTestingPersonBuilder().Build();
753+
await testingServiceScope.InsertAsync(fakePersonOne);
754+
755+
var input = $"""Title !^^ ["{fakePersonOne.Title}"]""";
756+
757+
// Act
758+
var queryablePeople = testingServiceScope.DbContext().People;
759+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input);
760+
var people = await appliedQueryable.ToListAsync();
761+
762+
// Assert
763+
people.FirstOrDefault(x => x.Id == fakePersonOne.Id).Should().BeNull();
764+
}
765+
699766
[Fact]
700767
public async Task can_filter_on_child_entity()
701768
{

QueryKit/Configuration/QueryKitConfiguration.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public interface IQueryKitConfiguration
1616
public string NotStartsWithOperator { get; set; }
1717
public string NotEndsWithOperator { get; set; }
1818
public string InOperator { get; set; }
19+
public string NotInOperator { get; set; }
1920
public string SoundsLikeOperator { get; set; }
2021
public string DoesNotSoundLikeOperator { get; set; }
2122
public string CaseInsensitiveAppendix { get; set; }
@@ -49,6 +50,7 @@ public class QueryKitConfiguration : IQueryKitConfiguration
4950
public string NotStartsWithOperator { get; set; }
5051
public string NotEndsWithOperator { get; set; }
5152
public string InOperator { get; set; }
53+
public string NotInOperator { get; set; }
5254
public string SoundsLikeOperator { get; set; }
5355
public string DoesNotSoundLikeOperator { get; set; }
5456
public string HasCountEqualToOperator { get; set; }

QueryKit/FilterParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ from rest in Parse.LetterOrDigit.XOr(Parse.Char('_')).Many()
5959
.Or(Parse.String(ComparisonOperator.NotStartsWithOperator().Operator()).Text())
6060
.Or(Parse.String(ComparisonOperator.NotEndsWithOperator().Operator()).Text())
6161
.Or(Parse.String(ComparisonOperator.InOperator().Operator()).Text())
62+
.Or(Parse.String(ComparisonOperator.NotInOperator().Operator()).Text())
6263
.Or(Parse.String(ComparisonOperator.SoundsLikeOperator().Operator()).Text())
6364
.Or(Parse.String(ComparisonOperator.DoesNotSoundLikeOperator().Operator()).Text())
6465
.Or(Parse.String(ComparisonOperator.HasCountEqualToOperator().Operator()).Text())

QueryKit/Operators/ComparisonOperator.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public abstract class ComparisonOperator : SmartEnum<ComparisonOperator>
2222
public static ComparisonOperator CaseSensitiveNotStartsWithOperator = new NotStartsWithType();
2323
public static ComparisonOperator CaseSensitiveNotEndsWithOperator = new NotEndsWithType();
2424
public static ComparisonOperator CaseSensitiveInOperator = new InType();
25+
public static ComparisonOperator CaseSensitiveNotInOperator = new NotInType();
2526
public static ComparisonOperator CaseSensitiveSoundsLikeOperator = new SoundsLikeType();
2627
public static ComparisonOperator CaseSensitiveDoesNotSoundLikeOperator = new DoesNotSoundLikeType();
2728
public static ComparisonOperator CaseSensitiveHasCountEqualToOperator = new HasCountEqualToType();
@@ -46,6 +47,7 @@ public abstract class ComparisonOperator : SmartEnum<ComparisonOperator>
4647
public static ComparisonOperator NotStartsWithOperator(bool caseInsensitive = false, bool usesAll = false) => new NotStartsWithType(caseInsensitive);
4748
public static ComparisonOperator NotEndsWithOperator(bool caseInsensitive = false, bool usesAll = false) => new NotEndsWithType(caseInsensitive);
4849
public static ComparisonOperator InOperator(bool caseInsensitive = false, bool usesAll = false) => new InType(caseInsensitive);
50+
public static ComparisonOperator NotInOperator(bool caseInsensitive = false, bool usesAll = false) => new NotInType(caseInsensitive);
4951
public static ComparisonOperator SoundsLikeOperator(bool caseInsensitive = false, bool usesAll = false) => new SoundsLikeType(caseInsensitive);
5052
public static ComparisonOperator DoesNotSoundLikeOperator(bool caseInsensitive = false, bool usesAll = false) => new DoesNotSoundLikeType(caseInsensitive);
5153
public static ComparisonOperator HasCountEqualToOperator(bool caseInsensitive = false, bool usesAll = false) => new HasCountEqualToType(caseInsensitive);
@@ -120,6 +122,10 @@ public static ComparisonOperator GetByOperatorString(string op, bool caseInsensi
120122
{
121123
newOperator = new InType(caseInsensitive, usesAll);
122124
}
125+
if (comparisonOperator is NotInType)
126+
{
127+
newOperator = new NotInType(caseInsensitive, usesAll);
128+
}
123129
if (comparisonOperator is SoundsLikeType)
124130
{
125131
newOperator = new SoundsLikeType(caseInsensitive, usesAll);
@@ -683,6 +689,54 @@ public override Expression GetExpression<T>(Expression left, Expression right, T
683689
throw new QueryKitParsingException("DoesNotHaveType is only supported for collections");
684690
}
685691
}
692+
693+
private class NotInType : ComparisonOperator
694+
{
695+
public NotInType(bool caseInsensitive = false, bool usesAll = false) : base("!^^", 23, caseInsensitive, usesAll)
696+
{
697+
}
698+
699+
public override string Operator() => CaseInsensitive ? $"{Name}{CaseSensitiveAppendix}" : Name;
700+
public override Expression GetExpression<T>(Expression left, Expression right, Type? dbContextType)
701+
{
702+
var leftType = left.Type;
703+
704+
if (right is NewArrayExpression newArrayExpression)
705+
{
706+
var listType = typeof(List<>).MakeGenericType(leftType);
707+
var list = Activator.CreateInstance(listType);
708+
709+
foreach (var value in newArrayExpression.Expressions)
710+
{
711+
listType.GetMethod("Add").Invoke(list, new[] { ((ConstantExpression)value).Value });
712+
}
713+
714+
right = Expression.Constant(list, listType);
715+
}
716+
717+
// Get the Contains method with the correct generic type
718+
var containsMethod = typeof(ICollection<>)
719+
.MakeGenericType(leftType)
720+
.GetMethod("Contains");
721+
722+
if (CaseInsensitive && leftType == typeof(string))
723+
{
724+
var listType = typeof(List<string>);
725+
var toLowerList = Activator.CreateInstance(listType);
726+
727+
var originalList = ((ConstantExpression)right).Value as IEnumerable<string>;
728+
foreach (var value in originalList)
729+
{
730+
listType.GetMethod("Add").Invoke(toLowerList, new[] { value.ToLower() });
731+
}
732+
right = Expression.Constant(toLowerList, listType);
733+
left = Expression.Call(left, typeof(string).GetMethod("ToLower", Type.EmptyTypes));
734+
}
735+
736+
var containsExpression = Expression.Call(right, containsMethod, left);
737+
return Expression.Not(containsExpression);
738+
}
739+
}
686740

687741
internal class ComparisonAliasMatch
688742
{
@@ -759,6 +813,11 @@ internal static List<ComparisonAliasMatch> GetAliasMatches(IQueryKitConfiguratio
759813
matches.Add(new ComparisonAliasMatch { Alias = aliases.InOperator, Operator = InOperator().Operator() });
760814
matches.Add(new ComparisonAliasMatch { Alias = $"{aliases.InOperator}{caseInsensitiveAppendix}", Operator = $"{InOperator(true).Operator()}" });
761815
}
816+
if(aliases.NotInOperator != NotInOperator().Operator())
817+
{
818+
matches.Add(new ComparisonAliasMatch { Alias = aliases.NotInOperator, Operator = NotInOperator().Operator() });
819+
matches.Add(new ComparisonAliasMatch { Alias = $"{aliases.NotInOperator}{caseInsensitiveAppendix}", Operator = $"{NotInOperator(true).Operator()}" });
820+
}
762821
if(aliases.HasCountEqualToOperator != HasCountEqualToOperator().Operator())
763822
{
764823
matches.Add(new ComparisonAliasMatch { Alias = aliases.HasCountEqualToOperator, Operator = HasCountEqualToOperator().Operator() });

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ There's a wide variety of comparison operators that use the same base syntax as
110110
| Has | ^$ | ^$* | N/A |
111111
| Does Not Have | !^$ | !^$* | N/A |
112112
| In | ^^ | ^^* | N/A |
113+
| Not In | !^^ | !^^* | N/A |
113114

114115
> `Sounds Like` and `Does Not Sound Like` requires a soundex configuration on your DbContext. For more info see [the docs below](#soundex)
115116

0 commit comments

Comments
 (0)