在 .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
}
}
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; }
}
}
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;
}
}
}
分散式快取 (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));
}
}
心得
.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 的本機快取,請看範例
案例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET