Skip to content

Custom ID Generators

Verified by tests

WhizbangIdProviderTests, Uuid7IdProviderTests, IWhizbangIdProviderGenericTests, WhizbangIdServiceCollectionExtensionsTests — library CI run #31657041675 (2026-08-13)

Custom ID generators provide alternative ID schemes beyond the default UUIDv7. Whizbang's extension point is the IWhizbangIdProvider interface - implement it to plug Snowflake IDs, ULID, sequential test IDs, or any custom strategy into every WhizbangId type.

:::note Whizbang uses UUIDv7 by default (via Uuid7IdProvider, which calls TrackedGuid.NewMedo()) for time-ordered, database-friendly IDs with sub-millisecond precision. Custom generators are for specialized scenarios. :::


The Extension Point: IWhizbangIdProvider

IWhizbangIdProvider Interface

namespace Whizbang.Core;

// Global provider - customizes ID generation for all WhizbangId types
public interface IWhizbangIdProvider {
  TrackedGuid NewGuid();
}

// Strongly-typed provider - resolve per WhizbangId type from DI
public interface IWhizbangIdProvider<TId> where TId : struct {
  TId NewId();
}

Key points:

  • NewGuid() returns a TrackedGuid - a Guid wrapper carrying metadata about how the value was generated (source, precision). Wrap external values with TrackedGuid.FromExternal(guid).
  • The default implementation is Uuid7IdProvider, which returns TrackedGuid.NewMedo() (UUIDv7, time-ordered).
  • Typed IWhizbangIdProvider<TId> implementations are source-generated for every [WhizbangId] type and delegate to the configured global provider.

Configuring a Custom Provider

Provider Registration

// Program.cs - set the global provider before any IDs are generated
services.ConfigureWhizbangIdProvider(new MyCustomIdProvider());

// Or without DI:
WhizbangIdProvider.SetProvider(new MyCustomIdProvider());

// Register the generated typed providers (IWhizbangIdProvider<TId>) in DI
services.AddWhizbangIdProviders();

Typed Provider Usage

public class OrderService {
  private readonly IWhizbangIdProvider<OrderId> _idProvider;

  public OrderService(IWhizbangIdProvider<OrderId> idProvider) {
    _idProvider = idProvider;
  }

  public Order CreateOrder() {
    var orderId = _idProvider.NewId();  // Type-safe, uses the configured strategy
    return new Order { Id = orderId };
  }
}

Why Custom ID Generators?

ID Scheme Benefits Trade-offs
UUIDv7 (default) Time-ordered, standard 128-bit size
Snowflake 64-bit, Twitter-scale Requires clock sync
ULID Lexicographically sortable Custom parsing
Sequential (testing) Deterministic tests Not production-safe

When to use custom IDs: - ✅ Deterministic IDs in tests - ✅ Specific ordering needs - ✅ Legacy system compatibility - ✅ Custom collision resistance


Snowflake ID Generator

Pattern 1: Twitter Snowflake

Pattern 1: Twitter Snowflake

public class SnowflakeIdGenerator {
  private readonly long _epoch = 1_640_995_200_000L;  // Jan 1, 2022
  private readonly long _machineId;
  private readonly object _lock = new();
  private long _sequence = 0L;
  private long _lastTimestamp = -1L;

  public SnowflakeIdGenerator(long machineId) {
    if (machineId < 0 || machineId > 1023) {
      throw new ArgumentException("Machine ID must be 0-1023");
    }
    _machineId = machineId;
  }

  public long NextId() {
    lock (_lock) {
      var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

      if (timestamp < _lastTimestamp) {
        throw new InvalidOperationException("Clock moved backwards");
      }

      if (timestamp == _lastTimestamp) {
        _sequence = (_sequence + 1) & 4095;  // 12-bit sequence
        if (_sequence == 0) {
          // Sequence overflow - wait for next millisecond
          timestamp = WaitNextMillis(_lastTimestamp);
        }
      } else {
        _sequence = 0;
      }

      _lastTimestamp = timestamp;

      // 41 bits: timestamp | 10 bits: machine | 12 bits: sequence
      return ((timestamp - _epoch) << 22) |
             (_machineId << 12) |
             _sequence;
    }
  }

  private long WaitNextMillis(long lastTimestamp) {
    var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    while (timestamp <= lastTimestamp) {
      timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    }
    return timestamp;
  }
}

Plugging It into Whizbang

Pack the 64-bit Snowflake value into a Guid and wrap it with TrackedGuid.FromExternal:

Snowflake Whizbang Provider

public class SnowflakeWhizbangIdProvider : IWhizbangIdProvider {
  private readonly SnowflakeIdGenerator _generator;

  public SnowflakeWhizbangIdProvider(long machineId) {
    _generator = new SnowflakeIdGenerator(machineId);
  }

  public TrackedGuid NewGuid() {
    Span<byte> bytes = stackalloc byte[16];
    BitConverter.TryWriteBytes(bytes, _generator.NextId());
    return TrackedGuid.FromExternal(new Guid(bytes));
  }
}

// Registration
services.ConfigureWhizbangIdProvider(new SnowflakeWhizbangIdProvider(machineId: 42));

:::warning Non-UUIDv7 schemes lose the time-ordering guarantees that Whizbang's event ordering and database indexing are tuned for. Ensure your custom scheme is still monotonically increasing per generator if events rely on ID ordering. :::


ULID Generator

Pattern 2: Universally Unique Lexicographically Sortable ID

Pattern 2: Universally Unique Lexicographically Sortable ID

// Ulid struct from the "Ulid" NuGet package (Cysharp)
public class UlidWhizbangIdProvider : IWhizbangIdProvider {
  public TrackedGuid NewGuid() {
    // ULID is 128-bit and time-ordered - converts cleanly to Guid
    return TrackedGuid.FromExternal(Ulid.NewUlid().ToGuid());
  }
}

Usage: ULID Provider Usage

services.ConfigureWhizbangIdProvider(new UlidWhizbangIdProvider());

var id = WhizbangIdProvider.NewGuid();  // TrackedGuid backed by a ULID

Further Reading

Core Concepts: - Message Context - MessageId, CorrelationId - WhizbangIds - Strongly-typed ID types and the [WhizbangId] attribute


Version 1.0.0 - Foundation Release | Last Updated: 2026-07-16