< Summary

Information
Class: Pozitron.QuerySpecification.Specification<T1, T2>
Assembly: Pozitron.QuerySpecification
File(s): /home/runner/work/QuerySpecification/QuerySpecification/src/QuerySpecification/Specification.cs
Tag: 52_11740816328
Line coverage
100%
Covered lines: 7
Uncovered lines: 0
Coverable lines: 7
Total lines: 512
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 Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
.ctor(...)100%11100%
Evaluate(...)100%11100%
get_Query()100%11100%
get_Selector()100%11100%
get_SelectorMany()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>
 20815    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>
 8738    public new ISpecificationBuilder<T, TResult> Query => new SpecificationBuilder<T, TResult>(this);
 39
 40    /// <summary>
 41    /// Gets the Select expression.
 42    /// </summary>
 3643    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 SpecificationInMemoryEvaluator Evaluator => SpecificationInMemoryEvaluator.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 number of items to take.
 181    /// </summary>
 182    public int Take => FirstOrDefault<SpecPaging>(ItemType.Paging)?.Take ?? -1;
 183
 184    /// <summary>
 185    /// Gets the number of items to skip.
 186    /// </summary>
 187    public int Skip => FirstOrDefault<SpecPaging>(ItemType.Paging)?.Skip ?? -1;
 188
 189    /// <summary>
 190    /// Gets a value indicating whether IgnoreQueryFilters is applied.
 191    /// </summary>
 192    public bool IgnoreQueryFilters => GetFlag(SpecFlags.IgnoreQueryFilters);
 193
 194    /// <summary>
 195    /// Gets a value indicating whether AsSplitQuery is applied.
 196    /// </summary>
 197    public bool AsSplitQuery => GetFlag(SpecFlags.AsSplitQuery);
 198
 199    /// <summary>
 200    /// Gets a value indicating whether AsNoTracking is applied.
 201    /// </summary>
 202    public bool AsNoTracking => GetFlag(SpecFlags.AsNoTracking);
 203
 204    /// <summary>
 205    /// Gets a value indicating whether AsNoTrackingWithIdentityResolution is applied.
 206    /// </summary>
 207    public bool AsNoTrackingWithIdentityResolution => GetFlag(SpecFlags.AsNoTrackingWithIdentityResolution);
 208
 209    /// <summary>
 210    /// Gets a value indicating whether AsTracking is applied.
 211    /// </summary>
 212    public bool AsTracking => GetFlag(SpecFlags.AsTracking);
 213
 214    /// <summary>
 215    /// Adds an item to the specification.
 216    /// </summary>
 217    /// <param name="type">The type of the item. It must be a positive number.</param>
 218    /// <param name="value">The object to be stored in the item.</param>
 219    /// <exception cref="ArgumentNullException">If value is null</exception>
 220    /// <exception cref="ArgumentOutOfRangeException">If type is zero or negative.</exception>
 221    public void Add(int type, object value)
 222    {
 223        ArgumentNullException.ThrowIfNull(value);
 224        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(type);
 225
 226        AddInternal(type, value);
 227    }
 228
 229    /// <summary>
 230    /// Adds or updates an item in the specification.
 231    /// </summary>
 232    /// <param name="type">The type of the item.</param>
 233    /// <param name="value">The object to be stored in the item.</param>
 234    /// <exception cref="ArgumentNullException">Thrown if value is null</exception>
 235    /// <exception cref="ArgumentOutOfRangeException">Thrown if type is zero or negative.</exception>
 236    public void AddOrUpdate(int type, object value)
 237    {
 238        ArgumentNullException.ThrowIfNull(value);
 239        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(type);
 240
 241        AddOrUpdateInternal(type, value);
 242    }
 243
 244    /// <summary>
 245    /// Gets the first item of the specified type or the default value if no item is found.
 246    /// </summary>
 247    /// <typeparam name="TObject">The type of the object stored in the item.</typeparam>
 248    /// <param name="type">The type of the item.</param>
 249    /// <returns>The first item of the specified type or the default value if no item is found.</returns>
 250    public TObject? FirstOrDefault<TObject>(int type)
 251    {
 252        if (IsEmpty) return default;
 253
 254        foreach (var item in _items)
 255        {
 256            if (item.Type == type && item.Reference is TObject reference)
 257            {
 258                return reference;
 259            }
 260        }
 261        return default;
 262    }
 263
 264    /// <summary>
 265    /// Gets the first item of the specified type.
 266    /// </summary>
 267    /// <typeparam name="TObject">The type of the object stored in the item.</typeparam>
 268    /// <param name="type">The type of the item.</param>
 269    /// <returns>The first item of the specified type.</returns>
 270    /// <exception cref="InvalidOperationException">Thrown if no matching item is found.</exception>
 271    public TObject First<TObject>(int type)
 272    {
 273        if (IsEmpty) throw new InvalidOperationException("Specification contains no items");
 274
 275        foreach (var item in _items)
 276        {
 277            if (item.Type == type && item.Reference is TObject reference)
 278            {
 279                return reference;
 280            }
 281        }
 282        throw new InvalidOperationException("Specification contains no matching item");
 283    }
 284
 285    /// <summary>
 286    /// Gets all items of the specified type.
 287    /// </summary>
 288    /// <typeparam name="TObject">The type of the object stored in the item.</typeparam>
 289    /// <param name="type">The type of the items.</param>
 290    /// <returns>An enumerable of items of the specified type.</returns>
 291    public IEnumerable<TObject> OfType<TObject>(int type) => _items is null
 292        ? Enumerable.Empty<TObject>()
 293        : new SpecIterator<TObject>(_items, type);
 294
 295    internal ReadOnlySpan<SpecItem> Items => _items ?? ReadOnlySpan<SpecItem>.Empty;
 296
 297    [MemberNotNull(nameof(_items))]
 298    internal void AddInternal(int type, object value, int bag = 0)
 299    {
 300        var newItem = new SpecItem
 301        {
 302            Type = type,
 303            Reference = value,
 304            Bag = bag
 305        };
 306
 307        if (IsEmpty)
 308        {
 309            // Specs with two items are very common, we'll optimize for that.
 310            _items = new SpecItem[2];
 311            _items[0] = newItem;
 312        }
 313        else
 314        {
 315            var items = _items;
 316
 317            // We have a special case for Paging, we're storing it in the same item with Flags.
 318            if (type == ItemType.Paging)
 319            {
 320                for (var i = 0; i < items.Length; i++)
 321                {
 322                    if (items[i].Type == ItemType.Paging)
 323                    {
 324                        _items[i].Reference = newItem.Reference;
 325                        return;
 326                    }
 327                }
 328            }
 329
 330            for (var i = 0; i < items.Length; i++)
 331            {
 332                if (items[i].Type == 0)
 333                {
 334                    items[i] = newItem;
 335                    return;
 336                }
 337            }
 338
 339            var originalLength = items.Length;
 340            var newArray = new SpecItem[originalLength + 4];
 341            Array.Copy(items, newArray, originalLength);
 342            newArray[originalLength] = newItem;
 343            _items = newArray;
 344        }
 345    }
 346    internal void AddOrUpdateInternal(int type, object value, int bag = 0)
 347    {
 348        if (IsEmpty)
 349        {
 350            AddInternal(type, value, bag);
 351            return;
 352        }
 353        var items = _items;
 354        for (var i = 0; i < items.Length; i++)
 355        {
 356            if (items[i].Type == type)
 357            {
 358                _items[i].Reference = value;
 359                _items[i].Bag = bag;
 360                return;
 361            }
 362        }
 363        AddInternal(type, value, bag);
 364    }
 365    internal SpecItem[] GetCompiledItems()
 366    {
 367        if (IsEmpty) return Array.Empty<SpecItem>();
 368
 369        var compilableItemsCount = CountCompilableItems(_items);
 370        if (compilableItemsCount == 0) return Array.Empty<SpecItem>();
 371
 372        var compiledItems = GetCompiledItems(_items);
 373
 374        // If the count of compilable items is equal to the count of compiled items, we don't need to recompile.
 375        if (compiledItems.Length == compilableItemsCount) return compiledItems;
 376
 377        compiledItems = GenerateCompiledItems(_items, compilableItemsCount);
 378        AddOrUpdateInternal(ItemType.Compiled, compiledItems);
 379        return compiledItems;
 380
 381        static SpecItem[] GetCompiledItems(SpecItem[] items)
 382        {
 383            foreach (var item in items)
 384            {
 385                if (item.Type == ItemType.Compiled && item.Reference is SpecItem[] compiledItems)
 386                {
 387                    return compiledItems;
 388                }
 389            }
 390            return Array.Empty<SpecItem>();
 391        }
 392
 393        static int CountCompilableItems(SpecItem[] items)
 394        {
 395            var count = 0;
 396            foreach (var item in items)
 397            {
 398                if (item.Type == ItemType.Where || item.Type == ItemType.Like || item.Type == ItemType.Order)
 399                    count++;
 400            }
 401            return count;
 402        }
 403
 404        static SpecItem[] GenerateCompiledItems(SpecItem[] items, int count)
 405        {
 406            var compiledItems = new SpecItem[count];
 407
 408            // We want to place the items contiguously by type. Sorting is more expensive than looping per type.
 409            var index = 0;
 410            foreach (var item in items)
 411            {
 412                if (item.Type == ItemType.Where && item.Reference is Expression<Func<T, bool>> expr)
 413                {
 414                    compiledItems[index++] = new SpecItem
 415                    {
 416                        Type = item.Type,
 417                        Reference = expr.Compile(),
 418                        Bag = item.Bag
 419                    };
 420                }
 421            }
 422            if (index == count) return compiledItems;
 423
 424            foreach (var item in items)
 425            {
 426                if (item.Type == ItemType.Order && item.Reference is Expression<Func<T, object?>> expr)
 427                {
 428                    compiledItems[index++] = new SpecItem
 429                    {
 430                        Type = item.Type,
 431                        Reference = expr.Compile(),
 432                        Bag = item.Bag
 433                    };
 434                }
 435            }
 436            if (index == count) return compiledItems;
 437
 438            var likeStartIndex = index;
 439            foreach (var item in items)
 440            {
 441                if (item.Type == ItemType.Like && item.Reference is SpecLike<T> like)
 442                {
 443                    compiledItems[index++] = new SpecItem
 444                    {
 445                        Type = item.Type,
 446                        Reference = new SpecLikeCompiled<T>(like.KeySelector.Compile(), like.Pattern),
 447                        Bag = item.Bag
 448                    };
 449                }
 450            }
 451
 452            // Sort Like items by the group, so we do it only once and not repeatedly in the Like evaluator).
 453            compiledItems.AsSpan()[likeStartIndex..count].Sort((x, y) => x.Bag.CompareTo(y.Bag));
 454
 455            return compiledItems;
 456        }
 457    }
 458
 459    internal TObject GetOrCreate<TObject>(int type) where TObject : new()
 460    {
 461        return FirstOrDefault<TObject>(type) ?? Create();
 462        TObject Create()
 463        {
 464            var reference = new TObject();
 465            AddInternal(type, reference);
 466            return reference;
 467        }
 468    }
 469    internal bool GetFlag(SpecFlags flag)
 470    {
 471        if (IsEmpty) return false;
 472
 473        foreach (var item in _items)
 474        {
 475            if (item.Type == ItemType.Flags)
 476            {
 477                return ((SpecFlags)item.Bag & flag) == flag;
 478            }
 479        }
 480        return false;
 481    }
 482    internal void AddOrUpdateFlag(SpecFlags flag, bool value)
 483    {
 484        if (IsEmpty)
 485        {
 486            if (value)
 487            {
 488                AddInternal(ItemType.Flags, null!, (int)flag);
 489            }
 490            return;
 491        }
 492
 493        var items = _items;
 494        for (var i = 0; i < items.Length; i++)
 495        {
 496            if (items[i].Type == ItemType.Flags)
 497            {
 498                var newValue = value
 499                    ? (SpecFlags)items[i].Bag | flag
 500                    : (SpecFlags)items[i].Bag & ~flag;
 501
 502                _items[i].Bag = (int)newValue;
 503                return;
 504            }
 505        }
 506
 507        if (value)
 508        {
 509            AddInternal(ItemType.Flags, null!, (int)flag);
 510        }
 511    }
 512}