.NET Core 下的快取 IMemoryCache 和 IDistributedCache

在 .NET Core 定義了兩種快取,本機快取(IMemoryCache);分散式快取(IDistributedCache),用於將快取資料儲存於外部儲存裝置中(如 Redis、SQL Server 或其他分散式快取提供者),使用上沒有甚麼太特別的,單純的記錄下。

開發環境

  • Windows 11 Home
  • .NET 8
  • Rider 2024.2.7

本機快取 (IMemoryCache)

安裝 Microsoft.Extensions.Caching.Memory 套件

dotnet add package Microsoft.Extensions.Caching.Memory --version 8.0.1

 

在 DI Container 註冊

services.AddMemoryCache()

 

讀寫快取資料

var cache = serviceProvider.GetService<IMemoryCache>();
var options = serviceProvider.GetService<MemoryCacheEntryOptions>();

var key = "Cache:Member:1";
var expected = JsonSerializer.Serialize(new { Name = "小心肝" });
cache.Set(key, expected, options);

var result = cache.Get<string>(key);

 

完整程式碼

private static IServiceProvider CreateServiceProvider()
{
    Environment.SetEnvironmentVariable(nameof(Config.DEFAULT_CACHE_EXPIRATION), "00:00:05");

    var configurationBuilder = new ConfigurationBuilder();
    configurationBuilder.AddEnvironmentVariables() ;
    var services = new ServiceCollection();
    var configuration = configurationBuilder.Build();
    services.AddSingleton<IConfiguration>(configuration);
    services.AddMemoryCache();
    services.AddSingleton(p =>
    {
        var expiration = configuration.GetValue<TimeSpan>(nameof(Config.DEFAULT_CACHE_EXPIRATION));

        var options = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration
        };
        return options;
    });
    var serviceProvider = services.BuildServiceProvider();
    return serviceProvider;
}

上述程式碼:

  • 讀取環境變數:DEFAULT_CACHE_EXPIRATION
  • 設定過期時間:AbsoluteExpirationRelativeToNow

 

[Fact]
public async Task 寫讀快取資料_Json_Memory()
{
    var serviceProvider = CreateServiceProvider();
    var cache = serviceProvider.GetService<IMemoryCache>();
    var options = serviceProvider.GetService<MemoryCacheEntryOptions>();

    var key = "Cache:Member:1";
    var expected = JsonSerializer.Serialize(new { Name = "小心肝" });
    cache.Set(key, expected, options);

    var result = cache.Get<string>(key);
    Assert.Equal(expected, result);
}

 

IMemoryCache

主要有 CRD 三個方法

namespace Microsoft.Extensions.Caching.Memory
{
    /// <summary>
    /// Represents a local in-memory cache whose values are not serialized.
    /// </summary>
    public interface IMemoryCache : IDisposable
    {
        /// <summary>
        /// Gets the item associated with this key if present.
        /// </summary>
        /// <param name="key">An object identifying the requested entry.</param>
        /// <param name="value">The located value or null.</param>
        /// <returns>True if the key was found.</returns>
        bool TryGetValue(object key, out object? value);

        /// <summary>
        /// Create or overwrite an entry in the cache.
        /// </summary>
        /// <param name="key">An object identifying the entry.</param>
        /// <returns>The newly created <see cref="ICacheEntry"/> instance.</returns>
        ICacheEntry CreateEntry(object key);

        /// <summary>
        /// Removes the object associated with the given key.
        /// </summary>
        /// <param name="key">An object identifying the entry.</param>
        void Remove(object key);

#if NET6_0_OR_GREATER
        /// <summary>
        /// Gets a snapshot of the cache statistics if available.
        /// </summary>
        /// <returns>An instance of <see cref="MemoryCacheStatistics"/> containing a snapshot of the cache statistics.</returns>
        MemoryCacheStatistics? GetCurrentStatistics() => null;
#endif
    }
}

runtime/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs at 5535e31a712343a63f5d7d796cd874e563e5ac14 · dotnet/runtime

 

ICacheEntry

ICacheEntry 代表一筆快取

namespace Microsoft.Extensions.Caching.Memory
{
  /// <summary>
  /// Represents an entry in the <see cref="T:Microsoft.Extensions.Caching.Memory.IMemoryCache" /> implementation.
  /// When Disposed, is committed to the cache.
  /// </summary>
  public interface ICacheEntry : IDisposable
  {
    /// <summary>Gets the key of the cache entry.</summary>
    object Key { get; }

    /// <summary>Gets or set the value of the cache entry.</summary>
    object? Value { get; set; }

    /// <summary>
    /// Gets or sets an absolute expiration date for the cache entry.
    /// </summary>
    DateTimeOffset? AbsoluteExpiration { get; set; }

