Skip to content

Commit

Permalink
feat: reorganize and document base library
Browse files Browse the repository at this point in the history
  • Loading branch information
skarllot committed Sep 24, 2023
1 parent 950e853 commit 02b26ef
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Linq.Expressions;

namespace Raiqub.Expressions;
namespace Raiqub.Expressions.Internal;

internal sealed class AllSpecification<T> : Specification<T>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Linq.Expressions;

namespace Raiqub.Expressions;
namespace Raiqub.Expressions.Internal;

internal sealed class AnonymousSpecification<T> : Specification<T>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Linq.Expressions;

namespace Raiqub.Expressions;
namespace Raiqub.Expressions.Internal;

internal static class ExpressionTreeExtensions
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Linq.Expressions;

namespace Raiqub.Expressions;
namespace Raiqub.Expressions.Internal;

internal sealed class ReplaceParameterExpressionVisitor : ExpressionVisitor
{
Expand Down
38 changes: 38 additions & 0 deletions src/Expressions/Specification.cs
Original file line number Diff line number Diff line change
@@ -1,44 +1,82 @@
using System.Linq.Expressions;
using Raiqub.Expressions.Internal;

namespace Raiqub.Expressions;

/// <summary>
/// Provides initialization and combination methods for instances of the <see cref="Specification{T}"/> class.
/// </summary>
public static class Specification
{
/// <summary>Creates a new specification that contains the specified expression.</summary>
/// <param name="expression">A lambda expression defining the business rule.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new instance of <see cref="Specification{T}"/> representing the specified business rule.</returns>
public static Specification<T> Create<T>(Expression<Func<T, bool>> expression)
{
return new AnonymousSpecification<T>(expression);
}

/// <summary>Combines the two specifications using the conditional logical AND operator.</summary>
/// <param name="left">The left operand of AND operator.</param>
/// <param name="right">The right operand of AND operator.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new specification that represents the conjunction of two specifications (both must be satisfied).</returns>
public static Specification<T> And<T>(this Specification<T> left, Specification<T> right)
{
return new AnonymousSpecification<T>(left.ToExpression().And(right.ToExpression()));
}

/// <summary>Combines multiple specifications using the conditional logical AND operator.</summary>
/// <param name="specifications">The specifications to combine sequentially.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new specification that represents the conjunction of all specifications (all must be satisfied).</returns>
public static Specification<T> And<T>(IEnumerable<Specification<T>> specifications)
{
return new AnonymousSpecification<T>(specifications.Select(static s => s.ToExpression()).And());
}

/// <summary>Combines multiple specifications using the conditional logical AND operator.</summary>
/// <param name="specifications">The specifications to combine sequentially.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new specification that represents the conjunction of all specifications (all must be satisfied).</returns>
public static Specification<T> And<T>(params Specification<T>[] specifications)
{
return And((IEnumerable<Specification<T>>)specifications);
}

/// <summary>Negate a specification using the logical negation operator.</summary>
/// <param name="specification">The specification to negate.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new specification that represents the negation of a specification (the opposite of it must be satisfied).</returns>
public static Specification<T> Not<T>(this Specification<T> specification)
{
return new AnonymousSpecification<T>(specification.ToExpression().Not());
}

/// <summary>Combines the two specifications using the conditional logical OR operator.</summary>
/// <param name="left">The left operand of OR operator.</param>
/// <param name="right">The right operand of OR operator.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new specification that represents the disjunction of two specifications (at least one of them must be satisfied).</returns>
public static Specification<T> Or<T>(this Specification<T> left, Specification<T> right)
{
return new AnonymousSpecification<T>(left.ToExpression().Or(right.ToExpression()));
}

/// <summary>Combines multiple specifications using the conditional logical OR operator.</summary>
/// <param name="specifications">The specifications to combine sequentially.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new specification that represents the disjunction of all specifications (at least one of them must be satisfied).</returns>
public static Specification<T> Or<T>(IEnumerable<Specification<T>> specifications)
{
return new AnonymousSpecification<T>(specifications.Select(static s => s.ToExpression()).Or());
}

/// <summary>Combines multiple specifications using the conditional logical OR operator.</summary>
/// <param name="specifications">The specifications to combine sequentially.</param>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
/// <returns>A new specification that represents the disjunction of all specifications (at least one of them must be satisfied).</returns>
public static Specification<T> Or<T>(params Specification<T>[] specifications)
{
return Or((IEnumerable<Specification<T>>)specifications);
Expand Down
82 changes: 81 additions & 1 deletion src/Expressions/SpecificationEnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,90 @@
namespace Raiqub.Expressions;
using System.Diagnostics.CodeAnalysis;

namespace Raiqub.Expressions;

/// <summary>
/// Provides extensions for using <see cref="Specification{T}"/> with <see cref="IEnumerable{T}"/>
/// and <see cref="IQueryable{T}"/> instances.
/// </summary>
public static class SpecificationEnumerableExtensions
{
/// <summary>Determines whether any element of a sequence satisfies a business rule.</summary>
/// <param name="source">An <see cref="IEnumerable{T}"/> whose elements to apply the business rule to.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <returns><see langword="true" /> if the source sequence is not empty and at least one of its elements passes the test in the specified business rule; otherwise, <see langword="false" />.</returns>
public static bool Any<T>(this IEnumerable<T> source, Specification<T> specification) =>
source.Any(specification.IsSatisfiedBy);

/// <summary>Returns a number that represents how many elements in the specified sequence satisfy a business rule.</summary>
/// <param name="source">A sequence that contains elements to be tested and counted.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <exception cref="OverflowException">The number of elements in <paramref name="source" /> is larger than <see cref="Int32.MaxValue" />.</exception>
/// <returns>A number that represents how many elements in the sequence satisfy the business rule.</returns>
public static int Count<T>(this IEnumerable<T> source, Specification<T> specification) =>
source.Count(specification.IsSatisfiedBy);

/// <summary>Returns the first element in a sequence that satisfies a specified business rule.</summary>
/// <param name="source">An <see cref="IEnumerable{T}"/> to return an element from.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <returns>The first element in the sequence that passes the test in the specified business rule.</returns>
/// <exception cref="InvalidOperationException">No element satisfies the business rule in the specification. -or- The source sequence is empty.</exception>
public static T First<T>(this IEnumerable<T> source, Specification<T> specification) =>
source.First(specification.IsSatisfiedBy);

/// <summary>Returns the first element of the sequence that satisfies a business rule or a default value if no such element is found.</summary>
/// <param name="source">An <see cref="IEnumerable{T}" /> to return an element from.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <returns>
/// <see langword="default" />(<typeparamref name="T" />) if <paramref name="source" /> is empty or if no element
/// passes the test specified by <paramref name="specification" />; otherwise, the first element in
/// <paramref name="source" /> that passes the test specified by <paramref name="specification" />.
/// </returns>
public static T? FirstOrDefault<T>(this IEnumerable<T> source, Specification<T> specification) =>
source.FirstOrDefault(specification.IsSatisfiedBy);

/// <summary>Returns the only element of a sequence that satisfies a business rule, and throws an exception if more than one such element exists.</summary>
/// <param name="source">An <see cref="IEnumerable{T}" /> to return a single element from.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <exception cref="InvalidOperationException">
/// No element satisfies the business rule in <paramref name="specification" />. -or-
/// More than one element satisfies the business rule in <paramref name="specification" />. -or-
/// The source sequence is empty.
/// </exception>
/// <returns>The single element of the input sequence that satisfies a business rule.</returns>
[SuppressMessage("Naming", "CA1720:Identifiers should not contain type names")]
public static T Single<T>(this IEnumerable<T> source, Specification<T> specification) =>
source.Single(specification.IsSatisfiedBy);

/// <summary>
/// Returns the only element of a sequence that satisfies a specified business rule or a default value if no such
/// element exists; this method throws an exception if more than one element satisfies the condition.
/// </summary>
/// <param name="source">An <see cref="IEnumerable{T}" /> to return a single element from.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <exception cref="InvalidOperationException">More than one element satisfies the business rule in <paramref name="specification" />.</exception>
/// <returns>The single element of the input sequence that satisfies the business rule, or <see langword="default" />(<typeparamref name="T" />) if no such element is found.</returns>
public static T? SingleOrDefault<T>(this IEnumerable<T> source, Specification<T> specification) =>
source.SingleOrDefault(specification.IsSatisfiedBy);

/// <summary>Filters a sequence of values based on a specification.</summary>
/// <param name="source">An <see cref="IEnumerable{T}"/> to filter.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <returns>An <see cref="IEnumerable{T}"/> that contains elements from the input sequence that satisfy the business rule.</returns>
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Specification<T> specification) =>
source.Where(specification.IsSatisfiedBy);

/// <summary>Filters a sequence of values based on a specification.</summary>
/// <param name="queryable">An <see cref="IQueryable{T}"/> to filter.</param>
/// <param name="specification">A specification to test each element for a business rule.</param>
/// <typeparam name="T">The type of the elements of source.</typeparam>
/// <returns>An <see cref="IQueryable{T}"/> that contains elements from the input sequence that satisfy the business rule.</returns>
public static IQueryable<T> Where<T>(this IQueryable<T> queryable, Specification<T> specification) =>
queryable.Where(specification.ToExpression());
}
16 changes: 6 additions & 10 deletions src/Expressions/Specification{T}.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System.Linq.Expressions;
using Raiqub.Expressions.Internal;

namespace Raiqub.Expressions;

/// <summary>
/// Represents a specification or a set of criteria that can be applied to an object or a collection of
/// objects of type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the object(s) that this specification can be applied to.</typeparam>
/// <summary>Represents a combinable and reusable business rule.</summary>
/// <typeparam name="T">The type of the object that this specification can be applied to.</typeparam>
public abstract class Specification<T>
{
private Func<T, bool>? _predicate;
Expand Down Expand Up @@ -43,8 +41,8 @@ public Specification<TDerived> CastDown<TDerived>()
}

/// <summary>
/// Evaluates the specification against a given object and returns a boolean value that indicates whether
/// the object satisfies the specification.
/// Evaluates the business rule represented by this instance against a given object and returns a boolean value
/// that indicates whether the object satisfies the specification.
/// </summary>
/// <param name="entity">The entity to check.</param>
/// <returns>True if the entity satisfies the specification, otherwise false.</returns>
Expand All @@ -54,8 +52,6 @@ public bool IsSatisfiedBy(T entity)
return _predicate(entity);
}

/// <summary>
/// Returns a string representation of the lambda expression.
/// </summary>
/// <summary>Returns a string representation of the lambda expression.</summary>
public override string ToString() => ToExpression().ToString();
}
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "1.0",
"version": "1.1",
"publicReleaseRefSpec": [
"^refs/tags/v\\\\d+\\\\.\\\\d+"
],
Expand Down

0 comments on commit 02b26ef

Please sign in to comment.