K-ordered, semi-random, distributed unique Id generator using base 36. Solves several weaknesses of integer, Guid and SequentialGuid identifiers.
$ dotnet add package Funcular.IdGeneratorsA cross-process thread-safe C# utility to create ordered (but non-sequential), human speakable, case-insensitive, partially random (non-guessable) identifiers in Base36. Identifiers are composed of (in this order), a timestamp component, a server hash component, an optional number of reserved characters, and a random component. Note: Source for the ExtensionMethods NuGet package dependency is available at Funcular.ExtensionMethods.
{7331d71b-d1f1-443b-97f6-f24eeb207828}040VKZ3C60SL3B1Z2RW5 or 040VK-Z3C60-SL3B1-Z2RW5Create a generator instance by passing the lengths of the various components, plus any desired delimiter character and layout (optional), to the constructor. To generate Ids, simply call NewId() for a plain identifier or NewId(true) for a delimited one. The class is thread-safe, so your DI container can share a single instance across the entire app domain. See the Wiki for a complete multithreaded stress and performance test.
var generator = new Base36IdGenerator(
numTimestampCharacters: 12,
numServerCharacters: 6,
numRandomCharacters: 7,
reservedValue: "",
delimiter: "-",
delimiterPositions: new[] {20, 15, 10, 5})
Console.WriteLine(generator.NewId());
// "00E4WG2E7NMXEMFY919O2PIHS"
Console.WriteLine(generator.NewId(delimited: true));
// "00E4W-G2GTO-0IEMF-Y911Q-KJI8E"
...your wish list here...
Ids are composed of some combination of a timestamp, a server hash, a reserved character group (optional), and a random component.
{7331d71b-d1f1-443b-97f6-f24eeb207828}040VZ3C6SL3BZ2RW or 040V-Z3C6-SL3B-Z2RW
040VZ-C6SL0-1003B-Z00R2
040VZ-C6SL0-1003B-Z00R2-01KR4
[TestClass]
public class IdGenerationTests
{
private Base36IdGenerator _idGenerator;
[TestInitialize]
public void Setup()
{
this._idGenerator = new Base36IdGenerator(
numTimestampCharacters: 11,
numServerCharacters: 5,
numRandomCharacters: 4,
reservedValue: "",
delimiter: "-",
// give the positions in reverse order if you
// don't want to have to account for modifying
// the loop internally. To do the same in ascending
// order, you would need to pass 5, 11, 17 instead.
delimiterPositions: new[] {15, 10, 5});
}
[TestMethod]
public void TestIdsAreAscending()
{
string id1 = this._idGenerator.NewId();
string id2 = this._idGenerator.NewId();
Assert.IsTrue(String.Compare(id2, id1, StringComparison.OrdinalIgnoreCase) > 0);
}
[TestMethod]
public void TestIdLengthsAreAsExpected()
{
// These are the segment lengths passed to the constructor:
int expectedLength = 11 + 5 + 0 + 4;
string id = this._idGenerator.NewId();
Assert.AreEqual(id.Length, expectedLength);
// Should include 3 delimiter dashes when called with (true):
id = this._idGenerator.NewId(true);
Assert.AreEqual(id.Length, expectedLength + 3);
}