Lunarium.Common 1.4.0

dotnet add package Lunarium.Common --version 1.4.0
                    
NuGet\Install-Package Lunarium.Common -Version 1.4.0
                    
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="Lunarium.Common" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Lunarium.Common" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Lunarium.Common" />
                    
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 Lunarium.Common --version 1.4.0
                    
#r "nuget: Lunarium.Common, 1.4.0"
                    
#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 Lunarium.Common@1.4.0
                    
#: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=Lunarium.Common&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Lunarium.Common&version=1.4.0
                    
Install as a Cake Tool

Lunarium.Common

Lunarium 项目的公共类库(shared class library),用于存放跨项目复用的通用类型与工具代码。

环境要求

  • .NET SDK 10.0 或更高版本(可同时构建 net8.0 / net9.0 目标)
  • 目标框架:net8.0、net9.0、net10.0(多目标)
    • .NET 9 及以上使用 System.Threading.Lock,.NET 8 自动退回 object 加锁(条件编译)
    • 运行单元测试需本机具备对应运行时(测试项目自身仅 net10.0)

项目结构

Lunarium.Common/
├── src/Lunarium.Common/        # 目录名 = 程序集名;tests/ 与之并列
│   ├── Lunarium.Common.csproj  # 类库项目文件(默认 glob 包含本目录下全部 .cs)
│   ├── Alphabet.cs             # Lunarium.Common
│   ├── EnvVar.cs               # Lunarium.Common
│   ├── Net/                    # Lunarium.Common.Net
│   ├── UID/                    # Lunarium.Common.UID
│   ├── Debounce/               # Lunarium.Common.Debounce
│   └── Caching/                # Lunarium.Common.Caching(含 CacheJsonContext 源生成上下文)
├── tests/Lunarium.Common.Tests/ # xUnit 单元测试(靠 InternalsVisibleTo 直接测 internal 类型)
├── Lunarium.Common.slnx        # 解决方案文件(.NET 10 新格式,含 /src/ 与 /tests/ 解决方案文件夹)
├── .gitignore                  # 标准 .NET 忽略规则
├── LICENSE                     # Apache License 2.0
└── README.md

模块

模块 命名空间 说明
Alphabet Lunarium.Common 常用字符集常量(Base16/32/36/62、防混淆、密码字符集等)
EnvVar Lunarium.Common 环境变量包装:读取 / 写入 / 删除 + 类型化解析(As<T>、AsEnum、宽松布尔字面量)
Net Lunarium.Common.Net 代理配置 ProxyUrl(转换为 IWebProxy / Uri);通用环境 URL EnvUrl(转换为 Uri)
UID Lunarium.Common.UID NanoID 无取模偏差 ID 生成(可直接输出 UTF-8 字节)、LocalSnowflake 无锁单机雪花 ID
Debounce Lunarium.Common.Debounce 脏状态防抖 DirtyDebouncer 与取消令牌防抖 CtsDebouncer
Caching Lunarium.Common.Caching 并发内存 KV + 防抖异步落盘(CacheStore);单文档泛型存储(JsonDocumentStore<T>)

所有模块均为同一程序集内的并列工具集,零外部 NuGet 依赖(仅使用 BCL)。不拆分独立项目—— 仅当某模块需引入其他模块无法接受的重依赖、或需独立发版时才考虑拆分。

环境变量读取(EnvVar)

// 读取:未设置或值为空白时返回 null;必须存在时用 GetRequired(否则抛异常)
string? port = EnvVar.Get("PORT")?.Value;

// 候选名按优先级依次尝试,第一个命中的生效
EnvVar? proxy = EnvVar.GetAny("http_proxy", "HTTP_PROXY");

// 写入 / 删除:名称与值都会被裁剪首尾空白
EnvVar.Set("MODE", "production");
EnvVar.Unset("MODE");

// 类型化解析:泛型走 IParsable<T>(固定不变文化),失败返回默认值 / 抛异常
int timeout = EnvVar.GetRequired("TIMEOUT").As(30);
DayOfWeek day = EnvVar.GetRequired("DAY").AsEnum(DayOfWeek.Monday);

语义要点(与 BCL 的 Environment.GetEnvironmentVariable 不同之处):

  • Name / Value 在构造时即裁剪首尾空白;Set 也按裁剪后的名称与值写入。
  • "未设置"与"值为空白"无法区分:两者都当作未设置(Get 返回 null,GetRequired 抛异常)。
  • Set 与构造函数拒绝空白值,因此写不出空字符串变量;要清空变量只能用 Unset(彻底删除)。
  • ToString() 只返回变量名、不返回值,避免日志或异常信息泄露密钥(取值请用 Value); 同时刻意不提供 implicit operator string——否则 "" + envVar 这类写法会绕过脱敏取得值。
  • As<bool>() / RequiredAs<bool>() 使用宽松字面量(true/false、1/yes/on、0/no/off,大小写不敏感), 而 IParsable<bool> 本身只认 true/false;需要自定义字面量时用 AsBoolean("enabled", "disabled")。
  • 枚举不实现 IParsable<T>,请用 AsEnum / RequiredAsEnum;后者默认 requireDefined: true, 会拒绝 "99" 这类能解析但未定义的纯数字值。

