Sage.Caching.Memory 1.0.0.8

dotnet add package Sage.Caching.Memory --version 1.0.0.8
                    
NuGet\Install-Package Sage.Caching.Memory -Version 1.0.0.8
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sage.Caching.Memory" Version="1.0.0.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sage.Caching.Memory" Version="1.0.0.8" />
                    
Directory.Packages.props
<PackageReference Include="Sage.Caching.Memory" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sage.Caching.Memory --version 1.0.0.8
                    
#r "nuget: Sage.Caching.Memory, 1.0.0.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sage.Caching.Memory@1.0.0.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sage.Caching.Memory&version=1.0.0.8
                    
Install as a Cake Addin
#tool nuget:?package=Sage.Caching.Memory&version=1.0.0.8
                    
Install as a Cake Tool

Sage.Caching.Memory

Sage.Caching.Memory 是对 Microsoft.Extensions.Caching.Memory 的轻量封装,提供了更直接的缓存读写接口,支持:

  • 线程安全的内存缓存操作
  • Region 分组管理与分组清理
  • 同步 / 异步缓存创建
  • 可选统计信息
  • KeyPrefix 键前缀隔离
  • AOT 兼容

适合在 ASP.NET Core、WebApi、Minimal API、Native AOT 项目中直接使用。

适用场景

  • 单机 WebApi 的热点数据缓存
  • 查询结果缓存
  • 下游接口返回值短时缓存
  • 按模块或业务分组清理缓存

不适合:

  • 多实例共享缓存
  • 需要跨进程一致性的缓存
  • 需要持久化的缓存

这类场景建议使用 Redis 等分布式缓存。

安装

dotnet add package Sage.Caching.Memory

快速开始

1. 注册服务

using Microsoft.Extensions.Caching.Memory;
using Sage.Caching.Memory.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMemoryCacheService(options =>
{
    // 是否启用统计
    options.EnableStats = false;

    // 当工厂方法创建缓存失败时,是否直接抛出异常
    options.ThrowOnCreateError = true;

    // 键前缀,建议按应用或服务名设置
    options.KeyPrefix = "WebService.Callback";

    // 是否启用类型检查
    options.EnableTypeChecking = false;

    // 最大缓存大小限制
    options.MaximumCacheSize = 10000;

    // 到达内存压力时压缩比例
    options.CompactionPercentage = 0.10;

    // 过期扫描频率
    options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);

    // 默认缓存项配置
    options.DefaultEntryOptions = new MemoryCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
        SlidingExpiration = TimeSpan.FromMinutes(10),
        Size = 1,
        Priority = CacheItemPriority.Normal
    };
});

var app = builder.Build();
app.Run();

2. 在 WebApi Minimal(AOT) 中使用

推荐直接在 endpoint 参数中注入 ICacheService

using Microsoft.Extensions.Caching.Memory;
using Sage.Caching.Memory.Services;

app.MapGet("/callbacks/{id}", async (string id, ICacheService cacheService) =>
{
    var result = await cacheService.GetOrCreateAsync(
        key: $"callback:{id}",
        valueFactory: async () =>
        {
            await Task.Delay(50);

            return new
            {
                Id = id,
                Name = $"Callback-{id}",
                Time = DateTimeOffset.UtcNow
            };
        },
        region: "Callback",
        options: new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
            SlidingExpiration = TimeSpan.FromMinutes(2),
            Size = 1,
            Priority = CacheItemPriority.Normal
        });

    return Results.Ok(result);
});

3. 在 Controller 中使用

using Microsoft.AspNetCore.Mvc;
using Sage.Caching.Memory.Services;

[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
    private readonly ICacheService _cacheService;

    public DataController(ICacheService cacheService)
    {
        _cacheService = cacheService;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(string id)
    {
        var data = await _cacheService.GetOrCreateAsync(
            key: $"data:{id}",
            valueFactory: async () =>
            {
                await Task.Delay(100);
                return new { Id = id, Name = $"Item-{id}" };
            },
            region: "Data");

        return Ok(data);
    }
}

核心接口

主要接口为 ICacheService

bool TryGetValue<T>(string key, out T? value);
void Set<T>(string key, T value, string? region = null, MemoryCacheEntryOptions? options = null);
void Remove(string key, string? region = null);
T? GetOrCreate<T>(string key, Func<T> valueFactory, string? region = null, MemoryCacheEntryOptions? options = null);
ValueTask<T?> GetOrCreateAsync<T>(string key, Func<ValueTask<T>> valueFactory, string? region = null, MemoryCacheEntryOptions? options = null);
void Clear();
int ClearRegion(string region);
void SetMany<T>(IEnumerable<KeyValuePair<string, T>> items, string? region = null, MemoryCacheEntryOptions? options = null);
int RemoveMany(IEnumerable<string> keys);
CacheStats GetCacheStats();
IReadOnlyCollection<string> GetRegionKeys(string region);

常用用法

1. 手动写入缓存

cacheService.Set(
    key: "user:1001",
    value: new UserDto(1001, "Alice"),
    region: "User");

2. 手动读取缓存

if (cacheService.TryGetValue<UserDto>("user:1001", out var user))
{
    return Results.Ok(user);
}

return Results.NotFound();

3. 缓存不存在时自动创建

同步:

var product = cacheService.GetOrCreate(
    key: "product:1",
    valueFactory: () => new ProductDto(1, "Apple"),
    region: "Product");

异步:

var product = await cacheService.GetOrCreateAsync(
    key: "product:1",
    valueFactory: async () =>
    {
        await Task.Delay(50);
        return new ProductDto(1, "Apple");
    },
    region: "Product");

4. 自定义当前缓存项过期时间

var order = await cacheService.GetOrCreateAsync(
    key: "order:2026001",
    valueFactory: async () =>
    {
        await Task.Delay(20);
        return new { Id = 2026001, Status = "Paid" };
    },
    region: "Order",
    options: new MemoryCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(3),
        SlidingExpiration = TimeSpan.FromMinutes(1),
        Size = 1,
        Priority = CacheItemPriority.High
    });