    /// <summary>
    /// Gets or sets an absolute expiration time, relative to now.
    /// </summary>
    TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }

    /// <summary>
    /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed.
    /// This will not extend the entry lifetime beyond the absolute expiration (if set).
    /// </summary>
    TimeSpan? SlidingExpiration { get; set; }

    /// <summary>
    /// Gets the <see cref="T:Microsoft.Extensions.Primitives.IChangeToken" /> instances which cause the cache entry to expire.
    /// </summary>
    IList<IChangeToken> ExpirationTokens { get; }

    /// <summary>
    /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache.
    /// </summary>
    IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; }

    /// <summary>
    /// Gets or sets the priority for keeping the cache entry in the cache during a
    ///  cleanup. The default is <see cref="F:Microsoft.Extensions.Caching.Memory.CacheItemPriority.Normal" />.
    /// </summary>
    CacheItemPriority Priority { get; set; }

    /// <summary>Gets or set the size of the cache entry value.</summary>
    long? Size { get; set; }
  }
}

runtime/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/ICacheEntry.cs at 5535e31a712343a63f5d7d796cd874e563e5ac14 · dotnet/runtime

 

CacheExtensions

CacheExtensions 針對 IMemoryCache 型別擴充了許多的方法,主要都是圍繞著 CRUD 的行為

namespace Microsoft.Extensions.Caching.Memory
{
    /// <summary>
    /// Provide extensions methods for <see cref="IMemoryCache"/> operations.
    /// </summary>
    public static class CacheExtensions
    {
        /// <summary>
        /// Gets the value associated with this key if present.
        /// </summary>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the value to get.</param>
        /// <returns>The value associated with this key, or <c>null</c> if the key is not present.</returns>
        public static object? Get(this IMemoryCache cache, object key)
        {
            cache.TryGetValue(key, out object? value);
            return value;
        }

        /// <summary>
        /// Gets the value associated with this key if present.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to get.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the value to get.</param>
        /// <returns>The value associated with this key, or <c>default(TItem)</c> if the key is not present.</returns>
        public static TItem? Get<TItem>(this IMemoryCache cache, object key)
        {
            return (TItem?)(cache.Get(key) ?? default(TItem));
        }

        /// <summary>
        /// Try to get the value associated with the given key.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to get.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the value to get.</param>
        /// <param name="value">The value associated with the given key.</param>
        /// <returns><c>true</c> if the key was found. <c>false</c> otherwise.</returns>
        public static bool TryGetValue<TItem>(this IMemoryCache cache, object key, out TItem? value)
        {
            if (cache.TryGetValue(key, out object? result))
            {
                if (result == null)
                {
                    value = default;
                    return true;
                }

                if (result is TItem item)
                {
                    value = item;
                    return true;
                }
            }

            value = default;
            return false;
        }

        /// <summary>
        /// Associate a value with a key in the <see cref="IMemoryCache"/>.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to set.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the entry to add.</param>
        /// <param name="value">The value to associate with the key.</param>
        /// <returns>The value that was set.</returns>
        public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value)
        {
            using ICacheEntry entry = cache.CreateEntry(key);
            entry.Value = value;

            return value;
        }

