Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce the size of specifications and avoid unnecessary memory allocations #441

Merged
merged 1 commit into from
Feb 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</ItemGroup>

<ItemGroup Label="Test projects dependencies">
<PackageVersion Include="ManagedObjectSize.ObjectPool" Version="0.0.7-gd53ba9da59" />
<PackageVersion Include="MartinCostello.SqlLocalDb" Version="3.4.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
<PackageVersion Include="Respawn" Version="6.2.1" />
Expand Down
10 changes: 4 additions & 6 deletions src/Ardalis.Specification/Builder/IncludableBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ public static IIncludableSpecificationBuilder<TEntity, TProperty> ThenInclude<TE
{
if (condition && !previousBuilder.IsChainDiscarded)
{
var info = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(TPreviousProperty));

((List<IncludeExpressionInfo>)previousBuilder.Specification.IncludeExpressions).Add(info);
var expr = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(TPreviousProperty));
previousBuilder.Specification.Add(expr);
}

var includeBuilder = new IncludableSpecificationBuilder<TEntity, TProperty>(previousBuilder.Specification, !condition || previousBuilder.IsChainDiscarded);
Expand All @@ -40,9 +39,8 @@ public static IIncludableSpecificationBuilder<TEntity, TProperty> ThenInclude<TE
{
if (condition && !previousBuilder.IsChainDiscarded)
{
var info = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(IEnumerable<TPreviousProperty>));

((List<IncludeExpressionInfo>)previousBuilder.Specification.IncludeExpressions).Add(info);
var expr = new IncludeExpressionInfo(thenIncludeExpression, typeof(TEntity), typeof(TProperty), typeof(IEnumerable<TPreviousProperty>));
previousBuilder.Specification.Add(expr);
}

