diff --git a/src/Ardalis.Specification/Evaluators/WhereEvaluator.cs b/src/Ardalis.Specification/Evaluators/WhereEvaluator.cs index 5aff7fdd..ac184bb4 100644 --- a/src/Ardalis.Specification/Evaluators/WhereEvaluator.cs +++ b/src/Ardalis.Specification/Evaluators/WhereEvaluator.cs @@ -9,9 +9,18 @@ private WhereEvaluator() { } public IQueryable GetQuery(IQueryable query, ISpecification specification) where T : class { - foreach (var info in specification.WhereExpressions) + if (specification is Specification spec) { - query = query.Where(info.Filter); + if (spec.OneOrManyWhereExpressions.IsEmpty) return query; + if (spec.OneOrManyWhereExpressions.SingleOrDefault is { } whereExpression) + { + return query.Where(whereExpression.Filter); + } + } + + foreach (var whereExpression in specification.WhereExpressions) + { + query = query.Where(whereExpression.Filter); } return query; @@ -19,9 +28,18 @@ public IQueryable GetQuery(IQueryable query, ISpecification specific public IEnumerable Evaluate(IEnumerable query, ISpecification specification) { - foreach (var info in specification.WhereExpressions) + if (specification is Specification spec) + { + if (spec.OneOrManyWhereExpressions.IsEmpty) return query; + if (spec.OneOrManyWhereExpressions.SingleOrDefault is { } whereExpression) + { + return query.Where(whereExpression.FilterFunc); + } + } + + foreach (var whereExpression in specification.WhereExpressions) { - query = query.Where(info.FilterFunc); + query = query.Where(whereExpression.FilterFunc); } return query; diff --git a/src/Ardalis.Specification/Internals/OneOrMany.cs b/src/Ardalis.Specification/Internals/OneOrMany.cs index 3173abc3..b6a3d98f 100644 --- a/src/Ardalis.Specification/Internals/OneOrMany.cs +++ b/src/Ardalis.Specification/Internals/OneOrMany.cs @@ -2,6 +2,7 @@ internal struct OneOrMany where T : class { + private const int DEFAULT_CAPACITY = 2; private object? _value; public readonly bool IsEmpty => _value is null; @@ -58,11 +59,11 @@ public void AddSorted(T item, IComparer comparer) { if (comparer.Compare(item, singleValue) <= 0) { - _value = new List(2) { item, singleValue }; + _value = new List(DEFAULT_CAPACITY) { item, singleValue }; } else { - _value = new List(2) { singleValue, item }; + _value = new List(DEFAULT_CAPACITY) { singleValue, item }; } } } diff --git a/src/Ardalis.Specification/Specification.cs b/src/Ardalis.Specification/Specification.cs index 2fd181bf..798edc7f 100644 --- a/src/Ardalis.Specification/Specification.cs +++ b/src/Ardalis.Specification/Specification.cs @@ -26,7 +26,6 @@ public class Specification : Specification, ISpecification public class Specification : ISpecification { - private const int DEFAULT_CAPACITY_WHERE = 2; private const int DEFAULT_CAPACITY_SEARCH = 2; private const int DEFAULT_CAPACITY_ORDER = 2; private const int DEFAULT_CAPACITY_INCLUDE = 2; @@ -42,7 +41,7 @@ public class Specification : ISpecification // The state is null initially, but we're spending 8 bytes per reference (on x64). // This will be reconsidered for version 10 where we may store the whole state as a single array of structs. - private List>? _whereExpressions; + private OneOrMany> _whereExpressions = new(); private List>? _searchExpressions; private List>? _orderExpressions; private List? _includeExpressions; @@ -94,7 +93,7 @@ public class Specification : ISpecification // Specs are not intended to be thread-safe, so we don't need to worry about thread-safety here. - internal void Add(WhereExpressionInfo whereExpression) => (_whereExpressions ??= new(DEFAULT_CAPACITY_WHERE)).Add(whereExpression); + internal void Add(WhereExpressionInfo whereExpression) => _whereExpressions.Add(whereExpression); internal void Add(OrderExpressionInfo orderExpression) => (_orderExpressions ??= new(DEFAULT_CAPACITY_ORDER)).Add(orderExpression); internal void Add(IncludeExpressionInfo includeExpression) => (_includeExpressions ??= new(DEFAULT_CAPACITY_INCLUDE)).Add(includeExpression); internal void Add(string includeString) => (_includeStrings ??= new(DEFAULT_CAPACITY_INCLUDESTRING)).Add(includeString); @@ -125,7 +124,7 @@ internal void Add(SearchExpressionInfo searchExpression) public Dictionary Items => _items ??= []; /// - public IEnumerable> WhereExpressions => _whereExpressions ?? Enumerable.Empty>(); + public IEnumerable> WhereExpressions => _whereExpressions.Values; /// public IEnumerable> SearchCriterias => _searchExpressions ?? Enumerable.Empty>(); @@ -142,6 +141,7 @@ internal void Add(SearchExpressionInfo searchExpression) /// public IEnumerable QueryTags => _queryTags.Values; + internal OneOrMany> OneOrManyWhereExpressions => _whereExpressions; internal OneOrMany OneOrManyQueryTags => _queryTags; /// @@ -174,9 +174,9 @@ void ISpecification.CopyTo(Specification otherSpec) // The expression containers are immutable, having the same instance is fine. // We'll just create new collections. - if (_whereExpressions is not null) + if (!_whereExpressions.IsEmpty) { - otherSpec._whereExpressions = _whereExpressions.ToList(); + otherSpec._whereExpressions = _whereExpressions.Clone(); } if (_includeExpressions is not null) diff --git a/src/Ardalis.Specification/Validators/WhereValidator.cs b/src/Ardalis.Specification/Validators/WhereValidator.cs index 2068d9d1..178d7c3d 100644 --- a/src/Ardalis.Specification/Validators/WhereValidator.cs +++ b/src/Ardalis.Specification/Validators/WhereValidator.cs @@ -7,9 +7,18 @@ private WhereValidator() { } public bool IsValid(T entity, ISpecification specification) { - foreach (var info in specification.WhereExpressions) + if (specification is Specification spec) { - if (info.FilterFunc(entity) == false) return false; + if (spec.OneOrManyWhereExpressions.IsEmpty) return true; + if (spec.OneOrManyWhereExpressions.SingleOrDefault is { } whereExpression) + { + return whereExpression.FilterFunc(entity); + } + } + + foreach (var whereExpression in specification.WhereExpressions) + { + if (whereExpression.FilterFunc(entity) == false) return false; } return true; diff --git a/tests/Ardalis.Specification.Tests/Evaluators/WhereEvaluatorTests.cs b/tests/Ardalis.Specification.Tests/Evaluators/WhereEvaluatorTests.cs index 2e3f5002..a4987c1e 100644 --- a/tests/Ardalis.Specification.Tests/Evaluators/WhereEvaluatorTests.cs +++ b/tests/Ardalis.Specification.Tests/Evaluators/WhereEvaluatorTests.cs @@ -7,7 +7,7 @@ public class WhereEvaluatorTests public record Customer(int Id); [Fact] - public void Filters_GivenWhereExpression() + public void Filters_GivenSingleWhereExpression() { List input = [new(1), new(2), new(3), new(4), new(5)]; List expected = [new(4), new(5)]; @@ -19,6 +19,20 @@ public void Filters_GivenWhereExpression() Assert(spec, input, expected); } + [Fact] + public void Filters_GivenMultipleWhereExpressions() + { + List input = [new(1), new(2), new(3), new(4), new(5)]; + List expected = [new(4)]; + + var spec = new Specification(); + spec.Query + .Where(x => x.Id > 3) + .Where(x => x.Id < 5); + + Assert(spec, input, expected); + } + [Fact] public void DoesNotFilter_GivenNoWhereExpression() {