5. 删除单个缓存

cacheService.Remove("user:1001");

如果你明确知道这个 key 属于哪个分组,也可以同时传入 region

cacheService.Remove("user:1001", region: "User");

6. 清空某个分组

var removedCount = cacheService.ClearRegion("User");

7. 清空全部缓存

cacheService.Clear();

8. 批量写入

var items = new Dictionary<string, string>
{
    ["config:siteName"] = "Sage",
    ["config:theme"] = "default",
    ["config:lang"] = "zh-CN"
};

cacheService.SetMany(items, region: "Config");

9. 批量删除

var removed = cacheService.RemoveMany(new[]
{
    "config:siteName",
    "config:theme"
});

10. 查看分组内有哪些键

var keys = cacheService.GetRegionKeys("User");

11. 获取缓存统计信息

var stats = cacheService.GetCacheStats();

Console.WriteLine($"Hits: {stats.Hits}");
Console.WriteLine($"Misses: {stats.Misses}");
Console.WriteLine($"HitRate: {stats.HitRate:P2}");
Console.WriteLine($"TotalItems: {stats.TotalItems}");

配置项说明

MemoryCacheServiceOptions

配置项 说明
DefaultEntryOptions 默认缓存项配置
EnableStats 是否启用统计信息,默认 false
ThrowOnCreateError 工厂方法异常时是否直接抛出,默认 true
StatsFlushInterval 统计信息刷新间隔
MaximumCacheSize 缓存总大小限制
ExpirationScanFrequency 过期扫描频率
CompactionPercentage 内存压力时压缩比例
EnableTypeChecking 是否校验读取类型是否匹配
KeyPrefix 键前缀,防止多模块键冲突

MemoryCacheEntryOptions 属性说明

这是单个缓存项的配置对象。最常用的是过期时间、优先级和 Size

属性 类型 说明 是否常用
AbsoluteExpiration DateTimeOffset? 绝对过期时间点。到指定时间后缓存失效。 常用
AbsoluteExpirationRelativeToNow TimeSpan? 相对于“当前时间”的绝对过期时长,例如 30 分钟后失效。 很常用
SlidingExpiration TimeSpan? 滑动过期时间。缓存项在一段时间内没有被访问就失效。再次访问会刷新这段时间,但不会超过绝对过期时间。 很常用
Priority CacheItemPriority 缓存优先级。在内存压力清理时,优先级高的项更不容易被清掉。默认是 Normal 常用
Size long? 当前缓存项占用的大小单位。只有配置了 MaximumCacheSize 时才需要重点关注。 很常用
ExpirationTokens IList<IChangeToken> 变更令牌集合。任一令牌触发后,缓存项过期。适合做“依赖某个配置变化自动失效”。 进阶
PostEvictionCallbacks IList<PostEvictionCallbackRegistration> 缓存项被移除后触发的回调。可用于日志、监控或清理附属资源。 进阶
常用属性怎么选
  1. 只想固定时间后过期:
new MemoryCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
    Size = 1
}
  1. 想“长时间没人访问才过期”:
new MemoryCacheEntryOptions
{
    SlidingExpiration = TimeSpan.FromMinutes(10),
    Size = 1
}
  1. 想“最多保留 30 分钟,但 10 分钟不访问也过期”:
new MemoryCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
    SlidingExpiration = TimeSpan.FromMinutes(10),
    Size = 1,
    Priority = CacheItemPriority.Normal
}
属性使用建议
  • AbsoluteExpirationAbsoluteExpirationRelativeToNow 二选一通常就够了,优先推荐后者。
  • SlidingExpiration 适合热点数据,但不适合要求“必须在固定时点后失效”的数据。
  • SlidingExpiration 与绝对过期可以同时使用,最终谁先到期就按谁失效。
  • 设置了 MaximumCacheSize 后,请务必为缓存项提供 Size
  • Priority 只影响内存压力触发的清理,不影响正常的时间过期。
