< Summary

Information
Class: AmbientServices.AmbientAtomicSplitCacheExtensions<THead, TPayload>
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Services/AtomicCache.MonotonicSplit.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 20
Uncovered lines: 0
Coverable lines: 20
Total lines: 165
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Services/AtomicCache.MonotonicSplit.cs

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.Threading;
 4using System.Threading.Tasks;
 5
 6namespace AmbientServices;
 7
 8/// <summary>
 9/// Extension-based monotonic split-cache API for <see cref="IAmbientAtomicCache"/> on all target frameworks
 10/// (versioned head + unversioned payload per revision). Implemented as extensions so netstandard2.0 and similar
 11/// targets get the same API as modern runtimes without relying on default interface members.
 12/// </summary>
 13/// <remarks>
 14/// <pitch>
 15/// The monotonic split-cache pattern packaged as extension methods: publish a tiny versioned head plus a separately-key
 16/// </pitch>
 17/// <pledge>
 18/// Head and payload keys are derived deterministically from a base logical key using a reserved separator character (<s
 19/// The head travels through the versioned operation family and the payload through the unversioned family keyed by the 
 20/// A combined read resolves the head first and touches the payload only when the head is present, unexpired, and at lea
 21/// These methods add no storage or synchronization of their own — every behavioral guarantee is inherited from the unde
 22/// Because the head and each revision's payload are separate entries with independent expirations and eviction, a paylo
 23/// </pledge>
 24/// <plan>
 25/// Pure key composition and delegation: keys are concatenated from the base key, <see cref="MonotonicSplitCacheKeySepar
 26/// No state, no I/O, no locking — cost and durability are exactly those of the underlying cache, plus one extra cache r
 27/// </plan>
 28/// <priority>
 29/// 1. Cheap staleness checks over a single round trip: a combined read resolves the small head first and touches the la
 30/// 2. Adding nothing of its own over doing more: these methods compose keys and delegate — no storage, no synchronizati
 31/// </priority>
 32/// </remarks>
 33public static class AmbientAtomicSplitCacheExtensions
 34{
 35    /// <summary>
 36    /// Separator (ASCII unit separator) reserved for split-cache key composition. It must not appear in <c>baseLogicalK
 37    /// </summary>
 38    public const char MonotonicSplitCacheKeySeparator = '\u001f';
 39
 40    /// <summary>
 41    /// Logical key passed to <see cref="IAmbientAtomicCache.VersionedGet{T}"/> / <see cref="IAmbientAtomicCache.Version
 42    /// </summary>
 43    public static string GetMonotonicSplitCacheHeadKey(string baseLogicalKey)
 44    {
 45#if NET5_0_OR_GREATER
 246        ArgumentNullException.ThrowIfNull(baseLogicalKey);
 47#else
 48        if (baseLogicalKey is null) throw new ArgumentNullException(nameof(baseLogicalKey));
 49#endif
 250        return baseLogicalKey + MonotonicSplitCacheKeySeparator + "ambient.split.head" + MonotonicSplitCacheKeySeparator
 51    }
 52
 53    /// <summary>
 54    /// Logical key passed to <see cref="IAmbientAtomicCache.GetOrAdd{T}"/> / <see cref="IAmbientAtomicCache.Remove{T}"/
 55    /// </summary>
 56    public static string GetMonotonicSplitCachePayloadKey(string baseLogicalKey, long headVersion)
 57    {
 58#if NET5_0_OR_GREATER
 259        ArgumentNullException.ThrowIfNull(baseLogicalKey);
 60#else
 61        if (baseLogicalKey is null) throw new ArgumentNullException(nameof(baseLogicalKey));
 62#endif
 263        return baseLogicalKey + MonotonicSplitCacheKeySeparator + "ambient.split.payload" + MonotonicSplitCacheKeySepara
 64    }
 65
 66    /// <summary>
 67    /// Reads the split-cache versioned head only (step one of a two-step read).
 68    /// </summary>
 69    public static ValueTask<(THead? Head, long HeadVersion)> MonotonicSplitCacheGetHeadAsync<THead>(
 70        this IAmbientAtomicCache cache,
 71        string baseLogicalKey,
 72        long minHeadVersion = -1,
 73        TimeSpan? headRefresh = null,
 74        TimeSpan? headTimeout = null,
 75        CancellationToken cancel = default)
 76        where THead : class
 77    {
 78#if NET5_0_OR_GREATER
 279        ArgumentNullException.ThrowIfNull(cache);
 80#else
 81        if (cache is null) throw new ArgumentNullException(nameof(cache));
 82#endif
 283        return cache.VersionedGet<THead>(GetMonotonicSplitCacheHeadKey(baseLogicalKey), minHeadVersion, headRefresh, hea
 84    }
 85
 86    /// <summary>
 87    /// Writes a new split-cache head revision (step one of a publish). Returns the new monotonic version to use for <se
 88    /// </summary>
 89    public static ValueTask<long> MonotonicSplitCachePutHeadAsync<THead>(
 90        this IAmbientAtomicCache cache,
 91        string baseLogicalKey,
 92        THead head,
 93        TimeSpan? maxCacheDuration = null,
 94        DateTime? expiration = null,
 95        TimeSpan? timeout = null,
 96        CancellationToken cancel = default)
 97        where THead : class
 98    {
 99#if NET5_0_OR_GREATER
 2100        ArgumentNullException.ThrowIfNull(cache);
 2101        ArgumentNullException.ThrowIfNull(head);
 102#else
 103        if (cache is null) throw new ArgumentNullException(nameof(cache));
 104        if (head is null) throw new ArgumentNullException(nameof(head));
 105#endif
 2106        return cache.VersionedPut(GetMonotonicSplitCacheHeadKey(baseLogicalKey), head, maxCacheDuration, expiration, tim
 107    }
 108
 109    /// <summary>
 110    /// Resolves or creates the split-cache payload for a known <paramref name="headVersion"/> (step two of a read/write
 111    /// </summary>
 112    public static ValueTask<TPayload> MonotonicSplitCacheGetOrAddPayloadAsync<TPayload>(
 113        this IAmbientAtomicCache cache,
 114        string baseLogicalKey,
 115        long headVersion,
 116        Func<ValueTask<(TPayload Item, DateTime? Expires)>> create,
 117        TimeSpan? payloadRefresh = null,
 118        TimeSpan? payloadTimeout = null,
 119        CancellationToken cancel = default)
 120        where TPayload : class
 121    {
 122#if NET5_0_OR_GREATER
 2123        ArgumentNullException.ThrowIfNull(cache);
 2124        ArgumentNullException.ThrowIfNull(create);
 125#else
 126        if (cache is null) throw new ArgumentNullException(nameof(cache));
 127        if (create is null) throw new ArgumentNullException(nameof(create));
 128#endif
 2129        return cache.GetOrAdd(GetMonotonicSplitCachePayloadKey(baseLogicalKey, headVersion), create, payloadRefresh, pay
 130    }
 131
 132    /// <summary>
 133    /// Two-step read: versioned head (staleness via <paramref name="minHeadVersion"/>), then unversioned payload for th
 134    /// When the head is missing, expired, or rejected as too old for <paramref name="minHeadVersion"/>, the payload is 
 135    /// </summary>
 136    public static async ValueTask<(THead? Head, TPayload? Payload, long HeadVersion)> MonotonicSplitCacheGetHeadAndPaylo
 137        this IAmbientAtomicCache cache,
 138        string baseLogicalKey,
 139        long minHeadVersion,
 140        Func<ValueTask<(TPayload Item, DateTime? Expires)>> getOrCreatePayload,
 141        TimeSpan? headRefresh = null,
 142        TimeSpan? headTimeout = null,
 143        TimeSpan? payloadRefresh = null,
 144        TimeSpan? payloadTimeout = null,
 145        CancellationToken cancel = default)
 146        where THead : class
 147        where TPayload : class
 148    {
 149#if NET5_0_OR_GREATER
 2150        ArgumentNullException.ThrowIfNull(cache);
 2151        ArgumentNullException.ThrowIfNull(getOrCreatePayload);
 152#else
 153        if (cache is null) throw new ArgumentNullException(nameof(cache));
 154        if (getOrCreatePayload is null) throw new ArgumentNullException(nameof(getOrCreatePayload));
 155#endif
 2156        (THead? head, long v) = await cache.MonotonicSplitCacheGetHeadAsync<THead>(baseLogicalKey, minHeadVersion, headR
 2157        if (head is null)
 158        {
 2159            return (null, null, v);
 160        }
 161
 2162        TPayload payload = await cache.MonotonicSplitCacheGetOrAddPayloadAsync(baseLogicalKey, v, getOrCreatePayload, pa
 2163        return (head, payload, v);
 2164    }
 165}