        /// <summary>
        /// Sets a cache entry with the given key and value that will expire in the given duration.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to set.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the entry to add.</param>
        /// <param name="value">The value to associate with the key.</param>
        /// <param name="absoluteExpiration">The point in time at which the cache entry will expire.</param>
        /// <returns>The value that was set.</returns>
        public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, DateTimeOffset absoluteExpiration)
        {
            using ICacheEntry entry = cache.CreateEntry(key);
            entry.AbsoluteExpiration = absoluteExpiration;
            entry.Value = value;

            return value;
        }

        /// <summary>
        /// Sets a cache entry with the given key and value that will expire in the given duration from now.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to set.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the entry to add.</param>
        /// <param name="value">The value to associate with the key.</param>
        /// <param name="absoluteExpirationRelativeToNow">The duration from now after which the cache entry will expire.</param>
        /// <returns>The value that was set.</returns>
        public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, TimeSpan absoluteExpirationRelativeToNow)
        {
            using ICacheEntry entry = cache.CreateEntry(key);
            entry.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow;
            entry.Value = value;

            return value;
        }

        /// <summary>
        /// Sets a cache entry with the given key and value that will expire when <see cref="IChangeToken"/> expires.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to set.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the entry to add.</param>
        /// <param name="value">The value to associate with the key.</param>
        /// <param name="expirationToken">The <see cref="IChangeToken"/> that causes the cache entry to expire.</param>
        /// <returns>The value that was set.</returns>
        public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, IChangeToken expirationToken)
        {
            using ICacheEntry entry = cache.CreateEntry(key);
            entry.AddExpirationToken(expirationToken);
            entry.Value = value;

            return value;
        }

        /// <summary>
        /// Sets a cache entry with the given key and value and apply the values of an existing <see cref="MemoryCacheEntryOptions"/> to the created entry.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to set.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the entry to add.</param>
        /// <param name="value">The value to associate with the key.</param>
        /// <param name="options">The existing <see cref="MemoryCacheEntryOptions"/> instance to apply to the new entry.</param>
        /// <returns>The value that was set.</returns>
        public static TItem Set<TItem>(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions? options)
        {
            using ICacheEntry entry = cache.CreateEntry(key);
            if (options != null)
            {
                entry.SetOptions(options);
            }

            entry.Value = value;

            return value;
        }

        /// <summary>
        /// Gets the value associated with this key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to get.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the entry to look for or create.</param>
        /// <param name="factory">The factory that creates the value associated with this key if the key does not exist in the cache.</param>
        /// <returns>The value associated with this key.</returns>
        public static TItem? GetOrCreate<TItem>(this IMemoryCache cache, object key, Func<ICacheEntry, TItem> factory)
        {
            if (!cache.TryGetValue(key, out object? result))
            {
                using ICacheEntry entry = cache.CreateEntry(key);

                result = factory(entry);
                entry.Value = result;
            }

            return (TItem?)result;
        }

        /// <summary>
        /// Asynchronously gets the value associated with this key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
        /// </summary>
        /// <typeparam name="TItem">The type of the object to get.</typeparam>
        /// <param name="cache">The <see cref="IMemoryCache"/> instance this method extends.</param>
        /// <param name="key">The key of the entry to look for or create.</param>
        /// <param name="factory">The factory task that creates the value associated with this key if the key does not exist in the cache.</param>
        /// <returns>The task object representing the asynchronous operation.</returns>
        public static async Task<TItem?> GetOrCreateAsync<TItem>(this IMemoryCache cache, object key, Func<ICacheEntry, Task<TItem>> factory)
        {
            if (!cache.TryGetValue(key, out object? result))
            {
                using ICacheEntry entry = cache.CreateEntry(key);

                result = await factory(entry).ConfigureAwait(false);
                entry.Value = result;
            }

            return (TItem?)result;
        }
    }
}

runtime/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/MemoryCacheExtensions.cs at 5535e31a712343a63f5d7d796cd874e563e5ac14 · dotnet/runtime

 

分散式快取 (IDistributedCache)

以 Redis 為例

安裝 Microsoft.Extensions.Caching.StackExchangeRedis套件

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis--version 8.0.10

安裝 Redis

在 docker-compose.yml 輸入以下

services:
  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

 

在 DI Container 註冊

services.AddStackExchangeRedisCache()

 

讀寫快取

var cache = serviceProvider.GetService<IDistributedCache>();
var options = serviceProvider.GetService<DistributedCacheEntryOptions>();

var key = "Cache:Member:1";
var expected = JsonSerializer.Serialize(new { Name = "小心肝" });
await cache.SetStringAsync(key, expected, options);

 

完整程式碼

private static IServiceProvider CreateServiceProvider()
{
    Environment.SetEnvironmentVariable(nameof(Config.SYS_REDIS_URL), "localhost:6379");
    Environment.SetEnvironmentVariable(nameof(Config.DEFAULT_CACHE_EXPIRATION), "00:00:05");

    var configurationBuilder = new ConfigurationBuilder();
    configurationBuilder.AddEnvironmentVariables() ;
    var services = new ServiceCollection();
    var configuration = configurationBuilder.Build();
    services.AddSingleton<IConfiguration>(configuration);
    services.AddSingleton(p =>
    {
        var expiration = configuration.GetValue<TimeSpan>(nameof(Config.DEFAULT_CACHE_EXPIRATION));

        var options = new DistributedCacheEntryOptions()
        {
            AbsoluteExpirationRelativeToNow = expiration
        };
        return options;
    });
    services.AddStackExchangeRedisCache(options =>
    {
        var connectionString = configuration.GetValue<string>(nameof(Config.SYS_REDIS_URL));
        options.Configuration = connectionString;
    });
    var serviceProvider = services.BuildServiceProvider();
    return serviceProvider;
}

上述程式碼:

  • 從環境變數讀取
    • Redis 位置:SYS_REDIS_URL
    • 預設快取時間:DEFAULT_CACHE_EXPIRATION
  • 設定過期時間:AbsoluteExpirationRelativeToNow

 

