diff --git a/Directory.Build.props b/Directory.Build.props index 23700e2..094a495 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,8 +8,8 @@ en Copyright © ONIXLabs 2020 https://github.com/onix-labs/onixlabs-dotnet - 11.2.0 - 11.2.0 - 11.2.0 + 11.3.0 + 11.3.0 + 11.3.0 diff --git a/OnixLabs.Core.UnitTests.Data/Location.cs b/OnixLabs.Core.UnitTests.Data/Location.cs new file mode 100644 index 0000000..a2814f4 --- /dev/null +++ b/OnixLabs.Core.UnitTests.Data/Location.cs @@ -0,0 +1,24 @@ +// Copyright 2020 ONIXLabs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace OnixLabs.Core.UnitTests.Data; + +public sealed record Location(string City, string Country) +{ + public static readonly Location London = new("London", "England"); + public static readonly Location Paris = new("Paris", "France"); + public static readonly Location Lisbon = new("Lisbon", "Postugal"); + public static readonly Location Berlin = new("Berlin", "Germany"); + public static readonly Location Brussels = new("Brussels", "Belgium"); +} diff --git a/OnixLabs.Core.UnitTests.Data/Person.cs b/OnixLabs.Core.UnitTests.Data/Person.cs new file mode 100644 index 0000000..03ac914 --- /dev/null +++ b/OnixLabs.Core.UnitTests.Data/Person.cs @@ -0,0 +1,26 @@ +// Copyright 2020 ONIXLabs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; + +namespace OnixLabs.Core.UnitTests.Data; + +public sealed record Person(string Name, int Age, IEnumerable Locations) +{ + public static readonly Person Alice = new("Alice", 12, [Location.London, Location.Paris]); + public static readonly Person Bob = new("Bob", 23, [Location.Lisbon, Location.London]); + public static readonly Person Charlie = new("Charlie", 34, [Location.Berlin, Location.Brussels]); + + public static readonly IEnumerable People = [Alice, Bob, Charlie]; +} diff --git a/OnixLabs.Core.UnitTests.Data/PersonSpecifications.cs b/OnixLabs.Core.UnitTests.Data/PersonSpecifications.cs new file mode 100644 index 0000000..785643b --- /dev/null +++ b/OnixLabs.Core.UnitTests.Data/PersonSpecifications.cs @@ -0,0 +1,37 @@ +// Copyright 2020 ONIXLabs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace OnixLabs.Core.UnitTests.Data; + +public class PersonSpecification(Expression> expression) : + CriteriaSpecification(expression); + +public sealed class PersonNameEqualsSpecification(string name) : + PersonSpecification(person => person.Name == name); + +public sealed class PersonNameStartsWithSpecification(string name) : + PersonSpecification(person => person.Name.StartsWith(name)); + +public sealed class PersonOlderThanSpecification(int age) : + PersonSpecification(person => person.Age > age); + +public sealed class PersonHasLocationSpecification(Location location) : + PersonSpecification(person => person.Locations.Contains(location)); + +public sealed class PersonHasLocationCitySpecification(string city) : + PersonSpecification(person => person.Locations.Any(location => location.City == city)); diff --git a/OnixLabs.Core.UnitTests/OptionalTests.cs b/OnixLabs.Core.UnitTests/OptionalTests.cs index 7eb8ca9..1b490f9 100644 --- a/OnixLabs.Core.UnitTests/OptionalTests.cs +++ b/OnixLabs.Core.UnitTests/OptionalTests.cs @@ -336,7 +336,7 @@ public void OptionalNoneGetValueOrDefaultShouldProduceExpectedResult() // Then Assert.Equal(0, actualNumber); - Assert.Equal(null, actualText); + Assert.Null(actualText); } [Fact(DisplayName = "Optional Some.GetValueOrDefault with default value should produce the expected result.")] diff --git a/OnixLabs.Core.UnitTests/SpecificationTests.cs b/OnixLabs.Core.UnitTests/SpecificationTests.cs new file mode 100644 index 0000000..faddb01 --- /dev/null +++ b/OnixLabs.Core.UnitTests/SpecificationTests.cs @@ -0,0 +1,175 @@ +// Copyright 2020 ONIXLabs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using System.Linq; +using OnixLabs.Core.Linq; +using OnixLabs.Core.UnitTests.Data; +using Xunit; + +namespace OnixLabs.Core.UnitTests; + +public sealed class SpecificationTests +{ + [Fact(DisplayName = "PersonNameEqualsSpecification should return the expected result")] + public void PersonNameEqualsSpecificationShouldReturnExpectedResult() + { + // Given + PersonNameEqualsSpecification specification = new("Alice"); + + // When + IEnumerable result = Person.People.Where(specification).ToList(); + + // Then + Assert.Single(result); + Assert.Contains(Person.Alice, result); + } + + [Fact(DisplayName = "PersonNameStartsWithSpecification should return the expected result")] + public void PersonNameStartsWithSpecificationShouldReturnExpectedResult() + { + // Given + PersonNameStartsWithSpecification specification = new("A"); + + // When + IEnumerable result = Person.People.Where(specification).ToList(); + + // Then + Assert.Single(result); + Assert.Contains(Person.Alice, result); + } + + [Fact(DisplayName = "PersonOlderThanSpecification should return the expected result")] + public void PersonOlderThanSpecificationShouldReturnExpectedResult() + { + // Given + PersonOlderThanSpecification specification = new(20); + + // When + IEnumerable result = Person.People.Where(specification).ToList(); + + // Then + Assert.Equal(2, IEnumerableExtensions.Count(result)); + Assert.Contains(Person.Bob, result); + Assert.Contains(Person.Charlie, result); + } + + [Fact(DisplayName = "PersonHasLocationSpecification should return the expected result")] + public void PersonHasLocationSpecificationShouldReturnExpectedResult() + { + // Given + PersonHasLocationSpecification specification = new(Location.London); + + // When + IEnumerable result = Person.People.Where(specification).ToList(); + + // Then + Assert.Equal(2, IEnumerableExtensions.Count(result)); + Assert.Contains(Person.Alice, result); + Assert.Contains(Person.Bob, result); + } + + [Fact(DisplayName = "PersonHasLocationCitySpecification should return the expected result")] + public void PersonHasLocationCitySpecificationShouldReturnExpectedResult() + { + // Given + PersonHasLocationCitySpecification specification = new(Location.London.City); + + // When + IEnumerable result = Person.People.Where(specification).ToList(); + + // Then + Assert.Equal(2, IEnumerableExtensions.Count(result)); + Assert.Contains(Person.Alice, result); + Assert.Contains(Person.Bob, result); + } + + [Fact(DisplayName = "PersonSpecification.And should return the expected result")] + public void PersonSpecificationAndShouldReturnExpectedResult() + { + // Given + Specification specification = PersonSpecification.And( + new PersonOlderThanSpecification(20), + new PersonHasLocationCitySpecification("London") + ); + + // When + IEnumerable result = Person.People.Where(specification).ToList(); + + // Then + Assert.Single(result); + Assert.Contains(Person.Bob, result); + } + + [Fact(DisplayName = "PersonSpecification.And should return true for an empty collection")] + public void PersonSpecificationAndShouldReturnTrueForEmptyCollection() + { + // Given + Specification specification = PersonSpecification.And(); + + // When + bool result = specification.IsSatisfiedBy(Person.Alice); + + // Then + Assert.True(result); + } + + [Fact(DisplayName = "PersonSpecification.Or should return the expected result")] + public void PersonSpecificationOrShouldReturnExpectedResult() + { + // Given + Specification specification = PersonSpecification.Or( + new PersonNameStartsWithSpecification("A"), + new PersonHasLocationCitySpecification("Lisbon") + ); + + // When + IEnumerable result = Person.People.Where(specification).ToList(); + + // Then + Assert.Equal(2, result.Count()); + Assert.Contains(Person.Alice, result); + Assert.Contains(Person.Bob, result); + } + + [Fact(DisplayName = "PersonSpecification.Or should return false for an empty collection")] + public void PersonSpecificationOrShouldReturnFalseForEmptyCollection() + { + // Given + Specification specification = PersonSpecification.Or(); + + // When + bool result = specification.IsSatisfiedBy(Person.Alice); + + // Then + Assert.False(result); + } + + [Fact(DisplayName = "PersonSpecification.Not should return the expected result")] + public void PersonSpecificationNotShouldReturnExpectedResult() + { + // Given + Specification specification = PersonSpecification.Or( + new PersonNameStartsWithSpecification("A"), + new PersonHasLocationCitySpecification("Lisbon") + ); + + // When + IEnumerable result = Person.People.WhereNot(specification).ToList(); + + // Then + Assert.Single(result); + Assert.Contains(Person.Charlie, result); + } +} diff --git a/OnixLabs.Core/Extensions.DateTime.cs b/OnixLabs.Core/Extensions.DateTime.cs index ce86296..1380886 100644 --- a/OnixLabs.Core/Extensions.DateTime.cs +++ b/OnixLabs.Core/Extensions.DateTime.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 ONIXLabs +// Copyright 2020 ONIXLabs // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/OnixLabs.Core/IBinaryConvertible.cs b/OnixLabs.Core/IBinaryConvertible.cs index 74b07a6..d3d3fe1 100644 --- a/OnixLabs.Core/IBinaryConvertible.cs +++ b/OnixLabs.Core/IBinaryConvertible.cs @@ -12,9 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; + namespace OnixLabs.Core; /// -/// Defines a type that is convertible to an instance of . +/// Defines a type that is convertible to a , or . /// public interface IBinaryConvertible : ISpanBinaryConvertible, IMemoryBinaryConvertible; diff --git a/OnixLabs.Core/IMemoryBinaryConvertible.cs b/OnixLabs.Core/IMemoryBinaryConvertible.cs index 9e7a447..9f6361b 100644 --- a/OnixLabs.Core/IMemoryBinaryConvertible.cs +++ b/OnixLabs.Core/IMemoryBinaryConvertible.cs @@ -1,4 +1,4 @@ -// Copyright 2020-2024 ONIXLabs +// Copyright 2020 ONIXLabs // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/OnixLabs.Core/Linq/Extensions.Expression.cs b/OnixLabs.Core/Linq/Extensions.Expression.cs new file mode 100644 index 0000000..71cb41c --- /dev/null +++ b/OnixLabs.Core/Linq/Extensions.Expression.cs @@ -0,0 +1,112 @@ +// Copyright 2020 ONIXLabs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Linq.Expressions; + +namespace OnixLabs.Core.Linq; + +/// +/// Provides extension methods for . +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class ExpressionExtensions +{ + private const string ParameterName = "$param"; + + /// + /// Combines two instances into a single expression using the logical AND operator. + /// + /// The left-hand expression to combine. + /// The right-hand expression to combine. + /// The underlying type of the expression. + /// + /// Returns a new that combines two instances + /// into a single expression using the logical AND operator. + /// + /// + /// Calling this method introduces a new expression parameter (named $param) to ensure both expressions share the same parameter. + /// Internal or nested lambda parameters that do not match the replaced parameter remain untouched. + /// + public static Expression> And(this Expression> left, Expression> right) + { + ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName); + Expression leftBody = new ReplaceParameterVisitor(left.Parameters.First(), parameter).Visit(left.Body); + Expression rightBody = new ReplaceParameterVisitor(right.Parameters.First(), parameter).Visit(right.Body); + BinaryExpression binaryExpression = Expression.AndAlso(leftBody, rightBody); + + return Expression.Lambda>(binaryExpression, parameter); + } + + /// + /// Combines two instances into a single expression using the logical OR operator. + /// + /// The left-hand expression to combine. + /// The right-hand expression to combine. + /// The underlying type of the expression. + /// + /// Returns a new that combines two instances + /// into a single expression using the logical OR operator. + /// + /// + /// Calling this method introduces a new expression parameter (named $param) to ensure both expressions share the same parameter. + /// Internal or nested lambda parameters that do not match the replaced parameter remain untouched. + /// + public static Expression> Or(this Expression> left, Expression> right) + { + ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName); + Expression leftBody = new ReplaceParameterVisitor(left.Parameters.First(), parameter).Visit(left.Body); + Expression rightBody = new ReplaceParameterVisitor(right.Parameters.First(), parameter).Visit(right.Body); + BinaryExpression binaryExpression = Expression.OrElse(leftBody, rightBody); + + return Expression.Lambda>(binaryExpression, parameter); + } + + /// + /// Negates the current instance using the logical NOT operator. + /// + /// The expression to negate. + /// The underlying type of the expression. + /// + /// Returns a new that negates the current instance using the logical NOT operator. + /// + /// + /// Calling this method introduces a new expression parameter (named $param) to ensure a uniform parameter expression. + /// Internal or nested lambda parameters that do not match the replaced parameter remain untouched. + /// + public static Expression> Not(this Expression> expression) + { + ParameterExpression parameter = Expression.Parameter(typeof(T), ParameterName); + Expression expressionBody = new ReplaceParameterVisitor(expression.Parameters.First(), parameter).Visit(expression.Body); + + UnaryExpression unaryExpression = Expression.Not(expressionBody); + return Expression.Lambda>(unaryExpression, parameter); + } + + /// + /// Represents an expression visitor that replaces parameter expressions in an expression tree. + /// + /// + /// This class is useful when combining multiple expression trees that each use their own parameter expressions. + /// It ensures all expressions use a consistent parameter expression, which is necessary for creating valid and + /// executable combined expressions. + /// + private sealed class ReplaceParameterVisitor(ParameterExpression source, ParameterExpression target) : ExpressionVisitor + { + protected override Expression VisitParameter(ParameterExpression node) => + node == source ? target : base.VisitParameter(node); + } +} diff --git a/OnixLabs.Core/Linq/Extensions.IEnumerable.cs b/OnixLabs.Core/Linq/Extensions.IEnumerable.cs index 8da96b8..8590a1d 100644 --- a/OnixLabs.Core/Linq/Extensions.IEnumerable.cs +++ b/OnixLabs.Core/Linq/Extensions.IEnumerable.cs @@ -38,6 +38,7 @@ public static class IEnumerableExtensions private const string EnumerableNullExceptionMessage = "Enumerable must not be null."; private const string SelectorNullExceptionMessage = "Selector must not be null."; private const string PredicateNullExceptionMessage = "Predicate must not be null."; + private const string SpecificationNullExceptionMessage = "Specification must not be null."; private const string ActionNullExceptionMessage = "Action must not be null."; /// @@ -110,16 +111,7 @@ public static int Count(this IEnumerable enumerable) // ReSharper disable PossibleMultipleEnumeration RequireNotNull(enumerable, EnumerableNullExceptionMessage, nameof(enumerable)); - int count = 0; - - // ReSharper disable once HeapView.ObjectAllocation.Possible - foreach (object? _ in enumerable) - checked - { - ++count; - } - - return count; + return Enumerable.Count(enumerable.Cast()); } /// @@ -551,6 +543,38 @@ public static TResult SumBy(this IEnumerable enumerable, Func + /// Filters a sequence of values based on a specification. + /// + /// The current to filter. + /// The to filter by. + /// The underlying type of the . + /// Returns an that contains elements from the input sequence that satisfy the specification. + public static IEnumerable Where(this IEnumerable enumerable, Specification specification) + { + // ReSharper disable PossibleMultipleEnumeration + RequireNotNull(enumerable, EnumerableNullExceptionMessage, nameof(enumerable)); + RequireNotNull(specification, SpecificationNullExceptionMessage, nameof(specification)); + + return enumerable.Where(specification.Criteria.Compile()); + } + + /// + /// Filters a sequence of values based on a negated specification. + /// + /// The current to filter. + /// The to filter by. + /// The underlying type of the . + /// Returns an that contains elements from the input sequence that satisfy the negated specification. + public static IEnumerable WhereNot(this IEnumerable enumerable, Specification specification) + { + // ReSharper disable PossibleMultipleEnumeration + RequireNotNull(enumerable, EnumerableNullExceptionMessage, nameof(enumerable)); + RequireNotNull(specification, SpecificationNullExceptionMessage, nameof(specification)); + + return enumerable.Where(specification.Not().Criteria.Compile()); + } + /// /// Filters the current elements that do not satisfy the specified predicate condition. /// diff --git a/OnixLabs.Core/Linq/Extensions.IQueryable.cs b/OnixLabs.Core/Linq/Extensions.IQueryable.cs new file mode 100644 index 0000000..2e398dd --- /dev/null +++ b/OnixLabs.Core/Linq/Extensions.IQueryable.cs @@ -0,0 +1,59 @@ +// Copyright 2020 ONIXLabs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.ComponentModel; +using System.Linq; + +namespace OnixLabs.Core.Linq; + +/// +/// Provides LINQ-like extension methods for . +/// +// ReSharper disable InconsistentNaming +[EditorBrowsable(EditorBrowsableState.Never)] +public static class IQueryableExtensions +{ + private const string QueryableNullExceptionMessage = "Queryable must not be null."; + private const string SpecificationNullExceptionMessage = "Specification must not be null."; + + /// + /// Filters a sequence of values based on a specification. + /// + /// The current to filter. + /// The to filter by. + /// The underlying type of the . + /// Returns an that contains elements from the input sequence that satisfy the specification. + public static IQueryable Where(this IQueryable queryable, Specification specification) + { + RequireNotNull(queryable, QueryableNullExceptionMessage, nameof(queryable)); + RequireNotNull(specification, SpecificationNullExceptionMessage, nameof(specification)); + + return queryable.Where(specification.Criteria); + } + + /// + /// Filters a sequence of values based on a negated specification. + /// + /// The current to filter. + /// The to filter by. + /// The underlying type of the . + /// Returns an that contains elements from the input sequence that satisfy the negated specification. + public static IQueryable WhereNot(this IQueryable queryable, Specification specification) + { + RequireNotNull(queryable, QueryableNullExceptionMessage, nameof(queryable)); + RequireNotNull(specification, SpecificationNullExceptionMessage, nameof(specification)); + + return queryable.Where(specification.Not().Criteria); + } +} diff --git a/OnixLabs.Core/Specification.cs b/OnixLabs.Core/Specification.cs new file mode 100644 index 0000000..05a969f --- /dev/null +++ b/OnixLabs.Core/Specification.cs @@ -0,0 +1,141 @@ +// Copyright 2020 ONIXLabs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using OnixLabs.Core.Linq; + +namespace OnixLabs.Core; + +/// +/// Represents the base class for implementing composable specifications using logical operations. +/// +/// The underlying type of the subject to which the specification applies. +// ReSharper disable PossibleMultipleEnumeration +public abstract class Specification +{ + /// + /// Represents an empty specification that always evaluates to . + /// + public static readonly Specification Empty = new BooleanSpecification(true); + + /// + /// Gets the underlying expression criteria of the current specification. + /// + public abstract Expression> Criteria { get; } + + /// + /// Combines multiple specifications using a logical AND operation. + /// + /// A collection of specifications to combine. + /// + /// Returns a combined specification that evaluates to if all the provided specifications are satisfied, + /// or the identity specification (always ) if no specifications are provided. + /// + public static Specification And(params IEnumerable> specifications) => specifications.IsNotEmpty() + ? specifications.Aggregate((left, right) => left.And(right)) + : new BooleanSpecification(true); + + /// + /// Combines multiple specifications using a logical OR operation. + /// + /// A collection of specifications to combine. + /// + /// Returns a combined specification that evaluates to if any of the provided specifications are satisfied, + /// or the identity specification (always ) if no specifications are provided. + /// + public static Specification Or(params IEnumerable> specifications) => specifications.IsNotEmpty() + ? specifications.Aggregate((left, right) => left.Or(right)) + : new BooleanSpecification(false); + + /// + /// Combines the current specification with another using a logical AND operation. + /// + /// The other specification to combine with. + /// + /// Returns a combined specification that evaluates to if both specifications are satisfied; + /// otherwise, the specification evaluates to . + /// + public Specification And(Specification other) => new AndSpecification(this, other); + + /// + /// Combines the current specification with another using a logical OR operation. + /// + /// The other specification to combine with. + /// + /// Returns a combined specification that evaluates to if either specification is satisfied; + /// otherwise, the specification evaluates to . + /// + public Specification Or(Specification other) => new OrSpecification(this, other); + + /// + /// Creates a specification that negates the current specification's logic using a logical NOT operation. + /// + /// + /// Returns a specification that evaluates to if the current specification is not satisfied; + /// otherwise, the specification evaluates to . + /// + public Specification Not() => new NotSpecification(this); + + /// + /// Evaluates whether the specified subject satisfies the current specification. + /// + /// The subject to evaluate. + /// Returns if the subject satisfies the specification; otherwise, . + public bool IsSatisfiedBy(T subject) => Criteria.Compile().Invoke(subject); +} + +/// +/// Represents a specification that wraps an expression argument. +/// +/// The expression criteria to wrap. +/// The underlying type of the subject to which the specification applies. +public class CriteriaSpecification(Expression> criteria) : Specification +{ + /// + /// Gets the underlying expression criteria of the current specification. + /// + public override Expression> Criteria => criteria; +} + +/// +/// Represents a specification that combines two specifications using a logical AND operation. +/// +/// The underlying type of the subject to which the specification applies. +file sealed class AndSpecification(Specification left, Specification right) : + CriteriaSpecification(left.Criteria.And(right.Criteria)); + +/// +/// Represents a specification that combines two specifications using a logical OR operation. +/// +/// The underlying type of the subject to which the specification applies. +file sealed class OrSpecification(Specification left, Specification right) : + CriteriaSpecification(left.Criteria.Or(right.Criteria)); + +/// +/// Represents a specification that negates another specification's logic using a logical NOT operation. +/// +/// The underlying type of the subject to which the specification applies. +file sealed class NotSpecification(Specification specification) : + CriteriaSpecification(specification.Criteria.Not()); + +/// +/// Represents a specification with a constant boolean value. +/// +/// The constant boolean value that the specification will return. +/// The underlying type of the subject to which the specification applies. +file sealed class BooleanSpecification(bool value) : + CriteriaSpecification(_ => value);