var includeBuilder = new IncludableSpecificationBuilder<TEntity, TProperty>(previousBuilder.Specification, !condition || previousBuilder.IsChainDiscarded);
Expand Down
6 changes: 4 additions & 2 deletions src/Ardalis.Specification/Builder/OrderedBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public static IOrderedSpecificationBuilder<T> ThenBy<T>(
{
if (condition && !orderedBuilder.IsChainDiscarded)
{
((List<OrderExpressionInfo<T>>)orderedBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenBy));
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenBy);
orderedBuilder.Specification.Add(expr);
}
else
{
Expand All @@ -36,7 +37,8 @@ public static IOrderedSpecificationBuilder<T> ThenByDescending<T>(
{
if (condition && !orderedBuilder.IsChainDiscarded)
{
((List<OrderExpressionInfo<T>>)orderedBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenByDescending));
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.ThenByDescending);
orderedBuilder.Specification.Add(expr);
}
else
{
Expand Down
25 changes: 13 additions & 12 deletions src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public static ISpecificationBuilder<T> Where<T>(
{
if (condition)
{
((List<WhereExpressionInfo<T>>)specificationBuilder.Specification.WhereExpressions).Add(new WhereExpressionInfo<T>(criteria));
var expr = new WhereExpressionInfo<T>(criteria);
specificationBuilder.Specification.Add(expr);
}

return specificationBuilder;
Expand Down Expand Up @@ -58,7 +59,8 @@ public static IOrderedSpecificationBuilder<T> OrderBy<T>(
{
if (condition)
{
((List<OrderExpressionInfo<T>>)specificationBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderBy));
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderBy);
specificationBuilder.Specification.Add(expr);
}

var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification, !condition);
Expand Down Expand Up @@ -91,7 +93,8 @@ public static IOrderedSpecificationBuilder<T> OrderByDescending<T>(
{
if (condition)
{
((List<OrderExpressionInfo<T>>)specificationBuilder.Specification.OrderExpressions).Add(new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderByDescending));
var expr = new OrderExpressionInfo<T>(orderExpression, OrderTypeEnum.OrderByDescending);
specificationBuilder.Specification.Add(expr);
}

var orderedSpecificationBuilder = new OrderedSpecificationBuilder<T>(specificationBuilder.Specification, !condition);
Expand Down Expand Up @@ -130,9 +133,8 @@ public static IIncludableSpecificationBuilder<T, TProperty> Include<T, TProperty
{
if (condition)
{
var info = new IncludeExpressionInfo(includeExpression, typeof(T), typeof(TProperty));

((List<IncludeExpressionInfo>)specificationBuilder.Specification.IncludeExpressions).Add(info);
var expr = new IncludeExpressionInfo(includeExpression, typeof(T), typeof(TProperty));
specificationBuilder.Specification.Add(expr);
}

var includeBuilder = new IncludableSpecificationBuilder<T, TProperty>(specificationBuilder.Specification, !condition);
Expand Down Expand Up @@ -165,7 +167,7 @@ public static ISpecificationBuilder<T> Include<T>(
{
if (condition)
{
((List<string>)specificationBuilder.Specification.IncludeStrings).Add(includeString);
specificationBuilder.Specification.Add(includeString);
}

return specificationBuilder;
Expand Down Expand Up @@ -204,7 +206,8 @@ public static ISpecificationBuilder<T> Search<T>(
{
if (condition)
{
((List<SearchExpressionInfo<T>>)specificationBuilder.Specification.SearchCriterias).Add(new SearchExpressionInfo<T>(selector, searchTerm, searchGroup));
var expr = new SearchExpressionInfo<T>(selector, searchTerm, searchGroup);
specificationBuilder.Specification.Add(expr);
}

return specificationBuilder;
Expand Down Expand Up @@ -233,7 +236,7 @@ public static ISpecificationBuilder<T> Take<T>(
{
if (condition)
{
if (specificationBuilder.Specification.Take != null) throw new DuplicateTakeException();
if (specificationBuilder.Specification.Take != -1) throw new DuplicateTakeException();

specificationBuilder.Specification.Take = take;
}
Expand Down Expand Up @@ -266,7 +269,7 @@ public static ISpecificationBuilder<T> Skip<T>(
{
if (condition)
{
if (specificationBuilder.Specification.Skip != null) throw new DuplicateSkipException();
if (specificationBuilder.Specification.Skip != -1) throw new DuplicateSkipException();

specificationBuilder.Specification.Skip = skip;
}
Expand Down Expand Up @@ -357,8 +360,6 @@ public static ICacheSpecificationBuilder<T> EnableCache<T>(
}

specificationBuilder.Specification.CacheKey = $"{specificationName}-{string.Join("-", args)}";

specificationBuilder.Specification.CacheEnabled = true;
}

var cacheBuilder = new CacheSpecificationBuilder<T>(specificationBuilder.Specification, !condition);
Expand Down
16 changes: 8 additions & 8 deletions src/Ardalis.Specification/Evaluators/PaginationEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ private PaginationEvaluator() { }
public IQueryable<T> GetQuery<T>(IQueryable<T> query, ISpecification<T> 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;
}

public IEnumerable<T> Evaluate<T>(IEnumerable<T> query, ISpecification<T> 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;
Expand Down
6 changes: 3 additions & 3 deletions src/Ardalis.Specification/ISpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public interface ISpecification<T>
/// <summary>
/// Arbitrary state to be accessed from builders and evaluators.
/// </summary>
IDictionary<string, object> Items { get; set; }
Dictionary<string, object> Items { get; }

/// <summary>
/// The collection of filters.
Expand Down Expand Up @@ -71,12 +71,12 @@ public interface ISpecification<T>
/// <summary>
/// The number of elements to return.
/// </summary>
int? Take { get; }
int Take { get; }

/// <summary>
/// The number of elements to skip before returning the remaining elements.
/// </summary>
int? Skip { get; }
int Skip { get; }

/// <summary>
/// The transform function to apply to the result of the query encapsulated by the <see cref="ISpecification{T}"/>.
Expand Down
121 changes: 57 additions & 64 deletions src/Ardalis.Specification/Specification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,7 @@
/// <inheritdoc cref="ISpecification{T, TResult}"/>
public class Specification<T, TResult> : Specification<T>, ISpecification<T, TResult>
{
public new virtual ISpecificationBuilder<T, TResult> Query { get; }

public Specification()
: this(InMemorySpecificationEvaluator.Default)
{
}

public Specification(IInMemorySpecificationEvaluator inMemorySpecificationEvaluator)
: base(inMemorySpecificationEvaluator)
{
Query = new SpecificationBuilder<T, TResult>(this);
}

public new virtual IEnumerable<TResult> Evaluate(IEnumerable<T> entities)
{
return Evaluator.Evaluate(entities, this);
}
public new ISpecificationBuilder<T, TResult> Query => new SpecificationBuilder<T, TResult>(this);

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

/// <inheritdoc/>
public new Func<IEnumerable<TResult>, IEnumerable<TResult>>? PostProcessingAction { get; internal set; } = null;

public new virtual IEnumerable<TResult> Evaluate(IEnumerable<T> entities)
{
var evaluator = Evaluator;
return evaluator.Evaluate(entities, this);
}
}

/// <inheritdoc cref="ISpecification{T}"/>
public class Specification<T> : ISpecification<T>
{
protected IInMemorySpecificationEvaluator Evaluator { get; }
protected ISpecificationValidator Validator { get; }
public virtual ISpecificationBuilder<T> 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<WhereExpressionInfo<T>>? _whereExpressions;
private List<SearchExpressionInfo<T>>? _searchExpressions;
private List<OrderExpressionInfo<T>>? _orderExpressions;
private List<IncludeExpressionInfo>? _includeExpressions;
private List<string>? _includeStrings;
private Dictionary<string, object>? _items;

public Specification()
: this(InMemorySpecificationEvaluator.Default, SpecificationValidator.Default)
{
}
public ISpecificationBuilder<T> Query => new SpecificationBuilder<T>(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<T>(this);
}
/// <inheritdoc/>
public Func<IEnumerable<T>, IEnumerable<T>>? PostProcessingAction { get; internal set; }

/// <inheritdoc/>
public virtual IEnumerable<T> Evaluate(IEnumerable<T> entities)
{
return Evaluator.Evaluate(entities, this);
}
public string? CacheKey { get; internal set; }

/// <inheritdoc/>
public virtual bool IsSatisfiedBy(T entity)
{
return Validator.IsValid(entity, this);
}
public bool CacheEnabled => CacheKey is not null;

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

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


public IEnumerable<OrderExpressionInfo<T>> OrderExpressions { get; } = new List<OrderExpressionInfo<T>>();
// 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.

/// <inheritdoc/>
public IEnumerable<IncludeExpressionInfo> IncludeExpressions { get; } = new List<IncludeExpressionInfo>();
public bool IgnoreQueryFilters { get; internal set; } = false;

/// <inheritdoc/>
public IEnumerable<string> IncludeStrings { get; } = new List<string>();
public bool AsSplitQuery { get; internal set; } = false;

/// <inheritdoc/>
public IEnumerable<SearchExpressionInfo<T>> SearchCriterias { get; } = new List<SearchExpressionInfo<T>>();
public bool AsNoTracking { get; internal set; } = false;

/// <inheritdoc/>
public int? Take { get; internal set; } = null;
public bool AsTracking { get; internal set; } = false;

/// <inheritdoc/>
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<T> whereExpression) => (_whereExpressions ??= new(2)).Add(whereExpression);
internal void Add(SearchExpressionInfo<T> searchExpression) => (_searchExpressions ??= new(2)).Add(searchExpression);
internal void Add(OrderExpressionInfo<T> 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);

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

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

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

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

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

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

/// <inheritdoc/>
public bool AsNoTrackingWithIdentityResolution { get; internal set; } = false;
public virtual IEnumerable<T> Evaluate(IEnumerable<T> entities)
{
var evaluator = Evaluator;
return evaluator.Evaluate(entities, this);
}

/// <inheritdoc/>
public bool IgnoreQueryFilters { get; internal set; } = false;
public virtual bool IsSatisfiedBy(T entity)
{
var validator = Validator;
return validator.IsValid(entity, this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net9.0' ">
<PackageReference Include="ManagedObjectSize.ObjectPool" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Ardalis.Specification\Ardalis.Specification.csproj" />
Expand Down
Loading
Loading