File: Internal\TagSet.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.Caching.Hybrid\Microsoft.Extensions.Caching.Hybrid.csproj (Microsoft.Extensions.Caching.Hybrid)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace Microsoft.Extensions.Caching.Hybrid.Internal;
 
/// <summary>
/// Represents zero (null), one (string) or more (string[]) tags, avoiding the additional array overhead when necessary.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1066:Implement IEquatable when overriding Object.Equals", Justification = "Equals throws by intent")]
[System.Diagnostics.DebuggerDisplay("{ToString()}")]
internal readonly struct TagSet
{
    public static readonly TagSet Empty = default!;
 
    // this array is used in CopyTo to efficiently copy out of collections
    [ThreadStatic]
    private static string[]? _perThreadSingleLengthArray;
 
    private readonly object? _tagOrTags;
 
    internal TagSet(string tag)
    {
        Validate(tag);
        _tagOrTags = tag;
    }
 
    internal TagSet(string[] tags)
    {
        Debug.Assert(tags is { Length: > 1 }, "should be non-trivial array");
        foreach (var tag in tags)
        {
            Validate(tag);
        }
 
        Array.Sort(tags, StringComparer.InvariantCulture);
        _tagOrTags = tags;
    }
 
    public string GetSinglePrechecked() => (string)_tagOrTags!; // we expect this to fail if used on incorrect types
    public Span<string> GetSpanPrechecked() => (string[])_tagOrTags!; // we expect this to fail if used on incorrect types
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Intentional; should not be used")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Blocker Code Smell", "S3877:Exceptions should not be thrown from unexpected methods", Justification = "Intentional; should not be used")]
    public override bool Equals(object? obj) => throw new NotSupportedException();
 
    // [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = "Intentional; should not be used")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Blocker Code Smell", "S3877:Exceptions should not be thrown from unexpected methods", Justification = "Intentional; should not be used")]
    public override int GetHashCode() => throw new NotSupportedException();
 
    public override string ToString() => _tagOrTags switch
    {
        string tag => tag,
        string[] tags => string.Join(", ", tags),
        _ => "(no tags)",
    };
 
    public bool IsEmpty => _tagOrTags is null;
 
    public int Count => _tagOrTags switch
    {
        null => 0,
        string => 1,
        string[] arr => arr.Length,
        _ => 0, // should never happen, but treat as empty
    };
 
    internal bool IsArray => _tagOrTags is string[];
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "This is the most appropriate exception here.")]
    public string this[int index] => _tagOrTags switch
    {
        string tag when index == 0 => tag,
        string[] tags => tags[index],
        _ => throw new IndexOutOfRangeException(nameof(index)),
    };
 
    public void CopyTo(Span<string> target)
    {
        switch (_tagOrTags)
        {
            case string tag:
                target[0] = tag;
                break;
            case string[] tags:
                tags.CopyTo(target);
                break;
        }
    }
 
    internal static TagSet Create(IEnumerable<string>? tags)
    {
        if (tags is null)
        {
            return Empty;
        }
 
        // note that in multi-tag scenarios we always create a defensive copy
        if (tags is ICollection<string> collection)
        {
            switch (collection.Count)
            {
                case 0:
                    return Empty;
                case 1 when collection is IList<string> list:
                    return new TagSet(list[0]);
                case 1:
                    // avoid the GetEnumerator() alloc
                    var arr = _perThreadSingleLengthArray ??= new string[1];
                    collection.CopyTo(arr, 0);
                    return new TagSet(arr[0]);
                default:
                    arr = new string[collection.Count];
                    collection.CopyTo(arr, 0);
                    return new TagSet(arr);
            }
        }
 
        // perhaps overkill, but: avoid as much as possible when unrolling
        using var iterator = tags.GetEnumerator();
        if (!iterator.MoveNext())
        {
            return Empty;
        }
 
        var firstTag = iterator.Current;
        if (!iterator.MoveNext())
        {
            return new TagSet(firstTag);
        }
 
        string[] oversized = ArrayPool<string>.Shared.Rent(8);
        oversized[0] = firstTag;
        int count = 1;
        do
        {
            if (count == oversized.Length)
            {
                // grow
                var bigger = ArrayPool<string>.Shared.Rent(count * 2);
                oversized.CopyTo(bigger, 0);
                ArrayPool<string>.Shared.Return(oversized);
                oversized = bigger;
            }
 
            oversized[count++] = iterator.Current;
        }
        while (iterator.MoveNext());
 
        if (count == oversized.Length)
        {
            return new TagSet(oversized);
        }
        else
        {
            var final = oversized.AsSpan(0, count).ToArray();
            ArrayPool<string>.Shared.Return(oversized);
            return new TagSet(final);
        }
    }
 
    internal string[] ToArray() // for testing only
    {
        var arr = new string[Count];
        CopyTo(arr);
        return arr;
    }
 
    internal const string WildcardTag = "*";
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1122:Use string.Empty for empty strings", Justification = "Not needed")]
    internal bool TryFind(ReadOnlySpan<char> span, [NotNullWhen(true)] out string? tag)
    {
        switch (_tagOrTags)
        {
            case string single when span.SequenceEqual(single.AsSpan()):
                tag = single;
                return true;
            case string[] tags:
                foreach (string test in tags)
                {
                    if (span.SequenceEqual(test.AsSpan()))
                    {
                        tag = test;
                        return true;
                    }
                }
 
                break;
        }
 
        tag = null;
        return false;
    }
 
    internal int MaxLength()
    {
        switch (_tagOrTags)
        {
            case string single:
                return single.Length;
            case string[] tags:
                int max = 0;
                foreach (string test in tags)
                {
                    max = Math.Max(max, test.Length);
                }
 
                return max;
            default:
                return 0;
        }
    }
 
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3928:Parameter names used into ArgumentException constructors should match an existing one ",
        Justification = "Using parameter name from public callable API")]
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "Using parameter name from public callable API")]
    private static void Validate(string tag)
    {
        if (string.IsNullOrWhiteSpace(tag))
        {
            ThrowEmpty();
        }
 
        if (tag == WildcardTag)
        {
            ThrowReserved();
        }
 
        static void ThrowEmpty() => throw new ArgumentException("Tags cannot be empty.", "tags");
        static void ThrowReserved() => throw new ArgumentException($"The tag '{WildcardTag}' is reserved and cannot be used in this context.", "tags");
    }
}