├── .gitattributes
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .nuget
├── NuGet.Config
├── NuGet.exe
└── NuGet.targets
├── .vs
├── ProjectSettings.json
├── VSWorkspaceState.json
└── slnx.sqlite
├── Funcular.IdGenerators.PerformanceTests
├── App.config
├── Funcular.IdGenerators.PerformanceTests.csproj
├── PerformanceTests.cs
├── Program.cs
├── Properties
│ └── AssemblyInfo.cs
└── funcular-logo-angle-brackets-dark.ico
├── Funcular.IdGenerators.UnitTests
├── Funcular.IdGenerators.UnitTests.csproj
├── IdGenerationTests.cs
└── Properties
│ └── AssemblyInfo.cs
├── Funcular.IdGenerators.sln
├── Funcular.IdGenerators
├── Base36
│ ├── Base36IdGenerator.cs
│ ├── ConcurrentStopwatch.cs
│ └── IdInformation.cs
├── BaseConversion
│ ├── Base36Converter.cs
│ └── BaseConverter.cs
├── ConcurrentRandom.cs
├── Enums
│ └── TimestampResolution.cs
├── Funcular.IdGenerators.csproj
├── Properties
│ └── AssemblyInfo.cs
├── README.md
├── funcular-logo-angle-brackets-dark.ico
└── funcular-logo-angle-brackets-dark.png
├── NuGet
├── Funcular.IdGenerators.0.0.7.1.nupkg
├── Funcular.IdGenerators.0.0.7.2.nupkg
├── Funcular.IdGenerators.0.0.7.3.nupkg
├── Funcular.IdGenerators.0.0.8.0.nupkg
├── Funcular.IdGenerators.0.0.9.0.nupkg
├── Funcular.IdGenerators.0.5.0.0.nupkg
├── Funcular.IdGenerators.0.5.0.1.nupkg
├── Funcular.IdGenerators.0.5.0.2.nupkg
├── Funcular.IdGenerators.1.1.0.nupkg
├── Funcular.IdGenerators.2.0.nupkg
├── Funcular.IdGenerators.2.1.nupkg
└── Funcular.IdGenerators.2.5.nupkg
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/.github/workflows/build.yml
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.sln.docstates
8 |
9 | # Build results
10 |
11 | [Dd]ebug/
12 | [Rr]elease/
13 | x64/
14 | build/
15 | [Bb]in/
16 | [Oo]bj/
17 |
18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
19 | !packages/*/build/
20 |
21 | # MSTest test Results
22 | [Tt]est[Rr]esult*/
23 | [Bb]uild[Ll]og.*
24 |
25 | *_i.c
26 | *_p.c
27 | *.ilk
28 | *.meta
29 | *.obj
30 | *.pch
31 | *.pdb
32 | *.pgc
33 | *.pgd
34 | *.rsp
35 | *.sbr
36 | *.tlb
37 | *.tli
38 | *.tlh
39 | *.tmp
40 | *.tmp_proj
41 | *.log
42 | *.vspscc
43 | *.vssscc
44 | .builds
45 | *.pidb
46 | *.log
47 | *.scc
48 |
49 | # Visual C++ cache files
50 | ipch/
51 | *.aps
52 | *.ncb
53 | *.opensdf
54 | *.sdf
55 | *.cachefile
56 |
57 | # Visual Studio profiler
58 | *.psess
59 | *.vsp
60 | *.vspx
61 |
62 | # Guidance Automation Toolkit
63 | *.gpState
64 |
65 | # ReSharper is a .NET coding add-in
66 | _ReSharper*/
67 | *.[Rr]e[Ss]harper
68 |
69 | # TeamCity is a build add-in
70 | _TeamCity*
71 |
72 | # DotCover is a Code Coverage Tool
73 | *.dotCover
74 |
75 | # NCrunch
76 | *.ncrunch*
77 | .*crunch*.local.xml
78 |
79 | # Installshield output folder
80 | [Ee]xpress/
81 |
82 | # DocProject is a documentation generator add-in
83 | DocProject/buildhelp/
84 | DocProject/Help/*.HxT
85 | DocProject/Help/*.HxC
86 | DocProject/Help/*.hhc
87 | DocProject/Help/*.hhk
88 | DocProject/Help/*.hhp
89 | DocProject/Help/Html2
90 | DocProject/Help/html
91 |
92 | # Click-Once directory
93 | publish/
94 |
95 | # Publish Web Output
96 | *.Publish.xml
97 |
98 | # NuGet Packages Directory
99 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
100 | #packages/
101 |
102 | # Windows Azure Build Output
103 | csx
104 | *.build.csdef
105 |
106 | # Windows Store app package directory
107 | AppPackages/
108 |
109 | # Others
110 | sql/
111 | *.Cache
112 | ClientBin/
113 | [Ss]tyle[Cc]op.*
114 | ~$*
115 | *~
116 | *.dbmdl
117 | *.[Pp]ublish.xml
118 | *.pfx
119 | *.publishsettings
120 |
121 | # RIA/Silverlight projects
122 | Generated_Code/
123 |
124 | # Backup & report files from converting an old project file to a newer
125 | # Visual Studio version. Backup files are not needed, because we have git ;-)
126 | _UpgradeReport_Files/
127 | Backup*/
128 | UpgradeLog*.XML
129 | UpgradeLog*.htm
130 |
131 | # SQL Server files
132 | App_Data/*.mdf
133 | App_Data/*.ldf
134 |
135 |
136 | #LightSwitch generated files
137 | GeneratedArtifacts/
138 | _Pvt_Extensions/
139 | ModelManifest.xml
140 |
141 | # =========================
142 | # Windows detritus
143 | # =========================
144 |
145 | # Windows image file caches
146 | Thumbs.db
147 | ehthumbs.db
148 |
149 | # Folder config file
150 | Desktop.ini
151 |
152 | # Recycle Bin used on file shares
153 | $RECYCLE.BIN/
154 |
155 | # Mac desktop service store files
156 | .DS_Store
157 | /.vs/Funcular.IdGenerators
158 | /.vs/*
--------------------------------------------------------------------------------
/.nuget/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.nuget/NuGet.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/.nuget/NuGet.exe
--------------------------------------------------------------------------------
/.nuget/NuGet.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildProjectDirectory)\..\
5 |
6 |
7 | false
8 |
9 |
10 | false
11 |
12 |
13 | true
14 |
15 |
16 | false
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget"))
31 |
32 |
33 |
34 |
35 | $(SolutionDir).nuget
36 |
37 |
38 |
39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config
40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config
41 |
42 |
43 |
44 | $(MSBuildProjectDirectory)\packages.config
45 | $(PackagesProjectConfig)
46 |
47 |
48 |
49 |
50 | $(NuGetToolsPath)\NuGet.exe
51 | @(PackageSource)
52 |
53 | "$(NuGetExePath)"
54 | mono --runtime=v4.0.30319 "$(NuGetExePath)"
55 |
56 | $(TargetDir.Trim('\\'))
57 |
58 | -RequireConsent
59 | -NonInteractive
60 |
61 | "$(SolutionDir) "
62 | "$(SolutionDir)"
63 |
64 |
65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)
66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols
67 |
68 |
69 |
70 | RestorePackages;
71 | $(BuildDependsOn);
72 |
73 |
74 |
75 |
76 | $(BuildDependsOn);
77 | BuildPackage;
78 |
79 |
80 |
81 |
82 |
83 |
84 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
99 |
100 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/.vs/ProjectSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "CurrentProjectSetting": null
3 | }
--------------------------------------------------------------------------------
/.vs/VSWorkspaceState.json:
--------------------------------------------------------------------------------
1 | {
2 | "ExpandedNodes": [
3 | ""
4 | ],
5 | "SelectedNode": "\\Funcular.IdGenerators.sln",
6 | "PreviewInSolutionExplorer": false
7 | }
--------------------------------------------------------------------------------
/.vs/slnx.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/.vs/slnx.sqlite
--------------------------------------------------------------------------------
/Funcular.IdGenerators.PerformanceTests/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators.PerformanceTests/Funcular.IdGenerators.PerformanceTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 | false
7 |
8 | funcular-logo-angle-brackets-dark.ico
9 |
10 | disable
11 |
12 | Exe
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators.PerformanceTests/PerformanceTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Threading;
6 | using Funcular.IdGenerators.Base36;
7 |
8 | namespace Funcular.IdGenerators.PerformanceTests
9 | {
10 | internal class PerformanceTests
11 | {
12 | private readonly Base36IdGenerator _generator = new Base36IdGenerator(11, 4, 5, null, "-", new[] {15, 10, 5});
13 | private readonly HashSet _ids = new HashSet();
14 |
15 | [ThreadStatic]
16 | private static string _newId;
17 |
18 | public PerformanceTests(Base36IdGenerator generator)
19 | {
20 |
21 | }
22 |
23 | internal int TestSingleThreaded(int seconds)
24 | {
25 | string newId = _generator.NewId();
26 | Console.WriteLine($"First Id generated: {newId}");
27 | var hashSet = new HashSet();
28 | var sw = Stopwatch.StartNew();
29 | while (sw.Elapsed.TotalSeconds < seconds)
30 | {
31 | newId = _generator.NewId();
32 | if (!hashSet.Add(newId))
33 | {
34 | throw new InvalidOperationException("Duplicate id!");
35 | }
36 | }
37 | Console.WriteLine($"Last Id generated: {newId}\r\n");
38 | return hashSet.Count;
39 | }
40 |
41 | internal int TestTimestampUniqueness(int seconds, int threads)
42 | {
43 | _ids.Clear();
44 | var source = new CancellationTokenSource();
45 | for (var i = 0; i < threads; i++)
46 | {
47 | ThreadPool.QueueUserWorkItem((MakeIdsMultithreaded), source.Token);
48 | }
49 | Thread.Sleep(TimeSpan.FromSeconds(seconds));
50 | source.Cancel();
51 | Console.WriteLine();
52 | return _ids.Count;
53 | }
54 |
55 | internal int TestMultithreaded(int seconds, int threads)
56 | {
57 | _ids.Clear();
58 | var source = new CancellationTokenSource();
59 | for (var i = 0; i < threads; i++)
60 | {
61 | ThreadPool.QueueUserWorkItem((MakeIdsMultithreaded), source.Token);
62 | }
63 | Thread.Sleep(TimeSpan.FromSeconds(seconds));
64 | source.Cancel();
65 | Console.WriteLine();
66 | return _ids.Count;
67 | }
68 |
69 | private void MakeIdsMultithreaded(object cancellationToken)
70 | {
71 | while (!((CancellationToken)cancellationToken).IsCancellationRequested)
72 | {
73 | _newId = _generator.NewId();
74 | lock (_ids)
75 | {
76 |
77 | if (!_ids.Add(_newId))
78 | {
79 | Console.WriteLine("Current Count: {0}", _ids.Count);
80 | Console.WriteLine("Last Id: {0}", _newId);
81 | Console.WriteLine("ThreadId: {0}", Thread.CurrentThread.ManagedThreadId);
82 | Console.WriteLine("ThreadId of duplicate value: {0}", _newId);
83 |
84 | throw new InvalidOperationException("Duplicate id!");
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators.PerformanceTests/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using Funcular.IdGenerators.Base36;
4 |
5 | namespace Funcular.IdGenerators.PerformanceTests
6 | {
7 | internal class Program
8 | {
9 | private static Base36IdGenerator _generator;
10 | private static PerformanceTests _tests;
11 |
12 | // ReSharper disable once UnusedParameter.Local
13 | private static void Main(string[] args)
14 | {
15 |
16 | var seconds = 20;
17 | Console.WriteLine("Begin performance testing; {0} seconds each async/sync…", seconds);
18 | Console.WriteLine();
19 | _tests = SetupTests();
20 | long asyncAggregate=0;
21 | long syncAggregate = 0;
22 | long timestampAggregate = 0;
23 | // get some initialization out of the way:
24 | RunAsyncTest(5);
25 | Console.Clear();
26 | var includeTimestampTests = args.Length > 0 && args[0].ToUpper() == "T";
27 | int i;
28 | for (i = 0; i < 3; i++)
29 | {
30 | if(includeTimestampTests)
31 | timestampAggregate += RunTimestampTest(seconds);
32 | asyncAggregate += RunAsyncTest(seconds);
33 | syncAggregate += RunSyncTest(seconds);
34 | }
35 | if (includeTimestampTests)
36 | Console.WriteLine("\r\nTimestamp avg: {0:n0}/s", timestampAggregate / i);
37 | Console.WriteLine("\r\nAsync average: {0:n0}/s", asyncAggregate / i);
38 | Console.WriteLine("\r\n Sync average: {0:n0}/s", syncAggregate / i);
39 |
40 | Console.WriteLine("\r\n");
41 | PromptKey("Press any key to exit... ");
42 | }
43 |
44 | private static int RunSyncTest(int seconds)
45 | {
46 | var sw = Stopwatch.StartNew();
47 | var testSingleThreaded = _tests.TestSingleThreaded(seconds);
48 | var count = testSingleThreaded;
49 | sw.Stop();
50 | Console.WriteLine();
51 | var rate = count/sw.Elapsed.Seconds;
52 | Console.WriteLine("Synchronously:\tCreated {0:n0} Ids in {1}; rate {2:n0}/s", count, sw.Elapsed,
53 | rate);
54 | return rate;
55 | }
56 |
57 | private static int RunAsyncTest(int seconds)
58 | {
59 | var processors = Environment.ProcessorCount + 1;
60 | var sw = Stopwatch.StartNew();
61 | var count = _tests.TestMultithreaded(seconds, processors);
62 | sw.Stop();
63 | Console.WriteLine();
64 | var rate = count/sw.Elapsed.Seconds;
65 | Console.WriteLine("Asynchronously:\tCreated {0:n0} Ids in {1}; rate {2:n0}/s", count, sw.Elapsed,
66 | rate);
67 | return rate;
68 | }
69 |
70 | private static int RunTimestampTest(int seconds)
71 | {
72 | var processors = Environment.ProcessorCount + 1;
73 | var sw = Stopwatch.StartNew();
74 | var count = _tests.TestTimestampUniqueness(seconds, processors);
75 | sw.Stop();
76 | Console.WriteLine();
77 | var rate = count / sw.Elapsed.Seconds;
78 | Console.WriteLine("Asynchronously:\tCreated {0:n0} timestamps in {1}; rate {2:n0}/s", count, sw.Elapsed,
79 | rate);
80 | return rate;
81 | }
82 |
83 |
84 | private static PerformanceTests SetupTests()
85 | {
86 | _generator = new Base36IdGenerator(11, 4, 5, null, "-", new[] {15, 10, 5});
87 | var tests = new PerformanceTests(_generator);
88 | return tests;
89 | }
90 |
91 | private static void PromptKey(string prompt)
92 | {
93 | Console.Write(prompt);
94 | Console.ReadKey(true);
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators.PerformanceTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // General Information about an assembly is controlled through the following
5 | // set of attributes. Change these attribute values to modify the information
6 | // associated with an assembly.
7 | [assembly: AssemblyDescription("")]
8 | [assembly: AssemblyCopyright("Copyright © 2015-2016 Paul C Smith and Funcular Labs")]
9 | [assembly: AssemblyTrademark("")]
10 | [assembly: AssemblyCulture("")]
11 |
12 | // Setting ComVisible to false makes the types in this assembly not visible
13 | // to COM components. If you need to access a type in this assembly from
14 | // COM, set the ComVisible attribute to true on that type.
15 | [assembly: ComVisible(false)]
16 |
17 | // The following GUID is for the ID of the typelib if this project is exposed to COM
18 | [assembly: Guid("057d9cc6-e692-4dc9-baf0-12fa04a29d2b")]
19 |
20 | // Version information for an assembly consists of the following four values:
21 | //
22 | // Major Version
23 | // Minor Version
24 | // Build Number
25 | // Revision
26 | //
27 | // You can specify all the values or you can default the Build and Revision Numbers
28 | // by using the '*' as shown below:
29 | // [assembly: AssemblyVersion("1.0.*")]
30 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators.PerformanceTests/funcular-logo-angle-brackets-dark.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/Funcular.IdGenerators.PerformanceTests/funcular-logo-angle-brackets-dark.ico
--------------------------------------------------------------------------------
/Funcular.IdGenerators.UnitTests/Funcular.IdGenerators.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net8.0
4 |
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators.UnitTests/IdGenerationTests.cs:
--------------------------------------------------------------------------------
1 | #region File info
2 |
3 | // *********************************************************************************************************
4 | // Funcular.IdGenerators>Funcular.IdGenerators.UnitTests>IdGenerationTests.cs
5 | // Created: 2015-06-29 12:32 PM
6 | // Updated: 2015-06-29 4:13 PM
7 | // By: Paul Smith
8 | //
9 | // *********************************************************************************************************
10 | // LICENSE: The MIT License (MIT)
11 | // *********************************************************************************************************
12 | // Copyright (c) 2010-2015
13 | //
14 | // Permission is hereby granted, free of charge, to any person obtaining a copy
15 | // of this software and associated documentation files (the "Software"), to deal
16 | // in the Software without restriction, including without limitation the rights
17 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 | // copies of the Software, and to permit persons to whom the Software is
19 | // furnished to do so, subject to the following conditions:
20 | //
21 | // The above copyright notice and this permission notice shall be included in
22 | // all copies or substantial portions of the Software.
23 | //
24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 | // THE SOFTWARE.
31 | //
32 | // *********************************************************************************************************
33 |
34 | #endregion
35 |
36 |
37 |
38 | #region Usings
39 |
40 | using System;
41 | using System.Collections.Concurrent;
42 | using System.Diagnostics;
43 | using System.Threading;
44 | using System.Threading.Tasks;
45 | using Funcular.IdGenerators.Base36;
46 | using Funcular.IdGenerators.Enums;
47 | using Microsoft.VisualStudio.TestTools.UnitTesting;
48 |
49 | #endregion
50 |
51 |
52 | // ReSharper disable RedundantArgumentDefaultValue
53 | namespace Funcular.IdGenerators.UnitTests
54 | {
55 | [TestClass]
56 | public class IdGenerationTests
57 | {
58 | private Base36IdGenerator _idGenerator;
59 | private string _delimiter;
60 | private int[] _delimiterPositions;
61 |
62 | [TestInitialize]
63 | public void Setup()
64 | {
65 | this._delimiter = "-";
66 | this._delimiterPositions = new[] {15, 10, 5};
67 | this._idGenerator = new Base36IdGenerator(
68 | numTimestampCharacters: 11,
69 | numServerCharacters: 5,
70 | numRandomCharacters: 4,
71 | reservedValue: "",
72 | delimiter: this._delimiter,
73 | // give the positions in reverse order if you
74 | // don't want to have to account for modifying
75 | // the loop internally. To do the same in ascending
76 | // order, you would need to pass 5, 11, 17:
77 | delimiterPositions: this._delimiterPositions);
78 | // delimiterPositions: new[] {5, 11, 17});
79 | }
80 |
81 | [TestMethod]
82 | public void Initialize()
83 | {
84 | _idGenerator.NewId();
85 | }
86 |
87 | [TestMethod]
88 | public void Ids_Are_Ascending()
89 | {
90 | string id1 = this._idGenerator.NewId();
91 | string id2 = this._idGenerator.NewId();
92 | Assert.IsTrue(String.Compare(id2, id1, StringComparison.OrdinalIgnoreCase) > 0);
93 | }
94 |
95 | [TestMethod]
96 | public void Server_Hash_Does_Not_Throw()
97 | {
98 | string result;
99 | try
100 | {
101 | Assert.IsTrue(!string.IsNullOrWhiteSpace(result = this._idGenerator.ComputeHostHash("RD00155DC193F9")));
102 | }
103 | catch (Exception e)
104 | {
105 | Console.WriteLine(e);
106 | Assert.Fail();
107 | }
108 | }
109 |
110 | [TestMethod]
111 | public void Id_Length_Is_Correct()
112 | {
113 | // These are the segment lengths passed to the constructor:
114 | int expectedLength = 11 + 5 + 0 + 4;
115 | string id = this._idGenerator.NewId();
116 | Assert.AreEqual(id.Length, expectedLength);
117 | // Should include 3 delimiter dashes when called with (true):
118 | id = this._idGenerator.NewId(true);
119 | Assert.AreEqual(id.Length, expectedLength + 3);
120 | }
121 |
122 | // Uncomment this method to run an extended, multithreaded test to ensure
123 | // Id generation is thread safe and maintains uniqueness across threads.
124 | // This method requires running from one to several seconds, so it needen't
125 | // be part of every build.
126 | [TestMethod]
127 | public void Ids_Do_Not_Collide()
128 | {
129 | var ids = new ConcurrentDictionary();
130 | var cancellationTokenSource = new CancellationTokenSource();
131 | // Increase the concurrent tasks for more thorough testing.
132 | var tasks = new Task[10];
133 | for (int i = 0; i < tasks.Length; i++)
134 | {
135 | tasks[i] = new Task(() =>
136 | {
137 | while (true)
138 | {
139 | if (cancellationTokenSource.IsCancellationRequested)
140 | return;
141 | if (!ids.TryAdd(this._idGenerator.NewId(), ""))
142 | Assert.Fail();
143 | }
144 | }, cancellationTokenSource.Token);
145 | tasks[i].Start();
146 | }
147 | // Lengthen the timespan for more thorough testing.
148 | cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(2));
149 | while (cancellationTokenSource.IsCancellationRequested == false)
150 | Thread.Yield();
151 | Debug.WriteLine(ids.Count);
152 | }
153 |
154 | [TestMethod]
155 | public void Formatted_Id_Has_Correct_Length()
156 | {
157 | var id = _idGenerator.NewId();
158 | var length = id.Length;
159 | var formatted = _idGenerator.Format(id);
160 | Assert.IsTrue(formatted.Contains(_delimiter) && formatted.Length == length + (_delimiter.Length * _delimiterPositions.Length));
161 | }
162 |
163 | [TestMethod]
164 | public void Id_With_20_Chars_Parses_Correctly()
165 | {
166 | var creationTimestamp = DateTime.UtcNow;
167 | var id = _idGenerator.NewId(creationTimestamp);
168 | var info = _idGenerator.Parse(id);
169 |
170 |
171 | Assert.IsTrue(info.TimestampComponent.Length == _idGenerator.NumTimestampCharacters);
172 | double totalMilliseconds = info.CreationTimestampUtc?.Subtract(creationTimestamp).TotalMilliseconds ?? 100;
173 | Assert.IsTrue(Math.Abs(totalMilliseconds) < 1);
174 |
175 | // make sure local datetime values work as well:
176 | creationTimestamp = DateTime.Now;
177 | id = _idGenerator.NewId(creationTimestamp);
178 | info = _idGenerator.Parse(id);
179 | Assert.IsTrue(info.TimestampComponent.Length == _idGenerator.NumTimestampCharacters);
180 | totalMilliseconds = info.CreationTimestampUtc.Value.ToLocalTime().Subtract(creationTimestamp).TotalMilliseconds;
181 | Assert.IsTrue(Math.Abs(totalMilliseconds) < 1);
182 |
183 |
184 | // make sure arbitrary datetime values work as well:
185 | creationTimestamp = new DateTime(2000, 1,1);
186 | id = _idGenerator.NewId(creationTimestamp);
187 | info = _idGenerator.Parse(id);
188 | Assert.IsTrue(info.TimestampComponent.Length == _idGenerator.NumTimestampCharacters);
189 | totalMilliseconds = info.CreationTimestampUtc.Value.ToLocalTime().Subtract(creationTimestamp).TotalMilliseconds;
190 | Assert.IsTrue(Math.Abs(totalMilliseconds) < 1);
191 |
192 |
193 | var length = id.Length;
194 | var formatted = _idGenerator.Format(id);
195 | Assert.IsTrue(formatted.Contains(_delimiter) && formatted.Length == length + (_delimiter.Length * _delimiterPositions.Length));
196 | }
197 |
198 |
199 | [TestMethod]
200 | public void Id_With_16_Chars_Parses_Correctly()
201 | {
202 | var generator = new Base36IdGenerator(11, 2,3);
203 | var creationTimestamp = DateTime.UtcNow;
204 | var id = _idGenerator.NewId(creationTimestamp);
205 | var info = _idGenerator.Parse(id);
206 |
207 | Assert.IsTrue(info.TimestampComponent.Length == _idGenerator.NumTimestampCharacters);
208 | double totalMilliseconds = info.CreationTimestampUtc?.Subtract(creationTimestamp).TotalMilliseconds ?? 100;
209 | Assert.IsTrue(Math.Abs(totalMilliseconds) < 1);
210 |
211 | // make sure local datetime values work as well:
212 | creationTimestamp = DateTime.Now;
213 | id = _idGenerator.NewId(creationTimestamp);
214 | info = _idGenerator.Parse(id);
215 | Assert.IsTrue(info.TimestampComponent.Length == _idGenerator.NumTimestampCharacters);
216 | totalMilliseconds = info.CreationTimestampUtc?.ToLocalTime().Subtract(creationTimestamp).TotalMilliseconds ?? 100;
217 | Assert.IsTrue(Math.Abs(totalMilliseconds) < 1);
218 |
219 |
220 | // make sure arbitrary datetime values work as well:
221 | creationTimestamp = new DateTime(2000, 1, 1);
222 | id = _idGenerator.NewId(creationTimestamp);
223 | info = _idGenerator.Parse(id);
224 | Assert.IsTrue(info.TimestampComponent.Length == _idGenerator.NumTimestampCharacters);
225 | totalMilliseconds = info.CreationTimestampUtc?.ToLocalTime().Subtract(creationTimestamp).TotalMilliseconds ?? 100;
226 | Assert.IsTrue(Math.Abs(totalMilliseconds) < 1);
227 |
228 | var length = id.Length;
229 | var formatted = _idGenerator.Format(id);
230 | Assert.IsTrue(formatted.Contains(_delimiter) && formatted.Length == length + (_delimiter.Length * _delimiterPositions.Length));
231 | }
232 |
233 | [TestMethod]
234 | [ExpectedException(typeof(ArgumentOutOfRangeException))]
235 | public void Timestamps_Throw_Out_Of_Range()
236 | {
237 | _idGenerator.GetTimestamp(13);
238 | }
239 |
240 | [TestMethod]
241 | [ExpectedException(typeof(OverflowException))]
242 | public void Timestamps_Throws_Overflow_When_Strict()
243 | {
244 | _idGenerator.GetTimestamp(length: 5, resolution: TimestampResolution.Ticks, strict: true);
245 | }
246 |
247 | [TestMethod]
248 | public void Timestamp_Only_Throws_Overflow_When_Strict()
249 | {
250 | Assert.IsTrue
251 | (!string.IsNullOrWhiteSpace(_idGenerator.GetTimestamp(length: 10, resolution: TimestampResolution.Day, strict: false)));
252 | }
253 |
254 | [TestMethod]
255 | public void Timestamp_Is_Expected_Length_1()
256 | {
257 | Assert.IsTrue
258 | (_idGenerator.GetTimestamp(length: 10, resolution: TimestampResolution.Day, strict: false).Length == 10);
259 | }
260 |
261 | [TestMethod]
262 | public void Id_Is_Expected_Length_1()
263 | {
264 | var generator = new Base36IdGenerator(
265 | numTimestampCharacters: 7,
266 | numServerCharacters: 6,
267 | numRandomCharacters: 12);
268 | var id = generator.NewId();
269 | Assert.AreEqual(25, id.Length);
270 | }
271 |
272 | [TestMethod]
273 | public void Id_Is_Expected_Length_2()
274 | {
275 | var shortGenerator = new Base36IdGenerator(
276 | numTimestampCharacters: 3,
277 | numServerCharacters: 0,
278 | numRandomCharacters: 12);
279 | var id2 = shortGenerator.NewId();
280 | Assert.AreEqual(15, id2.Length);
281 | }
282 |
283 |
284 | [TestMethod]
285 | public void Multiple_Generators_Produce_Expected_Lengths()
286 | {
287 | var generator = new Base36IdGenerator(
288 | numTimestampCharacters: 12,
289 | numServerCharacters: 6,
290 | numRandomCharacters: 7);
291 |
292 | var shortGenerator = new Base36IdGenerator(
293 | numTimestampCharacters: 3,
294 | numServerCharacters: 0,
295 | numRandomCharacters: 12);
296 | var id1 = generator.NewId();
297 | var id2 = shortGenerator.NewId();
298 | Assert.AreEqual(25, id1.Length);
299 | Assert.AreEqual(15, id2.Length);
300 | }
301 | }
302 | }
303 | // ReSharper restore RedundantArgumentDefaultValue
304 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators.UnitTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // General Information about an assembly is controlled through the following
5 | // set of attributes. Change these attribute values to modify the information
6 | // associated with an assembly.
7 | [assembly: AssemblyCopyright("Copyright © 2015-2016 Paul C Smith and Funcular Labs")]
8 | [assembly: AssemblyTrademark("")]
9 | [assembly: AssemblyCulture("")]
10 |
11 | // Setting ComVisible to false makes the types in this assembly not visible
12 | // to COM components. If you need to access a type in this assembly from
13 | // COM, set the ComVisible attribute to true on that type.
14 | [assembly: ComVisible(false)]
15 |
16 | // The following GUID is for the ID of the typelib if this project is exposed to COM
17 | [assembly: Guid("886813bb-3a53-4a79-8e5c-2e3afdd12f4b")]
18 |
19 | // Version information for an assembly consists of the following four values:
20 | //
21 | // Major Version
22 | // Minor Version
23 | // Build Number
24 | // Revision
25 | //
26 | // You can specify all the values or you can default the Build and Revision Numbers
27 | // by using the '*' as shown below:
28 | // [assembly: AssemblyVersion("1.0.*")]
29 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.31911.196
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Funcular.IdGenerators.UnitTests", "Funcular.IdGenerators.UnitTests\Funcular.IdGenerators.UnitTests.csproj", "{2F568598-A57D-4505-87C1-B9794B0FBCE5}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{002F9329-F351-454E-B196-BE3878C09058}"
9 | ProjectSection(SolutionItems) = preProject
10 | .nuget\NuGet.Config = .nuget\NuGet.Config
11 | .nuget\NuGet.exe = .nuget\NuGet.exe
12 | .nuget\NuGet.targets = .nuget\NuGet.targets
13 | EndProjectSection
14 | EndProject
15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A80DD9CE-A760-4BC3-B229-BDAE33F894A4}"
16 | ProjectSection(SolutionItems) = preProject
17 | Performance1.psess = Performance1.psess
18 | Performance2.psess = Performance2.psess
19 | EndProjectSection
20 | EndProject
21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Funcular.IdGenerators.PerformanceTests", "Funcular.IdGenerators.PerformanceTests\Funcular.IdGenerators.PerformanceTests.csproj", "{78E47237-C866-4D54-9B8B-A1719BFF0FA0}"
22 | EndProject
23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Funcular.IdGenerators", "Funcular.IdGenerators\Funcular.IdGenerators.csproj", "{11BA76CE-A9F9-44C4-AE83-8F0B3ABA8E47}"
24 | EndProject
25 | Global
26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
27 | Debug|Any CPU = Debug|Any CPU
28 | Release|Any CPU = Release|Any CPU
29 | EndGlobalSection
30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
31 | {2F568598-A57D-4505-87C1-B9794B0FBCE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {2F568598-A57D-4505-87C1-B9794B0FBCE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {2F568598-A57D-4505-87C1-B9794B0FBCE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
34 | {2F568598-A57D-4505-87C1-B9794B0FBCE5}.Release|Any CPU.Build.0 = Release|Any CPU
35 | {78E47237-C866-4D54-9B8B-A1719BFF0FA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {78E47237-C866-4D54-9B8B-A1719BFF0FA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {78E47237-C866-4D54-9B8B-A1719BFF0FA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
38 | {78E47237-C866-4D54-9B8B-A1719BFF0FA0}.Release|Any CPU.Build.0 = Release|Any CPU
39 | {11BA76CE-A9F9-44C4-AE83-8F0B3ABA8E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {11BA76CE-A9F9-44C4-AE83-8F0B3ABA8E47}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {11BA76CE-A9F9-44C4-AE83-8F0B3ABA8E47}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {11BA76CE-A9F9-44C4-AE83-8F0B3ABA8E47}.Release|Any CPU.Build.0 = Release|Any CPU
43 | EndGlobalSection
44 | GlobalSection(SolutionProperties) = preSolution
45 | HideSolutionNode = FALSE
46 | EndGlobalSection
47 | GlobalSection(ExtensibilityGlobals) = postSolution
48 | SolutionGuid = {4E0C540B-19EA-4EC3-96E1-A476F36F9F8F}
49 | EndGlobalSection
50 | EndGlobal
51 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators/Base36/Base36IdGenerator.cs:
--------------------------------------------------------------------------------
1 | #region File info
2 |
3 | // *********************************************************************************************************
4 | // Funcular.IdGenerators>Funcular.IdGenerators>Base36IdGenerator.cs
5 | // Created: 2013-03-17 10:18 AM
6 | // Updated: 2025-08-30 03:15 PM
7 | // By: Paul Smith
8 | //
9 | // *********************************************************************************************************
10 | // LICENSE: The MIT License (MIT)
11 | // *********************************************************************************************************
12 | // Copyright (c) 2010-2025
13 | //
14 | // Permission is hereby granted, free of charge, to any person obtaining a copy
15 | // of this software and associated documentation files (the "Software"), to deal
16 | // in the Software without restriction, including without limitation the rights
17 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 | // copies of the Software, and to permit persons to whom the Software is
19 | // furnished to do so, subject to the following conditions:
20 | //
21 | // The above copyright notice and this permission notice shall be included in
22 | // all copies or substantial portions of the Software.
23 | //
24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 | // THE SOFTWARE.
31 | //
32 | // *********************************************************************************************************
33 |
34 | #endregion
35 |
36 |
37 |
38 | #region Usings
39 |
40 | using System;
41 | using System.Configuration;
42 | using System.Diagnostics;
43 | using System.Linq;
44 | using System.Net;
45 | using System.Security.Cryptography;
46 | using System.Text;
47 | using Funcular.IdGenerators.BaseConversion;
48 | using Funcular.IdGenerators.Enums;
49 | #endregion
50 |
51 | // ReSharper disable RedundantCaseLabel
52 | namespace Funcular.IdGenerators.Base36
53 | {
54 | public class Base36IdGenerator
55 | {
56 | #region Private fields
57 | #region Static
58 | private static readonly object _randomLock;
59 | private static readonly Random _random = new Random();
60 | ///
61 | /// This is UTC Epoch. In shorter Id implementations it was configurable, to allow
62 | /// one to milk more longevity out of a shorter series of timestamps.
63 | ///
64 | private static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
65 | #endregion
66 |
67 |
68 |
69 | #region Instance
70 | private readonly string _hostHash;
71 | private readonly string _delimiter;
72 | private readonly int[] _delimiterPositions;
73 | private readonly long _maxRandom;
74 | private readonly int _numRandomCharacters;
75 | private readonly int _numServerCharacters;
76 | private readonly int _numTimestampCharacters;
77 | private readonly string _reservedValue;
78 | private readonly TimestampResolution _timestampResolution;
79 | private static string _hostHashBase36;
80 | private static readonly byte[] _randomBuffer = new byte[8];
81 | private static readonly StringBuilder _sb = new StringBuilder();
82 |
83 | #endregion
84 | #endregion
85 |
86 |
87 |
88 | #region Public properties
89 |
90 | public string HostHash { get { return _hostHash; } }
91 |
92 | public DateTime EpochDate { get { return _epoch; } }
93 |
94 | public int NumRandomCharacters => _numRandomCharacters;
95 |
96 | public int NumServerCharacters => _numServerCharacters;
97 |
98 | public int NumTimestampCharacters => _numTimestampCharacters;
99 |
100 | public TimestampResolution Resolution => _timestampResolution;
101 |
102 | #endregion
103 |
104 |
105 |
106 | #region Constructors
107 |
108 | ///
109 | /// Static constructor
110 | ///
111 | static Base36IdGenerator()
112 | {
113 | Debug.WriteLine("Static constructor begin");
114 | _randomLock = new object();
115 | Debug.WriteLine("Static constructor finish");
116 | }
117 |
118 | /// The default Id format is 11 characters for the timestamp (4170 year lifespan),
119 | /// 4 for the server hash (1.6m hashes), 5 for the random value (60m combinations),
120 | /// and no reserved character. The default delimited format will be four dash-separated
121 | /// groups of 5.
122 | public Base36IdGenerator()
123 | : this(11, 4, 5, "", "-")
124 | {
125 | }
126 |
127 | ///
128 | /// The layout is Timestamp + Server Hash [+ Reserved] + Random.
129 | ///
130 | public Base36IdGenerator(int numTimestampCharacters = 11, int numServerCharacters = 4, int numRandomCharacters = 5, string reservedValue = "", string delimiter = "-", int[] delimiterPositions = null, TimestampResolution resolution = TimestampResolution.Microsecond)
131 | {
132 | Debug.WriteLine("Instance constructor begin");
133 |
134 | // throw if any argument would cause out-of-range exceptions
135 | ValidateConstructorArguments(numTimestampCharacters, numServerCharacters, numRandomCharacters);
136 |
137 | this._delimiterPositions = new[] { 15, 10, 5 };
138 | this._numTimestampCharacters = numTimestampCharacters;
139 | this._numServerCharacters = numServerCharacters;
140 | this._numRandomCharacters = numRandomCharacters;
141 | this._reservedValue = reservedValue;
142 | this._delimiter = delimiter;
143 | this._timestampResolution = resolution;
144 | this._delimiterPositions = (delimiterPositions ?? new int[]{}).OrderByDescending(x => x).ToArray();
145 | this._maxRandom = (long)Math.Pow(36d, numRandomCharacters);
146 |
147 | var hostHash = ComputeHostHash();
148 | this._hostHash = hostHash;
149 |
150 | string base36IdInceptionDateAppSettingValue = null;
151 | if (ConfigurationManager.AppSettings.HasKeys()
152 | && ConfigurationManager.AppSettings.AllKeys.Any(s => s.Equals("base36IdInceptionDate", StringComparison.OrdinalIgnoreCase))
153 | && !string.IsNullOrWhiteSpace((base36IdInceptionDateAppSettingValue = ConfigurationManager.AppSettings["base36IdInceptionDate"]) ?? ""))
154 | {
155 | if (DateTime.TryParse(base36IdInceptionDateAppSettingValue, out var inService))
156 | _epoch = inService;
157 | }
158 |
159 | InitStaticMicroseconds();
160 |
161 | Debug.WriteLine("Instance constructor finish");
162 | }
163 |
164 | #endregion
165 |
166 |
167 |
168 | #region Public methods
169 |
170 | ///
171 | /// Generates a unique, sequential, Base36 string. If this instance was instantiated using
172 | /// the default constructor, it will be 20 characters long.
173 | /// The first 11 characters are the microseconds elapsed since the InService DateTime
174 | /// (Epoch by default).
175 | /// The next 4 characters are the SHA1 of the hostname in Base36.
176 | /// The last 5 characters are random Base36 number between 0 and 36 ^ 5.
177 | ///
178 | /// Returns a unique, sequential, 20-character Base36 string
179 | public string NewId()
180 | {
181 | return NewId(false);
182 | }
183 |
184 |
185 |
186 | ///
187 | /// Generates a unique, sequential, Base36 string with the timestamp component
188 | /// set as if it were created on .
189 | /// If this instance was instantiated using
190 | /// the default constructor, it will be 20 characters long.
191 | /// The first 11 characters are the microseconds elapsed since the InService DateTime
192 | /// (Epoch by default).
193 | /// The next 4 characters are the SHA1 of the hostname in Base36.
194 | /// The last 5 characters are random Base36 number between 0 and 36 ^ 5.
195 | ///
196 | /// Returns a unique, sequential, 20-character Base36 string
197 | public string NewId(DateTime creationTimestamp)
198 | {
199 | return NewId(false, creationTimestamp);
200 | }
201 |
202 | ///
203 | /// Generates a unique, sequential, Base36 string; you control the len
204 | /// The first 10 characters are the microseconds elapsed since the InService DateTime
205 | /// (constant field you hard-code in this file).
206 | /// The next 2 characters are a compressed checksum of the MD5 of this host.
207 | /// The next 1 character is a reserved constant of 36 ('Z' in Base36).
208 | /// The last 3 characters are random number less than 46655 additional for additional uniqueness.
209 | ///
210 | /// Returns a unique, sequential, 16-character Base36 string
211 | public string NewId(bool delimited, DateTime? creationTimestamp = null)
212 | {
213 | // Keep access sequential so threads cannot accidentally
214 | // read another thread's values within this method:
215 |
216 | // Microseconds since InService (using Stopwatch) provides the
217 | // first n chars (n = _numTimestampCharacters):
218 | lock (_sb)
219 | {
220 | _sb.Clear();
221 | long microseconds = creationTimestamp == null
222 | ? ConcurrentStopwatch.GetMicroseconds()
223 | : ConcurrentStopwatch.GetMicroseconds(creationTimestamp.Value.ToUniversalTime());
224 |
225 | string base36Microseconds = Base36Converter.FromLong(microseconds);
226 | if (base36Microseconds.Length > this._numTimestampCharacters)
227 | base36Microseconds = base36Microseconds.Substring(0, this._numTimestampCharacters);
228 | _sb.Append(base36Microseconds.PadLeft(this._numTimestampCharacters, '0'));
229 |
230 | if(_numServerCharacters > 0)
231 | _sb.Append(_hostHash.Substring(0, _numServerCharacters));
232 |
233 | if (!string.IsNullOrWhiteSpace(this._reservedValue))
234 | {
235 | _sb.Append(this._reservedValue);
236 | }
237 | // Add the random component:
238 | _sb.Append(GetRandomBase36DigitsSafe());
239 |
240 | if (!delimited || string.IsNullOrWhiteSpace(_delimiter) || this._delimiterPositions == null)
241 | return _sb.ToString();
242 | foreach (var pos in this._delimiterPositions)
243 | {
244 | _sb.Insert(pos, this._delimiter);
245 | }
246 | return _sb.ToString();
247 | }
248 | }
249 |
250 | ///
251 | /// Given a non-delimited Id, format it with the current instance’s
252 | /// delimiter and delimiter positions. If Id already contains delimiter,
253 | /// or is null or empty, returns Id unmodified.
254 | ///
255 | ///
256 | ///
257 | public string Format(string id)
258 | {
259 | if (string.IsNullOrWhiteSpace(id) || id.Contains(_delimiter))
260 | return id;
261 | StringBuilder sb = new StringBuilder(id);
262 | foreach (var pos in this._delimiterPositions)
263 | {
264 | sb.Insert(pos, _delimiter);
265 | }
266 | return sb.ToString();
267 | }
268 |
269 | ///
270 | /// Base36 representation of the SHA1 of the hostname. The constructor argument
271 | /// numServerCharacters controls the maximum length of this hash.
272 | ///
273 | /// 2 character Base36 checksum of MD5 of hostname
274 | public string ComputeHostHash(string hostname = null)
275 | {
276 | if (_hostHashBase36?.Length == _numServerCharacters)
277 | return _hostHashBase36;
278 | if (string.IsNullOrWhiteSpace(hostname))
279 | hostname = Dns.GetHostName()
280 | ?? Environment.MachineName;
281 | string hashHex;
282 | using (var sha1 = SHA1.Create())
283 | {
284 | hashHex = BitConverter.ToString(sha1.ComputeHash(Encoding.UTF8.GetBytes(hostname)));
285 | if (hashHex.Length > 14) // > 14 chars overflows int64
286 | hashHex = hashHex.Substring(0, 14);
287 | }
288 | return _hostHashBase36 = Base36Converter.FromHex(hashHex);
289 | }
290 |
291 | ///
292 | /// Gets a random Base36 string of the specified .
293 | ///
294 | ///
295 | public string GetRandomString(int length)
296 | {
297 | if (length < 1 || length > 12)
298 | throw new ArgumentOutOfRangeException(nameof(length), "Length must be between 1 and 12; 36^13 overflows Int64.MaxValue");
299 | lock (_randomLock)
300 | {
301 | var maxRandom = (long)Math.Pow(36, length);
302 | _random.NextBytes(_randomBuffer);
303 | var random = Math.Abs(BitConverter.ToInt64(_randomBuffer, 0) % maxRandom);
304 | string encoded = Base36Converter.FromLong(random);
305 | return encoded.Length > length ?
306 | encoded.Substring(0, length) :
307 | encoded.PadLeft(length, '0');
308 | }
309 | }
310 |
311 | ///
312 | /// Get a Base36 encoded timestamp string, based on Epoch. Use for disposable
313 | /// strings where global/universal uniqueness is not critical. If using the
314 | /// default resolution of Microseconds, 5 character values are exhausted in 1 minute.
315 | /// 6 characters = ½ hour. 7 characters = 21 hours. 8 characters = 1 month.
316 | /// 9 characters = 3 years. 10 characters = 115 years. 11 characters = 4170 years.
317 | /// 12 characters = 150 thousand years.
318 | ///
319 | ///
320 | ///
321 | /// Defaults to Epoch
322 | /// If false (default), overflow values will use the
323 | /// value modulus 36. Otherwise it will throw an overflow exception.
324 | ///
325 | public string GetTimestamp(int length, TimestampResolution resolution = TimestampResolution.Microsecond, DateTime? sinceUtc = null, bool strict = false)
326 | {
327 | if (length < 1 || length > 12)
328 | throw new ArgumentOutOfRangeException(nameof(length), "Length must be between 1 and 12; 36^13 overflows Int64.MaxValue");
329 | var origin = sinceUtc ?? _epoch;
330 | var elapsed = DateTime.UtcNow.Subtract(origin);
331 | long intervals;
332 | switch (resolution)
333 | {
334 | case TimestampResolution.Day:
335 | intervals = elapsed.Days;
336 | break;
337 | case TimestampResolution.Hour:
338 | intervals = Convert.ToInt64(elapsed.TotalHours);
339 | break;
340 | case TimestampResolution.Minute:
341 | intervals = Convert.ToInt64(elapsed.TotalMinutes);
342 | break;
343 | case TimestampResolution.Second:
344 | intervals = Convert.ToInt64(elapsed.TotalSeconds);
345 | break;
346 | case TimestampResolution.Millisecond:
347 | intervals = Convert.ToInt64(elapsed.TotalMilliseconds);
348 | break;
349 | case TimestampResolution.Microsecond:
350 | intervals = elapsed.Ticks / 10;
351 | break;
352 | case TimestampResolution.Ticks:
353 | intervals = elapsed.Ticks;
354 | break;
355 | case TimestampResolution.None:
356 | default:
357 | throw new ArgumentOutOfRangeException(nameof(resolution));
358 | }
359 | var combinations = Math.Pow(36, length);
360 | if (combinations < intervals)
361 | {
362 | if (strict)
363 | {
364 | throw new OverflowException(
365 | $"At resolution {resolution.ToString()}, value is greater than {length}-character timestamps can express.");
366 | }
367 | intervals = intervals % 36;
368 | }
369 | string encoded = Base36Converter.FromLong(intervals);
370 | return encoded.Length > length ?
371 | encoded.Substring(0, length) :
372 | encoded.PadLeft(length, '0');
373 | }
374 |
375 |
376 |
377 | #endregion
378 |
379 |
380 |
381 | #region Nonpublic methods
382 |
383 | private static void ValidateConstructorArguments(int numTimestampCharacters, int numServerCharacters, int numRandomCharacters)
384 | {
385 | if (numTimestampCharacters > 12)
386 | throw new ArgumentOutOfRangeException(nameof(numTimestampCharacters), "The maximum characters in any component is 12.");
387 | if (numServerCharacters > 12)
388 | throw new ArgumentOutOfRangeException(nameof(numServerCharacters), "The maximum characters in any component is 12.");
389 | if (numRandomCharacters > 12)
390 | throw new ArgumentOutOfRangeException(nameof(numRandomCharacters), "The maximum characters in any component is 12.");
391 |
392 | if (numTimestampCharacters < 0)
393 | throw new ArgumentOutOfRangeException(nameof(numTimestampCharacters), "Number must not be negative.");
394 | if (numServerCharacters < 0)
395 | throw new ArgumentOutOfRangeException(nameof(numServerCharacters), "Number must not be negative.");
396 | if (numRandomCharacters < 0)
397 | throw new ArgumentOutOfRangeException(nameof(numRandomCharacters), "Number must not be negative.");
398 | }
399 |
400 | ///
401 | /// Return the elapsed microseconds since the in-service DateTime; will never
402 | /// return the same value twice. Uses a high-resolution Stopwatch (not DateTime.Now)
403 | /// to measure durations.
404 | ///
405 | ///
406 | internal static long GetMicroseconds()
407 | {
408 | return ConcurrentStopwatch.GetMicroseconds();
409 | }
410 |
411 | private static void InitStaticMicroseconds()
412 | {
413 | // Just make sure ConcurrentStopwatch.GetMicroseconds gets called. That internally
414 | // handles all initialization:
415 | Console.WriteLine(GetMicroseconds());
416 | }
417 |
418 | ///
419 | /// Gets random component of Id, pre trimmed and padded to the correct length.
420 | ///
421 | ///
422 | private string GetRandomBase36DigitsSafe()
423 | {
424 | lock (_randomLock)
425 | {
426 | byte[] buffer = new byte[8];
427 | _random.NextBytes(buffer);
428 | var number = Math.Abs(BitConverter.ToInt64(buffer, 0) % this._maxRandom);
429 | string encoded = Base36Converter.FromLong(number);
430 | return
431 | encoded.Length == this._numRandomCharacters
432 | ? encoded
433 | : encoded.Length > this._numRandomCharacters
434 | ? encoded.Substring(0, _numRandomCharacters)
435 | : encoded.PadLeft(this._numRandomCharacters, '0');
436 | }
437 | }
438 |
439 | public IdInformation Parse(string id)
440 | {
441 | if(id == null)
442 | throw new ArgumentException("Id cannot be null", nameof(id));
443 | if (id.Length == 0)
444 | return IdInformation.Default;
445 |
446 | var info = new IdInformation(){ Base = 36 };
447 | int index = 0;
448 | if (_numTimestampCharacters > 0)
449 | {
450 | info.TimestampComponent = id.Substring(index, _numTimestampCharacters);
451 | index += _numTimestampCharacters;
452 | }
453 |
454 | if (_numServerCharacters > 0)
455 | {
456 | info.HashComponent = id.Substring(index - 1, _numServerCharacters);
457 | index += _numServerCharacters;
458 | }
459 |
460 | if (_numRandomCharacters > 0)
461 | {
462 | info.RandomComponent = id.Substring(index - 1, _numRandomCharacters);
463 | index += _numServerCharacters;
464 | }
465 |
466 | long intervals = Base36Converter.Decode(info.TimestampComponent);
467 | DateTime result;
468 | switch (this._timestampResolution)
469 | {
470 | case TimestampResolution.Day:
471 | result = _epoch.AddDays(intervals);
472 | break;
473 | case TimestampResolution.Hour:
474 | result = _epoch.AddHours(intervals);
475 | break;
476 | case TimestampResolution.Minute:
477 | result = _epoch.AddMinutes(intervals);
478 | break;
479 | case TimestampResolution.Second:
480 | result = _epoch.AddSeconds(intervals);
481 | break;
482 | case TimestampResolution.Millisecond:
483 | result = _epoch.AddMilliseconds(intervals);
484 | break;
485 | case TimestampResolution.Ticks:
486 | result = _epoch.AddTicks(intervals);
487 | break;
488 | case TimestampResolution.Microsecond:
489 | default:
490 | result = _epoch.AddTicks(intervals * 10L);
491 | break;
492 | }
493 |
494 | info.CreationTimestampUtc = result;
495 |
496 | return info;
497 | }
498 |
499 | #endregion
500 | }
501 | }
502 | // ReSharper restore RedundantCaseLabel
503 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators/Base36/ConcurrentStopwatch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 |
4 | namespace Funcular.IdGenerators.Base36
5 | {
6 | ///
7 | /// Thread safe microseconds stopwatch implementation.
8 | ///
9 | public static class ConcurrentStopwatch
10 | {
11 | private static readonly DateTime _utcEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
12 |
13 | private static readonly object _lock = new object();
14 |
15 | private static readonly Stopwatch _sw = Stopwatch.StartNew();
16 |
17 | private static long _lastMicroseconds;
18 |
19 | private static readonly long _timeZeroMicroseconds;
20 |
21 | static ConcurrentStopwatch()
22 | {
23 | var lastInitialized = DateTime.UtcNow;
24 | var timeZero = lastInitialized.Subtract(_utcEpoch);
25 | _timeZeroMicroseconds = timeZero.Ticks/10;
26 | }
27 |
28 | ///
29 | /// Returns the time in microseconds since
30 | ///
31 | ///
32 | ///
33 | public static long GetMicroseconds(DateTimeOffset since)
34 | {
35 | lock (_lock)
36 | {
37 | return since.Subtract(_utcEpoch).Ticks / 10;
38 | }
39 | }
40 |
41 | ///
42 | /// Returns the Unix time in microseconds (µ″ since UTC epoch)
43 | ///
44 | ///
45 | public static long GetMicroseconds()
46 | {
47 | lock (_lock)
48 | {
49 | long microseconds = 0;
50 | while (microseconds <= _lastMicroseconds)
51 | {
52 | microseconds = _timeZeroMicroseconds + (_sw.Elapsed.Ticks / 10);
53 | }
54 | _lastMicroseconds = microseconds;
55 | return microseconds;
56 | }
57 |
58 | }
59 |
60 | }
61 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators/Base36/IdInformation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Funcular.IdGenerators.Base36
4 | {
5 | public class IdInformation
6 | {
7 | public static IdInformation Default = new IdInformation();
8 | public int Base { get; set; }
9 | public string TimestampComponent { get; set; }
10 | public string HashComponent { get; set; }
11 | public string RandomComponent { get; set; }
12 | public DateTime? CreationTimestampUtc { get; set; }
13 | }
14 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators/BaseConversion/Base36Converter.cs:
--------------------------------------------------------------------------------
1 | #region File info
2 |
3 | // *********************************************************************************************************
4 | // Funcular.IdGenerators>Funcular.IdGenerators>Base36Converter.cs
5 | // Created: 2015-06-26 2:42 PM
6 | // Updated: 2015-06-26 2:43 PM
7 | // By: Paul Smith
8 | //
9 | // *********************************************************************************************************
10 | // LICENSE: The MIT License (MIT)
11 | // *********************************************************************************************************
12 | // Copyright (c) 2010-2015
13 | //
14 | // Permission is hereby granted, free of charge, to any person obtaining a copy
15 | // of this software and associated documentation files (the "Software"), to deal
16 | // in the Software without restriction, including without limitation the rights
17 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 | // copies of the Software, and to permit persons to whom the Software is
19 | // furnished to do so, subject to the following conditions:
20 | //
21 | // The above copyright notice and this permission notice shall be included in
22 | // all copies or substantial portions of the Software.
23 | //
24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 | // THE SOFTWARE.
31 | //
32 | // *********************************************************************************************************
33 |
34 | #endregion
35 |
36 |
37 |
38 | #region Usings
39 |
40 | using System;
41 | using System.Collections.Generic;
42 | using System.Globalization;
43 | using System.Linq;
44 |
45 | #endregion
46 |
47 |
48 |
49 | namespace Funcular.IdGenerators.BaseConversion
50 | {
51 | internal static class Base36Converter
52 | {
53 | private const int BITS_IN_LONG = 64;
54 | private const int BASE = 36;
55 | private static string _charList = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
56 | private static readonly char[] _digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
57 | private static readonly char[] _fromLongBuffer = new char[BITS_IN_LONG];
58 | private static readonly object _lock = new object();
59 |
60 | static Base36Converter()
61 | {
62 | BaseConverter.CharList = _charList;
63 | }
64 |
65 | ///
66 | /// The character set for encoding. Defaults to upper-case alphanumerics 0-9, A-Z.
67 | ///
68 | public static string CharList { get { return _charList; } set { _charList = value; } }
69 |
70 | public static string FromHex(string hex)
71 | {
72 | return BaseConverter.Convert(hex.ToUpper().Replace("-",""), 16, 36);
73 | }
74 |
75 | public static string FromGuid(Guid guid)
76 | {
77 | return BaseConverter.Convert(guid.ToString("N"), 16, 36);
78 | }
79 |
80 | public static string FromInt32(int int32)
81 | {
82 | return BaseConverter.Convert(int32.ToString(CultureInfo.InvariantCulture), 10, 36);
83 | }
84 |
85 | public static string FromInt64(long int64)
86 | {
87 | return BaseConverter.Convert(number: int64.ToString(CultureInfo.InvariantCulture), fromBase: 10, toBase: 36);
88 | }
89 |
90 | ///
91 | /// Converts the given decimal number to the numeral system with the
92 | /// specified radix (in the range [2, 36]).
93 | ///
94 | /// The number to convert.
95 | ///
96 | public static string FromLong(long decimalNumber)
97 | {
98 | unchecked
99 | {
100 | int index = BITS_IN_LONG - 1;
101 |
102 | if (decimalNumber == 0)
103 | return "0";
104 |
105 | long currentNumber = Math.Abs(decimalNumber);
106 |
107 | lock (_lock)
108 | {
109 | while (currentNumber != 0)
110 | {
111 | int remainder = (int)(currentNumber % BASE);
112 | _fromLongBuffer[index--] = _digits[remainder];
113 | currentNumber = currentNumber / BASE;
114 | }
115 | return new string(_fromLongBuffer, index + 1, BITS_IN_LONG - index - 1);
116 | }
117 | }
118 | }
119 |
120 | ///
121 | /// Encode the given number into a Base36 string
122 | ///
123 | ///
124 | ///
125 | public static String Encode(long input)
126 | {
127 | unchecked
128 | {
129 | //char[] clistarr = CharList.ToCharArray();
130 | var result = new Stack();
131 | while (input != 0)
132 | {
133 | result.Push(_digits[input % 36]);
134 | input /= 36;
135 | }
136 | return new string(result.ToArray());
137 | }
138 | }
139 |
140 | ///
141 | /// Decode the Base36 Encoded string into a number
142 | ///
143 | ///
144 | ///
145 | public static Int64 Decode(string input)
146 | {
147 | unchecked
148 | {
149 | IEnumerable reversed = input.ToUpper().Reverse();
150 | long result = 0;
151 | int pos = 0;
152 | foreach (var c in reversed)
153 | {
154 | result += CharList.IndexOf(c) * (long)Math.Pow(36, pos);
155 | pos++;
156 | }
157 | return result;
158 | }
159 | }
160 | }
161 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators/BaseConversion/BaseConverter.cs:
--------------------------------------------------------------------------------
1 | #region File info
2 |
3 | // *********************************************************************************************************
4 | // Funcular.IdGenerators>Funcular.IdGenerators>BaseConverter.cs
5 | // Created: 2015-06-26 2:41 PM
6 | // Updated: 2015-06-26 2:44 PM
7 | // By: Paul Smith
8 | //
9 | // *********************************************************************************************************
10 | // LICENSE: The MIT License (MIT)
11 | // *********************************************************************************************************
12 | // Copyright (c) 2010-2015
13 | //
14 | // Permission is hereby granted, free of charge, to any person obtaining a copy
15 | // of this software and associated documentation files (the "Software"), to deal
16 | // in the Software without restriction, including without limitation the rights
17 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18 | // copies of the Software, and to permit persons to whom the Software is
19 | // furnished to do so, subject to the following conditions:
20 | //
21 | // The above copyright notice and this permission notice shall be included in
22 | // all copies or substantial portions of the Software.
23 | //
24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30 | // THE SOFTWARE.
31 | //
32 | // *********************************************************************************************************
33 |
34 | #endregion
35 |
36 |
37 |
38 | #region Usings
39 |
40 | using System;
41 | using System.Collections.Generic;
42 | using System.Linq;
43 |
44 | #endregion
45 |
46 |
47 |
48 | namespace Funcular.IdGenerators.BaseConversion
49 | {
50 | ///
51 | /// A Base36 De- and Encoder
52 | ///
53 | ///
54 | /// Adapted from the base36 encoder at
55 | /// http://www.stum.de/2008/10/20/base36-encoderdecoder-in-c/
56 | ///
57 | internal static class BaseConverter
58 | {
59 | private static string _charList;
60 |
61 | ///
62 | /// The character set for encoding.
63 | ///
64 | public static string CharList { get { return _charList; } set { _charList = value; } }
65 |
66 | ///
67 | /// Convert a (expressed as a string) from to
68 | ///
69 | ///
70 | /// String representation of the number to be converted
71 | /// The current base of the number
72 | /// The desired base to convert to
73 | ///
74 | public static string Convert(string number, int fromBase, int toBase)
75 | {
76 | /*if (string.IsNullOrEmpty(_charList))
77 | throw new FormatException("You must populate .CharList before calling Convert().");*/
78 | number = string.Join("", number.Split(new[] {" ", "-", ",", "."}, StringSplitOptions.RemoveEmptyEntries));
79 | unchecked
80 | {
81 | string result = null;
82 | /*try
83 | {*/
84 | int length = number.Length;
85 | result = string.Empty;
86 | List nibbles = number.Select(c => CharList.IndexOf(c)).ToList();
87 | int newlen;
88 | do
89 | {
90 | int value = 0;
91 | newlen = 0;
92 | for (int i = 0; i < length; ++i)
93 | {
94 | value = value*fromBase + nibbles[i];
95 | if (value >= toBase)
96 | {
97 | if (newlen == nibbles.Count)
98 | nibbles.Add(0);
99 | nibbles[newlen++] = value/toBase;
100 | value %= toBase;
101 | }
102 | else if (newlen > 0)
103 | {
104 | if (newlen == nibbles.Count)
105 | nibbles.Add(0);
106 | nibbles[newlen++] = 0;
107 | }
108 | }
109 | length = newlen;
110 | result = CharList[value] + result;
111 | }
112 | while (newlen != 0);
113 | /*}
114 | catch (Exception e)
115 | {
116 | Console.WriteLine(e);
117 | }*/
118 | return result;
119 | }
120 | }
121 |
122 | ///
123 | /// Converts the given decimal number to the numeral system with the
124 | /// specified radix (in the range [2, 36]).
125 | ///
126 | /// The number to convert.
127 | /// The radix of the destination numeral system (in the range [2, 36]).
128 | ///
129 | public static string DecimalToArbitrarySystem(long decimalNumber, int radix)
130 | {
131 | const int BITS_IN_LONG = 64;
132 | const string DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
133 |
134 | if (radix < 2 || radix > DIGITS.Length)
135 | throw new ArgumentException("The radix must be >= 2 and <= " + DIGITS.Length.ToString());
136 |
137 | if (decimalNumber == 0)
138 | return "0";
139 |
140 | int index = BITS_IN_LONG - 1;
141 | long currentNumber = Math.Abs(decimalNumber);
142 | char[] charArray = new char[BITS_IN_LONG];
143 |
144 | while (currentNumber != 0)
145 | {
146 | int remainder = (int)(currentNumber % radix);
147 | charArray[index--] = DIGITS[remainder];
148 | currentNumber = currentNumber / radix;
149 | }
150 |
151 | string result = new String(charArray, index + 1, BITS_IN_LONG - index - 1);
152 | if (decimalNumber < 0)
153 | {
154 | result = "-" + result;
155 | }
156 |
157 | return result;
158 | }
159 | }
160 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators/ConcurrentRandom.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Cryptography;
3 |
4 | namespace Funcular.IdGenerators
5 | {
6 | public static class ConcurrentRandom
7 | {
8 | [ThreadStatic]
9 | private static Random _random;
10 | private static readonly object Lock = new object();
11 | private static long _lastValue;
12 | #if !NET6_0
13 | private static readonly RNGCryptoServiceProvider RngCryptoServiceProvider;
14 | #endif
15 |
16 |
17 | static ConcurrentRandom()
18 | {
19 | #if !NET6_0
20 | RngCryptoServiceProvider = new RNGCryptoServiceProvider();
21 | #endif
22 | }
23 |
24 | public static long NextLong()
25 | {
26 | lock (Lock)
27 | {
28 | long value;
29 | do
30 | {
31 | value = (long)(Random.NextDouble() * MaxRandom);
32 | } while (value == _lastValue);
33 | _lastValue = value;
34 | return value;
35 | }
36 | }
37 |
38 | public static Random Random
39 | {
40 | get
41 | {
42 | if (_random != null)
43 | return _random;
44 | var cryptoResult = new byte[4];
45 | #if NET6_0
46 | cryptoResult = RandomNumberGenerator.GetBytes(4);
47 | #else
48 | RngCryptoServiceProvider.GetBytes(cryptoResult);
49 | #endif
50 |
51 | int seed = BitConverter.ToInt32(cryptoResult, 0);
52 | _random = new Random(seed);
53 | return _random;
54 | }
55 | }
56 |
57 | public static long MaxRandom { get; set; }
58 | }
59 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators/Enums/TimestampResolution.cs:
--------------------------------------------------------------------------------
1 | namespace Funcular.IdGenerators.Enums
2 | {
3 | public enum TimestampResolution
4 | {
5 | None = 0,
6 | Day = 4,
7 | Hour = 8,
8 | Minute = 16,
9 | Second = 32,
10 | Millisecond = 64,
11 | Microsecond = 128,
12 | Ticks = 256
13 | }
14 | }
--------------------------------------------------------------------------------
/Funcular.IdGenerators/Funcular.IdGenerators.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net461;netstandard2.0;netstandard2.1;net5.0;net6.0;net8.0
4 | latest
5 | Funcular.IdGenerators
6 | true
7 | 3.0.2
8 | Funcular Labs Id Generators
9 | Paul C Smith and Funcular Labs
10 | Funcular Labs
11 | K-ordered, semi-random, distributed unique Id generator using base 36. Solves several weaknesses of integer, Guid and SequentialGuid identifiers.
12 | en
13 | c# base-36 id-generator
14 | https://github.com/piranout/Funcular.IdGenerators
15 |
16 | https://github.com/piranout/Funcular.IdGenerators
17 | false
18 | MIT
19 | - Added support for NetStandard, NET5, NET6, NET8
20 | README.md
21 |
22 | Distributed, K-ordered, stateless Id Generator, also creates random values and time stamps in base 36. Along the lines of Short Guid and Snowflake, with an eye towards human readability, concurrency,
23 | and having no external dependencies. These are much more amenable to clustered indexing than Guids, and easier than sequential guids to synchronize in distributed environments and SQL Server.
24 |
25 | ©2013 - 2025 Funcular Labs, Inc. and Paul C Smith
26 |
27 | true
28 | snupkg
29 |
30 | true
31 |
32 | true
33 | false
34 | funcular-logo-angle-brackets-dark.ico
35 | disable
36 | funcular-logo-angle-brackets-dark.png
37 |
38 |
39 |
40 | portable
41 | true
42 | 1701;1702;NU1701
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | True
52 | \
53 |
54 |
55 |
56 |
57 |
58 | True
59 | \
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | Always
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 |
4 | // General Information about an assembly is controlled through the following
5 | // set of attributes. Change these attribute values to modify the information
6 | // associated with an assembly.
7 | [assembly: AssemblyTrademark("")]
8 | [assembly: AssemblyCulture("")]
9 |
10 | // Setting ComVisible to false makes the types in this assembly not visible
11 | // to COM components. If you need to access a type in this assembly from
12 | // COM, set the ComVisible attribute to true on that type.
13 | [assembly: ComVisible(false)]
14 |
15 | // The following GUID is for the ID of the typelib if this project is exposed to COM
16 | [assembly: Guid("7331d71b-d1f1-443b-97f6-f24eeb207828")]
17 |
18 | // Version information for an assembly consists of the following four values:
19 | //
20 | // Major Version
21 | // Minor Version
22 | // Build Number
23 | // Revision
24 | //
25 | // You can specify all the values or you can default the Build and Revision Numbers
26 | // by using the '*' as shown below:
27 | // [assembly: AssemblyVersion("3.0.2")]
28 |
--------------------------------------------------------------------------------
/Funcular.IdGenerators/README.md:
--------------------------------------------------------------------------------
1 | # Funcular.IdGenerators
2 |
3 | A 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](https://github.com/piranout/Funcular.ExtensionMethods/ "Funcular Extension Methods")*.
4 |
5 | * Guid: `{7331d71b-d1f1-443b-97f6-f24eeb207828}`
6 | * Base36 [20]: `040VKZ3C60SL3B1Z2RW5` or `040VK-Z3C60-SL3B1-Z2RW5`
7 | * Dashes are cosmetic formatting, not part of the Id; store as a CHAR(20).
8 |
9 | #### Usage
10 | Create 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.
11 |
12 | ```csharp
13 | var generator = new Base36IdGenerator(
14 | numTimestampCharacters: 12,
15 | numServerCharacters: 6,
16 | numRandomCharacters: 7,
17 | reservedValue: "",
18 | delimiter: "-",
19 | delimiterPositions: new[] {20, 15, 10, 5})
20 | Console.WriteLine(generator.NewId());
21 | // "00E4WG2E7NMXEMFY919O2PIHS"
22 | Console.WriteLine(generator.NewId(delimited: true));
23 | // "00E4W-G2GTO-0IEMF-Y911Q-KJI8E"
24 | ```
25 |
26 | #### Why? Because...
27 | * SQL IDENTITY columns couple Id assignment with a database connection, creating a single point of failure, and restricting the ability to create object graphs in a disconnected operation.
28 | * Guids / SQL UNIQUEIDENTIFIERs are terrible for clustered indexing, are not practically speakable, and look ugly.
29 | * Sequential Guids / SQL SEQUENTIALIDs are extremely cumbersome to manage, aren't synchronized between app servers and database servers, nor in distributed environments. They also create tight coupling between application processes and the database server.
30 | * This approach facilitates datastore-agnostic platforms, eases replication, and makes data significantly more portable
31 |
32 | #### Requirements Met
33 | * Ids must be ascending across a distributed environment
34 | * Ids must not collide for the lifetime of the application, even in high-demand, distributed environments
35 | * Ids must not be guessable; potential attackers should not be able to deduce any actual Ids using previous examples
36 | * Ids must be assigned expecting case-insensitivity (SQL Server’s default collation)
37 | * Ids should be of shorter length than Guids / UNIQUEIDENTIFIERs
38 | * Dashes should be optional and not considered part of the Id
39 |
40 | ...your wish list here...
41 |
42 | #### Examples
43 | Ids are composed of some combination of a timestamp, a server hash, a reserved character group (optional), and a random component.
44 | * Guid: `{7331d71b-d1f1-443b-97f6-f24eeb207828}`
45 | * Base36 [16]: `040VZ3C6SL3BZ2RW` or `040V-Z3C6-SL3B-Z2RW`
46 | * Structure: 10 + 2 + 1 + 3 (1 reserved character for implementer's purposes)
47 | * Ascending over 115 year lifespan
48 | * Less than 1300 possible hash combinations for server component
49 | * ~46k hash combinations for random component
50 | * Base36 [20] (recommended): `040VZ-C6SL0-1003B-Z00R2`
51 | * Structure: 11 + 4 + 0 + 5 (no reserved character)
52 | * Ascending over 4,170 year lifespan
53 | * 1.6 million possible hash combinations for server component
54 | * 60 million possible hash combinations for random component
55 | * Base36 [25]: `040VZ-C6SL0-1003B-Z00R2-01KR4`
56 | * Structure: 12 + 6 + 0 + 7 (no reserved character)
57 | * Ascending over 150,000 year lifespan
58 | * 2 billion possible hash combinations for server component
59 | * 78 billion possible hash combinations for random component
60 |
61 |
62 | #### Testing
63 | ```csharp
64 | [TestClass]
65 | public class IdGenerationTests
66 | {
67 | private Base36IdGenerator _idGenerator;
68 |
69 | [TestInitialize]
70 | public void Setup()
71 | {
72 | this._idGenerator = new Base36IdGenerator(
73 | numTimestampCharacters: 11,
74 | numServerCharacters: 5,
75 | numRandomCharacters: 4,
76 | reservedValue: "",
77 | delimiter: "-",
78 | // give the positions in reverse order if you
79 | // don't want to have to account for modifying
80 | // the loop internally. To do the same in ascending
81 | // order, you would need to pass 5, 11, 17 instead.
82 | delimiterPositions: new[] {15, 10, 5});
83 | }
84 |
85 | [TestMethod]
86 | public void TestIdsAreAscending()
87 | {
88 | string id1 = this._idGenerator.NewId();
89 | string id2 = this._idGenerator.NewId();
90 | Assert.IsTrue(String.Compare(id2, id1, StringComparison.OrdinalIgnoreCase) > 0);
91 | }
92 |
93 | [TestMethod]
94 | public void TestIdLengthsAreAsExpected()
95 | {
96 | // These are the segment lengths passed to the constructor:
97 | int expectedLength = 11 + 5 + 0 + 4;
98 | string id = this._idGenerator.NewId();
99 | Assert.AreEqual(id.Length, expectedLength);
100 | // Should include 3 delimiter dashes when called with (true):
101 | id = this._idGenerator.NewId(true);
102 | Assert.AreEqual(id.Length, expectedLength + 3);
103 | }
104 | ```
--------------------------------------------------------------------------------
/Funcular.IdGenerators/funcular-logo-angle-brackets-dark.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/Funcular.IdGenerators/funcular-logo-angle-brackets-dark.ico
--------------------------------------------------------------------------------
/Funcular.IdGenerators/funcular-logo-angle-brackets-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/Funcular.IdGenerators/funcular-logo-angle-brackets-dark.png
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.0.7.1.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.0.7.1.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.0.7.2.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.0.7.2.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.0.7.3.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.0.7.3.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.0.8.0.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.0.8.0.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.0.9.0.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.0.9.0.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.5.0.0.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.5.0.0.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.5.0.1.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.5.0.1.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.0.5.0.2.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.0.5.0.2.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.1.1.0.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.1.1.0.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.2.0.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.2.0.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.2.1.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.2.1.nupkg
--------------------------------------------------------------------------------
/NuGet/Funcular.IdGenerators.2.5.nupkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/piranout/Funcular.IdGenerators/d85c102134d68a545884a75aa88e0c03fd20516a/NuGet/Funcular.IdGenerators.2.5.nupkg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Funcular.IdGenerators
2 |
3 | A 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](https://github.com/piranout/Funcular.ExtensionMethods/ "Funcular Extension Methods")*.
4 |
5 | * Guid: `{7331d71b-d1f1-443b-97f6-f24eeb207828}`
6 | * Base36 [20]: `040VKZ3C60SL3B1Z2RW5` or `040VK-Z3C60-SL3B1-Z2RW5`
7 | * Dashes are cosmetic formatting, not part of the Id; store as a CHAR(20).
8 |
9 | #### Usage
10 | Create 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.
11 |
12 | ```csharp
13 | var generator = new Base36IdGenerator(
14 | numTimestampCharacters: 12,
15 | numServerCharacters: 6,
16 | numRandomCharacters: 7,
17 | reservedValue: "",
18 | delimiter: "-",
19 | delimiterPositions: new[] {20, 15, 10, 5})
20 | Console.WriteLine(generator.NewId());
21 | // "00E4WG2E7NMXEMFY919O2PIHS"
22 | Console.WriteLine(generator.NewId(delimited: true));
23 | // "00E4W-G2GTO-0IEMF-Y911Q-KJI8E"
24 | ```
25 |
26 | #### Why? Because...
27 | * SQL IDENTITY columns couple Id assignment with a database connection, creating a single point of failure, and restricting the ability to create object graphs in a disconnected operation.
28 | * Guids / SQL UNIQUEIDENTIFIERs are terrible for clustered indexing, are not practically speakable, and look ugly.
29 | * Sequential Guids / SQL SEQUENTIALIDs are extremely cumbersome to manage, aren't synchronized between app servers and database servers, nor in distributed environments. They also create tight coupling between application processes and the database server.
30 | * This approach facilitates datastore-agnostic platforms, eases replication, and makes data significantly more portable
31 |
32 | #### Requirements Met
33 | * Ids must be ascending across a distributed environment
34 | * Ids must not collide for the lifetime of the application, even in high-demand, distributed environments
35 | * Ids must not be guessable; potential attackers should not be able to deduce any actual Ids using previous examples
36 | * Ids must be assigned expecting case-insensitivity (SQL Server’s default collation)
37 | * Ids should be of shorter length than Guids / UNIQUEIDENTIFIERs
38 | * Dashes should be optional and not considered part of the Id
39 |
40 | ...your wish list here...
41 |
42 | #### Examples
43 | Ids are composed of some combination of a timestamp, a server hash, a reserved character group (optional), and a random component.
44 | * Guid: `{7331d71b-d1f1-443b-97f6-f24eeb207828}`
45 | * Base36 [16]: `040VZ3C6SL3BZ2RW` or `040V-Z3C6-SL3B-Z2RW`
46 | * Structure: 10 + 2 + 1 + 3 (1 reserved character for implementer's purposes)
47 | * Ascending over 115 year lifespan
48 | * Less than 1300 possible hash combinations for server component
49 | * ~46k hash combinations for random component
50 | * Base36 [20] (recommended): `040VZ-C6SL0-1003B-Z00R2`
51 | * Structure: 11 + 4 + 0 + 5 (no reserved character)
52 | * Ascending over 4,170 year lifespan
53 | * 1.6 million possible hash combinations for server component
54 | * 60 million possible hash combinations for random component
55 | * Base36 [25]: `040VZ-C6SL0-1003B-Z00R2-01KR4`
56 | * Structure: 12 + 6 + 0 + 7 (no reserved character)
57 | * Ascending over 150,000 year lifespan
58 | * 2 billion possible hash combinations for server component
59 | * 78 billion possible hash combinations for random component
60 |
61 |
62 | #### Testing
63 | ```csharp
64 | [TestClass]
65 | public class IdGenerationTests
66 | {
67 | private Base36IdGenerator _idGenerator;
68 |
69 | [TestInitialize]
70 | public void Setup()
71 | {
72 | this._idGenerator = new Base36IdGenerator(
73 | numTimestampCharacters: 11,
74 | numServerCharacters: 5,
75 | numRandomCharacters: 4,
76 | reservedValue: "",
77 | delimiter: "-",
78 | // give the positions in reverse order if you
79 | // don't want to have to account for modifying
80 | // the loop internally. To do the same in ascending
81 | // order, you would need to pass 5, 11, 17 instead.
82 | delimiterPositions: new[] {15, 10, 5});
83 | }
84 |
85 | [TestMethod]
86 | public void TestIdsAreAscending()
87 | {
88 | string id1 = this._idGenerator.NewId();
89 | string id2 = this._idGenerator.NewId();
90 | Assert.IsTrue(String.Compare(id2, id1, StringComparison.OrdinalIgnoreCase) > 0);
91 | }
92 |
93 | [TestMethod]
94 | public void TestIdLengthsAreAsExpected()
95 | {
96 | // These are the segment lengths passed to the constructor:
97 | int expectedLength = 11 + 5 + 0 + 4;
98 | string id = this._idGenerator.NewId();
99 | Assert.AreEqual(id.Length, expectedLength);
100 | // Should include 3 delimiter dashes when called with (true):
101 | id = this._idGenerator.NewId(true);
102 | Assert.AreEqual(id.Length, expectedLength + 3);
103 | }
104 | ```
105 |
--------------------------------------------------------------------------------