代理可直接从环境变量构造(默认依次读取 http_proxy → HTTP_PROXY,小写优先,值同样会裁剪):

ProxyUrl? proxy = ProxyUrl.FromEnvironment();   // 也可指定变量名:FromEnvironment("MY_PROXY")
IWebProxy webProxy = proxy.ToWebProxy();
Uri uri = proxy.ToUri();

非代理场景的环境 URL 用 EnvUrl(语义更明确;没有默认变量名,也不提供 ToWebProxy):

EnvUrl? baseUrl = EnvUrl.FromEnvironment("API_BASE_URL");
Uri uri = baseUrl.ToUri();

缓存落盘位置

CacheStore 默认把数据写在 {AppContext.BaseDirectory}/CacheStore/{cacheNamespace}.json。 容器(需挂卷)、systemd 只读 root、安装在 /usr/lib 等不可写目录的场景,请显式指定可写目录:

// 默认:AppContext.BaseDirectory/CacheStore/{ns}.json
var store = new CacheStore("bot-state");

// 显式目录 → 直接落在 {dir}/{ns}.json(不会再追加 CacheStore 子目录)
var store = new CacheStore("bot-state", storageDirectory: "/var/lib/lunarium");

// 相对路径以 AppContext.BaseDirectory 为基准解析,不依赖进程当前目录
var other = new CacheStore("other", storageDirectory: "data");

CacheStore.DataDirectoryName(internal)只作为缺省目录名;进程内以解析后的完整文件路径去重, 同名命名空间落在不同目录下互不影响。测试通过向构造函数注入临时目录保证隔离,不要在测试里复刻路径约定。

单文档存储(JsonDocumentStore<T>)

当要持久化的是「一个完整的强类型文档」(配置、状态等)而不是键值对时,使用 JsonDocumentStore<T>:

// 消费方提供源生成的类型信息(AOT/裁剪安全);缩进、编码器、注释处理等选项也由该上下文决定
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(TicketConfig))]
internal partial class TicketConfigJsonContext : JsonSerializerContext;

await using var store = new JsonDocumentStore<TicketConfig>(
    "ticket-config",
    TicketConfigJsonContext.Default.TicketConfig,
    storageDirectory: "/var/lib/lunarium");

TicketConfig config = store.Read();                // 深拷贝快照,可直接使用/修改
store.Set(config);                                 // 保存快照并触发 5 秒防抖落盘
store.Update(c => { c.TicketCd = 2; return c; });  // 锁内原子更新
await store.FlushAsync();                          // 立即落盘(DisposeAsync 也会自动刷盘)

语义与约定:

  • 快照语义:Read / Set / Update 都通过 JSON 往返做深拷贝,调用方持有的实例与存储内部状态互不影响; 因此 [JsonIgnore] 成员不参与快照与落盘(本存储以 JSON 中的内容为准)。
  • 不做业务校验:校验是调用方策略(加载后校验、更新前校验)。不要放在保存阶段——防抖落盘是后台任务, 失败只能进 logError,调用方无法同步感知。
  • 落盘位置:{storageDirectory ?? AppContext.BaseDirectory/JsonDocumentStore}/{documentName}.json; 与 CacheStore 共享进程级路径注册表,同一个文件不允许被任一存储类型的两个实例同时占用。
  • 损坏处理:与 CacheStore 一致(CacheFileCorruptException;allowCorruptRebuild 必须同时提供 logError)。
  • AOT:只使用 JsonSerializer 的 JsonTypeInfo<T> 重载,调用方必须传入源生成上下文产出的类型信息。

常用命令

# 在仓库根目录执行(根目录的 .slnx 会定位到 src 下的项目)

# 还原依赖
dotnet restore

# 生成(产物位于 src/Lunarium.Common/bin/ 下)
dotnet build

# 只构建单个项目
dotnet build src/Lunarium.Common/Lunarium.Common.csproj

# 打包(同时产出 .nupkg 与 .snupkg 符号包)
dotnet pack -c Release

AOT / 裁剪兼容

本库已开启 <IsAotCompatible>true</IsAotCompatible>,可以安全用于 Native AOT(PublishAot) 与 trimming(PublishTrimmed)应用。

已实测:以 PublishAot=true 发布(dotnet publish -c Release -r linux-x64)得到原生可执行文件并可正常运行, Cache / CacheStore 的序列化与落盘重载、NanoID、LocalSnowflake、DirtyDebouncer / CtsDebouncer 均无异常。

新增需要 JSON 序列化的类型时必须同步登记

