├── logo.png
├── .vscode
└── settings.json
├── structure.png
├── .github
├── FUNDING.yml
└── workflows
│ ├── test.yml
│ └── publishnuget.yml
├── IdGen
├── IIdGenerator.cs
├── SequenceOverflowStrategy.cs
├── ITimeSource.cs
├── Id.cs
├── DefaultTimeSource.cs
├── InvalidSystemClockException.cs
├── SequenceOverflowException.cs
├── StopwatchTimeSource.cs
├── IdGen.csproj
├── IdGeneratorOptions.cs
├── IdStructure.cs
├── Translations.Designer.cs
├── Translations.resx
├── Translations.nl.resx
└── IdGenerator.cs
├── IdGenTests
├── Mocks
│ ├── MockAutoIncrementingIntervalTimeSource.cs
│ └── MockTimeSource.cs
├── App.config
├── IdGenTests.csproj
├── IDTests.cs
├── DependencyInjectionTests.cs
├── IdStructureTests.cs
├── ConfigTests.cs
└── IdGeneratorTests.cs
├── IdGen.Configuration
├── IdGeneratorsSection.cs
├── IdGeneratorsCollection.cs
├── IdGen.Configuration.csproj
├── AppConfigFactory.cs
└── IdGeneratorElement.cs
├── LICENSE
├── IdGen.DependencyInjection
├── IdGen.DependencyInjection.csproj
└── IdGenServiceCollectionExtensions.cs
├── IdGen.sln
├── .gitignore
├── .editorconfig
└── README.md
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RobThree/IdGen/HEAD/logo.png
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "dotnet.defaultSolution": "IdGen.sln"
3 | }
--------------------------------------------------------------------------------
/structure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RobThree/IdGen/HEAD/structure.png
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [RobThree]
2 | custom: ["https://paypal.me/robiii"]
3 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/IdGenTests/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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}}
--------------------------------------------------------------------------------
/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/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.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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/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/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/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.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.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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 | }
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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.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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  IdGen
2 |
3 |  [](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 | 
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 |
--------------------------------------------------------------------------------