[Fact]
public async Task 寫讀快取資料_Json()
{
    var serviceProvider = CreateServiceProvider();
    var cache = serviceProvider.GetService<IDistributedCache>();
    var options = serviceProvider.GetService<DistributedCacheEntryOptions>();

    var key = "Cache:Member:1";
    var expected = JsonSerializer.Serialize(new { Name = "小心肝" });
    await cache.SetStringAsync(key, expected, options);

    var result = await cache.GetStringAsync(key);
    Assert.Equal(expected, result);
}

 

IDistributedCache

一樣,是快取物件的 CUD 操作

namespace Microsoft.Extensions.Caching.Distributed
{
  /// <summary>Represents a distributed cache of serialized values.</summary>
  public interface IDistributedCache
  {
    /// <summary>Gets a value with the given key.</summary>
    /// <param name="key">A string identifying the requested value.</param>
    /// <returns>The located value or null.</returns>
    byte[]? Get(string key);

    /// <summary>Gets a value with the given key.</summary>
    /// <param name="key">A string identifying the requested value.</param>
    /// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
    /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation, containing the located value or null.</returns>
    Task<byte[]?> GetAsync(string key, CancellationToken token = default (CancellationToken));

    /// <summary>Sets a value with the given key.</summary>
    /// <param name="key">A string identifying the requested value.</param>
    /// <param name="value">The value to set in the cache.</param>
    /// <param name="options">The cache options for the value.</param>
    void Set(string key, byte[] value, DistributedCacheEntryOptions options);

    /// <summary>Sets the value with the given key.</summary>
    /// <param name="key">A string identifying the requested value.</param>
    /// <param name="value">The value to set in the cache.</param>
    /// <param name="options">The cache options for the value.</param>
    /// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
    /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
    Task SetAsync(
      string key,
      byte[] value,
      DistributedCacheEntryOptions options,
      CancellationToken token = default (CancellationToken));

    /// <summary>
    /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
    /// </summary>
    /// <param name="key">A string identifying the requested value.</param>
    void Refresh(string key);

    /// <summary>
    /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
    /// </summary>
    /// <param name="key">A string identifying the requested value.</param>
    /// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
    /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
    Task RefreshAsync(string key, CancellationToken token = default (CancellationToken));

    /// <summary>Removes the value with the given key.</summary>
    /// <param name="key">A string identifying the requested value.</param>
    void Remove(string key);

    /// <summary>Removes the value with the given key.</summary>
    /// <param name="key">A string identifying the requested value.</param>
    /// <param name="token">Optional. The <see cref="T:System.Threading.CancellationToken" /> used to propagate notifications that the operation should be canceled.</param>
    /// <returns>The <see cref="T:System.Threading.Tasks.Task" /> that represents the asynchronous operation.</returns>
    Task RemoveAsync(string key, CancellationToken token = default (CancellationToken));
  }
}

runtime/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs at 5535e31a712343a63f5d7d796cd874e563e5ac14 · dotnet/runtime

心得

.NET Core 的版本,微軟明確定義了兩種不同的快取機制合約,分別是 IMemoryCache、IDistributedCache,應該可以根據自己的需求來決定要使用哪一種快取,我嘗試著把這兩種快取收攏成一個合約,ICacheProvider

public interface ICacheProvider
{
    Task<bool> ExistsAsync(string key);

    Task<T?> GetAsync<T>(string key);

    Task RemoveAsync(string key);

    Task SetAsync<T>(string key,
                     T value,
                     CacheProviderOptions options);

    Task SetAsync<T>(string key,
                     T value);
}

 

再透過 ICacheProviderFactory 決定要用哪一種快取

public class CacheProviderFactory : ICacheProviderFactory
{
    private readonly IDistributedCache _distributedCache;
    private readonly IMemoryCache _memoryCache;
    private readonly CacheProviderOptions _cacheProviderOptions;

    public CacheProviderFactory(IMemoryCache memoryCache,
                                IDistributedCache distributedCache,
                                CacheProviderOptions cacheProviderOptions)
    {
        this._memoryCache = memoryCache;
        this._distributedCache = distributedCache;
        this._cacheProviderOptions = cacheProviderOptions;
    }

    public ICacheProvider Create(CacheProviderType type)
    {
        return type switch
        {
            CacheProviderType.Memory => new MemoryCacheProvider(this._memoryCache, this._cacheProviderOptions),
            CacheProviderType.Redis => new RedisCacheProvider(this._distributedCache, this._cacheProviderOptions),
            _ => throw new ArgumentException($"Unsupported cache provider: {type}")
        };
    }
}

 

後來想想,好像也不需要這樣做。再加上,微軟已經提供 services.AddDistributedMemoryCache(),這是實作 IDistributedCache 的本機快取,請看範例

案例位置

sample.dotblog/Cache/Lab.Cache at f9d3dc65c67cc0956e2a53a9da36e6087235c0bc · yaochangyu/sample.dotblog

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo