Skip to content

Commit

Permalink
Added support for IsNullOrWhiteSpace operator in linq queries
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Nov 28, 2023
1 parent 0e2158d commit b089bc2
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 18 deletions.
8 changes: 4 additions & 4 deletions docs/documents/querying/linq/child-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ var results = theSession
.Where(x => x.Children.Any(_ => _.Number == 6 && _.Double == -1))
.ToArray();
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L114-L121' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_any-query-through-child-collection-with-and' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L130-L137' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_any-query-through-child-collection-with-and' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Finally, you can query for child collections that do **not** contain a value:
Expand Down Expand Up @@ -112,7 +112,7 @@ public void query_against_string_array()
.Select(x => x.Id).ShouldHaveTheSameElementsAs(doc1.Id, doc2.Id);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L441-L459' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_against_string_array' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L457-L475' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_against_string_array' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Marten also allows you to query over IEnumerables using the Any method for equality (similar to Contains):
Expand Down Expand Up @@ -142,7 +142,7 @@ public void query_against_number_list_with_any()
.Count(x => x.Numbers.Any()).ShouldBe(3);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L557-L581' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_any_string_array' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L573-L597' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_any_string_array' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

As of 1.2, you can also query against the `Count()` or `Length` of a child collection with the normal comparison
Expand Down Expand Up @@ -170,7 +170,7 @@ public void query_against_number_list_with_count_method()
.Single(x => x.Numbers.Count() == 4).Id.ShouldBe(doc3.Id);
}
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L583-L604' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_against_number_list_with_count_method' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/ChildCollections/query_against_child_collections.cs#L599-L620' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_query_against_number_list_with_count_method' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## IsOneOf
Expand Down
2 changes: 1 addition & 1 deletion docs/documents/querying/linq/strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ A shorthand for case-insensitive string matching is provided through `EqualsIgno
query.Query<User>().Single(x => x.UserName.EqualsIgnoreCase("abc")).Id.ShouldBe(user1.Id);
query.Query<User>().Single(x => x.UserName.EqualsIgnoreCase("aBc")).Id.ShouldBe(user1.Id);
```
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/string_filtering.cs#L169-L174' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_sample-linq-equalsignorecase' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/marten/blob/master/src/LinqTests/Acceptance/string_filtering.cs#L222-L227' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_sample-linq-equalsignorecase' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

