Skip to content

Commit

Permalink
Implement SelectMany support (#320)
Browse files Browse the repository at this point in the history
* Added SelectMany support

* Renamed ISpecification SelectManyExpression to SelectorMany to match existing Selector

* Added repository integration tests for SelectMany spec
  • Loading branch information
amdavie authored Apr 8, 2023
1 parent 14bddfd commit 39e082b
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ public SpecificationEvaluator(IEnumerable<IEvaluator> evaluators)
public virtual IQueryable<TResult> GetQuery<T, TResult>(IQueryable<T> query, ISpecification<T, TResult> specification) where T : class
{
if (specification is null) throw new ArgumentNullException("Specification is required");
if (specification.Selector is null) throw new SelectorNotFoundException();
if (specification.Selector is null && specification.SelectorMany is null) throw new SelectorNotFoundException();
if (specification.Selector != null && specification.SelectorMany != null) throw new ConcurrentSelectorsException();

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

return query.Select(specification.Selector);
return specification.Selector != null
? query.Select(specification.Selector)
: query.SelectMany(specification.SelectorMany);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,5 +170,15 @@ public async Task ReturnsStoreContainingCity1_GivenStoreIncludeProductsSpec()
result[0].Id.Should().Be(StoreSeed.VALID_Search_ID);
result[0].City.Should().Contain(StoreSeed.VALID_Search_City_Key);
}

[Fact]
public virtual async Task ReturnsAllProducts_GivenStoreSelectManyProductsSpec()
{
var result = await storeRepository.ListAsync(new StoreProductNamesSpec());

result.Should().NotBeNull();
result.Should().HaveCount(ProductSeed.TOTAL_PRODUCT_COUNT);
result.OrderBy(x => x).First().Should().Be(ProductSeed.VALID_PRODUCT_NAME);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ public SpecificationEvaluator(IEnumerable<IEvaluator> evaluators)
public virtual IQueryable<TResult> GetQuery<T, TResult>(IQueryable<T> query, ISpecification<T, TResult> specification) where T : class
{
if (specification is null) throw new ArgumentNullException("Specification is required");
if (specification.Selector is null) throw new SelectorNotFoundException();
if (specification.Selector is null && specification.SelectorMany is null) throw new SelectorNotFoundException();
if (specification.Selector != null && specification.SelectorMany != null) throw new ConcurrentSelectorsException();

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

return query.Select(specification.Selector);
return specification.Selector is not null
? query.Select(specification.Selector)
: query.SelectMany(specification.SelectorMany!);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,15 @@ public virtual async Task ReturnsStoreContainingCity1_GivenStoreIncludeProductsS
result[0].Id.Should().Be(StoreSeed.VALID_Search_ID);
result[0].City.Should().Contain(StoreSeed.VALID_Search_City_Key);
}

[Fact]
public virtual async Task ReturnsAllProducts_GivenStoreSelectManyProductsSpec()
{
var result = await storeRepository.ListAsync(new StoreProductNamesSpec());

result.Should().NotBeNull();
result.Should().HaveCount(ProductSeed.TOTAL_PRODUCT_COUNT);
result.OrderBy(x => x).First().Should().Be(ProductSeed.VALID_PRODUCT_NAME);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,19 @@ public static ISpecificationBuilder<T, TResult> Select<T, TResult>(
return specificationBuilder;
}

/// <summary>
/// Specify a transform function to apply to the <typeparamref name="T"/> element
/// to produce a flattened sequence of <typeparamref name="TResult"/> elements.
/// </summary>
public static ISpecificationBuilder<T, TResult> SelectMany<T, TResult>(
this ISpecificationBuilder<T, TResult> specificationBuilder,
Expression<Func<T, IEnumerable<TResult>>> selector)
{
specificationBuilder.Specification.SelectorMany = selector;

return specificationBuilder;
}

/// <summary>
/// Specify a transform function to apply to the result of the query
/// and returns the same <typeparamref name="T"/> type
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
Expand Down Expand Up @@ -30,11 +31,14 @@ public InMemorySpecificationEvaluator(IEnumerable<IInMemoryEvaluator> evaluators

public virtual IEnumerable<TResult> Evaluate<T, TResult>(IEnumerable<T> source, ISpecification<T, TResult> specification)
{
_ = specification.Selector ?? throw new SelectorNotFoundException();
if (specification.Selector is null && specification.SelectorMany is null) throw new SelectorNotFoundException();
if (specification.Selector != null && specification.SelectorMany != null) throw new ConcurrentSelectorsException();

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

var resultQuery = baseQuery.Select(specification.Selector.Compile());
var resultQuery = specification.Selector != null
? baseQuery.Select(specification.Selector.Compile())
: baseQuery.SelectMany(specification.SelectorMany!.Compile());

return specification.PostProcessingAction == null
? resultQuery
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace Ardalis.Specification
{
public class ConcurrentSelectorsException : Exception
{
private const string message = "Concurrent specification selector transforms defined. Ensure only one of the Select() or SelectMany() transforms is used in the same specification!";

public ConcurrentSelectorsException()
: base(message)
{
}

public ConcurrentSelectorsException(Exception innerException)
: base(message, innerException)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;

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

public SelectorNotFoundException()
: base(message)
Expand Down
7 changes: 6 additions & 1 deletion Specification/src/Ardalis.Specification/ISpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ public interface ISpecification<T, TResult> : ISpecification<T>
ISpecificationBuilder<T, TResult> Query { get; }

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

/// <summary>
/// The SelectMany transform function to apply to the <typeparamref name="T"/> element.
/// </summary>
Expression<Func<T, IEnumerable<TResult>>>? SelectorMany { get; }

/// <summary>
/// The transform function to apply to the result of the query encapsulated by the <see cref="ISpecification{T, TResult}"/>.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions Specification/src/Ardalis.Specification/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ protected Specification(IInMemorySpecificationEvaluator inMemorySpecificationEva
/// <inheritdoc/>
public Expression<Func<T, TResult>>? Selector { get; internal set; }

/// <inheritdoc/>
public Expression<Func<T, IEnumerable<TResult>>>? SelectorMany { get; internal set; }

/// <inheritdoc/>
public new Func<IEnumerable<TResult>, IEnumerable<TResult>>? PostProcessingAction { get; internal set; } = null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Ardalis.Specification.UnitTests.Fixture.Specs;
using FluentAssertions;
using Xunit;

namespace Ardalis.Specification.UnitTests
{
public class SpecificationBuilderExtensions_SelectMany
{
[Fact]
public void SetsNothing_GivenNoSelectManyExpression()
{
var spec = new StoreProductNamesEmptySpec();

spec.SelectorMany.Should().BeNull();
}

[Fact]
public void SetsSelectorMany_GivenSelectManyExpression()
{
var spec = new StoreProductNamesSpec();

spec.SelectorMany.Should().NotBeNull();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using FluentAssertions;
using Xunit;

namespace Ardalis.Specification.UnitTests
{
public class ConcurrentSelectorsExceptionTests
{
private const string defaultMessage = "Concurrent specification selector transforms defined. Ensure only one of the Select() or SelectMany() transforms is used in the same specification!";

[Fact]
public void ThrowWithDefaultConstructor()
{
Action action = () => throw new ConcurrentSelectorsException();

action.Should().Throw<ConcurrentSelectorsException>().WithMessage(defaultMessage);
}

[Fact]
public void ThrowWithInnerException()
{
Exception inner = new Exception("test");
Action action = () => throw new ConcurrentSelectorsException(inner);

action.Should().Throw<ConcurrentSelectorsException>().WithMessage(defaultMessage).WithInnerException<Exception>().WithMessage("test");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using FluentAssertions;
using Xunit;

namespace Ardalis.Specification.UnitTests
{
public class SelectorNotFoundExceptionTests
{
private const string defaultMessage = "The specification must have Selector defined.";
private const string defaultMessage = "The specification must have a selector transform defined. Ensure either Select() or SelectMany() is used in the specification!";

[Fact]
public void ThrowWithDefaultConstructor()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.Generic;

namespace Ardalis.Specification.UnitTests.Fixture.Entities.Seeds
{
public class ProductSeed
{
public const int TOTAL_PRODUCT_COUNT = 100;
public const string VALID_PRODUCT_NAME = "Product 1";

public static List<Product> Get()
{
var products = new List<Product>();

for (int i = 1; i < 100; i = i + 2)
for (int i = 1; i < TOTAL_PRODUCT_COUNT; i = i + 2)
{
products.Add(new Product()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Ardalis.Specification.UnitTests.Fixture.Entities;

namespace Ardalis.Specification.UnitTests.Fixture.Specs
{
public class StoreProductNamesEmptySpec : Specification<Store, string?>
{
public StoreProductNamesEmptySpec()
{

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Linq;
using Ardalis.Specification.UnitTests.Fixture.Entities;

namespace Ardalis.Specification.UnitTests.Fixture.Specs
{
public class StoreProductNamesSpec : Specification<Store, string?>
{
public StoreProductNamesSpec()
{
Query.SelectMany(s => s.Products.Select(p => p.Name));
}
}
}

0 comments on commit 39e082b

Please sign in to comment.