File: CacheView\MemoryCacheViewStore.cs
Web Access
Project: src\aspnetcore\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
internal sealed partial class MemoryCacheViewStore : ICacheViewStore
{
    private readonly MemoryCache _cache;
    private readonly ILogger<MemoryCacheViewStore> _logger;
    private readonly ConcurrentDictionary<string, Task<SerializedRenderFragment>> _pending = new(StringComparer.Ordinal);
 
    public MemoryCacheViewStore(IOptions<RazorComponentsServiceOptions> options, ILogger<MemoryCacheViewStore> logger)
    {
        _cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = options.Value.CacheViewSizeLimit,
        });
        _logger = logger;
    }
 
    public async ValueTask<SerializedRenderFragment> GetOrCreateAsync(
        string key,
        Func<CancellationToken, ValueTask<SerializedRenderFragment>> factory,
        CacheStoreOptions options,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        if (_cache.TryGetValue<SerializedRenderFragment>(key, out var existing) && existing is not null)
        {
            return existing;
        }
 
        var tcs = new TaskCompletionSource<SerializedRenderFragment>();
        var pending = _pending.GetOrAdd(key, tcs.Task);
        if (!ReferenceEquals(pending, tcs.Task))
        {
            return await pending.WaitAsync(cancellationToken);
        }
 
        try
        {
            var payload = await factory(cancellationToken);
 
            if (payload.Nodes.Count > 0)
            {
                StoreEntry(key, payload, options);
            }
 
            tcs.SetResult(payload);
            return payload;
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
            throw;
        }
        finally
        {
            _pending.TryRemove(new KeyValuePair<string, Task<SerializedRenderFragment>>(key, tcs.Task));
        }
    }
 
    private void StoreEntry(string key, SerializedRenderFragment payload, CacheStoreOptions options)
    {
        try
        {
            var entryOptions = new MemoryCacheEntryOptions
            {
                Size = EstimateSize(payload),
            };
 
            if (options.ExpiresSliding.HasValue)
            {
                entryOptions.SlidingExpiration = options.ExpiresSliding.Value;
            }
 
            if (options.ExpiresOn.HasValue)
            {
                entryOptions.AbsoluteExpiration = options.ExpiresOn.Value;
            }
            else
            {
                entryOptions.AbsoluteExpirationRelativeToNow = options.ExpiresAfter ?? RazorComponentsServiceOptions.DefaultCacheViewExpiration;
            }
 
            _cache.Set(key, payload, entryOptions);
        }
        catch (Exception ex)
        {
            // Failing to cache the entry should not fail the request; the value was still
            // produced successfully, so log and continue without caching.
            // Identical behaviour to HybridCache
            Log.StoreEntryFailed(_logger, key, ex);
        }
    }
 
    public void Clear()
    {
        _cache.Clear();
    }
 
    private static long EstimateSize(SerializedRenderFragment payload)
        => payload.ContentSize;
 
    public void Dispose()
    {
        _cache.Dispose();
    }
 
    private static partial class Log
    {
        [LoggerMessage(1, LogLevel.Warning, "Failed to store CacheView entry for key '{Key}'.", EventName = "StoreEntryFailed")]
        public static partial void StoreEntryFailed(ILogger logger, string key, Exception exception);
    }
}