├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── publishnuget.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── IdGen.Configuration ├── AppConfigFactory.cs ├── IdGen.Configuration.csproj ├── IdGeneratorElement.cs ├── IdGeneratorsCollection.cs └── IdGeneratorsSection.cs ├── IdGen.DependencyInjection ├── IdGen.DependencyInjection.csproj └── IdGenServiceCollectionExtensions.cs ├── IdGen.sln ├── IdGen ├── DefaultTimeSource.cs ├── IIdGenerator.cs ├── ITimeSource.cs ├── Id.cs ├── IdGen.csproj ├── IdGenerator.cs ├── IdGeneratorOptions.cs ├── IdStructure.cs ├── InvalidSystemClockException.cs ├── SequenceOverflowException.cs ├── SequenceOverflowStrategy.cs ├── StopwatchTimeSource.cs ├── Translations.Designer.cs ├── Translations.nl.resx └── Translations.resx ├── IdGenTests ├── App.config ├── ConfigTests.cs ├── DependencyInjectionTests.cs ├── IDTests.cs ├── IdGenTests.csproj ├── IdGeneratorTests.cs ├── IdStructureTests.cs └── Mocks │ ├── MockAutoIncrementingIntervalTimeSource.cs │ └── MockTimeSource.cs ├── LICENSE ├── README.md ├── logo.png └── structure.png /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CA1303: Do not pass literals as localized parameters 4 | dotnet_diagnostic.CA1303.severity = suggestion 5 | 6 | # CA2208: Instantiate argument exceptions correctly 7 | dotnet_diagnostic.CA2208.severity = suggestion 8 | 9 | # CA1010: Collection 'Collection' directly or indirectly inherits 'ICollection' without implementing 'ICollection' 10 | dotnet_diagnostic.CA1010.severity = suggestion 11 | [*.cs] 12 | #### Naming styles #### 13 | 14 | # Naming rules 15 | 16 | dotnet_naming_rule.private_or_internal_field_should_be_lowercase__begins_with__.severity = suggestion 17 | dotnet_naming_rule.private_or_internal_field_should_be_lowercase__begins_with__.symbols = private_or_internal_field 18 | dotnet_naming_rule.private_or_internal_field_should_be_lowercase__begins_with__.style = lowercase__begins_with__ 19 | 20 | # Symbol specifications 21 | 22 | dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field 23 | dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected 24 | dotnet_naming_symbols.private_or_internal_field.required_modifiers = 25 | 26 | # Naming styles 27 | 28 | dotnet_naming_style.lowercase__begins_with__.required_prefix = _ 29 | dotnet_naming_style.lowercase__begins_with__.required_suffix = 30 | dotnet_naming_style.lowercase__begins_with__.word_separator = 31 | dotnet_naming_style.lowercase__begins_with__.capitalization = all_lower 32 | csharp_indent_labels = one_less_than_current 33 | csharp_using_directive_placement = outside_namespace:silent 34 | csharp_prefer_simple_using_statement = true:suggestion 35 | csharp_prefer_braces = true:silent 36 | csharp_style_namespace_declarations = file_scoped:silent 37 | csharp_style_prefer_method_group_conversion = true:silent 38 | csharp_style_expression_bodied_methods = true:silent 39 | csharp_style_expression_bodied_constructors = true:silent 40 | csharp_style_expression_bodied_operators = true:silent 41 | csharp_style_expression_bodied_properties = true:silent 42 | csharp_style_expression_bodied_indexers = true:silent 43 | csharp_style_expression_bodied_accessors = true:silent 44 | csharp_style_expression_bodied_lambdas = true:silent 45 | csharp_style_expression_bodied_local_functions = true:silent 46 | csharp_style_throw_expression = true:suggestion 47 | 48 | [*.{cs,vb}] 49 | #### Naming styles #### 50 | 51 | # Naming rules 52 | 53 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 54 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 55 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 56 | 57 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 58 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 59 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 60 | 61 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 62 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 63 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 64 | 65 | # Symbol specifications 66 | 67 | dotnet_naming_symbols.interface.applicable_kinds = interface 68 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 69 | dotnet_naming_symbols.interface.required_modifiers = 70 | 71 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 72 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 73 | dotnet_naming_symbols.types.required_modifiers = 74 | 75 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 76 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 77 | dotnet_naming_symbols.non_field_members.required_modifiers = 78 | 79 | # Naming styles 80 | 81 | dotnet_naming_style.begins_with_i.required_prefix = I 82 | dotnet_naming_style.begins_with_i.required_suffix = 83 | dotnet_naming_style.begins_with_i.word_separator = 84 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 85 | 86 | dotnet_naming_style.pascal_case.required_prefix = 87 | dotnet_naming_style.pascal_case.required_suffix = 88 | dotnet_naming_style.pascal_case.word_separator = 89 | dotnet_naming_style.pascal_case.capitalization = pascal_case 90 | 91 | dotnet_naming_style.pascal_case.required_prefix = 92 | dotnet_naming_style.pascal_case.required_suffix = 93 | dotnet_naming_style.pascal_case.word_separator = 94 | dotnet_naming_style.pascal_case.capitalization = pascal_case 95 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 96 | tab_width = 4 97 | indent_size = 4 98 | end_of_line = crlf 99 | dotnet_style_coalesce_expression = true:suggestion 100 | dotnet_style_null_propagation = true:suggestion 101 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 102 | dotnet_style_prefer_auto_properties = true:silent 103 | dotnet_style_object_initializer = true:suggestion 104 | dotnet_style_collection_initializer = true:suggestion 105 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 106 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 107 | dotnet_style_prefer_conditional_expression_over_return = true:silent 108 | dotnet_style_explicit_tuple_names = true:suggestion 109 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 110 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 111 | dotnet_style_prefer_compound_assignment = true:suggestion 112 | dotnet_style_prefer_simplified_interpolation = true:suggestion 113 | dotnet_style_namespace_match_folder = true:suggestion 114 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [RobThree] 2 | custom: ["https://paypal.me/robiii"] 3 | -------------------------------------------------------------------------------- /.github/workflows/publishnuget.yml: -------------------------------------------------------------------------------- 1 | name: Publish Nuget Package 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: windows-latest 12 | strategy: 13 | matrix: 14 | dotnet-version: [ '9.0.x' ] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Setup .NET ${{ matrix.dotnet-version }} 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: ${{ matrix.dotnet-version }} 23 | 24 | - name: Setup NuGet 25 | uses: NuGet/setup-nuget@v2 26 | 27 | - name: Restore dependencies 28 | run: dotnet restore 29 | 30 | - name: Build 31 | run: dotnet build -c Release --no-restore /p:Version="${{ github.event.release.tag_name }}" 32 | 33 | - name: Run tests 34 | run: dotnet test -c Release --no-restore --no-build 35 | 36 | - name: Create packages 37 | run: | 38 | dotnet pack ${{ github.event.repository.name }} -c Release --no-restore --no-build -p:Version="${{ github.event.release.tag_name }}" 39 | dotnet pack ${{ github.event.repository.name }}.Configuration -c Release --no-restore --no-build -p:Version="${{ github.event.release.tag_name }}" 40 | dotnet pack ${{ github.event.repository.name }}.DependencyInjection -c Release --no-restore --no-build -p:Version="${{ github.event.release.tag_name }}" 41 | 42 | - name: Publish 43 | run: dotnet nuget push **\*.nupkg -s 'https://api.nuget.org/v3/index.json' -k ${{secrets.NUGET_API_KEY}} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: windows-latest 10 | strategy: 11 | matrix: 12 | dotnet-version: [ '9.0.x' ] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Setup .NET ${{ matrix.dotnet-version }} 18 | uses: actions/setup-dotnet@v4 19 | with: 20 | dotnet-version: ${{ matrix.dotnet-version }} 21 | 22 | - name: Setup NuGet 23 | uses: NuGet/setup-nuget@v2 24 | 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | 28 | - name: Run tests 29 | run: dotnet test --no-restore -------------------------------------------------------------------------------- /.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 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | 198 | *.chm -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet.defaultSolution": "IdGen.sln" 3 | } -------------------------------------------------------------------------------- /IdGen.Configuration/AppConfigFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Configuration; 5 | using System.Linq; 6 | 7 | namespace IdGen.Configuration; 8 | 9 | /// 10 | /// Helper class to get IdGen configuration from the application configuration. 11 | /// 12 | public static class AppConfigFactory 13 | { 14 | private static readonly ITimeSource defaulttimesource = new DefaultTimeSource(IdGeneratorOptions.DefaultEpoch); 15 | private static readonly ConcurrentDictionary _namedgenerators = new(); 16 | 17 | /// 18 | /// Returns an instance of an based on the values in the corresponding idGenerator 19 | /// element in the idGenSection of the configuration file. The is used to 20 | /// retrieve timestamp information. 21 | /// 22 | /// The name of the in the idGenSection. 23 | /// 24 | /// An instance of an based on the values in the corresponding idGenerator 25 | /// element in the idGenSection of the configuration file. 26 | /// 27 | /// 28 | /// When the doesn't exist it is created; any consequent calls to this method with 29 | /// the same name will return the same instance. 30 | /// 31 | public static IdGenerator GetFromConfig(string name) 32 | { 33 | var result = _namedgenerators.GetOrAdd(name, (n) => 34 | { 35 | var idgenerators = (ConfigurationManager.GetSection(IdGeneratorsSection.SectionName) as IdGeneratorsSection).IdGenerators; 36 | var idgen = idgenerators.OfType().FirstOrDefault(e => e.Name.Equals(n, StringComparison.Ordinal)); 37 | if (idgen != null) 38 | { 39 | var ts = idgen.TickDuration == TimeSpan.Zero ? defaulttimesource : new DefaultTimeSource(idgen.Epoch, idgen.TickDuration); 40 | var options = new IdGeneratorOptions(new IdStructure(idgen.TimestampBits, idgen.GeneratorIdBits, idgen.SequenceBits), ts, idgen.SequenceOverflowStrategy); 41 | return new IdGenerator(idgen.Id, options); 42 | } 43 | 44 | throw new KeyNotFoundException(); 45 | }); 46 | 47 | return result; 48 | } 49 | } -------------------------------------------------------------------------------- /IdGen.Configuration/IdGen.Configuration.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | RobIII 6 | Devcorner.nl 7 | IdGen.Configuration 8 | IdGen.Configuration 9 | Copyright © 2015 - 2024 Devcorner.nl 10 | MIT 11 | https://github.com/RobThree/IdGen 12 | idgen configuration 13 | Added spinwait option (see #24) 14 | Configuration support for IdGen 15 | latest 16 | IdGen.Configuration 17 | logo.png 18 | 19 | https://github.com/RobThree/IdGen 20 | git 21 | true 22 | latest 23 | Debug;Release 24 | 25 | 26 | 27 | bin\Release\IdGen.Configuration.xml 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /IdGen.Configuration/IdGeneratorElement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Globalization; 4 | 5 | namespace IdGen.Configuration; 6 | 7 | /// 8 | /// Represents an IdGenerator configuration element. This class cannot be inherited. 9 | /// 10 | public sealed class IdGeneratorElement : ConfigurationElement 11 | { 12 | private readonly string[] DATETIMEFORMATS = ["yyyy-MM-dd\\THH:mm:ss", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd"]; 13 | 14 | /// 15 | /// Gets/sets the name of the . 16 | /// 17 | [ConfigurationProperty("name", IsRequired = true, IsKey = true)] 18 | public string Name 19 | { 20 | get => (string)this["name"]; 21 | set => this["name"] = value; 22 | } 23 | 24 | /// 25 | /// Gets/sets the GeneratorId of the . 26 | /// 27 | [ConfigurationProperty("id", IsRequired = true)] 28 | public int Id 29 | { 30 | get => (int)this["id"]; 31 | set => this["id"] = value; 32 | } 33 | 34 | 35 | [ConfigurationProperty("epoch", IsRequired = false, DefaultValue = "2015-01-01")] 36 | private string StringEpoch 37 | { 38 | get => (string)this["epoch"]; 39 | set => this["epoch"] = value; 40 | } 41 | 42 | /// 43 | /// Gets/sets the option of the . 44 | /// 45 | [ConfigurationProperty("sequenceOverflowStrategy", IsRequired = false, DefaultValue = SequenceOverflowStrategy.Throw)] 46 | public SequenceOverflowStrategy SequenceOverflowStrategy 47 | { 48 | get => (SequenceOverflowStrategy)this["sequenceOverflowStrategy"]; 49 | set => this["sequenceOverflowStrategy"] = value; 50 | } 51 | 52 | /// 53 | /// Gets/sets the Epoch of the . 54 | /// 55 | public DateTime Epoch 56 | { 57 | get => DateTime.SpecifyKind(DateTime.ParseExact(StringEpoch, DATETIMEFORMATS, CultureInfo.InvariantCulture, DateTimeStyles.None), DateTimeKind.Utc); 58 | set => StringEpoch = value.ToString(DATETIMEFORMATS[0], CultureInfo.InvariantCulture); 59 | } 60 | 61 | /// 62 | /// Gets/sets the of the . 63 | /// 64 | [ConfigurationProperty("timestampBits", IsRequired = true)] 65 | public byte TimestampBits 66 | { 67 | get => (byte)this["timestampBits"]; 68 | set => this["timestampBits"] = value; 69 | } 70 | 71 | /// 72 | /// Gets/sets the of the . 73 | /// 74 | [ConfigurationProperty("generatorIdBits", IsRequired = true)] 75 | public byte GeneratorIdBits 76 | { 77 | get => (byte)this["generatorIdBits"]; 78 | set => this["generatorIdBits"] = value; 79 | } 80 | 81 | /// 82 | /// Gets/sets the of the . 83 | /// 84 | [ConfigurationProperty("sequenceBits", IsRequired = true)] 85 | public byte SequenceBits 86 | { 87 | get => (byte)this["sequenceBits"]; 88 | set => this["sequenceBits"] = value; 89 | } 90 | 91 | /// 92 | /// Gets/sets the of the . 93 | /// 94 | [ConfigurationProperty("tickDuration", IsRequired = false, DefaultValue = "0:00:00.001")] 95 | public TimeSpan TickDuration 96 | { 97 | get => (TimeSpan)this["tickDuration"]; 98 | set => this["tickDuration"] = value; 99 | } 100 | } -------------------------------------------------------------------------------- /IdGen.Configuration/IdGeneratorsCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Configuration; 3 | using System.Linq; 4 | 5 | namespace IdGen.Configuration; 6 | 7 | /// 8 | /// Represents a IdGenerators configuration element containing a collection of child elements. 9 | /// 10 | public class IdGeneratorsCollection : ConfigurationElementCollection, IReadOnlyCollection 11 | { 12 | /// 13 | /// Creates a new . 14 | /// 15 | /// A newly created . 16 | protected override ConfigurationElement CreateNewElement() => new IdGeneratorElement(); 17 | 18 | /// 19 | /// Gets the element key for a specified . 20 | /// 21 | /// The to return the key for. 22 | /// An that acts as the key for the specified . 23 | protected override object GetElementKey(ConfigurationElement element) => ((IdGeneratorElement)element)?.Name; 24 | 25 | // Make compiler happy (CA1010) 26 | /// 27 | /// Returns an enumerator that iterates through the collection. 28 | /// 29 | /// An enumerator that can be used to iterate through the collection. 30 | public new IEnumerator GetEnumerator() 31 | => Enumerable.Range(0, Count).Select(BaseGet).GetEnumerator(); 32 | } -------------------------------------------------------------------------------- /IdGen.Configuration/IdGeneratorsSection.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace IdGen.Configuration; 4 | 5 | /// 6 | /// Represents an IdGenerators section within a configuration file. 7 | /// 8 | public class IdGeneratorsSection : ConfigurationSection 9 | { 10 | /// 11 | /// The default name of the section. 12 | /// 13 | public const string SectionName = "idGenSection"; 14 | 15 | /// 16 | /// The default name of the collection. 17 | /// 18 | private const string IdGensCollectionName = "idGenerators"; 19 | 20 | /// 21 | /// Gets an of all the objects in all 22 | /// participating configuration files. 23 | /// 24 | [ConfigurationProperty(IdGensCollectionName)] 25 | [ConfigurationCollection(typeof(IdGeneratorsCollection), AddItemName = "idGenerator")] 26 | public IdGeneratorsCollection IdGenerators => (IdGeneratorsCollection)base[IdGensCollectionName]; 27 | } -------------------------------------------------------------------------------- /IdGen.DependencyInjection/IdGen.DependencyInjection.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | RobIII 6 | Devcorner.nl 7 | IdGen.DependencyInjection 8 | IdGen.DependencyInjection 9 | Copyright © 2022 - 2024 Devcorner.nl 10 | MIT 11 | https://github.com/RobThree/IdGen 12 | idgen di dependency-injection 13 | Initial release 14 | Dependency injection support for IdGen 15 | IdGen.DependencyInjection 16 | logo.png 17 | https://github.com/RobThree/IdGen 18 | git 19 | enable 20 | latest 21 | true 22 | latest 23 | Debug;Release 24 | 25 | 26 | 27 | bin\Release\IdGen.DependencyInjection.xml 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | True 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /IdGen.DependencyInjection/IdGenServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using System; 4 | 5 | namespace IdGen.DependencyInjection; 6 | 7 | /// 8 | /// Helper class to integrate IdGen with Microsoft.Extensions.DependencyInjection 9 | /// 10 | public static class IdGenServiceCollectionExtensions 11 | { 12 | /// 13 | /// Registers a singleton with the given . 14 | /// 15 | /// The to register the singleton on. 16 | /// The generator-id to use for the singleton . 17 | /// The given with the registered singleton in it. 18 | public static IServiceCollection AddIdGen(this IServiceCollection services, int generatorId) 19 | => AddIdGen(services, generatorId, () => IdGeneratorOptions.Default); 20 | 21 | /// 22 | /// Registers a singleton with the given and . 23 | /// 24 | /// The to register the singleton on. 25 | /// The generator-id to use for the singleton . 26 | /// The for the singleton . 27 | /// The given with the registered singleton in it. 28 | /// Thrown when is null 29 | public static IServiceCollection AddIdGen(this IServiceCollection services, int generatorId, Func options) 30 | { 31 | if (options == null) 32 | { 33 | throw new ArgumentNullException(nameof(options)); 34 | } 35 | 36 | services.TryAddSingleton>(new IdGenerator(generatorId, options())); 37 | services.TryAddSingleton(c => (IdGenerator)c.GetRequiredService>()); 38 | 39 | return services; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /IdGen.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32505.173 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{51E6E025-F554-453A-8850-32378CA48C38}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | LICENSE = LICENSE 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdGen", "IdGen\IdGen.csproj", "{E6856E0A-523F-4451-9C95-621156A1B9F4}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdGenTests", "IdGenTests\IdGenTests.csproj", "{12E4642B-A533-400C-987B-67B21DB80EC7}" 16 | ProjectSection(ProjectDependencies) = postProject 17 | {651A0785-A880-4467-A92F-110C26F3FA28} = {651A0785-A880-4467-A92F-110C26F3FA28} 18 | EndProjectSection 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdGen.Configuration", "IdGen.Configuration\IdGen.Configuration.csproj", "{651A0785-A880-4467-A92F-110C26F3FA28}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdGen.DependencyInjection", "IdGen.DependencyInjection\IdGen.DependencyInjection.csproj", "{ED1E5B28-18FA-475D-A0FC-6CB08CD05185}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {E6856E0A-523F-4451-9C95-621156A1B9F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {E6856E0A-523F-4451-9C95-621156A1B9F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {E6856E0A-523F-4451-9C95-621156A1B9F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {E6856E0A-523F-4451-9C95-621156A1B9F4}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {12E4642B-A533-400C-987B-67B21DB80EC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {12E4642B-A533-400C-987B-67B21DB80EC7}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {12E4642B-A533-400C-987B-67B21DB80EC7}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {12E4642B-A533-400C-987B-67B21DB80EC7}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {651A0785-A880-4467-A92F-110C26F3FA28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {651A0785-A880-4467-A92F-110C26F3FA28}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {651A0785-A880-4467-A92F-110C26F3FA28}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {651A0785-A880-4467-A92F-110C26F3FA28}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {ED1E5B28-18FA-475D-A0FC-6CB08CD05185}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {ED1E5B28-18FA-475D-A0FC-6CB08CD05185}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {ED1E5B28-18FA-475D-A0FC-6CB08CD05185}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {ED1E5B28-18FA-475D-A0FC-6CB08CD05185}.Release|Any CPU.Build.0 = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(SolutionProperties) = preSolution 48 | HideSolutionNode = FALSE 49 | EndGlobalSection 50 | GlobalSection(ExtensibilityGlobals) = postSolution 51 | SolutionGuid = {3930A92E-30EC-4792-974B-35A1FC81F06E} 52 | EndGlobalSection 53 | EndGlobal 54 | -------------------------------------------------------------------------------- /IdGen/DefaultTimeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace IdGen; 4 | 5 | /// 6 | /// Provides time data to an . 7 | /// 8 | /// 9 | /// Unless specified the default duration of a tick for a is 1 millisecond. 10 | /// 11 | /// 12 | /// Initializes a new object. 13 | /// 14 | /// The epoch to use as an offset from now, 15 | /// The duration of a tick for this timesource. 16 | public class DefaultTimeSource(DateTimeOffset epoch, TimeSpan tickDuration) : StopwatchTimeSource(epoch, tickDuration) 17 | { 18 | /// 19 | /// Initializes a new object. 20 | /// 21 | /// The epoch to use as an offset from now. 22 | /// The default tickduration is 1 millisecond. 23 | public DefaultTimeSource(DateTimeOffset epoch) 24 | : this(epoch, TimeSpan.FromMilliseconds(1)) { } 25 | 26 | /// 27 | /// Returns the current number of ticks for the . 28 | /// 29 | /// The current number of ticks to be used by an when creating an Id. 30 | /// 31 | /// Note that a 'tick' is a period defined by the timesource; this may be any valid ; be 32 | /// it a millisecond, an hour, 2.5 seconds or any other value. 33 | /// 34 | public override long GetTicks() => (Offset.Ticks + Elapsed.Ticks) / TickDuration.Ticks; 35 | } -------------------------------------------------------------------------------- /IdGen/IIdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace IdGen; 4 | 5 | /// 6 | /// Provides the interface for Id-generators. 7 | /// 8 | /// The type for the generated ID's. 9 | public interface IIdGenerator : IEnumerable 10 | { 11 | /// 12 | /// Creates a new Id. 13 | /// 14 | /// Returns an Id. 15 | T CreateId(); 16 | } 17 | -------------------------------------------------------------------------------- /IdGen/ITimeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace IdGen; 4 | 5 | /// 6 | /// Provides the interface for timesources that provide time information to s. 7 | /// 8 | public interface ITimeSource 9 | { 10 | /// 11 | /// Gets the epoch of the . 12 | /// 13 | DateTimeOffset Epoch { get; } 14 | 15 | /// 16 | /// Returns the duration of a single tick. 17 | /// 18 | /// 19 | /// It's up to the to define what a 'tick' is; it may be nanoseconds, milliseconds, 20 | /// seconds or even days or years. 21 | /// 22 | TimeSpan TickDuration { get; } 23 | 24 | /// 25 | /// Returns the current number of ticks for the . 26 | /// 27 | /// The current number of ticks to be used by an when creating an Id. 28 | /// 29 | /// It's up to the to define what a 'tick' is; it may be nanoseconds, milliseconds, 30 | /// seconds or even days or years. 31 | /// 32 | long GetTicks(); 33 | } -------------------------------------------------------------------------------- /IdGen/Id.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace IdGen; 4 | 5 | /// 6 | /// Holds information about a decoded id. 7 | /// 8 | public record struct Id 9 | { 10 | /// 11 | /// Gets the sequence number of the id. 12 | /// 13 | public int SequenceNumber { get; private set; } 14 | 15 | /// 16 | /// Gets the generator id of the generator that generated the id. 17 | /// 18 | public int GeneratorId { get; private set; } 19 | 20 | /// 21 | /// Gets the date/time when the id was generated. 22 | /// 23 | public DateTimeOffset DateTimeOffset { get; private set; } 24 | 25 | /// 26 | /// Initializes a new instance of the struct. 27 | /// 28 | /// The sequence number of the id. 29 | /// The generator id of the generator that generated the id. 30 | /// The date/time when the id was generated. 31 | /// An . 32 | internal Id(int sequenceNumber, int generatorId, DateTimeOffset dateTimeOffset) 33 | { 34 | SequenceNumber = sequenceNumber; 35 | GeneratorId = generatorId; 36 | DateTimeOffset = dateTimeOffset; 37 | } 38 | } -------------------------------------------------------------------------------- /IdGen/IdGen.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard1.1;netstandard2.0 5 | RobIII 6 | Devcorner.nl 7 | IdGen 8 | IdGen 9 | Copyright © 2015 - 2024 Devcorner.nl 10 | MIT 11 | https://github.com/RobThree/IdGen 12 | scalable unique id generator distributed 13 | Twitter Snowflake-alike ID generator for .Net 14 | IdGen 15 | logo.png 16 | https://github.com/RobThree/IdGen 17 | git 18 | enable 19 | true 20 | latest 21 | latest 22 | Debug;Release 23 | README.md 24 | 25 | 26 | 27 | bin\Release\IdGen.xml 28 | 29 | 30 | 31 | 32 | True 33 | \ 34 | 35 | 36 | True 37 | 38 | 39 | 40 | 41 | 42 | 43 | all 44 | runtime; build; native; contentfiles; analyzers; buildtransitive 45 | 46 | 47 | 48 | 49 | 50 | True 51 | True 52 | Translations.resx 53 | 54 | 55 | 56 | 57 | 58 | ResXFileCodeGenerator 59 | Translations.Designer.cs 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /IdGen/IdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading; 6 | 7 | namespace IdGen; 8 | 9 | /// 10 | /// Generates Id's inspired by Twitter's (late) Snowflake project. 11 | /// 12 | public class IdGenerator : IIdGenerator 13 | { 14 | private readonly long _generatorid; 15 | private int _sequence = 0; 16 | private long _lastgen = -1; 17 | 18 | private readonly long MASK_SEQUENCE; 19 | private readonly long MASK_TIME; 20 | private readonly long MASK_GENERATOR; 21 | 22 | private readonly int SHIFT_TIME; 23 | private readonly int SHIFT_GENERATOR; 24 | 25 | 26 | // Object to lock() on while generating Id's 27 | private readonly object _genlock = new(); 28 | 29 | /// 30 | /// Gets the . 31 | /// 32 | public IdGeneratorOptions Options { get; } 33 | 34 | 35 | /// 36 | /// Gets the Id of the generator. 37 | /// 38 | public int Id => (int)_generatorid; 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// 43 | /// The Id of the generator. 44 | public IdGenerator(int generatorId) 45 | : this(generatorId, new IdGeneratorOptions()) { } 46 | 47 | /// 48 | /// Initializes a new instance of the class with the specified . 49 | /// 50 | /// The Id of the generator. 51 | /// The for the .. 52 | /// Thrown when is null. 53 | public IdGenerator(int generatorId, IdGeneratorOptions options) 54 | { 55 | _generatorid = generatorId; 56 | Options = options ?? throw new ArgumentNullException(nameof(options)); 57 | 58 | var maxgeneratorid = (1U << Options.IdStructure.GeneratorIdBits) - 1; 59 | 60 | if (_generatorid < 0 || _generatorid > maxgeneratorid) 61 | { 62 | throw new ArgumentOutOfRangeException(nameof(generatorId), string.Format(Translations.ERR_INVALID_GENERATORID, maxgeneratorid)); 63 | } 64 | 65 | // Precalculate some values 66 | MASK_TIME = GetMask(options.IdStructure.TimestampBits); 67 | MASK_GENERATOR = GetMask(options.IdStructure.GeneratorIdBits); 68 | MASK_SEQUENCE = GetMask(options.IdStructure.SequenceBits); 69 | SHIFT_TIME = options.IdStructure.GeneratorIdBits + options.IdStructure.SequenceBits; 70 | SHIFT_GENERATOR = options.IdStructure.SequenceBits; 71 | } 72 | 73 | /// 74 | /// Creates a new Id. 75 | /// 76 | /// Returns an Id based on the 's epoch, generatorid and sequence. 77 | /// Thrown when clock going backwards is detected. 78 | /// Thrown when sequence overflows. 79 | /// Note that this method MAY throw an one of the documented exceptions. 80 | public long CreateId() 81 | { 82 | var id = CreateIdImpl(out var ex); 83 | return ex != null ? throw ex : id; 84 | } 85 | 86 | /// 87 | /// Attempts to a new Id. A return value indicates whether the operation succeeded. 88 | /// 89 | /// 90 | /// When this method returns, contains the generated Id if the method succeeded. If the method failed, as 91 | /// indicated by the return value, no guarantees can be made about the id. This parameter is passed uninitialized; 92 | /// any value originally supplied in result will be overwritten. 93 | /// 94 | /// true if an Id was generated successfully; false otherwise. 95 | /// This method will not throw exceptions but rather indicate success by the return value. 96 | public bool TryCreateId(out long id) 97 | { 98 | id = CreateIdImpl(out var ex); 99 | return ex == null; 100 | } 101 | 102 | /// 103 | /// Creates a new Id. 104 | /// 105 | /// If any exceptions occur they will be returned in this argument. 106 | /// 107 | /// Returns an Id based on the 's epoch, generatorid and sequence or 108 | /// a negative value when an exception occurred. 109 | /// 110 | /// Thrown when clock going backwards is detected. 111 | /// Thrown when sequence overflows. 112 | private long CreateIdImpl(out Exception? exception) 113 | { 114 | lock (_genlock) 115 | { 116 | // Determine "timeslot" and make sure it's >= last timeslot (if any) 117 | var ticks = GetTicks(); 118 | var timestamp = ticks & MASK_TIME; 119 | 120 | if (timestamp < _lastgen || ticks < 0) 121 | { 122 | exception = new InvalidSystemClockException(string.Format(Translations.ERR_CLOCK_MOVED_BACKWARDS, _lastgen - timestamp)); 123 | return -1; 124 | } 125 | 126 | // If we're in the same "timeslot" as previous time we generated an Id, up the sequence number 127 | if (timestamp == _lastgen) 128 | { 129 | if (_sequence >= MASK_SEQUENCE) 130 | { 131 | switch (Options.SequenceOverflowStrategy) 132 | { 133 | case SequenceOverflowStrategy.SpinWait: 134 | SpinWait.SpinUntil(() => _lastgen != GetTicks()); 135 | return CreateIdImpl(out exception); // Try again 136 | case SequenceOverflowStrategy.Throw: 137 | default: 138 | exception = new SequenceOverflowException(Translations.ERR_SEQUENCE_OVERFLOW_EX); 139 | return -1; 140 | } 141 | } 142 | _sequence++; 143 | } 144 | else // We're in a new(er) "timeslot", so we can reset the sequence and store the new(er) "timeslot" 145 | { 146 | _sequence = 0; 147 | _lastgen = timestamp; 148 | } 149 | 150 | // If we made it here then no exceptions occurred; make sure we communicate that to the caller by setting `exception` to null 151 | exception = null; 152 | // Build id by shifting all bits into their place 153 | return (timestamp << SHIFT_TIME) 154 | | (_generatorid << SHIFT_GENERATOR) 155 | | (long)_sequence; 156 | } 157 | } 158 | 159 | /// 160 | /// Returns information about an Id such as the sequence number, generator id and date/time the Id was generated 161 | /// based on the current of the generator. 162 | /// 163 | /// The Id to extract information from. 164 | /// Returns an that contains information about the 'decoded' Id. 165 | /// 166 | /// IMPORTANT: note that this method relies on the and timesource; if the id was 167 | /// generated with a diffferent IdStructure and/or timesource than the current one the 'decoded' ID will NOT 168 | /// contain correct information. 169 | /// 170 | public Id FromId(long id) => 171 | // Deconstruct Id by unshifting the bits into the proper parts 172 | new( 173 | (int)(id & MASK_SEQUENCE), 174 | (int)((id >> SHIFT_GENERATOR) & MASK_GENERATOR), 175 | Options.TimeSource.Epoch.Add(TimeSpan.FromTicks(((id >> SHIFT_TIME) & MASK_TIME) * Options.TimeSource.TickDuration.Ticks)) 176 | ); 177 | 178 | /// 179 | /// Gets the number of ticks since the 's epoch. 180 | /// 181 | /// Returns the number of ticks since the 's epoch. 182 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 183 | private long GetTicks() => Options.TimeSource.GetTicks(); 184 | 185 | /// 186 | /// Returns a bitmask masking out the desired number of bits; a bitmask of 2 returns 000...000011, a bitmask of 187 | /// 5 returns 000...011111. 188 | /// 189 | /// The number of bits to mask. 190 | /// Returns the desired bitmask. 191 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 192 | private static long GetMask(byte bits) => (1L << bits) - 1; 193 | 194 | /// 195 | /// Returns a 'never ending' stream of Id's. 196 | /// 197 | /// A 'never ending' stream of Id's. 198 | private IEnumerable IdStream() 199 | { 200 | while (true) 201 | { 202 | yield return CreateId(); 203 | } 204 | } 205 | 206 | /// 207 | /// Returns an enumerator that iterates over Id's. 208 | /// 209 | /// An object that can be used to iterate over Id's. 210 | public IEnumerator GetEnumerator() => IdStream().GetEnumerator(); 211 | 212 | /// 213 | /// Returns an enumerator that iterates over Id's. 214 | /// 215 | /// An object that can be used to iterate over Id's. 216 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 217 | } -------------------------------------------------------------------------------- /IdGen/IdGeneratorOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace IdGen; 4 | 5 | /// 6 | /// Represents the options an can be configured with. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The for ID's to be generated. 12 | /// The to use when generating ID's. 13 | /// The to use when generating ID's. 14 | public class IdGeneratorOptions( 15 | IdStructure? idStructure = null, 16 | ITimeSource? timeSource = null, 17 | SequenceOverflowStrategy sequenceOverflowStrategy = SequenceOverflowStrategy.Throw) 18 | { 19 | /// 20 | /// Returns the default epoch. 21 | /// 22 | public static readonly DateTime DefaultEpoch = new(2015, 1, 1, 0, 0, 0, DateTimeKind.Utc); 23 | 24 | private static readonly IdStructure _defaultidstructure = IdStructure.Default; 25 | private static readonly ITimeSource _defaulttimesource = new DefaultTimeSource(DefaultEpoch); 26 | private static readonly SequenceOverflowStrategy _defaultsequenceoverflowstrategy = SequenceOverflowStrategy.Throw; 27 | 28 | /// 29 | /// Returns a default instance of . 30 | /// 31 | public static readonly IdGeneratorOptions Default = new() 32 | { 33 | IdStructure = _defaultidstructure, 34 | TimeSource = _defaulttimesource, 35 | SequenceOverflowStrategy = _defaultsequenceoverflowstrategy 36 | }; 37 | 38 | /// 39 | /// Gets the of the generated ID's 40 | /// 41 | public IdStructure IdStructure { get; init; } = idStructure ?? _defaultidstructure; 42 | 43 | /// 44 | /// Gets the to use when generating ID's. 45 | /// 46 | public ITimeSource TimeSource { get; init; } = timeSource ?? _defaulttimesource; 47 | 48 | /// 49 | /// Gets the to use when generating ID's. 50 | /// 51 | public SequenceOverflowStrategy SequenceOverflowStrategy { get; init; } = sequenceOverflowStrategy; 52 | } -------------------------------------------------------------------------------- /IdGen/IdStructure.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace IdGen; 3 | 4 | /// 5 | /// Specifies the number of bits to use for the different parts of an Id for an . 6 | /// 7 | public class IdStructure 8 | { 9 | /// 10 | /// Gets number of bits to use for the timestamp part of the Id's to generate. 11 | /// 12 | public byte TimestampBits { get; private set; } 13 | 14 | /// 15 | /// Gets number of bits to use for the generator-id part of the Id's to generate. 16 | /// 17 | public byte GeneratorIdBits { get; private set; } 18 | 19 | /// 20 | /// Gets number of bits to use for the sequence part of the Id's to generate. 21 | /// 22 | public byte SequenceBits { get; private set; } 23 | 24 | /// 25 | /// Returns the maximum number of intervals for this configuration. 26 | /// 27 | public long MaxIntervals => 1L << TimestampBits; 28 | 29 | /// 30 | /// Returns the maximum number of generators available for this configuration. 31 | /// 32 | public int MaxGenerators => 1 << GeneratorIdBits; 33 | 34 | /// 35 | /// Returns the maximum number of sequential Id's for a time-interval (e.g. max. number of Id's generated 36 | /// within a single interval). 37 | /// 38 | public int MaxSequenceIds => 1 << SequenceBits; 39 | 40 | /// 41 | /// Gets a default with 41 bits for the timestamp part, 10 bits for the generator-id 42 | /// part and 12 bits for the sequence part of the id. 43 | /// 44 | public static IdStructure Default => new(41, 10, 12); 45 | 46 | /// 47 | /// Initializes an for s. 48 | /// 49 | /// Number of bits to use for the timestamp-part of Id's. 50 | /// Number of bits to use for the generator-id of Id's. 51 | /// Number of bits to use for the sequence-part of Id's. 52 | public IdStructure(byte timestampBits, byte generatorIdBits, byte sequenceBits) 53 | { 54 | if (timestampBits + generatorIdBits + sequenceBits != 63) 55 | { 56 | throw new InvalidOperationException(Translations.ERR_MUST_BE_63BITS_EXACTLY); 57 | } 58 | 59 | if (generatorIdBits > 31) 60 | { 61 | throw new ArgumentOutOfRangeException(nameof(generatorIdBits), Translations.ERR_GENERATORID_CANNOT_EXCEED_31BITS); 62 | } 63 | 64 | if (sequenceBits > 31) 65 | { 66 | throw new ArgumentOutOfRangeException(nameof(sequenceBits), Translations.ERR_SEQUENCE_CANNOT_EXCEED_31BITS); 67 | } 68 | 69 | TimestampBits = timestampBits; 70 | GeneratorIdBits = generatorIdBits; 71 | SequenceBits = sequenceBits; 72 | } 73 | 74 | /// 75 | /// Calculates the last date for an Id before a 'wrap around' will occur in the timestamp-part of an Id for the 76 | /// given . 77 | /// 78 | /// The used epoch for the to use as offset.' 79 | /// The used for the . 80 | /// The last date for an Id before a 'wrap around' will occur in the timestamp-part of an Id. 81 | /// 82 | /// Please note that for dates exceeding the an 83 | /// will be thrown. 84 | /// 85 | /// 86 | /// Thrown when any combination of a and 87 | /// results in a date exceeding the value. 88 | /// 89 | public DateTimeOffset WraparoundDate(DateTimeOffset epoch, ITimeSource timeSource) => timeSource == null 90 | ? throw new ArgumentNullException(nameof(timeSource)) 91 | : epoch.AddDays(timeSource.TickDuration.TotalDays * MaxIntervals); 92 | 93 | /// 94 | /// Calculates the interval at wich a 'wrap around' will occur in the timestamp-part of an Id for the given 95 | /// . 96 | /// 97 | /// The used for the . 98 | /// 99 | /// The interval at wich a 'wrap around' will occur in the timestamp-part of an Id for the given 100 | /// . 101 | /// 102 | /// 103 | /// Please note that for intervals exceeding the an 104 | /// will be thrown. 105 | /// 106 | /// 107 | /// Thrown when is null. 108 | /// 109 | /// 110 | /// Thrown when any combination of a and 111 | /// results in a TimeSpan exceeding the value. 112 | /// 113 | public TimeSpan WraparoundInterval(ITimeSource timeSource) => timeSource == null 114 | ? throw new ArgumentNullException(nameof(timeSource)) 115 | : TimeSpan.FromDays(timeSource.TickDuration.TotalDays * MaxIntervals); 116 | } -------------------------------------------------------------------------------- /IdGen/InvalidSystemClockException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace IdGen; 4 | 5 | /// 6 | /// The exception that is thrown when a clock going backwards is detected. 7 | /// 8 | /// 9 | /// Initializes a new instance of the class with a message that describes 10 | /// the error and underlying exception. 11 | /// 12 | /// 13 | /// The message that describes the exception. The caller of this constructor is required to ensure that this 14 | /// string has been localized for the current system culture. 15 | /// 16 | /// 17 | /// The exception that is the cause of the current . If the 18 | /// innerException parameter is not null, the current exception is raised in a catch block that handles the 19 | /// inner exception. 20 | /// 21 | public class InvalidSystemClockException(string message, Exception? innerException) : Exception(message, innerException) 22 | { 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | public InvalidSystemClockException() : this(Translations.ERR_INVALID_SYSTEM_CLOCK) { } 27 | 28 | /// 29 | /// Initializes a new instance of the class with a message that describes the error. 30 | /// 31 | /// 32 | /// The message that describes the exception. The caller of this constructor is required to ensure that this 33 | /// string has been localized for the current system culture. 34 | /// 35 | public InvalidSystemClockException(string message) 36 | : this(message, null) { } 37 | } 38 | -------------------------------------------------------------------------------- /IdGen/SequenceOverflowException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace IdGen; 4 | 5 | /// 6 | /// The exception that is thrown when a sequence overflows (e.g. too many Id's generated within the same timespan (ms)). 7 | /// 8 | /// 9 | /// Initializes a new instance of the class with a message that describes 10 | /// the error and underlying exception. 11 | /// 12 | /// 13 | /// The message that describes the exception. The caller of this constructor is required to ensure that this 14 | /// string has been localized for the current system culture. 15 | /// 16 | /// 17 | /// The exception that is the cause of the current . If the 18 | /// innerException parameter is not null, the current exception is raised in a catch block that handles the 19 | /// inner exception. 20 | /// 21 | public class SequenceOverflowException(string message, Exception? innerException) : Exception(message, innerException) 22 | { 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | public SequenceOverflowException() : this(Translations.ERR_SEQUENCE_OVERFLOW) { } 27 | 28 | /// 29 | /// Initializes a new instance of the class with a message that describes the error. 30 | /// 31 | /// 32 | /// The message that describes the exception. The caller of this constructor is required to ensure that this 33 | /// string has been localized for the current system culture. 34 | /// 35 | public SequenceOverflowException(string message) 36 | : this(message, null) { } 37 | } -------------------------------------------------------------------------------- /IdGen/SequenceOverflowStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace IdGen; 2 | 3 | /// 4 | /// Specifies the strategy to use when a sequence overflow occurs during generation of an ID. 5 | /// 6 | public enum SequenceOverflowStrategy 7 | { 8 | /// 9 | /// Throw a on sequence overflow. 10 | /// 11 | Throw = 0, 12 | /// 13 | /// Wait, using a , for the tick te pass before generating a new ID. 14 | /// 15 | SpinWait = 1 16 | } 17 | -------------------------------------------------------------------------------- /IdGen/StopwatchTimeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace IdGen; 5 | 6 | /// 7 | /// Provides time data to an . This timesource uses a for timekeeping. 8 | /// 9 | public abstract class StopwatchTimeSource : ITimeSource 10 | { 11 | private static readonly Stopwatch _sw = new(); 12 | private static readonly DateTimeOffset _initialized = DateTimeOffset.UtcNow; 13 | 14 | /// 15 | /// Gets the epoch of the . 16 | /// 17 | public DateTimeOffset Epoch { get; private set; } 18 | 19 | /// 20 | /// Gets the elapsed time since this was initialized. 21 | /// 22 | protected static TimeSpan Elapsed => _sw.Elapsed; 23 | 24 | /// 25 | /// Gets the offset for this which is defined as the difference of it's creationdate 26 | /// and it's epoch which is specified in the object's constructor. 27 | /// 28 | protected TimeSpan Offset { get; private set; } 29 | 30 | /// 31 | /// Initializes a new object. 32 | /// 33 | /// The epoch to use as an offset from now, 34 | /// The duration of a single tick for this timesource. 35 | public StopwatchTimeSource(DateTimeOffset epoch, TimeSpan tickDuration) 36 | { 37 | Epoch = epoch; 38 | Offset = _initialized - Epoch; 39 | TickDuration = tickDuration; 40 | 41 | // Start (or resume) stopwatch 42 | _sw.Start(); 43 | } 44 | 45 | /// 46 | /// Returns the duration of a single tick. 47 | /// 48 | public TimeSpan TickDuration { get; private set; } 49 | 50 | /// 51 | /// Returns the current number of ticks for the . 52 | /// 53 | /// The current number of ticks to be used by an when creating an Id. 54 | public abstract long GetTicks(); 55 | } 56 | -------------------------------------------------------------------------------- /IdGen/Translations.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace IdGen { 12 | using System; 13 | using System.Reflection; 14 | 15 | 16 | /// 17 | /// A strongly-typed resource class, for looking up localized strings, etc. 18 | /// 19 | // This class was auto-generated by the StronglyTypedResourceBuilder 20 | // class via a tool like ResGen or Visual Studio. 21 | // To add or remove a member, edit your .ResX file then rerun ResGen 22 | // with the /str option, or rebuild your VS project. 23 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 24 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 25 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 26 | internal class Translations { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Translations() { 34 | } 35 | 36 | /// 37 | /// Returns the cached ResourceManager instance used by this class. 38 | /// 39 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 40 | internal static global::System.Resources.ResourceManager ResourceManager { 41 | get { 42 | if (object.ReferenceEquals(resourceMan, null)) { 43 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IdGen.Translations", typeof(Translations).GetTypeInfo().Assembly); 44 | resourceMan = temp; 45 | } 46 | return resourceMan; 47 | } 48 | } 49 | 50 | /// 51 | /// Overrides the current thread's CurrentUICulture property for all 52 | /// resource lookups using this strongly typed resource class. 53 | /// 54 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 55 | internal static global::System.Globalization.CultureInfo Culture { 56 | get { 57 | return resourceCulture; 58 | } 59 | set { 60 | resourceCulture = value; 61 | } 62 | } 63 | 64 | /// 65 | /// Looks up a localized string similar to Clock moved backwards or wrapped around. Refusing to generate id for {0} ticks.. 66 | /// 67 | internal static string ERR_CLOCK_MOVED_BACKWARDS { 68 | get { 69 | return ResourceManager.GetString("ERR_CLOCK_MOVED_BACKWARDS", resourceCulture); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized string similar to GeneratorId cannot have more than 31 bits.. 75 | /// 76 | internal static string ERR_GENERATORID_CANNOT_EXCEED_31BITS { 77 | get { 78 | return ResourceManager.GetString("ERR_GENERATORID_CANNOT_EXCEED_31BITS", resourceCulture); 79 | } 80 | } 81 | 82 | /// 83 | /// Looks up a localized string similar to GeneratorId must be from 0 to {0}.. 84 | /// 85 | internal static string ERR_INVALID_GENERATORID { 86 | get { 87 | return ResourceManager.GetString("ERR_INVALID_GENERATORID", resourceCulture); 88 | } 89 | } 90 | 91 | /// 92 | /// Looks up a localized string similar to Invalid system clock.. 93 | /// 94 | internal static string ERR_INVALID_SYSTEM_CLOCK { 95 | get { 96 | return ResourceManager.GetString("ERR_INVALID_SYSTEM_CLOCK", resourceCulture); 97 | } 98 | } 99 | 100 | /// 101 | /// Looks up a localized string similar to Number of bits used to generate Id's is not equal to 63.. 102 | /// 103 | internal static string ERR_MUST_BE_63BITS_EXACTLY { 104 | get { 105 | return ResourceManager.GetString("ERR_MUST_BE_63BITS_EXACTLY", resourceCulture); 106 | } 107 | } 108 | 109 | /// 110 | /// Looks up a localized string similar to Sequence cannot have more than 31 bits.. 111 | /// 112 | internal static string ERR_SEQUENCE_CANNOT_EXCEED_31BITS { 113 | get { 114 | return ResourceManager.GetString("ERR_SEQUENCE_CANNOT_EXCEED_31BITS", resourceCulture); 115 | } 116 | } 117 | 118 | /// 119 | /// Looks up a localized string similar to Sequence overflow.. 120 | /// 121 | internal static string ERR_SEQUENCE_OVERFLOW { 122 | get { 123 | return ResourceManager.GetString("ERR_SEQUENCE_OVERFLOW", resourceCulture); 124 | } 125 | } 126 | 127 | /// 128 | /// Looks up a localized string similar to Sequence overflow. Refusing to generate id for rest of tick.. 129 | /// 130 | internal static string ERR_SEQUENCE_OVERFLOW_EX { 131 | get { 132 | return ResourceManager.GetString("ERR_SEQUENCE_OVERFLOW_EX", resourceCulture); 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /IdGen/Translations.nl.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Klok is teruggelopen of omgewenteld. Weigering id's te genereren voor {0} kloktikken. 122 | 123 | 124 | GeneratorId mag niet meer dan 31 bits bevatten. 125 | 126 | 127 | GeneratorId moet tussen 0 en {0] liggen. 128 | 129 | 130 | Volgnummer mag niet meer dan 31 bits bevatten. 131 | 132 | 133 | Volgnummer overloop. Weigering id's te genereren voor de rest van de kloktik. 134 | 135 | 136 | Volgnummer overloop. 137 | 138 | 139 | Ongeldige systeemklok. 140 | 141 | 142 | Het aantal bits dat wordt gebruikt om Id's te genereren is niet gelijk aan 63. 143 | 144 | -------------------------------------------------------------------------------- /IdGen/Translations.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Clock moved backwards or wrapped around. Refusing to generate id for {0} ticks. 122 | 123 | 124 | GeneratorId cannot have more than 31 bits. 125 | 126 | 127 | GeneratorId must be from 0 to {0}. 128 | 129 | 130 | Sequence cannot have more than 31 bits. 131 | 132 | 133 | Sequence overflow. Refusing to generate id for rest of tick. 134 | 135 | 136 | Sequence overflow. 137 | 138 | 139 | Invalid system clock. 140 | 141 | 142 | Number of bits used to generate Id's is not equal to 63. 143 | 144 | -------------------------------------------------------------------------------- /IdGenTests/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /IdGenTests/ConfigTests.cs: -------------------------------------------------------------------------------- 1 | using IdGen; 2 | using IdGen.Configuration; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace IdGenTests; 8 | 9 | [TestClass] 10 | public class ConfigTests 11 | { 12 | [TestMethod] 13 | public void IdGenerator_GetFromConfig_CreatesCorrectGenerator1() 14 | { 15 | var target = AppConfigFactory.GetFromConfig("foo"); 16 | 17 | Assert.AreEqual(123, target.Id); 18 | Assert.AreEqual(new DateTime(2016, 1, 2, 12, 34, 56, DateTimeKind.Utc), target.Options.TimeSource.Epoch); 19 | Assert.AreEqual(39, target.Options.IdStructure.TimestampBits); 20 | Assert.AreEqual(11, target.Options.IdStructure.GeneratorIdBits); 21 | Assert.AreEqual(13, target.Options.IdStructure.SequenceBits); 22 | Assert.AreEqual(TimeSpan.FromMilliseconds(50), target.Options.TimeSource.TickDuration); 23 | Assert.AreEqual(SequenceOverflowStrategy.Throw, target.Options.SequenceOverflowStrategy); 24 | } 25 | 26 | [TestMethod] 27 | public void IdGenerator_GetFromConfig_CreatesCorrectGenerator2() 28 | { 29 | var target = AppConfigFactory.GetFromConfig("baz"); 30 | 31 | Assert.AreEqual(2047, target.Id); 32 | Assert.AreEqual(new DateTime(2016, 2, 29, 0, 0, 0, DateTimeKind.Utc), target.Options.TimeSource.Epoch); 33 | Assert.AreEqual(21, target.Options.IdStructure.TimestampBits); 34 | Assert.AreEqual(21, target.Options.IdStructure.GeneratorIdBits); 35 | Assert.AreEqual(21, target.Options.IdStructure.SequenceBits); 36 | Assert.AreEqual(TimeSpan.FromTicks(7), target.Options.TimeSource.TickDuration); 37 | Assert.AreEqual(SequenceOverflowStrategy.SpinWait, target.Options.SequenceOverflowStrategy); 38 | } 39 | 40 | [TestMethod] 41 | [ExpectedException(typeof(KeyNotFoundException))] 42 | public void IdGenerator_GetFromConfig_IsCaseSensitive() => AppConfigFactory.GetFromConfig("Foo"); 43 | 44 | [TestMethod] 45 | [ExpectedException(typeof(KeyNotFoundException))] 46 | public void IdGenerator_GetFromConfig_ThrowsOnNonExisting() => AppConfigFactory.GetFromConfig("xxx"); 47 | 48 | 49 | [TestMethod] 50 | [ExpectedException(typeof(InvalidOperationException))] 51 | public void IdGenerator_GetFromConfig_ThrowsOnInvalidIdStructure() => AppConfigFactory.GetFromConfig("e1"); 52 | 53 | [TestMethod] 54 | [ExpectedException(typeof(FormatException))] 55 | public void IdGenerator_GetFromConfig_ThrowsOnInvalidEpoch() => AppConfigFactory.GetFromConfig("e2"); 56 | 57 | [TestMethod] 58 | public void IdGenerator_GetFromConfig_ReturnsSameInstanceForSameName() 59 | { 60 | var target1 = AppConfigFactory.GetFromConfig("foo"); 61 | var target2 = AppConfigFactory.GetFromConfig("foo"); 62 | 63 | Assert.IsTrue(ReferenceEquals(target1, target2)); 64 | } 65 | 66 | [TestMethod] 67 | public void IdGenerator_GetFromConfig_ParsesEpochCorrectly() 68 | { 69 | Assert.AreEqual(new DateTime(2016, 1, 2, 12, 34, 56, DateTimeKind.Utc), AppConfigFactory.GetFromConfig("foo").Options.TimeSource.Epoch); 70 | Assert.AreEqual(new DateTime(2016, 2, 1, 1, 23, 45, DateTimeKind.Utc), AppConfigFactory.GetFromConfig("bar").Options.TimeSource.Epoch); 71 | Assert.AreEqual(new DateTime(2016, 2, 29, 0, 0, 0, DateTimeKind.Utc), AppConfigFactory.GetFromConfig("baz").Options.TimeSource.Epoch); 72 | Assert.AreEqual(IdGeneratorOptions.DefaultEpoch, AppConfigFactory.GetFromConfig("nt").Options.TimeSource.Epoch); 73 | } 74 | 75 | [TestMethod] 76 | public void IdGenerator_GetFromConfig_ParsesTickDurationCorrectly() 77 | { 78 | Assert.AreEqual(TimeSpan.FromMilliseconds(50), AppConfigFactory.GetFromConfig("foo").Options.TimeSource.TickDuration); 79 | Assert.AreEqual(new TimeSpan(1, 2, 3), AppConfigFactory.GetFromConfig("bar").Options.TimeSource.TickDuration); 80 | Assert.AreEqual(TimeSpan.FromTicks(7), AppConfigFactory.GetFromConfig("baz").Options.TimeSource.TickDuration); 81 | 82 | // Make sure the default tickduration is 1 ms 83 | Assert.AreEqual(TimeSpan.FromMilliseconds(1), AppConfigFactory.GetFromConfig("nt").Options.TimeSource.TickDuration); 84 | } 85 | 86 | [TestMethod] 87 | public void IdGeneratorElement_Property_Setters() 88 | { 89 | // We create an IdGeneratorElement from code and compare it to an IdGeneratorElement from config. 90 | var target = new IdGeneratorElement() 91 | { 92 | Name = "newfoo", 93 | Id = 123, 94 | Epoch = new DateTime(2016, 1, 2, 12, 34, 56, DateTimeKind.Utc), 95 | TimestampBits = 39, 96 | GeneratorIdBits = 11, 97 | SequenceBits = 13, 98 | TickDuration = TimeSpan.FromMilliseconds(50), 99 | SequenceOverflowStrategy = SequenceOverflowStrategy.Throw 100 | }; 101 | var expected = AppConfigFactory.GetFromConfig("foo"); 102 | 103 | Assert.AreEqual(expected.Id, target.Id); 104 | Assert.AreEqual(expected.Options.TimeSource.Epoch, target.Epoch); 105 | Assert.AreEqual(expected.Options.IdStructure.TimestampBits, target.TimestampBits); 106 | Assert.AreEqual(expected.Options.IdStructure.GeneratorIdBits, target.GeneratorIdBits); 107 | Assert.AreEqual(expected.Options.IdStructure.SequenceBits, target.SequenceBits); 108 | Assert.AreEqual(expected.Options.TimeSource.TickDuration, target.TickDuration); 109 | Assert.AreEqual(expected.Options.SequenceOverflowStrategy, target.SequenceOverflowStrategy); 110 | } 111 | } -------------------------------------------------------------------------------- /IdGenTests/DependencyInjectionTests.cs: -------------------------------------------------------------------------------- 1 | using IdGen; 2 | using IdGen.DependencyInjection; 3 | using IdGenTests.Mocks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using System; 7 | 8 | namespace IdGenTests; 9 | 10 | [TestClass] 11 | public class DependencyInjectionTests 12 | { 13 | [TestMethod] 14 | public void DependencyInjection_Resolves_IdGenerator() 15 | { 16 | var serviceProvider = new ServiceCollection() 17 | .AddIdGen(123) 18 | .BuildServiceProvider(); 19 | 20 | var idgenerator = serviceProvider.GetRequiredService(); 21 | Assert.IsNotNull(idgenerator); 22 | 23 | var id = idgenerator.CreateId(); 24 | var target = idgenerator.FromId(id); 25 | 26 | Assert.AreEqual(123, target.GeneratorId); 27 | } 28 | 29 | [TestMethod] 30 | public void DependencyInjection_Resolves_IIdGenerator() 31 | { 32 | var serviceProvider = new ServiceCollection() 33 | .AddIdGen(456) 34 | .BuildServiceProvider(); 35 | 36 | var idgenerator = serviceProvider.GetRequiredService>(); 37 | Assert.IsNotNull(idgenerator); 38 | } 39 | 40 | [TestMethod] 41 | public void DependencyInjection_Resolves_Singleton() 42 | { 43 | var serviceProvider = new ServiceCollection() 44 | .AddIdGen(789) 45 | .AddIdGen(654) // This should be a no-op 46 | .BuildServiceProvider(); 47 | 48 | var idgen1 = serviceProvider.GetRequiredService>(); 49 | var idgen2 = serviceProvider.GetRequiredService(); 50 | 51 | Assert.ReferenceEquals(idgen1, idgen2); 52 | Assert.AreEqual(789, idgen2.FromId(idgen1.CreateId()).GeneratorId); 53 | Assert.AreEqual(789, idgen2.FromId(idgen2.CreateId()).GeneratorId); 54 | } 55 | 56 | [TestMethod] 57 | public void DependencyInjection_AppliesOptions() 58 | { 59 | var epoch = new DateTimeOffset(2022, 5, 18, 0, 0, 0, TimeSpan.Zero); 60 | var idstruct = new IdStructure(39, 11, 13); 61 | var ts = new MockTimeSource(69, TimeSpan.FromMinutes(1), epoch); 62 | 63 | var serviceProvider = new ServiceCollection() 64 | .AddIdGen(420, () => new IdGeneratorOptions(idstruct, ts)) 65 | .BuildServiceProvider(); 66 | 67 | var idgen = serviceProvider.GetRequiredService(); 68 | var id = idgen.FromId(idgen.CreateId()); 69 | 70 | Assert.AreEqual(0, id.SequenceNumber); 71 | Assert.AreEqual(420, id.GeneratorId); 72 | Assert.AreEqual(epoch.AddMinutes(69), id.DateTimeOffset); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /IdGenTests/IDTests.cs: -------------------------------------------------------------------------------- 1 | using IdGen; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace IdGenTests; 5 | 6 | [TestClass] 7 | public class IDTests 8 | { 9 | [TestMethod] 10 | public void ID_DoesNotEqual_RandomObject() 11 | { 12 | var g = new IdGenerator(0); 13 | var i = g.FromId(0); 14 | Assert.IsFalse(i.Equals(new object())); 15 | Assert.IsTrue(i.Equals((object)g.FromId(0))); 16 | Assert.IsTrue(i != g.FromId(1)); 17 | Assert.IsTrue(i == g.FromId(0)); 18 | Assert.AreEqual(i.GetHashCode(), g.FromId(0).GetHashCode()); 19 | } 20 | 21 | [TestMethod] 22 | public void ID_Equals_OtherId() 23 | { 24 | var g = new IdGenerator(0); 25 | var i = g.FromId(1234567890); 26 | Assert.IsTrue(i.Equals(g.FromId(1234567890))); 27 | Assert.IsTrue(i.Equals((object)g.FromId(1234567890))); 28 | Assert.IsTrue(i != g.FromId(0)); 29 | Assert.IsTrue(i == g.FromId(1234567890)); 30 | Assert.AreEqual(i.GetHashCode(), g.FromId(1234567890).GetHashCode()); 31 | } 32 | 33 | [TestMethod] 34 | public void ID_FromZeroInt_HasCorrectValue() 35 | { 36 | var g = new IdGenerator(0); 37 | var i = g.FromId(0); 38 | 39 | Assert.AreEqual(0, i.SequenceNumber); 40 | Assert.AreEqual(0, i.GeneratorId); 41 | Assert.AreEqual(g.Options.TimeSource.Epoch, i.DateTimeOffset); 42 | } 43 | 44 | 45 | [TestMethod] 46 | public void ID_FromOneInt_HasCorrectValue() 47 | { 48 | var g = new IdGenerator(0); 49 | var i = g.FromId(1); 50 | 51 | Assert.AreEqual(1, i.SequenceNumber); 52 | Assert.AreEqual(0, i.GeneratorId); 53 | Assert.AreEqual(g.Options.TimeSource.Epoch, i.DateTimeOffset); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /IdGenTests/IdGenTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net48;net9 5 | enable 6 | latest 7 | false 8 | Debug;Release 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | testhost.dll.config 32 | PreserveNewest 33 | 34 | 35 | -------------------------------------------------------------------------------- /IdGenTests/IdGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using IdGen; 2 | using IdGenTests.Mocks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Collections; 6 | using System.Linq; 7 | 8 | namespace IdGenTests; 9 | 10 | [TestClass] 11 | public class IdGeneratorTests 12 | { 13 | [TestMethod] 14 | public void Sequence_ShouldIncrease_EveryInvocation() 15 | { 16 | // We setup our generator so that the time is 0, generator id 0 and we're only left with the sequence 17 | // increasing each invocation of CreateId(); 18 | var ts = new MockTimeSource(0); 19 | var g = new IdGenerator(0, new IdGeneratorOptions(timeSource: ts)); 20 | 21 | Assert.AreEqual(0, g.CreateId()); 22 | Assert.AreEqual(1, g.CreateId()); 23 | Assert.AreEqual(2, g.CreateId()); 24 | } 25 | 26 | [TestMethod] 27 | public void Sequence_ShouldReset_EveryNewTick() 28 | { 29 | // We setup our generator so that the time is 0, generator id 0 and we're only left with the sequence 30 | // increasing each invocation of CreateId(); 31 | var ts = new MockTimeSource(0); 32 | var g = new IdGenerator(0, new IdGeneratorOptions(timeSource: ts)); 33 | 34 | Assert.AreEqual(0, g.CreateId()); 35 | Assert.AreEqual(1, g.CreateId()); 36 | ts.NextTick(); 37 | // Since the timestamp has increased, we should now have a much higher value (since the timestamp is 38 | // shifted left a number of bits (specifically GeneratorIdBits + SequenceBits) 39 | Assert.AreEqual((1 << (g.Options.IdStructure.GeneratorIdBits + g.Options.IdStructure.SequenceBits)) + 0, g.CreateId()); 40 | Assert.AreEqual((1 << (g.Options.IdStructure.GeneratorIdBits + g.Options.IdStructure.SequenceBits)) + 1, g.CreateId()); 41 | } 42 | 43 | [TestMethod] 44 | public void GeneratorId_ShouldBePresent_InID1() 45 | { 46 | // We setup our generator so that the time is 0 and generator id equals 1023 so that all 10 bits are set 47 | // for the generator. 48 | var ts = new MockTimeSource(0); 49 | var g = new IdGenerator(1023, new IdGeneratorOptions(timeSource: ts)); 50 | 51 | // Make sure all expected bits are set 52 | Assert.AreEqual(((1 << g.Options.IdStructure.GeneratorIdBits) - 1) << g.Options.IdStructure.SequenceBits, g.CreateId()); 53 | } 54 | 55 | [TestMethod] 56 | public void GeneratorId_ShouldBePresent_InID2() 57 | { 58 | // We setup our generator so that the time is 0 and generator id equals 4095 so that all 12 bits are set 59 | // for the generator. 60 | var ts = new MockTimeSource(); 61 | var s = new IdStructure(40, 12, 11); // We use a custom IdStructure with 12 bits for the generator this time 62 | var g = new IdGenerator(4095, new IdGeneratorOptions(s, ts)); 63 | 64 | // Make sure all expected bits are set 65 | Assert.AreEqual(-1 & ((1 << 12) - 1), g.Id); 66 | Assert.AreEqual(((1 << 12) - 1) << 11, g.CreateId()); 67 | } 68 | 69 | [TestMethod] 70 | public void GeneratorId_ShouldBeMasked_WhenReadFromProperty() 71 | { 72 | // We setup our generator so that the time is 0 and generator id equals 1023 so that all 10 bits are set 73 | // for the generator. 74 | var ts = new MockTimeSource(); 75 | var g = new IdGenerator(1023, new IdGeneratorOptions(timeSource: ts)); 76 | 77 | // Make sure all expected bits are set 78 | Assert.AreEqual((1 << g.Options.IdStructure.GeneratorIdBits) - 1, g.Id); 79 | } 80 | 81 | [TestMethod] 82 | public void Constructor_DoesNotThrow_OnMaxGeneratorId() 83 | { 84 | var structure = new IdStructure(41, 10, 12); 85 | // 1023 is the max generator id for 10 bits. 86 | var maxgeneratorid = 1023; 87 | _ = new IdGenerator(maxgeneratorid, new IdGeneratorOptions(structure)); 88 | } 89 | 90 | [TestMethod] 91 | public void Constructor_DoesNotThrow_OnGeneratorId_0() 92 | => _ = new IdGenerator(0, new IdGeneratorOptions(new IdStructure(41, 10, 12))); 93 | 94 | [TestMethod] 95 | [ExpectedException(typeof(ArgumentNullException))] 96 | public void Constructor_Throws_OnNull_Options() 97 | => new IdGenerator(1024, null!); 98 | 99 | [TestMethod] 100 | [ExpectedException(typeof(ArgumentOutOfRangeException))] 101 | public void Constructor_Throws_OnInvalidGeneratorId_Positive_MaxPlusOne() 102 | { 103 | var structure = new IdStructure(41, 10, 12); 104 | // 1023 is the max generator id for 10 bits. 105 | var maxgeneratorid = 1023; 106 | var maxPlusOne = maxgeneratorid + 1; 107 | _ = new IdGenerator(maxPlusOne, new IdGeneratorOptions(structure)); 108 | } 109 | 110 | [TestMethod] 111 | [ExpectedException(typeof(ArgumentOutOfRangeException))] 112 | public void Constructor_Throws_OnInvalidGeneratorId_Negative() 113 | => new IdGenerator(-1); 114 | 115 | [TestMethod] 116 | public void Constructor_DoesNotThrow_OnMaxValidatorId() 117 | => _ = new IdGenerator(int.MaxValue, new IdGeneratorOptions { IdStructure = new IdStructure(16, 31, 16) }); 118 | 119 | [TestMethod] 120 | public void Constructor_UsesCorrectId() 121 | => Assert.AreEqual(42, new IdGenerator(42).Id); 122 | 123 | [TestMethod] 124 | [ExpectedException(typeof(SequenceOverflowException))] 125 | public void CreateId_Throws_OnSequenceOverflow() 126 | { 127 | var ts = new MockTimeSource(); 128 | var s = new IdStructure(41, 20, 2); 129 | var g = new IdGenerator(0, new IdGeneratorOptions(idStructure: s, timeSource: ts)); 130 | 131 | // We have a 2-bit sequence; generating 4 id's shouldn't be a problem 132 | for (var i = 0; i < 4; i++) 133 | { 134 | Assert.AreEqual(i, g.CreateId()); 135 | } 136 | 137 | // However, if we invoke once more we should get an SequenceOverflowException 138 | g.CreateId(); 139 | } 140 | 141 | [TestMethod] 142 | public void TryCreateId_Returns_False_OnSequenceOverflow() 143 | { 144 | var ts = new MockTimeSource(); 145 | var s = new IdStructure(41, 20, 2); 146 | var g = new IdGenerator(0, new IdGeneratorOptions(idStructure: s, timeSource: ts)); 147 | 148 | // We have a 2-bit sequence; generating 4 id's shouldn't be a problem 149 | for (var i = 0; i < 4; i++) 150 | { 151 | Assert.IsTrue(g.TryCreateId(out var _)); 152 | } 153 | 154 | // However, if we invoke once more we should get an SequenceOverflowException 155 | // which should be indicated by the false return value 156 | Assert.IsFalse(g.TryCreateId(out var _)); 157 | } 158 | 159 | [TestMethod] 160 | public void Enumerable_ShoudReturn_Ids() 161 | { 162 | var g = new IdGenerator(0, IdGeneratorOptions.Default); 163 | var ids = g.Take(1000).ToArray(); 164 | 165 | Assert.AreEqual(1000, ids.Distinct().Count()); 166 | } 167 | 168 | [TestMethod] 169 | public void Enumerable_ShoudReturn_Ids_InterfaceExplicit() 170 | { 171 | var g = (IEnumerable)new IdGenerator(0, IdGeneratorOptions.Default); 172 | var ids = g.OfType().Take(1000).ToArray(); 173 | Assert.AreEqual(1000, ids.Distinct().Count()); 174 | } 175 | 176 | [TestMethod] 177 | [ExpectedException(typeof(InvalidSystemClockException))] 178 | public void CreateId_Throws_OnClockBackwards() 179 | { 180 | var ts = new MockTimeSource(100); 181 | var g = new IdGenerator(0, new IdGeneratorOptions(timeSource: ts)); 182 | 183 | g.CreateId(); 184 | ts.PreviousTick(); // Set clock back 1 'tick', this results in the time going from "100" to "99" 185 | g.CreateId(); 186 | } 187 | 188 | [TestMethod] 189 | public void TryCreateId_Returns_False_OnClockBackwards() 190 | { 191 | var ts = new MockTimeSource(100); 192 | var g = new IdGenerator(0, new IdGeneratorOptions(timeSource: ts)); 193 | 194 | Assert.IsTrue(g.TryCreateId(out var _)); 195 | ts.PreviousTick(); // Set clock back 1 'tick', this results in the time going from "100" to "99" 196 | Assert.IsFalse(g.TryCreateId(out var _)); 197 | } 198 | 199 | [TestMethod] 200 | [ExpectedException(typeof(InvalidSystemClockException))] 201 | public void CreateId_Throws_OnTimestampWraparound() 202 | { 203 | var ts = new MockTimeSource(long.MaxValue); // Set clock to 1 'tick' before wraparound 204 | var g = new IdGenerator(0, new IdGeneratorOptions(timeSource: ts)); 205 | 206 | Assert.IsTrue(g.CreateId() > 0); // Should succeed; 207 | ts.NextTick(); 208 | g.CreateId(); // Should fail 209 | } 210 | 211 | [TestMethod] 212 | public void TryCreateId_Returns_False_OnTimestampWraparound() 213 | { 214 | var ts = new MockTimeSource(long.MaxValue); // Set clock to 1 'tick' before wraparound 215 | var g = new IdGenerator(0, new IdGeneratorOptions(timeSource: ts)); 216 | 217 | Assert.IsTrue(g.TryCreateId(out var _)); // Should succeed; 218 | ts.NextTick(); 219 | Assert.IsFalse(g.TryCreateId(out var _)); // Should fail 220 | } 221 | 222 | [TestMethod] 223 | public void FromId_Returns_CorrectValue() 224 | { 225 | var s = new IdStructure(42, 8, 13); 226 | var epoch = new DateTimeOffset(2018, 7, 31, 14, 48, 2, TimeSpan.FromHours(2)); // Just some "random" epoch... 227 | var ts = new MockTimeSource(5, TimeSpan.FromSeconds(7), epoch); // Set clock at 5 ticks; each tick being 7 seconds... 228 | // Set generator ID to 234 229 | var g = new IdGenerator(234, new IdGeneratorOptions(s, ts)); 230 | 231 | // Generate a bunch of id's 232 | long id = 0; 233 | for (var i = 0; i < 35; i++) 234 | { 235 | id = g.CreateId(); 236 | } 237 | 238 | var target = g.FromId(id); 239 | 240 | 241 | Assert.AreEqual(34, target.SequenceNumber); // We generated 35 id's in the same tick, so sequence should be at 34. 242 | Assert.AreEqual(234, target.GeneratorId); // With generator id 234 243 | Assert.AreEqual(epoch.Add(TimeSpan.FromSeconds(5 * 7)), target.DateTimeOffset); // And the clock was at 5 ticks, with each tick being 244 | // 7 seconds (so 35 seconds from epoch) 245 | // And epoch was 2018-7-31 14:48:02 +02:00... 246 | } 247 | 248 | [TestMethod] 249 | public void CreateId_Waits_OnSequenceOverflow() 250 | { 251 | // Use timesource that generates a new tick every 10 calls to GetTicks() 252 | var ts = new MockAutoIncrementingIntervalTimeSource(10); 253 | var s = new IdStructure(61, 0, 2); 254 | var g = new IdGenerator(0, new IdGeneratorOptions(idStructure: s, timeSource: ts, sequenceOverflowStrategy: SequenceOverflowStrategy.SpinWait)); 255 | 256 | // We have a 2-bit sequence; generating 4 id's in a single time slot - wait for other then 257 | Assert.AreEqual(0, g.CreateId()); 258 | Assert.AreEqual(1, g.CreateId()); 259 | Assert.AreEqual(2, g.CreateId()); 260 | Assert.AreEqual(3, g.CreateId()); 261 | Assert.AreEqual(4, g.CreateId()); // This should trigger a spinwait and return the next ID 262 | Assert.AreEqual(5, g.CreateId()); 263 | } 264 | } -------------------------------------------------------------------------------- /IdGenTests/IdStructureTests.cs: -------------------------------------------------------------------------------- 1 | using IdGen; 2 | using IdGenTests.Mocks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | 6 | namespace IdGenTests; 7 | 8 | [TestClass] 9 | public class IdStructureTests 10 | { 11 | [TestMethod] 12 | public void DefaultIdStructure_Matches_Expectations() 13 | { 14 | var s = IdStructure.Default; 15 | 16 | Assert.AreEqual(41, s.TimestampBits); 17 | Assert.AreEqual(10, s.GeneratorIdBits); 18 | Assert.AreEqual(12, s.SequenceBits); 19 | 20 | // We should be able to generate a total of 63 bits worth of Id's 21 | Assert.AreEqual(long.MaxValue, (s.MaxGenerators * s.MaxIntervals * s.MaxSequenceIds) - 1); 22 | } 23 | 24 | [TestMethod] 25 | [ExpectedException(typeof(InvalidOperationException))] 26 | public void Constructor_Throws_OnIdStructureNotExactly63Bits() => new IdStructure(41, 10, 11); 27 | 28 | [TestMethod] 29 | [ExpectedException(typeof(ArgumentOutOfRangeException))] 30 | public void Constructor_Throws_OnGeneratorIdMoreThan31Bits() => new IdStructure(21, 32, 10); 31 | 32 | [TestMethod] 33 | [ExpectedException(typeof(ArgumentOutOfRangeException))] 34 | public void Constructor_Throws_OnSequenceMoreThan31Bits() => new IdStructure(21, 10, 32); 35 | 36 | [TestMethod] 37 | public void IdStructure_CalculatesWraparoundInterval_Correctly() 38 | { 39 | var mc_ms = new MockTimeSource(); 40 | 41 | // 40 bits of Timestamp should give us about 34 years worth of Id's 42 | Assert.AreEqual(34, (int)(new IdStructure(40, 11, 12).WraparoundInterval(mc_ms).TotalDays / 365.25)); 43 | // 41 bits of Timestamp should give us about 69 years worth of Id's 44 | Assert.AreEqual(69, (int)(new IdStructure(41, 11, 11).WraparoundInterval(mc_ms).TotalDays / 365.25)); 45 | // 42 bits of Timestamp should give us about 139 years worth of Id's 46 | Assert.AreEqual(139, (int)(new IdStructure(42, 11, 10).WraparoundInterval(mc_ms).TotalDays / 365.25)); 47 | 48 | var mc_s = new MockTimeSource(TimeSpan.FromSeconds(0.1)); 49 | 50 | // 40 bits of Timestamp should give us about 3484 years worth of Id's 51 | Assert.AreEqual(3484, (int)(new IdStructure(40, 11, 12).WraparoundInterval(mc_s).TotalDays / 365.25)); 52 | // 41 bits of Timestamp should give us about 6968 years worth of Id's 53 | Assert.AreEqual(6968, (int)(new IdStructure(41, 11, 11).WraparoundInterval(mc_s).TotalDays / 365.25)); 54 | // 42 bits of Timestamp should give us about 13936 years worth of Id's 55 | Assert.AreEqual(13936, (int)(new IdStructure(42, 11, 10).WraparoundInterval(mc_s).TotalDays / 365.25)); 56 | 57 | var mc_d = new MockTimeSource(TimeSpan.FromDays(1)); 58 | 59 | // 21 bits of Timestamp should give us about 5741 years worth of Id's 60 | Assert.AreEqual(5741, (int)(new IdStructure(21, 11, 31).WraparoundInterval(mc_d).TotalDays / 365.25)); 61 | // 22 bits of Timestamp should give us about 11483 years worth of Id's 62 | Assert.AreEqual(11483, (int)(new IdStructure(22, 11, 30).WraparoundInterval(mc_d).TotalDays / 365.25)); 63 | // 23 bits of Timestamp should give us about 22966 years worth of Id's 64 | Assert.AreEqual(22966, (int)(new IdStructure(23, 11, 29).WraparoundInterval(mc_d).TotalDays / 365.25)); 65 | } 66 | 67 | [TestMethod] 68 | public void IdStructure_Calculates_WraparoundDate_Correctly() 69 | { 70 | var s = IdStructure.Default; 71 | var mc = new MockTimeSource(TimeSpan.FromMilliseconds(1)); 72 | var d = s.WraparoundDate(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), mc); 73 | 74 | 75 | #if NETCOREAPP3_0_OR_GREATER 76 | Assert.AreEqual(new DateTime(643346200555519999, DateTimeKind.Utc), d); 77 | #else //https://learn.microsoft.com/en-us/dotnet/core/compatibility/3.0#floating-point-formatting-and-parsing-behavior-changed 78 | Assert.AreEqual(new DateTime(643346200555520000, DateTimeKind.Utc), d); 79 | #endif 80 | } 81 | 82 | [TestMethod] 83 | [ExpectedException(typeof(ArgumentNullException))] 84 | public void WraparoundDate_ThrowsOnNullTimeSource() => IdStructure.Default.WraparoundDate(IdGeneratorOptions.DefaultEpoch, null!); 85 | 86 | [TestMethod] 87 | [ExpectedException(typeof(ArgumentNullException))] 88 | public void WraparoundInterval_ThrowsOnNullTimeSource() => IdStructure.Default.WraparoundInterval(null!); 89 | } 90 | -------------------------------------------------------------------------------- /IdGenTests/Mocks/MockAutoIncrementingIntervalTimeSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace IdGenTests.Mocks; 5 | 6 | public class MockAutoIncrementingIntervalTimeSource(int incrementEvery, long? current = null, TimeSpan? tickDuration = null, DateTimeOffset? epoch = null) 7 | : MockTimeSource(current ?? 0, tickDuration ?? TimeSpan.FromMilliseconds(1), epoch ?? DateTimeOffset.MinValue) 8 | { 9 | private int _count = 0; 10 | 11 | public override long GetTicks() 12 | { 13 | if (_count == incrementEvery) 14 | { 15 | NextTick(); 16 | _count = 0; 17 | } 18 | Interlocked.Increment(ref _count); 19 | 20 | return base.GetTicks(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /IdGenTests/Mocks/MockTimeSource.cs: -------------------------------------------------------------------------------- 1 | using IdGen; 2 | using System; 3 | using System.Threading; 4 | 5 | namespace IdGenTests.Mocks; 6 | 7 | public class MockTimeSource(long current, TimeSpan tickDuration, DateTimeOffset epoch) : ITimeSource 8 | { 9 | public MockTimeSource() 10 | : this(0) { } 11 | 12 | public DateTimeOffset Epoch { get; private set; } = epoch; 13 | 14 | public TimeSpan TickDuration { get; } = tickDuration; 15 | 16 | public MockTimeSource(long current) 17 | : this(current, TimeSpan.FromMilliseconds(1), DateTimeOffset.MinValue) { } 18 | 19 | public MockTimeSource(TimeSpan tickDuration) 20 | : this(0, tickDuration, DateTimeOffset.MinValue) { } 21 | 22 | public virtual long GetTicks() => current; 23 | 24 | public void NextTick() => Interlocked.Increment(ref current); 25 | 26 | public void PreviousTick() => Interlocked.Decrement(ref current); 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rob Janssen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Logo](https://raw.githubusercontent.com/RobThree/IdGen/master/logo.png) IdGen 2 | 3 | ![Build Status](https://img.shields.io/github/actions/workflow/status/RobThree/IdGen/test.yml?branch=master&style=flat-square) [![Nuget version](https://img.shields.io/nuget/v/IdGen.svg?style=flat-square)](https://www.nuget.org/packages/IdGen/) 4 | 5 | Twitter Snowflake-alike ID generator for .Net. Available as [Nuget package](https://www.nuget.org/packages/IdGen) 6 | 7 | ## Why 8 | 9 | In certain situations you need a low-latency, distributed, uncoordinated, (roughly) time ordered, compact and highly available Id generation system. This project was inspired by [Twitter's Snowflake](https://github.com/twitter/snowflake) project which has been retired. Note that this project was inspired by Snowflake but is not an *exact* implementation. This library provides a basis for Id generation; it does **not** provide a service for handing out these Id's nor does it provide generator-id ('worker-id') coordination. 10 | 11 | ## How it works 12 | 13 | IdGen generates, like Snowflake, 64 bit Id's. The [Sign Bit](https://en.wikipedia.org/wiki/Sign_bit) is unused since this can cause incorrect ordering on some systems that cannot use unsigned types and/or make it hard to get correct ordering. So, in effect, IdGen generates 63 bit Id's. An Id consists of 3 parts: 14 | 15 | * Timestamp 16 | * Generator-id 17 | * Sequence 18 | 19 | An Id generated with a **Default** `IdStructure` is structured as follows: 20 | 21 | ![Id structure](https://raw.githubusercontent.com/RobThree/IdGen/master/structure.png) 22 | 23 | However, using the `IdStructure` class you can tune the structure of the created Id's to your own needs; you can use 45 bits for the timestamp, 2 bits for the generator-id and 16 bits for the sequence if you prefer. As long as all 3 parts (timestamp, generator and sequence) add up to 63 bits you're good to go! 24 | 25 | The **timestamp**-part of the Id should speak for itself; by default this is incremented every millisecond and represents the number of milliseconds since a certain epoch. However, IdGen relies on an [`ITimeSource`](IdGen/ITimeSource.cs) which uses a 'tick' that can be defined to be anything; be it a millisecond (default), a second or even a day or nanosecond (hardware support etc. permitting). By default IdGen uses 2015-01-01 0:00:00Z as epoch, but you can specify a custom epoch too. 26 | 27 | The **generator-id**-part of the Id is the part that you 'configure'; it could correspond to a host, thread, datacenter or continent: it's up to you. However, the generator-id should be unique in the system: if you have several hosts or threads generating Id's, each host or thread should have it's own generator-id. This could be based on the hostname, a config-file value or even be retrieved from an coordinating service. Remember: a generator-id should be unique within the entire system to avoid collisions! 28 | 29 | The **sequence**-part is simply a value that is incremented each time a new Id is generated within the same tick (again, by default, a millisecond but can be anything); it is reset every time the tick changes. 30 | 31 | ## System Clock Dependency 32 | 33 | We recommend you use NTP to keep your system clock accurate. IdGen protects from non-monotonic clocks, i.e. clocks that run backwards. The [`DefaultTimeSource`](IdGen/DefaultTimeSource.cs) relies on a 64bit monotonic, increasing only, system counter. However, we still recommend you use NTP to keep your system clock accurate; this will prevent duplicate Id's between system restarts for example. 34 | 35 | The [`DefaultTimeSource`](IdGen/DefaultTimeSource.cs) relies on a [`Stopwatch`](https://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx) for calculating the 'ticks' but you can implement your own time source by simply implementing the [`ITimeSource`](IdGen/ITimeSource.cs) interface. 36 | 37 | 38 | ## Getting started 39 | 40 | Install the [Nuget package](https://www.nuget.org/packages/IdGen) and write the following code: 41 | 42 | ```c# 43 | using IdGen; 44 | using System.Linq; 45 | 46 | class Program 47 | { 48 | static void Main(string[] args) 49 | { 50 | var generator = new IdGenerator(0); 51 | var id = generator.CreateId(); 52 | // Example id: 862817670527975424 53 | } 54 | } 55 | ``` 56 | 57 | Voila. You have created your first Id! Want to create 100 Id's? Instead of: 58 | 59 | `var id = generator.CreateId();` 60 | 61 | write: 62 | 63 | `var id = generator.Take(100);` 64 | 65 | This is because the `IdGenerator()` implements `IEnumerable` providing you with a never-ending stream of Id's (so you might want to be careful doing a `.Select(...)` or `Count()` on it!). 66 | 67 | The above example creates a default `IdGenerator` with the GeneratorId (or: 'Worker Id') set to 0 and using a [`DefaultTimeSource`](IdGen/DefaultTimeSource.cs). If you're using multiple generators (across machines or in separate threads or...) you'll want to make sure each generator is assigned it's own unique Id. One way of doing this is by simply storing a value in your configuration file for example, another way may involve a service handing out GeneratorId's to machines/threads. IdGen **does not** provide a solution for this since each project or setup may have different requirements or infrastructure to provide these generator-id's. 68 | 69 | The below sample is a bit more complicated; we set a custom epoch, define our own id-structure for generated Id's and then display some information about the setup: 70 | 71 | ```c# 72 | using IdGen; 73 | using System; 74 | 75 | class Program 76 | { 77 | static void Main(string[] args) 78 | { 79 | // Let's say we take april 1st 2020 as our epoch 80 | var epoch = new DateTime(2020, 4, 1, 0, 0, 0, DateTimeKind.Utc); 81 | 82 | // Create an ID with 45 bits for timestamp, 2 for generator-id 83 | // and 16 for sequence 84 | var structure = new IdStructure(45, 2, 16); 85 | 86 | // Prepare options 87 | var options = new IdGeneratorOptions(structure, new DefaultTimeSource(epoch)); 88 | 89 | // Create an IdGenerator with it's generator-id set to 0, our custom epoch 90 | // and id-structure 91 | var generator = new IdGenerator(0, options); 92 | 93 | // Let's ask the id-structure how many generators we could instantiate 94 | // in this setup (2 bits) 95 | Console.WriteLine("Max. generators : {0}", structure.MaxGenerators); 96 | 97 | // Let's ask the id-structure how many sequential Id's we could generate 98 | // in a single ms in this setup (16 bits) 99 | Console.WriteLine("Id's/ms per generator : {0}", structure.MaxSequenceIds); 100 | 101 | // Let's calculate the number of Id's we could generate, per ms, should we use 102 | // the maximum number of generators 103 | Console.WriteLine("Id's/ms total : {0}", structure.MaxGenerators * structure.MaxSequenceIds); 104 | 105 | 106 | // Let's ask the id-structure configuration for how long we could generate Id's before 107 | // we experience a 'wraparound' of the timestamp 108 | Console.WriteLine("Wraparound interval : {0}", structure.WraparoundInterval(generator.Options.TimeSource)); 109 | 110 | // And finally: let's ask the id-structure when this wraparound will happen 111 | // (we'll have to tell it the generator's epoch) 112 | Console.WriteLine("Wraparound date : {0}", structure.WraparoundDate(generator.Options.TimeSource.Epoch, generator.Options.TimeSource).ToString("O")); 113 | } 114 | } 115 | ``` 116 | 117 | Output: 118 | ``` 119 | Max. generators : 4 120 | Id's/ms per generator : 65536 121 | Id's/ms total : 262144 122 | Wraparound interval : 407226.12:41:28.8320000 (about 1114 years) 123 | Wraparound date : 3135-03-14T12:41:28.8320000+00:00 124 | ``` 125 | 126 | IdGen also provides an `ITimeSouce` interface; this can be handy for [unittesting](IdGenTests/IdGeneratorTests.cs) purposes or if you want to provide a time-source for the timestamp part of your Id's that is not based on the system time. For unittesting we use our own [`MockTimeSource`](IdGenTests/Mocks/MockTimeSource.cs). 127 | 128 | ## Configuration 129 | 130 | A configuration package for .Net Framework projects can be found in [IdGen.Configuration](https://www.nuget.org/packages/IdGen.Configuration). This package allows you to configure your IdGenerators in your `app.config` or `web.config` file. The configuration section looks like this: 131 | 132 | ```xml 133 | 134 | 135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ``` 148 | 149 | The attributes (`name`, `id`, `epoch`, `timestampBits`, `generatorIdBits` and `sequenceBits`) are required. The `tickDuration` is optional and defaults to the default tickduration from a `DefaultTimeSource`. The `sequenceOverflowStrategy` is optional too and defaults to `Throw`. Valid DateTime notations for the epoch are: 150 | 151 | * `yyyy-MM-ddTHH:mm:ss` 152 | * `yyyy-MM-dd HH:mm:ss` 153 | * `yyyy-MM-dd` 154 | 155 | You can get the IdGenerator from the config using the following code: 156 | 157 | `var generator = AppConfigFactory.GetFromConfig("foo");` 158 | 159 | ## Dependency Injection 160 | 161 | There is an [IdGen.DependencyInjection NuGet package](https://www.nuget.org/packages/IdGen.DependencyInjection) available that allows for easy integration with the commonly used [Microsoft.Extensions.DependencyInjection](https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection). 162 | 163 | Usage is straightforward: 164 | 165 | ```c# 166 | services.AddIdGen(123); // Where 123 is the generator-id 167 | ``` 168 | 169 | Or, when you want to use non-default options: 170 | 171 | ```c# 172 | services.AddIdGen(123, () => new IdGeneratorOptions(...)); // Where 123 is the generator-id 173 | ``` 174 | 175 | This registers both an `IdGenerator` as well as an `IIdGenerator`, both pointing to the same singleton generator. 176 | 177 | ## Upgrading from 2.x to 3.x 178 | 179 | Upgrading from 2.x to 3.x should be pretty straightforward. The following things have changed: 180 | 181 | * Most of the constructor overloads for the `IdGenerator` have been replaced with a single constructor which accepts `IdGeneratorOptions` that contains the `ITimeSource`, `IdStructure` and `SequenceOverflowStrategy` 182 | * The `MaskConfig` class is now more appropriately named `IdStructure` since it describes the structure of the generated ID's. 183 | * The `UseSpinWait` property has moved to the `IdGeneratorOptions` and is now an enum of type `SequenceOverflowStrategy` instead of a boolean value. Note that this property has also been renamed in the config file (from `useSpinWait` to `sequenceOverflowStrategy`) and is no longer a boolean but requires one of the values from `SequenceOverflowStrategy`. 184 | * `ID` is now `Id` (only used as return value by the `FromId()` method) 185 | 186 | The generated 2.x ID's are still compatible with 3.x ID's. This release is mostly better and more consistent naming of objects. 187 | 188 | # FAQ 189 | 190 | **Q**: Help, I'm getting duplicate ID's or collisions? 191 | 192 | **A**: Then you're probably not using IdGen as intended: It should be a singleton (per thread/process/host/...), and if you insist on having multiple instances around they should all have their own unique GeneratorId. 193 | 194 | **A**: Also: Don't change the structure; once you've picked an `IdStructure` and go into production commit to it, stick with it. This means that careful planning is needed to ensure enough ID's can be generated by enough generators for long enough. Although changing the structure at a later stage isn't impossible, careful consideration is needed to ensure no collisions will occur. 195 | 196 | **Q**: I'm experiencing weird results when these ID's are used in Javascript? 197 | 198 | **A**: Remember that generated ID's are 64 (actually 63) bits wide. Javascript uses floats to store all numbers and the [maximum integer value you can safely store](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER) is 53 bits. If you need to handle these ID's in Javascript, treat them as `strings`. 199 | 200 |
201 | 202 | Icon made by [Freepik](http://www.flaticon.com/authors/freepik) from [www.flaticon.com](http://www.flaticon.com) is licensed by [CC 3.0](http://creativecommons.org/licenses/by/3.0/). 203 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobThree/IdGen/e084d23b1ec6883e0d18cf733430aacbfce71d1d/logo.png -------------------------------------------------------------------------------- /structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobThree/IdGen/e084d23b1ec6883e0d18cf733430aacbfce71d1d/structure.png --------------------------------------------------------------------------------