File: Hybrid\HybridCacheEntryContext.cs
Web Access
Project: src\runtime\src\libraries\Microsoft.Extensions.Caching.Abstractions\src\Microsoft.Extensions.Caching.Abstractions.csproj (Microsoft.Extensions.Caching.Abstractions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Provides a mutable view of the cache entry options in effect for a single <see cref="HybridCache"/> operation,
/// supplied to a factory callback so that it can adjust those options based on the result of the data fetch.
/// </summary>
/// <remarks>
/// When a <see cref="HybridCache"/> implementation constructs the context for a factory callback, it seeds the
/// properties from the effective options for the operation (composed from the per-call and any global options), so
/// the factory observes the current values and can change them to influence how the resulting value is cached.
/// A context constructed directly reflects only whatever <see cref="HybridCacheEntryOptions"/> were supplied to
/// its constructor. Implementations can use <see cref="Revision"/> to detect whether the factory changed any value.
/// </remarks>
public sealed class HybridCacheEntryContext
{
    /// <summary>
    /// Initializes a new instance of the <see cref="HybridCacheEntryContext"/> class, seeding its values from
    /// the supplied options.
    /// </summary>
    /// <param name="options">
    /// The options to seed the context from, or <see langword="null"/> to start with unset values. This is
    /// typically the effective options for the operation, so that the factory callback observes the current values.
    /// </param>
    /// <remarks>
    /// This is primarily intended for <see cref="HybridCache"/> implementations that need to construct a context
    /// to pass to a factory callback.
    /// </remarks>
    public HybridCacheEntryContext(HybridCacheEntryOptions? options)
    {
        if (options is not null)
        {
            Expiration = options.Expiration;
            LocalCacheExpiration = options.LocalCacheExpiration;
            Flags = options.Flags;
            LocalSize = options.LocalSize;
        }

        // Seeding from the supplied options is not a caller change, so the context starts at revision zero.
        Revision = 0;
    }

    /// <summary>
    /// Gets or sets the overall cache duration of this entry, passed to the backend distributed cache.
    /// </summary>
    public TimeSpan? Expiration
    {
        get;
        set
        {
            if (field != value)
            {
                field = value;
                BumpRevision();
            }
        }
    }

    /// <summary>
    /// Gets or sets the expiration for the local (in-process) cache entry.
    /// </summary>
    /// <remarks>
    /// When retrieving a cached value from an external cache store, this value will be used to calculate the local
    /// cache expiration, not exceeding the remaining overall cache lifetime.
    /// </remarks>
    public TimeSpan? LocalCacheExpiration
    {
        get;
        set
        {
            if (field != value)
            {
                field = value;
                BumpRevision();
            }
        }
    }

    /// <summary>
    /// Gets or sets additional flags that apply to the requested operation.
    /// </summary>
    public HybridCacheEntryFlags? Flags
    {
        get;
        set
        {
            if (field != value)
            {
                field = value;
                BumpRevision();
            }
        }
    }

    /// <summary>
    /// Gets or sets the size to assign to entries in the local (in-process) cache.
    /// </summary>
    /// <remarks>
    /// The units are determined by the underlying local cache implementation. When the local cache
    /// is an <see cref="Memory.IMemoryCache"/> configured with a size limit, this value corresponds to
    /// <see cref="Memory.ICacheEntry.Size"/>.
    /// <para>
    /// When <see langword="null"/>, the implementation may compute a default size (for example, from
    /// the serialized payload length).
    /// </para>
    /// </remarks>
    public long? LocalSize
    {
        get;
        set
        {
            if (field != value)
            {
                field = value;
                BumpRevision();
            }
        }
    }

    /// <summary>
    /// Gets a value that increments whenever a property on this instance is changed.
    /// </summary>
    /// <remarks>
    /// Implementations of <see cref="HybridCache"/> can capture this value before invoking a factory
    /// callback and compare it afterwards to determine whether the factory changed any value.
    /// The value increments only when a setter assigns a different value than the current one.
    /// </remarks>
    public int Revision { get; private set; }

    private void BumpRevision()
    {
        // unchecked: rollover is fine because callers compare for inequality, not ordering.
        unchecked
        {
            Revision++;
        }
    }

    internal HybridCacheEntryOptions ToOptions()
        => new HybridCacheEntryOptions
        {
            Expiration = Expiration,
            LocalCacheExpiration = LocalCacheExpiration,
            Flags = Flags,
            LocalSize = LocalSize,
        };
}