Skip to content

Commit 1efbb7c

Browse files
authored
Refactored internal state of specifications. Reduced the size and memory allocations. (#441)
1 parent e85e4b7 commit 1efbb7c

File tree

11 files changed

+142
-103
lines changed

11 files changed

+142
-103
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
</ItemGroup>
1414

1515
<ItemGroup Label="Test projects dependencies">
16+
<PackageVersion Include="ManagedObjectSize.ObjectPool" Version="0.0.7-gd53ba9da59" />
1617
<PackageVersion Include="MartinCostello.SqlLocalDb" Version="3.4.0" />
1718
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
1819
<PackageVersion Include="Respawn" Version="6.2.1" />

src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ public static IIncludableSpecificationBuilder<TEntity, TProperty> ThenInclude<TE
1616
{
1717
if (condition && !previousBuilder.IsChainDiscarded)
1818
{
19-
var info = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(TPreviousProperty));
20-
21-
((List<IncludeExpressionInfo>)previousBuilder.Specification.IncludeExpressions).Add(info);
19+
var expr = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(TPreviousProperty));
20+
previousBuilder.Specification.Add(expr);
2221
}
2322

2423
var includeBuilder = new IncludableSpecificationBuilder<TEntity, TProperty>(previousBuilder.Specification, !condition || previousBuilder.IsChainDiscarded);
@@ -40,9 +39,8 @@ public static IIncludableSpecificationBuilder<TEntity, TProperty> ThenInclude<TE
4039
{
4140
if (condition && !previousBuilder.IsChainDiscarded)
4241
{
43-
var info = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(IEnumerable<TPreviousProperty>));
44-
45-
((List<IncludeExpressionInfo>)previousBuilder.Specification.IncludeExpressions).Add(info);
42+
var expr = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(IEnumerable<TPreviousProperty>));
43+
previousBuilder.Specification.Add(expr);
4644
}
4745

4846
var includeBuilder = new IncludableSpecificationBuilder<TEntity, TProperty>(previousBuilder.Specification, !condition || previousBuilder.IsChainDiscarded);

