Sparkdo.IdGeneration
3.0.0-preview.1
dotnet add package Sparkdo.IdGeneration --version 3.0.0-preview.1
NuGet\Install-Package Sparkdo.IdGeneration -Version 3.0.0-preview.1
<PackageReference Include="Sparkdo.IdGeneration" Version="3.0.0-preview.1" />
<PackageVersion Include="Sparkdo.IdGeneration" Version="3.0.0-preview.1" />
<PackageReference Include="Sparkdo.IdGeneration" />
paket add Sparkdo.IdGeneration --version 3.0.0-preview.1
#r "nuget: Sparkdo.IdGeneration, 3.0.0-preview.1"
#:package Sparkdo.IdGeneration@3.0.0-preview.1
#addin nuget:?package=Sparkdo.IdGeneration&version=3.0.0-preview.1&prerelease
#tool nuget:?package=Sparkdo.IdGeneration&version=3.0.0-preview.1&prerelease
Sparkdo.IdGeneration
Sparkdo.IdGeneration 是 Id 生成域的运行时核心包:Guid 用 UUID v7(Guid.CreateVersion7())、string 用 26 位 Crockford Base32 ULID(48-bit Unix 毫秒 + 80-bit CSPRNG 随机值),外加按类型/名称解析的 DefaultIdGeneratorProvider 与一行 DI 接线 AddSparkdoIdGeneration()。net11.0 分支零外部 NuGet 依赖(DI/Options 抽象已内置于平台;net10.0 分支实测只多一条 Microsoft.Extensions.DependencyInjection.Abstractions),无反射、无程序集扫描,Native AOT / trimming 可用,也可完全脱离 DI 直接 new。
long(Snowflake)不在本包:由选装包 Sparkdo.IdGeneration.Snowflake 提供(它依赖分布式锁租约分配 worker id)。只装本包时解析 long 生成器抛 DI 标准异常。
快速开始
builder.Services.AddSparkdoIdGeneration();
var app = builder.Build();
var guid = app.Services.GetRequiredService<IGuidGenerator>().Create(); // 01a0fd6c-2363-7507-a36e-2135ff13a318
var ulid = app.Services.GetRequiredService<IStringIdGenerator>().Create(); // 01KXCFXN606DXYTSYZQW8TVXXR
// 或者一个入口按类型取
var ids = app.Services.GetRequiredService<IIdGeneratorProvider>();
var g = ids.Get<Guid>().Create();
var s = ids.Get<string>("ulid").Create();
不用 DI:
using Sparkdo.IdGeneration;
var gids = new Version7GuidGenerator();
var sids = new UlidStringIdGenerator();
AddSparkdoIdGeneration() 注册了什么
| 服务 | 实现 | Lifetime | 注册方式 |
|---|---|---|---|
IGuidGenerator |
Version7GuidGenerator |
Singleton | TryAdd |
IIdGenerator<Guid> |
Version7GuidGenerator |
Singleton | TryAdd |
IStringIdGenerator |
UlidStringIdGenerator |
Singleton | TryAdd |
IIdGenerator<string> |
UlidStringIdGenerator |
Singleton | TryAdd |
IIdGeneratorProvider |
DefaultIdGeneratorProvider |
Transient | TryAdd |
- 两个服务形态指向同一类型:
TryAdd按服务类型判重,所以IGuidGenerator与IIdGenerator<Guid>是两条描述符、两个独立实例(各自 Singleton)。要彻底替换Guid生成器,两个服务类型都得覆盖。 - 全部
TryAdd,调用顺序敏感的是「谁先注册」:宿主先注册的实现优先,默认实现让位。重复调用本方法幂等(描述符数量不变)。 - 方法返回入参
IServiceCollection,可链式调用;services为null抛ArgumentNullException。 - 本包不注册
ILongIdGenerator/IIdGenerator<long>,也不引用任何 Snowflake 类型。 IIdGeneratorProvider是 Transient,但它只包一个IServiceProvider,无状态可共享。装了Sparkdo.IdGeneration.Snowflake时该包也会TryAdd同一个 provider,两侧各注册一次不冲突,调用顺序不影响结果。
Version7GuidGenerator
public sealed class Version7GuidGenerator : IGuidGenerator
{
public Guid Create() => Guid.CreateVersion7();
}
无状态、无构造参数、线程安全(可直接当静态实例并发调用)。
| 方面 | 语义(实测) |
|---|---|
| 布局 | 48-bit Unix 毫秒时间戳 + 4-bit version(=7) + 12-bit rand_a + 2-bit variant(=10) + 62-bit rand_b,共 128 bit |
| 时间有序粒度 | 毫秒。连续 30 万次调用中时间戳分量从未回退 |
| 同毫秒内有序 | ❌ 不保证。尾段是 CSPRNG 随机值——实测同毫秒相邻对约 50% 呈降序 |
| 排序 | 大端 16 字节序 == "N"/"D" 字符串的序数比较(实测 5 万对随机组合零分歧)。ToByteArray() 无参重载是 Windows 小端字段序,不能用来排序;取可排序字节用 ToByteArray(bigEndian: true) 或 TryWriteBytes(span, bigEndian: true, out var written) |
| 吞吐 | 100 万次约 90 ms(~1100 万/s,Release,单机) |
| 依赖前提 | 只需系统 UTC 时钟;无协调、无配置 |
反解创建时刻:
var b = guid.ToByteArray(bigEndian: true);
var ms = ((long)b[0] << 40) | ((long)b[1] << 32) | ((long)b[2] << 24)
| ((long)b[3] << 16) | ((long)b[4] << 8) | b[5];
var createdAt = DateTimeOffset.FromUnixTimeMilliseconds(ms);
需要「同一毫秒内严格递增」的 v7 时,本包不提供(非目标);跨毫秒有序通常已足够,若必须同毫秒有序请用 Snowflake(12-bit sequence 保证同毫秒递增)。
UlidStringIdGenerator
public sealed class UlidStringIdGenerator : IStringIdGenerator
{
public UlidStringIdGenerator(); // 公共:系统时钟
internal UlidStringIdGenerator(TimeProvider timeProvider); // 仅测试程序集可见
}
26 字符 Crockford Base32 ULID:
| 前 10 字符 = 48-bit Unix 毫秒 | 后 16 字符 = 80-bit CSPRNG 随机值 |
字母表:0123456789ABCDEFGHJKMNPQRSTVWXYZ(排除 I、L、O、U)
| 方面 | 语义(实测) |
|---|---|
| 长度与字符集 | 恒 26 字符,全部落在上述字母表内(10 万次抽样验证) |
| 时间前缀 | 同一毫秒生成的 Id 前 10 字符完全相同;由前 10 字符可反解毫秒(见下) |
| 跨毫秒有序 | ✅ 字典序即时间序(实测同毫秒 10 万个 Id 全部小于下一毫秒的 Id) |
| 同毫秒有序 | ❌ 不保证(不做单调尾递增);实测约 50% 降序 |
| 同毫秒唯一性 | 80-bit 随机。实测同毫秒 10 万次调用全部互异;按生日界 p ≈ n²/2⁸¹,单毫秒 100 万个 Id 时碰撞概率约 4×10⁻¹³ |
| 时间戳范围 | 0 ≤ ms ≤ 2⁴⁸-1(约公元 10889 年)。早于 1970 抛 InvalidOperationException("ULID 时间戳超出 48-bit 范围。");实践中会先撞上 DateTimeOffset 自身 9999-12-31 上限(该值仍可正常生成) |
| 分配 | stackalloc 中转(16 字节 + 26 字符),每次只额外分配结果 string |
| 吞吐 | 100 万次约 210 ms(~460 万/s,Release,单机);与 v7 的差额来自每次 10 字节 CSPRNG 取随机 |
| 时钟注入 | ❌ 公共面不提供。接受 TimeProvider 的构造函数是 internal(仅 Sparkdo.IdGeneration.Tests)。测试里要固定时钟请实现 IStringIdGenerator 自己注册,或让被测代码接受 Func<string> |
反解创建时刻:
const string Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
long ms = 0;
foreach (var c in ulid.AsSpan(0, 10))
{
ms = (ms << 5) + Alphabet.IndexOf(c);
}
var createdAt = DateTimeOffset.FromUnixTimeMilliseconds(ms);
大小写敏感:输出为大写。用 StringComparer.Ordinal 排序即时间序;若持久化时被统一转成小写,字典序仍与时间序一致(字母表相对顺序不变),但不要用 StringComparer.OrdinalIgnoreCase 之外的区域敏感比较器。
DefaultIdGeneratorProvider
public sealed class DefaultIdGeneratorProvider(IServiceProvider serviceProvider) : IIdGeneratorProvider
{
public IIdGenerator<TId> Get<TId>(string? name = null);
}
解析决策顺序(IdGeneratorProviderTests / 接线测试逐项守卫):
| 输入 | 解析到 | 未注册时 |
|---|---|---|
name 为 null / 空串 / 全空白 |
Guid→IGuidGenerator,string→IStringIdGenerator,long→ILongIdGenerator,其他→IIdGenerator<TId> |
InvalidOperationException(DI 的 No service for type '…' has been registered.) |
内置名且与 TId 匹配(大小写不敏感) |
同上("GUID"、"Ulid"、"SnowFlake" 均命中) |
同上 |
内置名但与 TId 不匹配 |
不跨类型短路,走 keyed 分支 | KeyNotFoundException("未知 Id 生成器:System.Guid:ulid。") |
其他 name |
GetKeyedService<IIdGenerator<TId>>(name) |
KeyNotFoundException("未知 Id 生成器:System.Int32:order-no。") |
// 接入自己的具名实现
services.AddKeyedSingleton<IIdGenerator<int>, OrderNoGenerator>("order-no");
services.AddKeyedSingleton<IIdGenerator<string>, ShortCodeGenerator>("short-code");
var provider = app.Services.GetRequiredService<IIdGeneratorProvider>();
_ = provider.Get<int>("order-no").Create();
_ = provider.Get<string>("short-code").Create();
_ = provider.Get<string>("ulid").Create(); // 内置名 → UlidStringIdGenerator
注意三点:
- 内置名是保留字:
guid/ulid/snowflake及其大小写变体被短路到专用接口,用它们做 keyed 名称的注册永远读不到。 - 每次
Get都走一次容器解析,不做缓存;生成器本体是 Singleton,所以重复Get返回同一实例(测试以ShouldBeSameAs钉住)。 Get<long>()只解析ILongIdGenerator接口,因此本包在未装 Snowflake 包时的失败是标准 DI 异常,而不是自定义异常类型。
替换与扩展
| 想做的事 | 做法 |
|---|---|
换 Guid 算法(如需要可预测 Id、或 v4) |
在 AddSparkdoIdGeneration() 之前注册 IGuidGenerator 与 IIdGenerator<Guid> 两个服务类型 |
换 string 算法(如短码、前缀 Id) |
同上,覆盖 IStringIdGenerator 与 IIdGenerator<string> |
| 增加按名称并存的第二套策略 | AddKeyedSingleton<IIdGenerator<TId>, TImpl>("name"),用 provider.Get<TId>("name") 取 |
| 只要契约不要默认实现 | 只引用 Sparkdo.IdGeneration.Abstractions,自行注册实现 |
| 在测试里固定时钟产 ULID | 实现 IStringIdGenerator(本包 internal ctor 不可用于生产代码) |
加 long 能力 |
引用 Sparkdo.IdGeneration.Snowflake 并调 AddSparkdoIdGenerationSnowflake() |
// 覆盖默认 Guid 生成器:两个服务形态都要注册
services.AddSingleton<IGuidGenerator, MyGuidGenerator>();
services.AddSingleton<IIdGenerator<Guid>>(sp => sp.GetRequiredService<IGuidGenerator>());
services.AddSparkdoIdGeneration(); // TryAdd 让位,ULID 与 provider 仍按默认注册
常见坑
ToByteArray()排序:无参重载是小端字段序,按它排序不等于时间序。要字节序排序请用ToByteArray(bigEndian: true)(或"N"字符串的序数比较)。- 指望同毫秒有序:ULID 与 v7 都不保证;需要它请改用 Snowflake,或在自己的写入侧加序号。
- 只覆盖一个服务形态:只注册
IGuidGenerator时,注入IIdGenerator<Guid>的地方仍拿到默认实现(反之亦然)。 - 用内置名做 keyed 注册:
AddKeyedSingleton<IIdGenerator<Guid>>(…, "guid")读不到,名称被短路。 Get<long>()未装选装包:抛的是 DIInvalidOperationException,不是「名称未知」的KeyNotFoundException——前者说明没注册,后者说明名字写错。- 把
stringId 当不透明定长 26 处理时注意校验:Ordinal比较与截断前缀查询(前 10 字符 = 同一毫秒)都依赖字符集与大写输出。 - 时钟回拨:ULID 与 v7 都不做单调保护。NTP 回拨时可能生成时间戳更小的 Id——字典序会把它排到前面,业务上表现为「新 Id 排在旧 Id 之前」。若不能容忍,用 Snowflake(回拨时 fail closed 抛异常)或在写入侧加序号。
- ULID 大小写规范化:如果某层管道把 Id 转小写,
Ordinal排序仍与时间一致(字母表内部相对顺序不变),但混用大小写会破坏相等性比较。请全程Ordinal一致。
边界
- 本包只提供
Guid与string两种默认算法;long、号段、数据库自增、Redis 计数器一概没有。 - 不做 Id 解析辅助 API(无
TryGetTimestamp、无 worker id 提取)——反解公式见上文,可复制到自己的代码里。 - 不提供批量
CreateMany、不提供异步生成器、不提供配置项(本包无 Options 类型)。 - 不承诺同毫秒单调、不承诺 Id 不可预测(时间戳明文可见)。
- 不注册
IDistributedLock、不依赖 Hosting、不做启动钩子。
验证
dotnet test src/id-generation/Sparkdo.IdGeneration.slnx
dotnet pack src/id-generation/Sparkdo.IdGeneration.slnx -c Release
本文即包内 README.md(PackageReadmeFile,实测随 nupkg 发布),并与 Sparkdo.IdGeneration.xml 一起提供 IntelliSense。因此文中只以包名明文引用其他包,不使用相对路径链接。
安装
dotnet add package Sparkdo.IdGeneration
需要 long Id 时再加 dotnet add package Sparkdo.IdGeneration.Snowflake。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. net11.0 is compatible. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
- Sparkdo.IdGeneration.Abstractions (>= 3.0.0-preview.1)
-
net11.0
- Sparkdo.IdGeneration.Abstractions (>= 3.0.0-preview.1)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Sparkdo.IdGeneration:
| Package | Downloads |
|---|---|
|
Sparkdo.Ddd.Domain
Sparkdo DDD 领域模型库,提供领域驱动设计的核心组件实现 |
|
|
Sparkdo.BackgroundJobs
Sparkdo 后台作业库,提供后台任务调度和执行功能实现 |
|
|
Sparkdo.IdGeneration.Snowflake
Sparkdo 的 Snowflake long Id 生成运行时:worker lease、逻辑时间状态与 DI 接线。 |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-preview.1 | 63 | 10/30/2026 |
| 2.0.0-preview.2 | 88 | 7/20/2026 |
| 2.0.0-preview.1 | 81 | 7/18/2026 |
| 1.0.4-preview.3 | 224 | 6/8/2026 |
| 1.0.4-preview.2 | 224 | 6/7/2026 |
| 1.0.4-preview.1 | 231 | 6/7/2026 |
| 1.0.3 | 546 | 6/6/2026 |
| 1.0.3-preview.1 | 222 | 5/17/2026 |
| 1.0.2 | 387 | 4/27/2026 |
| 1.0.2-preview.5 | 141 | 4/15/2026 |
| 1.0.2-preview.4 | 141 | 2/8/2026 |
| 1.0.2-preview.3 | 139 | 2/1/2026 |
| 1.0.2-preview.2 | 160 | 1/31/2026 |
| 1.0.2-preview.1 | 229 | 12/4/2025 |
| 1.0.1 | 1,098 | 11/27/2025 |
| 1.0.0 | 1,044 | 11/25/2025 |
| 1.0.0-preview.5 | 193 | 10/24/2025 |