Cache 的 JSON 元数据由源生成器在编译期静态产出(见 Caching/CacheJsonContext.cs)。 任何库内需要被 JsonSerializer 序列化 / 反序列化的类型,都必须登记到 CacheJsonContext:

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Cache))]
[JsonSerializable(typeof(新类型))]   // ← 忘登记不会编译失败,只会在 AOT / 裁剪后运行时才报错
internal partial class CacheJsonContext : JsonSerializerContext;

JsonDocumentStore<T> 不在此列:文档根类型位于消费方程序集,由调用方自己的 JsonSerializerContext 提供 JsonTypeInfo<T>(见上文示例),库内不需要也无法登记。

忘记登记属于「编译期无感、运行时炸」的静默失败:库本身编译通过、无任何告警, 但 AOT / 裁剪后的应用会抛 InvalidOperationException: Reflection-based serialization has been disabled for this application.。

禁止的写法

// ✗ 这两个泛型重载挂了 RequiresUnreferencedCode / RequiresDynamicCode,
//   即使给 JsonSerializerOptions 配了 TypeInfoResolver,IL2026 / IL3050 也照样报
JsonSerializer.Serialize(value, options);
JsonSerializer.Deserialize<T>(json, options);

// ✓ 只能走源生成的 JsonTypeInfo
JsonSerializer.Serialize(value, JsonContext.Cache);
JsonSerializer.Deserialize(json, JsonContext.Cache);

其他同类禁忌:Activator.CreateInstance、Type.GetType、MakeGenericType、Expression / DynamicMethod、Assembly.Load;Enum 的字符串互转与 Regex(非源生成)在 AOT 下也需格外小心。

分析器是常驻的

IsAotCompatible=true 会隐含 IsTrimmable=true,并在每次 dotnet build(含 IDE)中启用 IL2026 / IL3050 等分析器,因此上面那些写法在写下的当次构建就会被拦下:

# 无需任何额外参数,本库自检就是普通构建
dotnet build

同时程序集会带上 IsTrimmable / IsAotCompatible 元数据,消费者裁剪本库时不会产生额外告警。 注意该设置只开启分析器与标记,不会让库自身被 AOT 编译(类库不支持 PublishAot), 真正的 AOT 编译只由最终的可执行项目触发。

测试项目另通过 <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault> 关闭了 System.Text.Json 的反射回退;CacheStore 与 JsonDocumentStore<T> 的全部读写测试都在该模式下运行。 一旦误用反射序列化,测试会立即失败,而不是等到 AOT 运行时才抛 Reflection-based serialization has been disabled for this application.。

Encoder 需经由 options 实例传递(易踩)

[JsonSourceGenerationOptions] 没有 Encoder 参数(写了会 CS0246); 「非 ASCII 不转义」只能通过构造上下文时传入的 JsonSerializerOptions 实例实现, 且该实例必须静态复用(上下文按 options 实例缓存元数据,首次使用后不可再修改):

private static readonly JsonSerializerOptions JsonOptions = new()
{
    WriteIndented = true,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

private static readonly CacheJsonContext JsonContext = new(JsonOptions);

约定

  • 启用 ImplicitUsings 与 Nullable(见 Lunarium.Common.csproj)。
  • 命名空间根为 Lunarium.Common,子模块使用 Lunarium.Common.<Module>;目录结构与命名空间对齐, 即 src/Lunarium.Common/<Module>/Xxx.cs 对应 namespace Lunarium.Common.<Module>;。
  • 新增模块时只在 src/Lunarium.Common/ 下建一个同名目录,不新建项目。
  • 代码统一使用文件范围命名空间(file-scoped namespace)与中文 XML 文档注释。
  • 命名空间不得与其中的类型同名(例如曾经的 Lunarium.Common.CacheStore 命名空间 + CacheStore 类型)。 否则任何位于 Lunarium.Common.* 之下的代码引用该类型时会报 CS0118: 'X' is a namespace but is used like a type: 简单名查找会自内向外逐层查找命名空间成员,在 Lunarium.Common 这层先撞上同名命名空间后 就不再继续看 using 导入的同名类型了。该模块命名空间因此改为 Lunarium.Common.Caching。
  • 改动涉及 JSON 序列化时,先读上面的「AOT / 裁剪兼容」一节。
  • 涉及落盘路径时,先读「缓存落盘位置」一节:路径约定只在库内维护一处,测试一律注入临时目录。

许可证

本项目基于 Apache License 2.0 授权,Copyright © 2026 Cyanflower。

在遵守许可条款的前提下,可用于商业与非商业项目;再分发时请保留 LICENSE 与本文件的版权声明。

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  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.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

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.4.0 39 10/22/2026
1.3.0 45 10/22/2026
1.2.1 82 10/21/2026
1.2.0 79 10/20/2026
1.1.0 100 10/15/2026
1.0.0 86 10/14/2026