Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement SelectMany support #320

Merged
merged 3 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}
}