Skip to content

Commit 39e082b

Browse files
authored
Implement SelectMany support (#320)
* Added SelectMany support * Renamed ISpecification SelectManyExpression to SelectorMany to match existing Selector * Added repository integration tests for SelectMany spec
1 parent 14bddfd commit 39e082b

16 files changed

Lines changed: 162 additions & 17 deletions

File tree

Specification.EntityFramework6/src/Ardalis.Specification.EntityFramework6/Evaluators/SpecificationEvaluator.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ public SpecificationEvaluator(IEnumerable<IEvaluator> evaluators)
3333
public virtual IQueryable<TResult> GetQuery<T, TResult>(IQueryable<T> query, ISpecification<T, TResult> specification) where T : class
3434
{
3535
if (specification is null) throw new ArgumentNullException("Specification is required");
36-
if (specification.Selector is null) throw new SelectorNotFoundException();
36+
if (specification.Selector is null && specification.SelectorMany is null) throw new SelectorNotFoundException();
37+
if (specification.Selector != null && specification.SelectorMany != null) throw new ConcurrentSelectorsException();
3738

3839
query = GetQuery(query, (ISpecification<T>)specification);
3940

40-
return query.Select(specification.Selector);
41+
return specification.Selector != null
42+
? query.Select(specification.Selector)
43+
: query.SelectMany(specification.SelectorMany);
4144
}
4245

4346
/// <inheritdoc/>

Specification.EntityFramework6/tests/Ardalis.Specification.EntityFramework6.IntegrationTests/RepositoryOfT_ListAsync.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,5 +170,15 @@ public async Task ReturnsStoreContainingCity1_GivenStoreIncludeProductsSpec()
170170
result[0].Id.Should().Be(StoreSeed.VALID_Search_ID);
171171
result[0].City.Should().Contain(StoreSeed.VALID_Search_City_Key);
172172
}
173+
174+
[Fact]
175+
public virtual async Task ReturnsAllProducts_GivenStoreSelectManyProductsSpec()
176+
{
177+
var result = await storeRepository.ListAsync(new StoreProductNamesSpec());
178+
179+
result.Should().NotBeNull();
180+
result.Should().HaveCount(ProductSeed.TOTAL_PRODUCT_COUNT);
181+
result.OrderBy(x => x).First().Should().Be(ProductSeed.VALID_PRODUCT_NAME);
182+
}
173183
}
174184
}

Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/Evaluators/SpecificationEvaluator.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ public SpecificationEvaluator(IEnumerable<IEvaluator> evaluators)
4747
public virtual IQueryable<TResult> GetQuery<T, TResult>(IQueryable<T> query, ISpecification<T, TResult> specification) where T : class
4848
{
4949
if (specification is null) throw new ArgumentNullException("Specification is required");
50-
if (specification.Selector is null) throw new SelectorNotFoundException();
50+
if (specification.Selector is null && specification.SelectorMany is null) throw new SelectorNotFoundException();
51+
if (specification.Selector != null && specification.SelectorMany != null) throw new ConcurrentSelectorsException();
5152

5253
query = GetQuery(query, (ISpecification<T>)specification);
5354

54-
return query.Select(specification.Selector);
55+
return specification.Selector is not null
56+
? query.Select(specification.Selector)
57+
: query.SelectMany(specification.SelectorMany!);
5558
}
5659

5760
/// <inheritdoc/>

Specification.EntityFrameworkCore/tests/Ardalis.Specification.EntityFrameworkCore.IntegrationTests/RepositoryOfT_ListAsync.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,15 @@ public virtual async Task ReturnsStoreContainingCity1_GivenStoreIncludeProductsS
196196
result[0].Id.Should().Be(StoreSeed.VALID_Search_ID);
197197
result[0].City.Should().Contain(StoreSeed.VALID_Search_City_Key);
198198
}
199+
200+
[Fact]
201+
public virtual async Task ReturnsAllProducts_GivenStoreSelectManyProductsSpec()
202+
{
203+
var result = await storeRepository.ListAsync(new StoreProductNamesSpec());
204+
205+
result.Should().NotBeNull();
206+
result.Should().HaveCount(ProductSeed.TOTAL_PRODUCT_COUNT);
207+
result.OrderBy(x => x).First().Should().Be(ProductSeed.VALID_PRODUCT_NAME);
208+
}
199209
}
200210
}

Specification/src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,19 @@ public static ISpecificationBuilder<T, TResult> Select<T, TResult>(
291291
return specificationBuilder;
292292
}
293293

