Sage.Caching.Memory 1.0.0.7

There is a newer version of this package available.
See the version list below for details.
dotnet add package Sage.Caching.Memory --version 1.0.0.7
                    
NuGet\Install-Package Sage.Caching.Memory -Version 1.0.0.7
                    
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.7" />
                    
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.7" />
                    
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.7
                    
#r "nuget: Sage.Caching.Memory, 1.0.0.7"
                    
#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.7
                    
#: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.7
                    
Install as a Cake Addin
#tool nuget:?package=Sage.Caching.Memory&version=1.0.0.7
                    
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 键前缀,防止多模块键冲突

使用建议

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 126 4/24/2026
1.0.0.7 120 4/20/2026
1.0.0.6 112 4/16/2026
1.0.0.5 128 1/14/2026
1.0.0.4 238 10/15/2025
1.0.0.3 227 10/13/2025
1.0.0.2 224 10/2/2025
1.0.0.1 227 9/23/2025
1.0.0 242 7/16/2025

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