This defaults to `String.Equals` with `StringComparison.CurrentCultureIgnoreCase` as comparison type.
69 changes: 61 additions & 8 deletions src/LinqTests/Acceptance/string_filtering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ public class string_filtering: IntegrationContext
{
protected override Task fixtureSetup()
{
var entry = new User { FirstName = "Beeblebrox", NickName = "Beebl" };
var entry2 = new User { FirstName = "Bee", NickName = "Bee" };
var entry3 = new User { FirstName = "Zaphod", NickName = "" };
var entry4 = new User { FirstName = "Zap", NickName = null };
var entry = new User { FirstName = "Beeblebrox", Nickname = "" };
var entry2 = new User { FirstName = "Bee", Nickname = " " };
var entry3 = new User { FirstName = "Zaphod", Nickname = "Zaph" };
var entry4 = new User { FirstName = "Zap", Nickname = null };

return theStore.BulkInsertAsync(new[] { entry, entry2, entry3, entry4 });
}
Expand Down Expand Up @@ -124,20 +124,40 @@ public void CanQueryByNotEndsWith(string search, StringComparison comparison, in
public void CanQueryByIsNullOrEmpty()
{
using var s = theStore.QuerySession();
var fromDb = s.Query<User>().Where(x => string.IsNullOrEmpty(x.NickName)).ToList();
var fromDb = s.Query<User>().Where(x => string.IsNullOrEmpty(x.Nickname)).ToList();

Assert.Equal(2, fromDb.Count);
Assert.True(fromDb.All(x => string.IsNullOrEmpty(x.NickName)));
Assert.True(fromDb.All(x => string.IsNullOrEmpty(x.Nickname)));
}

[Fact]
public void CanQueryByNotIsNullOrEmpty()
{
using var s = theStore.QuerySession();
var fromDb = s.Query<User>().Where(x => !string.IsNullOrEmpty(x.NickName)).ToList();
var fromDb = s.Query<User>().Where(x => !string.IsNullOrEmpty(x.Nickname)).ToList();

Assert.Equal(2, fromDb.Count);
Assert.True(fromDb.All(x => !string.IsNullOrEmpty(x.NickName)));
Assert.True(fromDb.All(x => !string.IsNullOrEmpty(x.Nickname)));
}

[Fact]
public void CanQueryByIsNullOrWhiteSpace()
{
using var s = theStore.QuerySession();
var fromDb = s.Query<User>().Where(x => string.IsNullOrWhiteSpace(x.Nickname)).ToList();

Assert.Equal(3, fromDb.Count);
Assert.True(fromDb.All(x => string.IsNullOrWhiteSpace(x.Nickname)));
}

[Fact]
public void CanQueryByNotIsNullOrWhiteSpace()
{
using var s = theStore.QuerySession();
var fromDb = s.Query<User>().Where(x => !string.IsNullOrWhiteSpace(x.Nickname)).ToList();

Assert.Single(fromDb);
Assert.True(fromDb.All(x => !string.IsNullOrWhiteSpace(x.Nickname)));
}

[Theory]
Expand All @@ -149,7 +169,40 @@ public void CanMixContainsAndNotContains(string contains, string notContains, St
using var s = theStore.QuerySession();
var fromDb = s.Query<User>().Where(x =>
!x.FirstName.Contains(notContains, comparison) && x.FirstName.Contains(contains, comparison)).ToList();

Assert.Equal(expectedCount, fromDb.Count);
Assert.True(fromDb.All(x =>
!x.FirstName.Contains(notContains, comparison) && x.FirstName.Contains(contains, comparison)));
}

[Theory]
[InlineData("hod", StringComparison.OrdinalIgnoreCase, 1)]
[InlineData("HOD", StringComparison.OrdinalIgnoreCase, 1)]
[InlineData("Hod", StringComparison.CurrentCulture, 2)]
public void CanMixNotEndsWithWithNotIsNullOrEmpty(string search, StringComparison comparison,
int expectedCount)
{
using var s = theStore.QuerySession();
var fromDb = s.Query<User>()
.Where(x => !x.FirstName.EndsWith(search, comparison) && !string.IsNullOrEmpty(x.Nickname)).ToList();

Assert.Equal(expectedCount, fromDb.Count);
Assert.True(
fromDb.All(x => !x.FirstName.EndsWith(search, comparison) && !string.IsNullOrEmpty(x.Nickname)));
}

[Theory]
[InlineData("zap", StringComparison.OrdinalIgnoreCase, 1)]
[InlineData("zap", StringComparison.CurrentCulture, 0)]
public void CanMixStartsWithAndIsNullOrWhiteSpace(string search, StringComparison comparison, int expectedCount)
{
using var s = theStore.QuerySession();
var fromDb = s.Query<User>()
.Where(x => x.FirstName.StartsWith(search, comparison) && string.IsNullOrWhiteSpace(x.Nickname)).ToList();

Assert.Equal(expectedCount, fromDb.Count);
Assert.True(
fromDb.All(x => x.FirstName.StartsWith(search, comparison) && string.IsNullOrWhiteSpace(x.Nickname)));
}

[Fact]
Expand Down
24 changes: 20 additions & 4 deletions src/LinqTests/ChildCollections/query_against_child_collections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ private void buildUpTargetData()
targets[9].Children[0].Number = 6;
targets[12].Children[0].Number = 6;

targets[9].Children[0].NullableString = "";
targets[12].Children[0].NullableString = Guid.NewGuid().ToString();
targets[9].Children.Each(c => c.NullableString = "");
targets[12].Children.Each(c => c.NullableString = Guid.NewGuid().ToString());

targets[5].Children[0].Double = -1;
targets[9].Children[0].Double = -1;
Expand Down Expand Up @@ -87,7 +87,7 @@ public void can_query_with_an_any_operator()
}

[Fact]
public void can_query_with_an_any_operator_and_string_IsNullOrEmpty()
public void can_query_with_an_any_operator_and_string_NotIsNullOrEmpty()
{
buildUpTargetData();

Expand All @@ -99,11 +99,27 @@ public void can_query_with_an_any_operator_and_string_IsNullOrEmpty()

results
.Select(x => x.Id)
.OrderBy(x => x)
.ShouldHaveSingleItem()
.ShouldBe(targets[12].Id);
}

[Fact]
public void can_query_with_an_any_operator_and_string_IsNullOrWhitespace()
{
buildUpTargetData();

theSession.Logger = new TestOutputMartenLogger(_output);

var results = theSession.Query<Target>()
.Where(x => x.Children.Any(c => string.IsNullOrWhiteSpace(c.NullableString)))
.ToArray();

var ids = results.Select(x => x.Id).ToArray();

ids.Length.ShouldBe(targets.Length - 1);
ids.ShouldNotContain(targets[12].Id);
}

[Fact]
public void can_query_with_an_any_operator_that_does_a_multiple_search_within_the_collection()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Marten.Testing/Documents/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public User()
public string FirstName { get; set; }
public string LastName { get; set; }

public string? NickName { get; set; }
public string? Nickname { get; set; }
public bool Internal { get; set; }
public string Department { get; set; } = "";
public string FullName => $"{FirstName} {LastName}";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Linq.Expressions;
using Marten.Linq.Members;
using Weasel.Postgresql.SqlGeneration;

namespace Marten.Linq.Parsing.Methods.Strings;

internal class StringIsNullOrWhiteSpace: IMethodCallParser
{
public bool Matches(MethodCallExpression expression)
{
return expression.Method.Name == nameof(string.IsNullOrWhiteSpace)
&& expression.Method.DeclaringType == typeof(string);
}

public ISqlFragment Parse(IQueryableMemberCollection memberCollection, IReadOnlyStoreOptions options,
MethodCallExpression expression)
{
var locator = memberCollection.MemberFor(expression.Arguments[0]).RawLocator;

return new WhereFragment($"({locator} IS NULL OR trim({locator}) = '')");
}
}
1 change: 1 addition & 0 deletions src/Marten/LinqParsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class LinqParsing: IReadOnlyLinqParsing
new StringEndsWith(),
new StringStartsWith(),
new StringIsNullOrEmpty(),
new StringIsNullOrWhiteSpace(),
new StringEquals(),
new SimpleEqualsParser(),
new AnySubQueryParser(),
Expand Down

0 comments on commit b089bc2

Please sign in to comment.