294+
/// <summary>
295+
/// Specify a transform function to apply to the <typeparamref name="T"/> element
296+
/// to produce a flattened sequence of <typeparamref name="TResult"/> elements.
297+
/// </summary>
298+
public static ISpecificationBuilder<T, TResult> SelectMany<T, TResult>(
299+
this ISpecificationBuilder<T, TResult> specificationBuilder,
300+
Expression<Func<T, IEnumerable<TResult>>> selector)
301+
{
302+
specificationBuilder.Specification.SelectorMany = selector;
303+
304+
return specificationBuilder;
305+
}
306+
294307
/// <summary>
295308
/// Specify a transform function to apply to the result of the query
296309
/// and returns the same <typeparamref name="T"/> type

Specification/src/Ardalis.Specification/Evaluators/InMemorySpecificationEvaluator.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Linq.Expressions;
@@ -30,11 +31,14 @@ public InMemorySpecificationEvaluator(IEnumerable<IInMemoryEvaluator> evaluators
3031

3132
public virtual IEnumerable<TResult> Evaluate<T, TResult>(IEnumerable<T> source, ISpecification<T, TResult> specification)
3233
{
33-
_ = specification.Selector ?? throw new SelectorNotFoundException();
34+
if (specification.Selector is null && specification.SelectorMany is null) throw new SelectorNotFoundException();
35+
if (specification.Selector != null && specification.SelectorMany != null) throw new ConcurrentSelectorsException();
3436

3537
var baseQuery = Evaluate(source, (ISpecification<T>)specification);
3638

37-
var resultQuery = baseQuery.Select(specification.Selector.Compile());
39+
var resultQuery = specification.Selector != null
40+
? baseQuery.Select(specification.Selector.Compile())
41+
: baseQuery.SelectMany(specification.SelectorMany!.Compile());
3842

3943
return specification.PostProcessingAction == null
4044
? resultQuery
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
3+
namespace Ardalis.Specification
4+
{
5+
public class ConcurrentSelectorsException : Exception
6+
{
7+
private const string message = "Concurrent specification selector transforms defined. Ensure only one of the Select() or SelectMany() transforms is used in the same specification!";
8+
9+
public ConcurrentSelectorsException()
10+
: base(message)
11+
{
12+
}
13+
14+
public ConcurrentSelectorsException(Exception innerException)
15+
: base(message, innerException)
16+
{
17+
}
18+
}
19+
}

Specification/src/Ardalis.Specification/Exceptions/SelectorNotFoundException.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
42

53
namespace Ardalis.Specification
64
{
75
public class SelectorNotFoundException : Exception
86
{
9-
private const string message = "The specification must have Selector defined.";
7+
private const string message = "The specification must have a selector transform defined. Ensure either Select() or SelectMany() is used in the specification!";
108

119
public SelectorNotFoundException()
1210
: base(message)

Specification/src/Ardalis.Specification/ISpecification.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ public interface ISpecification<T, TResult> : ISpecification<T>
1515
ISpecificationBuilder<T, TResult> Query { get; }
1616

1717
/// <summary>
18-
/// The transform function to apply to the <typeparamref name="T"/> element.
18+
/// The Select transform function to apply to the <typeparamref name="T"/> element.
1919
/// </summary>
2020
Expression<Func<T, TResult>>? Selector { get; }
2121

22+
/// <summary>
23+
/// The SelectMany transform function to apply to the <typeparamref name="T"/> element.
24+
/// </summary>
25+
Expression<Func<T, IEnumerable<TResult>>>? SelectorMany { get; }
26+
2227
/// <summary>
2328
/// The transform function to apply to the result of the query encapsulated by the <see cref="ISpecification{T, TResult}"/>.
2429
/// </summary>

Specification/src/Ardalis.Specification/Specification.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ protected Specification(IInMemorySpecificationEvaluator inMemorySpecificationEva
2828
/// <inheritdoc/>
2929
public Expression<Func<T, TResult>>? Selector { get; internal set; }
3030

31+
/// <inheritdoc/>
32+
public Expression<Func<T, IEnumerable<TResult>>>? SelectorMany { get; internal set; }
33+
3134
/// <inheritdoc/>
3235
public new Func<IEnumerable<TResult>, IEnumerable<TResult>>? PostProcessingAction { get; internal set; } = null;
3336
}

0 commit comments

Comments
 (0)