当前库实现注意
  • 本库内部会克隆 MemoryCacheEntryOptions 后再写入缓存,因此常用属性 AbsoluteExpirationAbsoluteExpirationRelativeToNowSlidingExpirationPrioritySize 和逐出回调会生效。
  • 如果你计划大量依赖 ExpirationTokens,建议先确认当前版本是否已完整透传该属性;当前实现更推荐优先使用时间过期和业务主动 Remove 的方式控制失效。

使用建议

1. key 要稳定、可预测

建议按业务维度命名:

"user:1001"
"order:2026001"
"callback:task-001"

不要直接把随机字符串当业务主键缓存,后期不方便排查和清理。

2. region 只是分组标签,不参与键隔离

例如下面两段代码:

cacheService.Set("user:1", userA, region: "A");
cacheService.Set("user:1", userB, region: "B");

它们依然是在操作同一个业务 key。

也就是说:

  • region 主要用于 ClearRegion("xxx")
  • 真正避免冲突靠的是 key
  • 不同分组里不要复用同名业务 key

3. 不要手动拼接 KeyPrefix

如果你配置了:

options.KeyPrefix = "WebService.Callback";

业务代码里依然只写原始 key:

cacheService.Set("callback:1001", value);

不要自己写成:

cacheService.Set("WebService.Callback:callback:1001", value);

前缀应由缓存服务内部统一处理。

4. 设置了 MaximumCacheSize 后,要注意每项的 Size

如果你启用了总大小限制:

options.MaximumCacheSize = 10000;

那么每个缓存项都应提供 Size。你可以:

  • DefaultEntryOptions 里统一设置 Size
  • 或者在单次写入时单独指定 Size

例如:

options.DefaultEntryOptions = new MemoryCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
    Size = 1
};

注意:MaximumCacheSizeIMemoryCache 的大小单位,不等同于“字节数”。

5. 统计信息有轻微性能开销

如果你只关心缓存功能,不关心命中率、未命中数、逐出数,建议保持:

options.EnableStats = false;

WebApi Minimal(AOT) 推荐示例

下面是一份适合直接复制到 Program.cs 的最小示例:

using Microsoft.Extensions.Caching.Memory;
using Sage.Caching.Memory.Extensions;
using Sage.Caching.Memory.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMemoryCacheService(options =>
{
    options.EnableStats = false;
    options.ThrowOnCreateError = true;
    options.KeyPrefix = "WebService.Callback";
    options.EnableTypeChecking = false;
    options.MaximumCacheSize = 10000;
    options.CompactionPercentage = 0.10;
    options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
    options.DefaultEntryOptions = new MemoryCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
        SlidingExpiration = TimeSpan.FromMinutes(10),
        Size = 1,
        Priority = CacheItemPriority.Normal
    };
});

var app = builder.Build();

app.MapGet("/callback/{id}", async (string id, ICacheService cacheService) =>
{
    var result = await cacheService.GetOrCreateAsync(
        key: $"callback:{id}",
        valueFactory: async () =>
        {
            await Task.Delay(50);
            return new
            {
                Id = id,
                Time = DateTimeOffset.UtcNow
            };
        },
        region: "Callback");

    return Results.Ok(result);
});

app.MapDelete("/cache/region/{region}", (string region, ICacheService cacheService) =>
{
    var removed = cacheService.ClearRegion(region);
    return Results.Ok(new { Region = region, Removed = removed });
});

app.MapGet("/cache/stats", (ICacheService cacheService) =>
{
    return Results.Ok(cacheService.GetCacheStats());
});

app.Run();

常见问题

1. 为什么我设置了 region,不同分组还是可能冲突?

因为 region 的作用是“分组清理”,不是“键命名空间隔离”。
真正决定缓存项唯一性的核心仍然是 key

2. 为什么建议加 KeyPrefix

当同一个进程里存在多个模块、多个服务、多个业务域时,统一加前缀更容易避免键冲突。

3. Native AOT 可以用吗?

可以。本项目已按 AOT 兼容方式设计,适合在 Minimal API / Native AOT 项目中使用。

许可证

本项目采用 Apache 2.0 许可证。

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0.8 106 4/24/2026
1.0.0.7 112 4/20/2026
1.0.0.6 102 4/16/2026
1.0.0.5 120 1/14/2026
1.0.0.4 229 10/15/2025
1.0.0.3 220 10/13/2025
1.0.0.2 218 10/2/2025
1.0.0.1 220 9/23/2025
1.0.0 235 7/16/2025

修复 开启了 KeyPrefix 时 GetOrCreate / GetOrCreateAsync 的实现里会先加一次前缀的问题