Skip to content

Commit 8513acf

Browse files
authored
Refactor the state of where expressions as OneOrMany. (#508)
1 parent fde098d commit 8513acf

File tree

5 files changed

+57
-15
lines changed

5 files changed

+57
-15
lines changed

src/Ardalis.Specification/Evaluators/WhereEvaluator.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,37 @@ private WhereEvaluator() { }
99

1010
public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification) where T : class
1111
{
12-
foreach (var info in specification.WhereExpressions)
12+
if (specification is Specification<T> spec)
1313
{
14-
query = query.Where(info.Filter);
14+
if (spec.OneOrManyWhereExpressions.IsEmpty) return query;
15+
if (spec.OneOrManyWhereExpressions.SingleOrDefault is { } whereExpression)
16+
{
17+
return query.Where(whereExpression.Filter);
18+
}
19+
}
20+
21+
foreach (var whereExpression in specification.WhereExpressions)
22+
{
23+
query = query.Where(whereExpression.Filter);
1524
}
1625

1726
return query;
1827
}
1928

2029
public IEnumerable<T> Evaluate<T>(IEnumerable<T> query, ISpecification<T> specification)
2130
{
22-
foreach (var info in specification.WhereExpressions)
31+
if (specification is Specification<T> spec)
32+
{
33+
if (spec.OneOrManyWhereExpressions.IsEmpty) return query;
34+
if (spec.OneOrManyWhereExpressions.SingleOrDefault is { } whereExpression)
35+
{
36+
return query.Where(whereExpression.FilterFunc);
37+
}
38+
}
39+
40+
foreach (var whereExpression in specification.WhereExpressions)
2341
{
24-
query = query.Where(info.FilterFunc);
42+
query = query.Where(whereExpression.FilterFunc);
2543
}
2644

2745
return query;

src/Ardalis.Specification/Internals/OneOrMany.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
internal struct OneOrMany<T> where T : class
44
{
5+
private const int DEFAULT_CAPACITY = 2;
56
private object? _value;
67

78
public readonly bool IsEmpty => _value is null;
@@ -58,11 +59,11 @@ public void AddSorted(T item, IComparer<T> comparer)
5859
{
5960
if (comparer.Compare(item, singleValue) <= 0)
6061
{
61-
_value = new List<T>(2) { item, singleValue };
62+
_value = new List<T>(DEFAULT_CAPACITY) { item, singleValue };
6263
}
6364
else
6465
{
65-
_value = new List<T>(2) { singleValue, item };
66+
_value = new List<T>(DEFAULT_CAPACITY) { singleValue, item };
6667
}
6768
}
6869
}

src/Ardalis.Specification/Specification.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public class Specification<T, TResult> : Specification<T>, ISpecification<T, TRe
2626
/// <inheritdoc cref="ISpecification{T}"/>
2727
public class Specification<T> : ISpecification<T>
2828
{
29-
private const int DEFAULT_CAPACITY_WHERE = 2;
3029
private const int DEFAULT_CAPACITY_SEARCH = 2;
3130
private const int DEFAULT_CAPACITY_ORDER = 2;
3231
private const int DEFAULT_CAPACITY_INCLUDE = 2;
@@ -42,7 +41,7 @@ public class Specification<T> : ISpecification<T>
4241

4342
// The state is null initially, but we're spending 8 bytes per reference (on x64).
4443
// This will be reconsidered for version 10 where we may store the whole state as a single array of structs.
45-
private List<WhereExpressionInfo<T>>? _whereExpressions;
44+
private OneOrMany<WhereExpressionInfo<T>> _whereExpressions = new();
4645
private List<SearchExpressionInfo<T>>? _searchExpressions;
4746
private List<OrderExpressionInfo<T>>? _orderExpressions;
4847
private List<IncludeExpressionInfo>? _includeExpressions;
@@ -94,7 +93,7 @@ public class Specification<T> : ISpecification<T>
9493

9594

9695
// Specs are not intended to be thread-safe, so we don't need to worry about thread-safety here.
97-
internal void Add(WhereExpressionInfo<T> whereExpression) => (_whereExpressions ??= new(DEFAULT_CAPACITY_WHERE)).Add(whereExpression);
96+
internal void Add(WhereExpressionInfo<T> whereExpression) => _whereExpressions.Add(whereExpression);
9897
internal void Add(OrderExpressionInfo<T> orderExpression) => (_orderExpressions ??= new(DEFAULT_CAPACITY_ORDER)).Add(orderExpression);
9998
internal void Add(IncludeExpressionInfo includeExpression) => (_includeExpressions ??= new(DEFAULT_CAPACITY_INCLUDE)).Add(includeExpression);
10099
internal void Add(string includeString) => (_includeStrings ??= new(DEFAULT_CAPACITY_INCLUDESTRING)).Add(includeString);
@@ -125,7 +124,7 @@ internal void Add(SearchExpressionInfo<T> searchExpression)
125124
public Dictionary<string, object> Items => _items ??= [];
126125

127126
/// <inheritdoc/>
128-
public IEnumerable<WhereExpressionInfo<T>> WhereExpressions => _whereExpressions ?? Enumerable.Empty<WhereExpressionInfo<T>>();
127+
public IEnumerable<WhereExpressionInfo<T>> WhereExpressions => _whereExpressions.Values;
129128

130129
/// <inheritdoc/>
131130
public IEnumerable<SearchExpressionInfo<T>> SearchCriterias => _searchExpressions ?? Enumerable.Empty<SearchExpressionInfo<T>>();
@@ -142,6 +141,7 @@ internal void Add(SearchExpressionInfo<T> searchExpression)
142141
/// <inheritdoc/>
143142
public IEnumerable<string> QueryTags => _queryTags.Values;
144143

144+
internal OneOrMany<WhereExpressionInfo<T>> OneOrManyWhereExpressions => _whereExpressions;
145145
internal OneOrMany<string> OneOrManyQueryTags => _queryTags;
146146

147147
/// <inheritdoc/>
@@ -174,9 +174,9 @@ void ISpecification<T>.CopyTo(Specification<T> otherSpec)
174174
// The expression containers are immutable, having the same instance is fine.
175175
// We'll just create new collections.
176176

177-
if (_whereExpressions is not null)
177+
if (!_whereExpressions.IsEmpty)
178178
{
179-
otherSpec._whereExpressions = _whereExpressions.ToList();
179+
otherSpec._whereExpressions = _whereExpressions.Clone();
180180
}
181181

182182
if (_includeExpressions is not null)

src/Ardalis.Specification/Validators/WhereValidator.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,18 @@ private WhereValidator() { }
77

88
public bool IsValid<T>(T entity, ISpecification<T> specification)
99
{
10-
foreach (var info in specification.WhereExpressions)
10+
if (specification is Specification<T> spec)
1111
{
12-
if (info.FilterFunc(entity) == false) return false;
12+
if (spec.OneOrManyWhereExpressions.IsEmpty) return true;
13+
if (spec.OneOrManyWhereExpressions.SingleOrDefault is { } whereExpression)
14+
{
15+
return whereExpression.FilterFunc(entity);
16+
}
17+
}
18+
19+
foreach (var whereExpression in specification.WhereExpressions)
20+
{
21+
if (whereExpression.FilterFunc(entity) == false) return false;
1322
}
1423

1524
return true;

tests/Ardalis.Specification.Tests/Evaluators/WhereEvaluatorTests.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public class WhereEvaluatorTests
77
public record Customer(int Id);
88

99
[Fact]
10-
public void Filters_GivenWhereExpression()
10+
public void Filters_GivenSingleWhereExpression()
1111
{
1212
List<Customer> input = [new(1), new(2), new(3), new(4), new(5)];
1313
List<Customer> expected = [new(4), new(5)];
@@ -19,6 +19,20 @@ public void Filters_GivenWhereExpression()
1919
Assert(spec, input, expected);
2020
}
2121

22+
[Fact]
23+
public void Filters_GivenMultipleWhereExpressions()
24+
{
25+
List<Customer> input = [new(1), new(2), new(3), new(4), new(5)];
26+
List<Customer> expected = [new(4)];
27+
28+
var spec = new Specification<Customer>();
29+
spec.Query
30+
.Where(x => x.Id > 3)
31+
.Where(x => x.Id < 5);
32+
33+
Assert(spec, input, expected);
34+
}
35+
2236
[Fact]
2337
public void DoesNotFilter_GivenNoWhereExpression()
2438
{

0 commit comments

Comments
 (0)