src/Ardalis.Specification/Builder/OrderedBuilderExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ public static IOrderedSpecificationBuilder<T> ThenBy<T>(
1414
{
1515
if (condition && !orderedBuilder.IsChainDiscarded)
1616
{
17-
((List<OrderExpressionInfo<T>>)orderedBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenBy));
17+
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenBy);
18+
orderedBuilder.Specification.Add(expr);
1819
}
1920
else
2021
{
@@ -36,7 +37,8 @@ public static IOrderedSpecificationBuilder<T> ThenByDescending<T>(
3637
{
3738
if (condition && !orderedBuilder.IsChainDiscarded)
3839
{
39-
((List<OrderExpressionInfo<T>>)orderedBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenByDescending));
40+
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenByDescending);
41+
orderedBuilder.Specification.Add(expr);
4042
}
4143
else
4244
{

src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public static ISpecificationBuilder<T> Where<T>(
2727
{
2828
if (condition)
2929
{
30-
((List<WhereExpressionInfo<T>>)specificationBuilder.Specification.WhereExpressions).Add(new WhereExpressionInfo<T>(criteria));
30+
var expr = new WhereExpressionInfo<T>(criteria);
31+
specificationBuilder.Specification.Add(expr);
3132
}
3233

3334
return specificationBuilder;
@@ -58,7 +59,8 @@ public static IOrderedSpecificationBuilder<T> OrderBy<T>(
5859
{
5960
if (condition)
6061
{
61-
((List<OrderExpressionInfo<T>>)specificationBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderBy));
62+
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderBy);
63+
specificationBuilder.Specification.Add(expr);
6264
}
6365

6466
var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification, !condition);
@@ -91,7 +93,8 @@ public static IOrderedSpecificationBuilder<T> OrderByDescending<T>(
9193
{
9294
if (condition)
9395
{
94-
((List<OrderExpressionInfo<T>>)specificationBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderByDescending));
96+
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderByDescending);
97+
specificationBuilder.Specification.Add(expr);
9598
}
9699

97100
var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification, !condition);
@@ -130,9 +133,8 @@ public static IIncludableSpecificationBuilder<T, TProperty> Include<T, TProperty
130133
{
131134
if (condition)
132135
{
133-
var info = new IncludeExpressionInfo(includeExpression, typeof(T), typeof(TProperty));
134-
135-
((List<IncludeExpressionInfo>)specificationBuilder.Specification.IncludeExpressions).Add(info);
136+
var expr = new IncludeExpressionInfo(includeExpression, typeof(T), typeof(TProperty));
137+
specificationBuilder.Specification.Add(expr);
136138
}
137139

138140
var includeBuilder = new IncludableSpecificationBuilder<T, TProperty>(specificationBuilder.Specification, !condition);
@@ -165,7 +167,7 @@ public static ISpecificationBuilder<T> Include<T>(
165167
{
166168
if (condition)
167169
{
168-
((List<string>)specificationBuilder.Specification.IncludeStrings).Add(includeString);
170+
specificationBuilder.Specification.Add(includeString);
169171
}
170172

171173
return specificationBuilder;
@@ -204,7 +206,8 @@ public static ISpecificationBuilder<T> Search<T>(
204206
{
205207
if (condition)
206208
{
207-
((List<SearchExpressionInfo<T>>)specificationBuilder.Specification.SearchCriterias).Add(new SearchExpressionInfo<T>(selector, searchTerm, searchGroup));
209+
var expr = new SearchExpressionInfo<T>(selector, searchTerm, searchGroup);
210+
specificationBuilder.Specification.Add(expr);
208211
}
209212

210213
return specificationBuilder;
@@ -233,7 +236,7 @@ public static ISpecificationBuilder<T> Take<T>(
233236
{
234237
if (condition)
235238
{
236-
if (specificationBuilder.Specification.Take != null) throw new DuplicateTakeException();
239+
if (specificationBuilder.Specification.Take != -1) throw new DuplicateTakeException();
237240

238241
specificationBuilder.Specification.Take = take;
239242
}
@@ -266,7 +269,7 @@ public static ISpecificationBuilder<T> Skip<T>(
266269
{
267270
if (condition)
268271
{
269-
if (specificationBuilder.Specification.Skip != null) throw new DuplicateSkipException();
272+
if (specificationBuilder.Specification.Skip != -1) throw new DuplicateSkipException();
270273

271274
specificationBuilder.Specification.Skip = skip;
272275
}
@@ -357,8 +360,6 @@ public static ICacheSpecificationBuilder<T> EnableCache<T>(
357360
}
358361

359362
specificationBuilder.Specification.CacheKey = $"{specificationName}-{string.Join("-", args)}";
360-
361-
specificationBuilder.Specification.CacheEnabled = true;
362363
}
363364

364365
var cacheBuilder = new CacheSpecificationBuilder<T>(specificationBuilder.Specification, !condition);

src/Ardalis.Specification/Evaluators/PaginationEvaluator.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,29 @@ private PaginationEvaluator() { }
1010
public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> specification) where T : class
1111
{
1212
// If skip is 0, avoid adding to the IQueryable. It will generate more optimized SQL that way.
13-
if (specification.Skip != null && specification.Skip != 0)
13+
if (specification.Skip > 0)
1414
{
15-
query = query.Skip(specification.Skip.Value);
15+
query = query.Skip(specification.Skip);
1616
}
1717

18-
if (specification.Take != null)
18+
if (specification.Take >= 0)
1919
{
20-
query = query.Take(specification.Take.Value);
20+
query = query.Take(specification.Take);
2121
}
2222

2323
return query;
2424
}
2525

2626
public IEnumerable<T> Evaluate<T>(IEnumerable<T> query, ISpecification<T> specification)
2727
{
28-
if (specification.Skip != null && specification.Skip != 0)
28+
if (specification.Skip > 0)
2929
{
30-
query = query.Skip(specification.Skip.Value);
30+
query = query.Skip(specification.Skip);
3131
}
3232

33-
if (specification.Take != null)
33+
if (specification.Take >= 0)
3434
{
35-
query = query.Take(specification.Take.Value);
35+
query = query.Take(specification.Take);
3636
}
3737

3838
return query;

src/Ardalis.Specification/ISpecification.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public interface ISpecification<T>
3939
/// <summary>
4040
/// Arbitrary state to be accessed from builders and evaluators.
4141
/// </summary>
42-
IDictionary<string, object> Items { get; set; }
42+
Dictionary<string, object> Items { get; }
4343

4444
/// <summary>
4545
/// The collection of filters.
@@ -71,12 +71,12 @@ public interface ISpecification<T>
7171
/// <summary>
7272
/// The number of elements to return.
7373
/// </summary>
74-
int? Take { get; }
74+
int Take { get; }
7575

7676
/// <summary>
7777
/// The number of elements to skip before returning the remaining elements.
7878
/// </summary>
79-
int? Skip { get; }
79+
int Skip { get; }
8080

8181
/// <summary>
8282
/// The transform function to apply to the result of the query encapsulated by the <see cref="ISpecification{T}"/>.

src/Ardalis.Specification/Specification.cs

Lines changed: 57 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,7 @@
33
/// <inheritdoc cref="ISpecification{T, TResult}"/>
44
public class Specification<T, TResult> : Specification<T>, ISpecification<T, TResult>
55
{
6-
public new virtual ISpecificationBuilder<T, TResult> Query { get; }
7-
8-
public Specification()
9-
: this(InMemorySpecificationEvaluator.Default)
10-
{
11-
}
12-
13-
public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator)
14-
: base(inMemorySpecificationEvaluator)
15-
{
16-
Query = new SpecificationBuilder<T, TResult>(this);
17-
}
18-
19-
public new virtual IEnumerable<TResult> Evaluate(IEnumerable<T> entities)
20-
{
21-
return Evaluator.Evaluate(entities, this);
22-
}
6+
public new ISpecificationBuilder<T, TResult> Query => new SpecificationBuilder<T, TResult>(this);
237

248
/// <inheritdoc/>
259
public Expression<Func<T, TResult>>? Selector { get; internal set; }
@@ -29,93 +13,102 @@ public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvalua
2913

3014
/// <inheritdoc/>
3115
public new Func<IEnumerable<TResult>, IEnumerable<TResult>>? PostProcessingAction { get; internal set; } = null;
16+
17+
public new virtual IEnumerable<TResult> Evaluate(IEnumerable<T> entities)
18+
{
19+
var evaluator = Evaluator;
20+
return evaluator.Evaluate(entities, this);
21+
}
3222
}
3323

3424
/// <inheritdoc cref="ISpecification{T}"/>
3525
public class Specification<T> : ISpecification<T>
3626
{
37-
protected IInMemorySpecificationEvaluator Evaluator { get; }
38-
protected ISpecificationValidator Validator { get; }
39-
public virtual ISpecificationBuilder<T> Query { get; }
27+
// The state is null initially, but we're spending 8 bytes per reference (on x64).
28+
// This will be reconsidered for version 10 where we may store the whole state as a single array of structs.
29+
private List<WhereExpressionInfo<T>>? _whereExpressions;
30+
private List<SearchExpressionInfo<T>>? _searchExpressions;
31+
private List<OrderExpressionInfo<T>>? _orderExpressions;
32+
private List<IncludeExpressionInfo>? _includeExpressions;
33+
private List<string>? _includeStrings;
34+
private Dictionary<string, object>? _items;
4035

41-
public Specification()
42-
: this(InMemorySpecificationEvaluator.Default, SpecificationValidator.Default)
43-
{
44-
}
36+
public ISpecificationBuilder<T> Query => new SpecificationBuilder<T>(this);
37+
protected virtual IInMemorySpecificationEvaluator Evaluator => InMemorySpecificationEvaluator.Default;
38+
protected virtual ISpecificationValidator Validator => SpecificationValidator.Default;
4539

46-
public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator)
47-
: this(inMemorySpecificationEvaluator, SpecificationValidator.Default)
48-
{
49-
}
50-
51-
public Specification(ISpecificationValidator specificationValidator)
52-
: this(InMemorySpecificationEvaluator.Default, specificationValidator)
53-
{
54-
}
55-
56-
public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator, ISpecificationValidator specificationValidator)
57-
{
58-
Evaluator = inMemorySpecificationEvaluator;
59-
Validator = specificationValidator;
60-
Query = new SpecificationBuilder<T>(this);
61-
}
40+
/// <inheritdoc/>
41+
public Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; internal set; }
6242

6343
/// <inheritdoc/>
64-
public virtual IEnumerable<T> Evaluate(IEnumerable<T> entities)
65-
{
66-
return Evaluator.Evaluate(entities, this);
67-
}
44+
public string? CacheKey { get; internal set; }
6845

6946
/// <inheritdoc/>
70-
public virtual bool IsSatisfiedBy(T entity)
71-
{
72-
return Validator.IsValid(entity, this);
73-
}
47+
public bool CacheEnabled => CacheKey is not null;
7448

7549
/// <inheritdoc/>
76-
public IDictionary<string, object> Items { get; set; } = new Dictionary<string, object>();
50+
public int Take { get; internal set; } = -1;
7751

7852
/// <inheritdoc/>
79-
public IEnumerable<WhereExpressionInfo<T>> WhereExpressions { get; } = new List<WhereExpressionInfo<T>>();
53+
public int Skip { get; internal set; } = -1;
54+
8055

81-
public IEnumerable<OrderExpressionInfo<T>> OrderExpressions { get; } = new List<OrderExpressionInfo<T>>();
56+
// 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.
57+
// And we'll have unnecessary overhead with enum flags for now. This will be reconsidered for version 10.
58+
// 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.
8259

8360
/// <inheritdoc/>
84-
public IEnumerable<IncludeExpressionInfo> IncludeExpressions { get; } = new List<IncludeExpressionInfo>();
61+
public bool IgnoreQueryFilters { get; internal set; } = false;
8562

8663
/// <inheritdoc/>
87-
public IEnumerable<string> IncludeStrings { get; } = new List<string>();
64+
public bool AsSplitQuery { get; internal set; } = false;
8865

8966
/// <inheritdoc/>
90-
public IEnumerable<SearchExpressionInfo<T>> SearchCriterias { get; } = new List<SearchExpressionInfo<T>>();
67+
public bool AsNoTracking { get; internal set; } = false;
9168

9269
/// <inheritdoc/>
93-
public int? Take { get; internal set; } = null;
70+
public bool AsTracking { get; internal set; } = false;
9471

9572
/// <inheritdoc/>
96-
public int? Skip { get; internal set; } = null;
73+
public bool AsNoTrackingWithIdentityResolution { get; internal set; } = false;
74+
75+
76+
// Specs are not intended to be thread-safe, so we don't need to worry about thread-safety here.
77+
internal void Add(WhereExpressionInfo<T> whereExpression) => (_whereExpressions ??= new(2)).Add(whereExpression);
78+
internal void Add(SearchExpressionInfo<T> searchExpression) => (_searchExpressions ??= new(2)).Add(searchExpression);
79+
internal void Add(OrderExpressionInfo<T> orderExpression) => (_orderExpressions ??= new(2)).Add(orderExpression);
80+
internal void Add(IncludeExpressionInfo includeExpression) => (_includeExpressions ??= new(2)).Add(includeExpression);
81+
internal void Add(string includeString) => (_includeStrings ??= new(1)).Add(includeString);
9782

9883
/// <inheritdoc/>
99-
public Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; internal set; } = null;
84+
public Dictionary<string, object> Items => _items ??= [];
10085

10186
/// <inheritdoc/>
102-
public string? CacheKey { get; internal set; }
87+
public IEnumerable<WhereExpressionInfo<T>> WhereExpressions => _whereExpressions ?? Enumerable.Empty<WhereExpressionInfo<T>>();
10388

10489
/// <inheritdoc/>
105-
public bool CacheEnabled { get; internal set; }
90+
public IEnumerable<SearchExpressionInfo<T>> SearchCriterias => _searchExpressions ?? Enumerable.Empty<SearchExpressionInfo<T>>();
10691

10792
/// <inheritdoc/>
108-
public bool AsTracking { get; internal set; } = false;
93+
public IEnumerable<OrderExpressionInfo<T>> OrderExpressions => _orderExpressions ?? Enumerable.Empty<OrderExpressionInfo<T>>();
10994

11095
/// <inheritdoc/>
111-
public bool AsNoTracking { get; internal set; } = false;
96+
public IEnumerable<IncludeExpressionInfo> IncludeExpressions => _includeExpressions ?? Enumerable.Empty<IncludeExpressionInfo>();
11297

11398
/// <inheritdoc/>
114-
public bool AsSplitQuery { get; internal set; } = false;
99+
public IEnumerable<string> IncludeStrings => _includeStrings ?? Enumerable.Empty<string>();
115100

116101
/// <inheritdoc/>
117-
public bool AsNoTrackingWithIdentityResolution { get; internal set; } = false;
102+
public virtual IEnumerable<T> Evaluate(IEnumerable<T> entities)
103+
{
104+
var evaluator = Evaluator;
105+
return evaluator.Evaluate(entities, this);
106+
}
118107

119108
/// <inheritdoc/>
120-
public bool IgnoreQueryFilters { get; internal set; } = false;
109+
public virtual bool IsSatisfiedBy(T entity)
110+
{
111+
var validator = Validator;
112+
return validator.IsValid(entity, this);
113+
}
121114
}

tests/Ardalis.Specification.Tests/Ardalis.Specification.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1111
</PackageReference>
1212
</ItemGroup>
13+
14+
<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
15+
<PackageReference Include="ManagedObjectSize.ObjectPool" />
16+
</ItemGroup>
1317

1418
<ItemGroup>
1519
<ProjectReference Include="..\..\src\Ardalis.Specification\Ardalis.Specification.csproj" />

0 commit comments

Comments
 (0)