< Summary

Information
Class: Pozitron.QuerySpecification.Specification<T1, T2>
Assembly: Pozitron.QuerySpecification
File(s): /home/runner/work/QuerySpecification/QuerySpecification/src/QuerySpecification/Specification.cs
Tag: 67_15587897385
Line coverage
100%
Covered lines: 7
Uncovered lines: 0
Coverable lines: 7
Total lines: 568
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor()100%11100%
.ctor(...)100%11100%
Evaluate(...)100%11100%

File(s)

/home/runner/work/QuerySpecification/QuerySpecification/src/QuerySpecification/Specification.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2
 3namespace Pozitron.QuerySpecification;
 4
 5/// <summary>
 6/// Represents a specification with a result type.
 7/// </summary>
 8/// <typeparam name="T">The type of the entity.</typeparam>
 9/// <typeparam name="TResult">The type of the result.</typeparam>
 10public class Specification<T, TResult> : Specification<T>
 11{
 12    /// <summary>
 13    /// Initializes a new instance of the <see cref="Specification{T, TResult}"/> class.
 14    /// </summary>
 24815    public Specification() : base() { }
 16
 17    /// <summary>
 18    /// Initializes a new instance of the <see cref="Specification{T, TResult}"/> class with the specified initial capac
 19    /// </summary>
 20    /// <param name="initialCapacity">The initial capacity of the specification.</param>
 221    public Specification(int initialCapacity) : base(initialCapacity) { }
 22
 23    /// <summary>
 24    /// Evaluates the given entities according to the specification and returns the result.
 25    /// </summary>
 26    /// <param name="entities">The entities to evaluate.</param>
 27    /// <param name="ignorePaging">Whether to ignore paging settings (Take/Skip) defined in the specification.</param>
 28    /// <returns>The evaluated result.</returns>
 29    public new virtual IEnumerable<TResult> Evaluate(IEnumerable<T> entities, bool ignorePaging = false)
 30    {
 431        var evaluator = Evaluator;
 432        return evaluator.Evaluate(entities, this, ignorePaging);
 33    }
 34
 35    /// <summary>
 36    /// Gets the specification builder.
 37    /// </summary>
 9738    public new ISpecificationBuilder<T, TResult> Query => new SpecificationBuilder<T, TResult>(this);
 39
 40    /// <summary>
 41    /// Gets the Select expression.
 42    /// </summary>
 4043    public Expression<Func<T, TResult>>? Selector => FirstOrDefault<Expression<Func<T, TResult>>>(ItemType.Select);
 44
 45    /// <summary>
 46    /// Gets the SelectMany expression.
 47    /// </summary>
 3648    public Expression<Func<T, IEnumerable<TResult>>>? SelectorMany => FirstOrDefault<Expression<Func<T, IEnumerable<TRes
 49}
 50
 51/// <summary>
 52/// Represents a specification.
 53/// </summary>
 54/// <typeparam name="T">The type of the entity.</typeparam>
 55public partial class Specification<T>
 56{
 57    private SpecItem[]? _items;
 58
 59    // It is utilized only during the building stage for the builder chains. Once the state is built, we don't care abou
 60    // The initial value is not important since the value is always initialized by the root of the chain. Therefore, we 
 61    // With this we're saving 8 bytes per include builder, and we don't need an order builder at all (saving 32 bytes pe
 62    [ThreadStatic]
 63    internal static bool IsChainDiscarded;
 64
 65    /// <summary>
 66    /// Initializes a new instance of the <see cref="Specification{T}"/> class.
 67    /// </summary>
 68    public Specification() { }
 69
 70    /// <summary>
 71    /// Initializes a new instance of the <see cref="Specification{T}"/> class with the specified initial capacity.
 72    /// </summary>
 73    /// <param name="initialCapacity">The initial capacity of the specification.</param>
 74    public Specification(int initialCapacity)
 75    {
 76        _items = new SpecItem[initialCapacity];
 77    }
 78
 79    /// <summary>
 80    /// Evaluates the given entities according to the specification and returns the result.
 81    /// </summary>
 82    /// <param name="entities">The entities to evaluate.</param>
 83    /// <param name="ignorePaging">Whether to ignore paging settings (Take/Skip) defined in the specification.</param>
 84    /// <returns>The evaluated result.</returns>
 85    public virtual IEnumerable<T> Evaluate(IEnumerable<T> entities, bool ignorePaging = false)
 86    {
 87        var evaluator = Evaluator;
 88        return evaluator.Evaluate(entities, this, ignorePaging);
 89    }
 90
 91    /// <summary>
 92    /// Determines whether the specified entity satisfies the specification.
 93    /// </summary>
 94    /// <param name="entity">The entity to evaluate.</param>
 95    /// <returns>true if the entity satisfies the specification; otherwise, false.</returns>
 96    public virtual bool IsSatisfiedBy(T entity)
 97    {
 98        var validator = Validator;
 99        return validator.IsValid(entity, this);
 100    }
 101
 102    /// <summary>
 103    /// Gets the evaluator.
 104    /// </summary>
 105    protected virtual SpecificationMemoryEvaluator Evaluator => SpecificationMemoryEvaluator.Default;
 106
 107    /// <summary>
 108    /// Gets the validator.
 109    /// </summary>
 110    protected virtual SpecificationValidator Validator => SpecificationValidator.Default;
 111
 112    /// <summary>
 113    /// Gets the specification builder.
 114    /// </summary>
 115    public ISpecificationBuilder<T> Query => new SpecificationBuilder<T>(this);
 116
 117    /// <summary>
 118    /// Gets a value indicating whether the specification is empty.
 119    /// </summary>
 120    [MemberNotNullWhen(false, nameof(_items))]
 121    public bool IsEmpty => _items is null;
 122
 123    /// <summary>
 124    /// Gets the compiled where expressions.
 125    /// </summary>
 126    public IEnumerable<WhereExpressionCompiled<T>> WhereExpressionsCompiled => _items is null
 127        ? Enumerable.Empty<WhereExpressionCompiled<T>>()
 128        : new SpecSelectIterator<Func<T, bool>, WhereExpressionCompiled<T>>(GetCompiledItems(), ItemType.Where, (x, bag)
 129
 130    /// <summary>
 131    /// Gets the compiled order expressions.
 132    /// </summary>
 133    public IEnumerable<OrderExpressionCompiled<T>> OrderExpressionsCompiled => _items is null
 134        ? Enumerable.Empty<OrderExpressionCompiled<T>>()
 135        : new SpecSelectIterator<Func<T, object?>, OrderExpressionCompiled<T>>(GetCompiledItems(), ItemType.Order, (x, b
 136
 137    /// <summary>
 138    /// Gets the compiled like expressions.
 139    /// </summary>
 140    public IEnumerable<LikeExpressionCompiled<T>> LikeExpressionsCompiled => _items is null
 141        ? Enumerable.Empty<LikeExpressionCompiled<T>>()
 142        : new SpecSelectIterator<SpecLikeCompiled<T>, LikeExpressionCompiled<T>>(GetCompiledItems(), ItemType.Like, (x, 
 143
 144    /// <summary>
 145    /// Gets the where expressions.
 146    /// </summary>
 147    public IEnumerable<WhereExpression<T>> WhereExpressions => _items is null
 148        ? Enumerable.Empty<WhereExpression<T>>()
 149        : new SpecSelectIterator<Expression<Func<T, bool>>, WhereExpression<T>>(_items, ItemType.Where, (x, bag) => new 
 150
 151    /// <summary>
 152    /// Gets the include expressions.
 153    /// </summary>
 154    public IEnumerable<IncludeExpression<T>> IncludeExpressions => _items is null
 155        ? Enumerable.Empty<IncludeExpression<T>>()
 156        : new SpecSelectIterator<LambdaExpression, IncludeExpression<T>>(_items, ItemType.Include, (x, bag) => new Inclu
 157
 158    /// <summary>
 159    /// Gets the order expressions.
 160    /// </summary>
 161    public IEnumerable<OrderExpression<T>> OrderExpressions => _items is null
 162        ? Enumerable.Empty<OrderExpression<T>>()
 163        : new SpecSelectIterator<Expression<Func<T, object?>>, OrderExpression<T>>(_items, ItemType.Order, (x, bag) => n
 164
 165    /// <summary>
 166    /// Gets the like expressions.
 167    /// </summary>
 168    public IEnumerable<LikeExpression<T>> LikeExpressions => _items is null
 169        ? Enumerable.Empty<LikeExpression<T>>()
 170        : new SpecSelectIterator<SpecLike<T>, LikeExpression<T>>(_items, ItemType.Like, (x, bag) => new LikeExpression<T
 171
 172    /// <summary>
 173    /// Gets the include strings.
 174    /// </summary>
 175    public IEnumerable<string> IncludeStrings => _items is null
 176        ? Enumerable.Empty<string>()
 177        : new SpecSelectIterator<string, string>(_items, ItemType.IncludeString, (x, bag) => x);
 178
 179    /// <summary>
 180    /// Gets the Query tags.
 181    /// </summary>
 182    public IEnumerable<string> QueryTags => _items is null
 183        ? Enumerable.Empty<string>()
 184        : new SpecSelectIterator<string, string>(_items, ItemType.QueryTag, (x, bag) => x);
 185
 186    /// <summary>
 187    /// Return whether or not a cache key is set.
 188    /// </summary>
 189    public bool HasCacheKey => Contains(ItemType.CacheKey);
 190
 191    /// <summary>
 192    /// Gets the cache key.
 193    /// </summary>
 194    public string? CacheKey => FirstOrDefault<string>(ItemType.CacheKey);
 195
 196    /// <summary>
 197    /// Gets the number of items to take.
 198    /// </summary>
 199    public int Take => FirstOrDefault<SpecPaging>(ItemType.Paging)?.Take ?? -1;
 200
 201    /// <summary>
 202    /// Gets the number of items to skip.
 203    /// </summary>
 204    public int Skip => FirstOrDefault<SpecPaging>(ItemType.Paging)?.Skip ?? -1;
 205
 206    /// <summary>
 207    /// Gets a value indicating whether IgnoreQueryFilters is applied.
 208    /// </summary>
 209    public bool IgnoreQueryFilters => GetFlag(SpecFlags.IgnoreQueryFilters);
 210
 211    /// <summary>
 212    /// Gets a value indicating whether IgnoreAutoIncludes is applied.
 213    /// </summary>
 214    public bool IgnoreAutoIncludes => GetFlag(SpecFlags.IgnoreAutoIncludes);
 215
 216    /// <summary>
 217    /// Gets a value indicating whether AsSplitQuery is applied.
 218    /// </summary>
 219    public bool AsSplitQuery => GetFlag(SpecFlags.AsSplitQuery);
 220
 221    /// <summary>
 222    /// Gets a value indicating whether AsNoTracking is applied.
 223    /// </summary>
 224    public bool AsNoTracking => GetFlag(SpecFlags.AsNoTracking);
 225
 226    /// <summary>
 227    /// Gets a value indicating whether AsNoTrackingWithIdentityResolution is applied.
 228    /// </summary>
 229    public bool AsNoTrackingWithIdentityResolution => GetFlag(SpecFlags.AsNoTrackingWithIdentityResolution);
 230
 231    /// <summary>
 232    /// Gets a value indicating whether AsTracking is applied.
 233    /// </summary>
 234    public bool AsTracking => GetFlag(SpecFlags.AsTracking);
 235
 236    /// <summary>
 237    /// Adds an item to the specification.
 238    /// </summary>
 239    /// <param name="type">The type of the item. It must be a positive number.</param>
 240    /// <param name="value">The object to be stored in the item.</param>
 241    /// <exception cref="ArgumentNullException">If value is null</exception>
 242    /// <exception cref="ArgumentOutOfRangeException">If type is zero or negative.</exception>
 243    public void Add(int type, object value)
 244    {
 245        ArgumentNullException.ThrowIfNull(value);
 246        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(type);
 247
 248        AddInternal(type, value);
 249    }
 250
 251    /// <summary>
 252    /// Adds or updates an item in the specification.
 253    /// </summary>
 254    /// <param name="type">The type of the item.</param>
 255    /// <param name="value">The object to be stored in the item.</param>
 256    /// <exception cref="ArgumentNullException">Thrown if value is null</exception>
 257    /// <exception cref="ArgumentOutOfRangeException">Thrown if type is zero or negative.</exception>
 258    public void AddOrUpdate(int type, object value)
 259    {
 260        ArgumentNullException.ThrowIfNull(value);
 261        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(type);
 262
 263        AddOrUpdateInternal(type, value);
 264    }
 265
 266    /// <summary>
 267    /// Gets the first item of the specified type or the default value if no item is found.
 268    /// </summary>
 269    /// <typeparam name="TObject">The type of the object stored in the item.</typeparam>
 270    /// <param name="type">The type of the item.</param>
 271    /// <returns>The first item of the specified type or the default value if no item is found.</returns>
 272    public TObject? FirstOrDefault<TObject>(int type)
 273    {
 274        if (IsEmpty) return default;
 275
 276        foreach (var item in _items)
 277        {
 278            if (item.Type == type && item.Reference is TObject reference)
 279            {
 280                return reference;
 281            }
 282        }
 283        return default;
 284    }
 285
 286    /// <summary>
 287    /// Gets the first item of the specified type.
 288    /// </summary>
 289    /// <typeparam name="TObject">The type of the object stored in the item.</typeparam>
 290    /// <param name="type">The type of the item.</param>
 291    /// <returns>The first item of the specified type.</returns>
 292    /// <exception cref="InvalidOperationException">Thrown if no matching item is found.</exception>
 293    public TObject First<TObject>(int type)
 294    {
 295        if (IsEmpty) throw new InvalidOperationException("Specification contains no items");
 296
 297        foreach (var item in _items)
 298        {
 299            if (item.Type == type && item.Reference is TObject reference)
 300            {
 301                return reference;
 302            }
 303        }
 304        throw new InvalidOperationException("Specification contains no matching item");
 305    }
 306
 307    /// <summary>
 308    /// Gets all items of the specified type.
 309    /// </summary>
 310    /// <typeparam name="TObject">The type of the object stored in the item.</typeparam>
 311    /// <param name="type">The type of the items.</param>
 312    /// <returns>An enumerable of items of the specified type.</returns>
 313    public IEnumerable<TObject> OfType<TObject>(int type) => _items is null
 314        ? Enumerable.Empty<TObject>()
 315        : new SpecIterator<TObject>(_items, type);
 316
 317    internal ReadOnlySpan<SpecItem> Items => _items ?? ReadOnlySpan<SpecItem>.Empty;
 318
 319    internal bool Contains(int type)
 320    {
 321        if (IsEmpty) return false;
 322
 323        foreach (var item in _items)
 324        {
 325            if (item.Type == type)
 326            {
 327                return true;
 328            }
 329        }
 330        return false;
 331    }
 332
 333    [MemberNotNull(nameof(_items))]
 334    internal void AddInternal(int type, object value, int bag = 0)
 335    {
 336        var newItem = new SpecItem
 337        {
 338            Type = type,
 339            Reference = value,
 340            Bag = bag
 341        };
 342
 343        if (IsEmpty)
 344        {
 345            // Specs with two items are very common, we'll optimize for that.
 346            _items = new SpecItem[2];
 347            _items[0] = newItem;
 348        }
 349        else
 350        {
 351            var items = _items;
 352
 353            // We have a special case for Paging, we're storing it in the same item with Flags.
 354            if (type == ItemType.Paging)
 355            {
 356                for (var i = 0; i < items.Length; i++)
 357                {
 358                    if (items[i].Type == ItemType.Paging)
 359                    {
 360                        _items[i].Reference = newItem.Reference;
 361                        return;
 362                    }
 363                }
 364            }
 365
 366            for (var i = 0; i < items.Length; i++)
 367            {
 368                if (items[i].Type == 0)
 369                {
 370                    items[i] = newItem;
 371                    return;
 372                }
 373            }
 374
 375            var originalLength = items.Length;
 376            var newArray = new SpecItem[originalLength + 4];
 377            Array.Copy(items, newArray, originalLength);
 378            newArray[originalLength] = newItem;
 379            _items = newArray;
 380        }
 381    }
 382    internal void AddOrUpdateInternal(int type, object value, int bag = 0)
 383    {
 384        if (IsEmpty)
 385        {
 386            AddInternal(type, value, bag);
 387            return;
 388        }
 389        var items = _items;
 390        for (var i = 0; i < items.Length; i++)
 391        {
 392            if (items[i].Type == type)
 393            {
 394                _items[i].Reference = value;
 395                _items[i].Bag = bag;
 396                return;
 397            }
 398        }
 399        AddInternal(type, value, bag);
 400    }
 401    internal SpecItem[] GetCompiledItems()
 402    {
 403        if (IsEmpty) return Array.Empty<SpecItem>();
 404
 405        var compilableItemsCount = CountCompilableItems(_items);
 406        if (compilableItemsCount == 0) return Array.Empty<SpecItem>();
 407
 408        var compiledItems = GetCompiledItems(_items);
 409
 410        // If the count of compilable items is equal to the count of compiled items, we don't need to recompile.
 411        if (compiledItems.Length == compilableItemsCount) return compiledItems;
 412
 413        compiledItems = GenerateCompiledItems(_items, compilableItemsCount);
 414        AddOrUpdateInternal(ItemType.Compiled, compiledItems);
 415        return compiledItems;
 416
 417        static SpecItem[] GetCompiledItems(SpecItem[] items)
 418        {
 419            foreach (var item in items)
 420            {
 421                if (item.Type == ItemType.Compiled && item.Reference is SpecItem[] compiledItems)
 422                {
 423                    return compiledItems;
 424                }
 425            }
 426            return Array.Empty<SpecItem>();
 427        }
 428
 429        static int CountCompilableItems(SpecItem[] items)
 430        {
 431            var count = 0;
 432            foreach (var item in items)
 433            {
 434                if (item.Type == ItemType.Where || item.Type == ItemType.Like || item.Type == ItemType.Order)
 435                    count++;
 436            }
 437            return count;
 438        }
 439
 440        static SpecItem[] GenerateCompiledItems(SpecItem[] items, int count)
 441        {
 442            var compiledItems = new SpecItem[count];
 443
 444            // We want to place the items contiguously by type. Sorting is more expensive than looping per type.
 445            var index = 0;
 446            foreach (var item in items)
 447            {
 448                if (item.Type == ItemType.Where && item.Reference is Expression<Func<T, bool>> expr)
 449                {
 450                    compiledItems[index++] = new SpecItem
 451                    {
 452                        Type = item.Type,
 453                        Reference = expr.Compile(),
 454                        Bag = item.Bag
 455                    };
 456                }
 457            }
 458            if (index == count) return compiledItems;
 459
 460            foreach (var item in items)
 461            {
 462                if (item.Type == ItemType.Order && item.Reference is Expression<Func<T, object?>> expr)
 463                {
 464                    compiledItems[index++] = new SpecItem
 465                    {
 466                        Type = item.Type,
 467                        Reference = expr.Compile(),
 468                        Bag = item.Bag
 469                    };
 470                }
 471            }
 472            if (index == count) return compiledItems;
 473
 474            var likeStartIndex = index;
 475            foreach (var item in items)
 476            {
 477                if (item.Type == ItemType.Like && item.Reference is SpecLike<T> like)
 478                {
 479                    compiledItems[index++] = new SpecItem
 480                    {
 481                        Type = item.Type,
 482                        Reference = new SpecLikeCompiled<T>(like.KeySelector.Compile(), like.Pattern),
 483                        Bag = item.Bag
 484                    };
 485                }
 486            }
 487
 488            // Sort Like items by the group, so we do it only once and not repeatedly in the Like evaluator).
 489            compiledItems.AsSpan()[likeStartIndex..count].Sort((x, y) => x.Bag.CompareTo(y.Bag));
 490
 491            return compiledItems;
 492        }
 493    }
 494
 495    internal TObject GetOrCreate<TObject>(int type) where TObject : new()
 496    {
 497        return FirstOrDefault<TObject>(type) ?? Create();
 498        TObject Create()
 499        {
 500            var reference = new TObject();
 501            AddInternal(type, reference);
 502            return reference;
 503        }
 504    }
 505    internal bool GetFlag(SpecFlags flag)
 506    {
 507        if (IsEmpty) return false;
 508
 509        foreach (var item in _items)
 510        {
 511            if (item.Type == ItemType.Flags)
 512            {
 513                return ((SpecFlags)item.Bag & flag) == flag;
 514            }
 515        }
 516        return false;
 517    }
 518    internal void AddOrUpdateFlag(SpecFlags flag, bool value)
 519    {
 520        if (IsEmpty)
 521        {
 522            if (value)
 523            {
 524                AddInternal(ItemType.Flags, null!, (int)flag);
 525            }
 526            return;
 527        }
 528
 529        var items = _items;
 530        for (var i = 0; i < items.Length; i++)
 531        {
 532            if (items[i].Type == ItemType.Flags)
 533            {
 534                var newValue = value
 535                    ? (SpecFlags)items[i].Bag | flag
 536                    : (SpecFlags)items[i].Bag & ~flag;
 537
 538                _items[i].Bag = (int)newValue;
 539                return;
 540            }
 541        }
 542
 543        if (value)
 544        {
 545            AddInternal(ItemType.Flags, null!, (int)flag);
 546        }
 547    }
 548
 549    internal Specification<T> Clone()
 550    {
 551        if (IsEmpty) return new Specification<T>();
 552
 553        var items = _items;
 554        var newItems = new SpecItem[items.Length];
 555        Array.Copy(items, newItems, items.Length);
 556        return new Specification<T> { _items = newItems };
 557    }
 558
 559    internal Specification<T, TResult> Clone<TResult>()
 560    {
 561        if (IsEmpty) return new Specification<T, TResult>();
 562
 563        var items = _items;
 564        var newItems = new SpecItem[items.Length];
 565        Array.Copy(items, newItems, items.Length);
 566        return new Specification<T, TResult> { _items = newItems };
 567    }
 568}