Skip to content

Commit 254472a

Browse files
authored
release: v1.2.0
Develop
2 parents 1417e7b + 6eb5824 commit 254472a

File tree

9 files changed

+147
-2
lines changed

9 files changed

+147
-2
lines changed

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.WebApiTestProject/Database/PersonConfiguration.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,18 @@ public void Configure(EntityTypeBuilder<TestingPerson> builder)
2626
opts.Property(x => x.Country).HasColumnName("physical_address_country");
2727
}).Navigation(x => x.PhysicalAddress)
2828
.IsRequired();
29+
30+
// builder.ComplexProperty(
31+
// x => x.PhysicalAddress,
32+
// y =>
33+
// {
34+
// y.Property(e => e.Line1).HasColumnName("physical_address_line1");
35+
// y.Property(e => e.Line2).HasColumnName("physical_address_line2");
36+
// y.Property(e => e.City).HasColumnName("physical_address_city");
37+
// y.Property(e => e.State).HasColumnName("physical_address_state");
38+
// y.ComplexProperty(e => e.PostalCode, z
39+
// => z.Property(s => s.Value).HasColumnName("physical_address_postal_code"));
40+
// y.Property(e => e.Country).HasColumnName("physical_address_country");
41+
// });
2942
}
3043
}

QueryKit.WebApiTestProject/Database/RecipeConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void Configure(EntityTypeBuilder<Recipe> builder)
1616
builder.ComplexProperty(x => x.CollectionEmail,
1717
x => x.Property(y => y.Value)
1818
.HasColumnName("collection_email"));
19-
19+
2020
// example for a simple 1:1 value object
2121
// builder.Property(x => x.Percent)
2222
// .HasConversion(x => x.Value, x => new Percent(x))

QueryKit.WebApiTestProject/Entities/Address.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ public Address(string line1, string line2, string city, string state, PostalCode
2222
PostalCode = postalCode;
2323
Country = country;
2424
}
25+
26+
private Address() { }
2527
}

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() });

QueryKit/QueryKit.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Nullable>enable</Nullable>
77
<PackageId>QueryKit</PackageId>
88
<PackageTags>QueryKit;Fitler;Filtering;Sort;Sorting</PackageTags>
9-
<Version>1.1.0</Version>
9+
<Version>1.2.0</Version>
1010
<Authors>Paul DeVito</Authors>
1111
<Summary>QueryKit is a .NET library that makes it easier to query your data by providing a fluent and intuitive syntax for filtering and sorting.</Summary>
1212
<Description>QueryKit is a .NET library that makes it easier to query your data by providing a fluent and intuitive syntax for filtering and sorting.</Description>

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)