Sparkdo.IdGeneration 3.0.0-preview.1

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

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 仍按默认注册

常见坑

  1. ToByteArray() 排序:无参重载是小端字段序,按它排序不等于时间序。要字节序排序请用 ToByteArray(bigEndian: true)(或 "N" 字符串的序数比较)。
  2. 指望同毫秒有序:ULID 与 v7 都不保证;需要它请改用 Snowflake,或在自己的写入侧加序号。
  3. 只覆盖一个服务形态:只注册 IGuidGenerator 时,注入 IIdGenerator<Guid> 的地方仍拿到默认实现(反之亦然)。
  4. 用内置名做 keyed 注册:AddKeyedSingleton<IIdGenerator<Guid>>(…, "guid") 读不到,名称被短路。
  5. Get<long>() 未装选装包:抛的是 DI InvalidOperationException,不是「名称未知」的 KeyNotFoundException——前者说明没注册,后者说明名字写错。
  6. 把 string Id 当不透明定长 26 处理时注意校验:Ordinal 比较与截断前缀查询(前 10 字符 = 同一毫秒)都依赖字符集与大写输出。
  7. 时钟回拨:ULID 与 v7 都不做单调保护。NTP 回拨时可能生成时间戳更小的 Id——字典序会把它排到前面,业务上表现为「新 Id 排在旧 Id 之前」。若不能容忍,用 Snowflake(回拨时 fail closed 抛异常)或在写入侧加序号。
  8. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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