├── .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 | --------------------------------------------------------------------------------