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