diff --git a/Directory.Packages.props b/Directory.Packages.props index 8894a5f6..50937db5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,7 @@ + diff --git a/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs b/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs index 1f6854fa..d9d80b8e 100644 --- a/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs +++ b/src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs @@ -16,9 +16,8 @@ public static IIncludableSpecificationBuilder ThenInclude)previousBuilder.Specification.IncludeExpressions).Add(info); + var expr = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(TPreviousProperty)); + previousBuilder.Specification.Add(expr); } var includeBuilder = new IncludableSpecificationBuilder(previousBuilder.Specification, !condition || previousBuilder.IsChainDiscarded); @@ -40,9 +39,8 @@ public static IIncludableSpecificationBuilder ThenInclude)); - - ((List)previousBuilder.Specification.IncludeExpressions).Add(info); + var expr = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(IEnumerable)); + previousBuilder.Specification.Add(expr); } var includeBuilder = new IncludableSpecificationBuilder(previousBuilder.Specification, !condition || previousBuilder.IsChainDiscarded); diff --git a/src/Ardalis.Specification/Builder/OrderedBuilderExtensions.cs b/src/Ardalis.Specification/Builder/OrderedBuilderExtensions.cs index a4fd9b0e..69c8073e 100644 --- a/src/Ardalis.Specification/Builder/OrderedBuilderExtensions.cs +++ b/src/Ardalis.Specification/Builder/OrderedBuilderExtensions.cs @@ -14,7 +14,8 @@ public static IOrderedSpecificationBuilder ThenBy( { if (condition && !orderedBuilder.IsChainDiscarded) { - ((List>)orderedBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo(orderExpression, OrderTypeEnum.ThenBy)); + var expr = new OrderExpressionInfo(orderExpression, OrderTypeEnum.ThenBy); + orderedBuilder.Specification.Add(expr); } else { @@ -36,7 +37,8 @@ public static IOrderedSpecificationBuilder ThenByDescending( { if (condition && !orderedBuilder.IsChainDiscarded) { - ((List>)orderedBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo(orderExpression, OrderTypeEnum.ThenByDescending)); + var expr = new OrderExpressionInfo(orderExpression, OrderTypeEnum.ThenByDescending); + orderedBuilder.Specification.Add(expr); } else { diff --git a/src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs b/src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs index 10c9fbd5..21f59751 100644 --- a/src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs +++ b/src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs @@ -27,7 +27,8 @@ public static ISpecificationBuilder Where( { if (condition) { - ((List>)specificationBuilder.Specification.WhereExpressions).Add(new WhereExpressionInfo(criteria)); + var expr = new WhereExpressionInfo(criteria); + specificationBuilder.Specification.Add(expr); } return specificationBuilder; @@ -58,7 +59,8 @@ public static IOrderedSpecificationBuilder OrderBy( { if (condition) { - ((List>)specificationBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo(orderExpression, OrderTypeEnum.OrderBy)); + var expr = new OrderExpressionInfo(orderExpression, OrderTypeEnum.OrderBy); + specificationBuilder.Specification.Add(expr); } var orderedSpecificationBuilder = new OrderedSpecificationBuilder(specificationBuilder.Specification, !condition); @@ -91,7 +93,8 @@ public static IOrderedSpecificationBuilder OrderByDescending( { if (condition) { - ((List>)specificationBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo(orderExpression, OrderTypeEnum.OrderByDescending)); + var expr = new OrderExpressionInfo(orderExpression, OrderTypeEnum.OrderByDescending); + specificationBuilder.Specification.Add(expr); } var orderedSpecificationBuilder = new OrderedSpecificationBuilder(specificationBuilder.Specification, !condition); @@ -130,9 +133,8 @@ public static IIncludableSpecificationBuilder Include)specificationBuilder.Specification.IncludeExpressions).Add(info); + var expr = new IncludeExpressionInfo(includeExpression, typeof(T), typeof(TProperty)); + specificationBuilder.Specification.Add(expr); } var includeBuilder = new IncludableSpecificationBuilder(specificationBuilder.Specification, !condition); @@ -165,7 +167,7 @@ public static ISpecificationBuilder Include( { if (condition) { - ((List)specificationBuilder.Specification.IncludeStrings).Add(includeString); + specificationBuilder.Specification.Add(includeString); } return specificationBuilder; @@ -204,7 +206,8 @@ public static ISpecificationBuilder Search( { if (condition) { - ((List>)specificationBuilder.Specification.SearchCriterias).Add(new SearchExpressionInfo(selector, searchTerm, searchGroup)); + var expr = new SearchExpressionInfo(selector, searchTerm, searchGroup); + specificationBuilder.Specification.Add(expr); } return specificationBuilder; @@ -233,7 +236,7 @@ public static ISpecificationBuilder Take( { if (condition) { - if (specificationBuilder.Specification.Take != null) throw new DuplicateTakeException(); + if (specificationBuilder.Specification.Take != -1) throw new DuplicateTakeException(); specificationBuilder.Specification.Take = take; } @@ -266,7 +269,7 @@ public static ISpecificationBuilder Skip( { if (condition) { - if (specificationBuilder.Specification.Skip != null) throw new DuplicateSkipException(); + if (specificationBuilder.Specification.Skip != -1) throw new DuplicateSkipException(); specificationBuilder.Specification.Skip = skip; } @@ -357,8 +360,6 @@ public static ICacheSpecificationBuilder EnableCache( } specificationBuilder.Specification.CacheKey = $"{specificationName}-{string.Join("-", args)}"; - - specificationBuilder.Specification.CacheEnabled = true; } var cacheBuilder = new CacheSpecificationBuilder(specificationBuilder.Specification, !condition); diff --git a/src/Ardalis.Specification/Evaluators/PaginationEvaluator.cs b/src/Ardalis.Specification/Evaluators/PaginationEvaluator.cs index 7b1cc6fe..87318c92 100644 --- a/src/Ardalis.Specification/Evaluators/PaginationEvaluator.cs +++ b/src/Ardalis.Specification/Evaluators/PaginationEvaluator.cs @@ -10,14 +10,14 @@ private PaginationEvaluator() { } public IQueryable GetQuery(IQueryable query, ISpecification specification) where T : class { // If skip is 0, avoid adding to the IQueryable. It will generate more optimized SQL that way. - if (specification.Skip != null && specification.Skip != 0) + if (specification.Skip > 0) { - query = query.Skip(specification.Skip.Value); + query = query.Skip(specification.Skip); } - if (specification.Take != null) + if (specification.Take >= 0) { - query = query.Take(specification.Take.Value); + query = query.Take(specification.Take); } return query; @@ -25,14 +25,14 @@ public IQueryable GetQuery(IQueryable query, ISpecification specific public IEnumerable Evaluate(IEnumerable query, ISpecification specification) { - if (specification.Skip != null && specification.Skip != 0) + if (specification.Skip > 0) { - query = query.Skip(specification.Skip.Value); + query = query.Skip(specification.Skip); } - if (specification.Take != null) + if (specification.Take >= 0) { - query = query.Take(specification.Take.Value); + query = query.Take(specification.Take); } return query; diff --git a/src/Ardalis.Specification/ISpecification.cs b/src/Ardalis.Specification/ISpecification.cs index d758a81e..75b70b80 100644 --- a/src/Ardalis.Specification/ISpecification.cs +++ b/src/Ardalis.Specification/ISpecification.cs @@ -39,7 +39,7 @@ public interface ISpecification /// /// Arbitrary state to be accessed from builders and evaluators. /// - IDictionary Items { get; set; } + Dictionary Items { get; } /// /// The collection of filters. @@ -71,12 +71,12 @@ public interface ISpecification /// /// The number of elements to return. /// - int? Take { get; } + int Take { get; } /// /// The number of elements to skip before returning the remaining elements. /// - int? Skip { get; } + int Skip { get; } /// /// The transform function to apply to the result of the query encapsulated by the . diff --git a/src/Ardalis.Specification/Specification.cs b/src/Ardalis.Specification/Specification.cs index eba4ee0c..5ddfe86b 100644 --- a/src/Ardalis.Specification/Specification.cs +++ b/src/Ardalis.Specification/Specification.cs @@ -3,23 +3,7 @@ /// public class Specification : Specification, ISpecification { - public new virtual ISpecificationBuilder Query { get; } - - public Specification() - : this(InMemorySpecificationEvaluator.Default) - { - } - - public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator) - : base(inMemorySpecificationEvaluator) - { - Query = new SpecificationBuilder(this); - } - - public new virtual IEnumerable Evaluate(IEnumerable entities) - { - return Evaluator.Evaluate(entities, this); - } + public new ISpecificationBuilder Query => new SpecificationBuilder(this); /// public Expression>? Selector { get; internal set; } @@ -29,93 +13,102 @@ public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvalua /// public new Func, IEnumerable>? PostProcessingAction { get; internal set; } = null; + + public new virtual IEnumerable Evaluate(IEnumerable entities) + { + var evaluator = Evaluator; + return evaluator.Evaluate(entities, this); + } } /// public class Specification : ISpecification { - protected IInMemorySpecificationEvaluator Evaluator { get; } - protected ISpecificationValidator Validator { get; } - public virtual ISpecificationBuilder Query { get; } + // The state is null initially, but we're spending 8 bytes per reference (on x64). + // This will be reconsidered for version 10 where we may store the whole state as a single array of structs. + private List>? _whereExpressions; + private List>? _searchExpressions; + private List>? _orderExpressions; + private List? _includeExpressions; + private List? _includeStrings; + private Dictionary? _items; - public Specification() - : this(InMemorySpecificationEvaluator.Default, SpecificationValidator.Default) - { - } + public ISpecificationBuilder Query => new SpecificationBuilder(this); + protected virtual IInMemorySpecificationEvaluator Evaluator => InMemorySpecificationEvaluator.Default; + protected virtual ISpecificationValidator Validator => SpecificationValidator.Default; - public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator) - : this(inMemorySpecificationEvaluator, SpecificationValidator.Default) - { - } - - public Specification(ISpecificationValidator specificationValidator) - : this(InMemorySpecificationEvaluator.Default, specificationValidator) - { - } - - public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator, ISpecificationValidator specificationValidator) - { - Evaluator = inMemorySpecificationEvaluator; - Validator = specificationValidator; - Query = new SpecificationBuilder(this); - } + /// + public Func, IEnumerable>? PostProcessingAction { get; internal set; } /// - public virtual IEnumerable Evaluate(IEnumerable entities) - { - return Evaluator.Evaluate(entities, this); - } + public string? CacheKey { get; internal set; } /// - public virtual bool IsSatisfiedBy(T entity) - { - return Validator.IsValid(entity, this); - } + public bool CacheEnabled => CacheKey is not null; /// - public IDictionary Items { get; set; } = new Dictionary(); + public int Take { get; internal set; } = -1; /// - public IEnumerable> WhereExpressions { get; } = new List>(); + public int Skip { get; internal set; } = -1; + - public IEnumerable> OrderExpressions { get; } = new List>(); + // We may store all the flags in a single byte. But, based on the object alignment of 8 bytes, we won't save any space anyway. + // And we'll have unnecessary overhead with enum flags for now. This will be reconsidered for version 10. + // Based on the alignment of 8 bytes (on x64) we can store 8 flags here. So, we have space for 3 more flags for free. /// - public IEnumerable IncludeExpressions { get; } = new List(); + public bool IgnoreQueryFilters { get; internal set; } = false; /// - public IEnumerable IncludeStrings { get; } = new List(); + public bool AsSplitQuery { get; internal set; } = false; /// - public IEnumerable> SearchCriterias { get; } = new List>(); + public bool AsNoTracking { get; internal set; } = false; /// - public int? Take { get; internal set; } = null; + public bool AsTracking { get; internal set; } = false; /// - public int? Skip { get; internal set; } = null; + public bool AsNoTrackingWithIdentityResolution { get; internal set; } = false; + + + // Specs are not intended to be thread-safe, so we don't need to worry about thread-safety here. + internal void Add(WhereExpressionInfo whereExpression) => (_whereExpressions ??= new(2)).Add(whereExpression); + internal void Add(SearchExpressionInfo searchExpression) => (_searchExpressions ??= new(2)).Add(searchExpression); + internal void Add(OrderExpressionInfo orderExpression) => (_orderExpressions ??= new(2)).Add(orderExpression); + internal void Add(IncludeExpressionInfo includeExpression) => (_includeExpressions ??= new(2)).Add(includeExpression); + internal void Add(string includeString) => (_includeStrings ??= new(1)).Add(includeString); /// - public Func, IEnumerable>? PostProcessingAction { get; internal set; } = null; + public Dictionary Items => _items ??= []; /// - public string? CacheKey { get; internal set; } + public IEnumerable> WhereExpressions => _whereExpressions ?? Enumerable.Empty>(); /// - public bool CacheEnabled { get; internal set; } + public IEnumerable> SearchCriterias => _searchExpressions ?? Enumerable.Empty>(); /// - public bool AsTracking { get; internal set; } = false; + public IEnumerable> OrderExpressions => _orderExpressions ?? Enumerable.Empty>(); /// - public bool AsNoTracking { get; internal set; } = false; + public IEnumerable IncludeExpressions => _includeExpressions ?? Enumerable.Empty(); /// - public bool AsSplitQuery { get; internal set; } = false; + public IEnumerable IncludeStrings => _includeStrings ?? Enumerable.Empty(); /// - public bool AsNoTrackingWithIdentityResolution { get; internal set; } = false; + public virtual IEnumerable Evaluate(IEnumerable entities) + { + var evaluator = Evaluator; + return evaluator.Evaluate(entities, this); + } /// - public bool IgnoreQueryFilters { get; internal set; } = false; + public virtual bool IsSatisfiedBy(T entity) + { + var validator = Validator; + return validator.IsValid(entity, this); + } } diff --git a/tests/Ardalis.Specification.Tests/Ardalis.Specification.Tests.csproj b/tests/Ardalis.Specification.Tests/Ardalis.Specification.Tests.csproj index fc028b63..55dc68ac 100644 --- a/tests/Ardalis.Specification.Tests/Ardalis.Specification.Tests.csproj +++ b/tests/Ardalis.Specification.Tests/Ardalis.Specification.Tests.csproj @@ -10,6 +10,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tests/Ardalis.Specification.Tests/Builders/Builder_Skip.cs b/tests/Ardalis.Specification.Tests/Builders/Builder_Skip.cs index c179b94a..10c2b696 100644 --- a/tests/Ardalis.Specification.Tests/Builders/Builder_Skip.cs +++ b/tests/Ardalis.Specification.Tests/Builders/Builder_Skip.cs @@ -10,8 +10,8 @@ public void DoesNothing_GivenNoSkip() var spec1 = new Specification(); var spec2 = new Specification(); - spec1.Skip.Should().BeNull(); - spec2.Skip.Should().BeNull(); + spec1.Skip.Should().Be(-1); + spec2.Skip.Should().Be(-1); } [Fact] @@ -27,8 +27,8 @@ public void DoesNothing_GivenSkipWithFalseCondition() spec2.Query .Skip(skip, false); - spec1.Skip.Should().BeNull(); - spec2.Skip.Should().BeNull(); + spec1.Skip.Should().Be(-1); + spec2.Skip.Should().Be(-1); } [Fact] diff --git a/tests/Ardalis.Specification.Tests/Builders/Builder_Take.cs b/tests/Ardalis.Specification.Tests/Builders/Builder_Take.cs index 84aa1033..71a1da06 100644 --- a/tests/Ardalis.Specification.Tests/Builders/Builder_Take.cs +++ b/tests/Ardalis.Specification.Tests/Builders/Builder_Take.cs @@ -10,8 +10,8 @@ public void DoesNothing_GivenNoTake() var spec1 = new Specification(); var spec2 = new Specification(); - spec1.Take.Should().BeNull(); - spec2.Take.Should().BeNull(); + spec1.Take.Should().Be(-1); + spec2.Take.Should().Be(-1); } [Fact] @@ -27,8 +27,8 @@ public void DoesNothing_GivenTakeWithFalseCondition() spec2.Query .Take(take, false); - spec1.Take.Should().BeNull(); - spec2.Take.Should().BeNull(); + spec1.Take.Should().Be(-1); + spec2.Take.Should().Be(-1); } [Fact] diff --git a/tests/Ardalis.Specification.Tests/SpecificationSizeTests.cs b/tests/Ardalis.Specification.Tests/SpecificationSizeTests.cs new file mode 100644 index 00000000..be079f94 --- /dev/null +++ b/tests/Ardalis.Specification.Tests/SpecificationSizeTests.cs @@ -0,0 +1,40 @@ +#if NET8_0_OR_GREATER + +using ManagedObjectSize; +using System.Runtime.CompilerServices; +using Xunit.Abstractions; + +namespace Tests; + +public class SpecificationSizeTests +{ + private readonly ITestOutputHelper _output; + + public SpecificationSizeTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Spec_Empty() + { + var spec = new Specification(); + + var size = ObjectSize.GetObjectInclusiveSize(spec); + + size.Should().BeLessThan(100); + PrintObjectSize(spec); + } + + private record Customer(int id); + + private void PrintObjectSize(object obj, [CallerArgumentExpression(nameof(obj))] string caller = "") + { + _output.WriteLine(""); + _output.WriteLine(caller); + _output.WriteLine($"Inclusive: {ObjectSize.GetObjectInclusiveSize(obj):N0}"); + _output.WriteLine($"Exclusive: {ObjectSize.GetObjectExclusiveSize(obj):N0}"); + } +} + +#endif