├── IntegrationTests
├── appsettings.json
├── ITestScope.cs
├── Properties
│ └── AssemblyInfo.cs
├── File.cs
├── IntegrationTests.csproj
├── Azure.cs
├── DependencyInjectionTest.cs
└── Scenarios.cs
├── AutoNumber
├── ScopeState.cs
├── Properties
│ └── AssemblyInfo.cs
├── Exceptions
│ └── UniqueIdGenerationException.cs
├── Interfaces
│ ├── IOptimisticDataStore.cs
│ └── IUniqueIdGenerator.cs
├── Options
│ ├── AutoNumberOptions.cs
│ └── AutoNumberOptionsBuilder.cs
├── Extensions
│ ├── DictionaryExtensions.cs
│ └── ServiceCollectionExtensions.cs
├── DebugOnlyFileDataStore.cs
├── AutoNumber.csproj
├── UniqueIdGenerator.cs
└── BlobOptimisticDataStore.cs
├── packages
└── repositories.config
├── UnitTests
├── Properties
│ └── AssemblyInfo.cs
├── UnitTests.csproj
├── DictionaryExtentionsTests.cs
└── UniqueIdGeneratorTest.cs
├── .github
└── workflows
│ └── dotnet.yml
├── AutoNumber.sln
├── LICENSE
├── README.md
└── .gitignore
/IntegrationTests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AutoNumber": {
3 | "BatchSize": 50,
4 | "MaxWriteAttempts": 25,
5 | "StorageContainerName": "unique-urls"
6 | },
7 | "ConnectionStrings": {
8 | "test": "test123"
9 | }
10 | }
--------------------------------------------------------------------------------
/AutoNumber/ScopeState.cs:
--------------------------------------------------------------------------------
1 | namespace AutoNumber
2 | {
3 | internal class ScopeState
4 | {
5 | public readonly object IdGenerationLock = new object();
6 | public long HighestIdAvailableInBatch;
7 | public long LastId;
8 | }
9 | }
--------------------------------------------------------------------------------
/IntegrationTests/ITestScope.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace IntegrationTests.cs
4 | {
5 | public interface ITestScope : IDisposable
6 | {
7 | string IdScopeName { get; }
8 |
9 | string ReadCurrentPersistedValue();
10 | }
11 | }
--------------------------------------------------------------------------------
/AutoNumber/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Runtime.InteropServices;
3 |
4 | [assembly: ComVisible(false)]
5 |
6 | [assembly: Guid("1e20fb86-8ec2-4796-baa4-664ec1b525c2")]
7 |
8 | [assembly: InternalsVisibleTo("AutoNumber.UnitTests")]
--------------------------------------------------------------------------------
/AutoNumber/Exceptions/UniqueIdGenerationException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace AutoNumber.Exceptions
4 | {
5 | public class UniqueIdGenerationException : Exception
6 | {
7 | public UniqueIdGenerationException(string message)
8 | : base(message)
9 | {
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/packages/repositories.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/UnitTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | // Setting ComVisible to false makes the types in this assembly not visible
4 | // to COM components. If you need to access a type in this assembly from
5 | // COM, set the ComVisible attribute to true on that type.
6 | [assembly: ComVisible(false)]
7 |
8 | // The following GUID is for the ID of the typelib if this project is exposed to COM
9 | [assembly: Guid("2fab9ef2-5117-41a2-9456-0bc29219db05")]
--------------------------------------------------------------------------------
/IntegrationTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | // Setting ComVisible to false makes the types in this assembly not visible
4 | // to COM components. If you need to access a type in this assembly from
5 | // COM, set the ComVisible attribute to true on that type.
6 | [assembly: ComVisible(false)]
7 |
8 | // The following GUID is for the ID of the typelib if this project is exposed to COM
9 | [assembly: Guid("203d520d-65e6-46cd-8fbd-f0ed0e3f6b9a")]
--------------------------------------------------------------------------------
/AutoNumber/Interfaces/IOptimisticDataStore.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace AutoNumber.Interfaces
4 | {
5 | public interface IOptimisticDataStore
6 | {
7 | string GetData(string blockName);
8 | Task GetDataAsync(string blockName);
9 | bool TryOptimisticWrite(string blockName, string data);
10 | Task TryOptimisticWriteAsync(string blockName, string data);
11 | Task InitAsync();
12 | bool Init();
13 | }
14 | }
--------------------------------------------------------------------------------
/AutoNumber/Options/AutoNumberOptions.cs:
--------------------------------------------------------------------------------
1 | using Azure.Storage.Blobs;
2 |
3 | namespace AutoNumber.Options
4 | {
5 | public class AutoNumberOptions
6 | {
7 | public int BatchSize { get; set; } = 100;
8 | public int MaxWriteAttempts { get; set; } = 100;
9 | public string StorageContainerName { get; set; } = "unique-urls";
10 | public string StorageAccountConnectionString { get; set; }
11 | public BlobServiceClient BlobServiceClient { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/AutoNumber/Interfaces/IUniqueIdGenerator.cs:
--------------------------------------------------------------------------------
1 | namespace AutoNumber.Interfaces
2 | {
3 | public interface IUniqueIdGenerator
4 | {
5 | int BatchSize { get; set; }
6 | int MaxWriteAttempts { get; set; }
7 |
8 | ///
9 | /// Generate a new incremental id regards the scope name
10 | ///
11 | /// Generator use this scope name to generate different ids for different scopes
12 | ///
13 | long NextId(string scopeName);
14 | }
15 | }
--------------------------------------------------------------------------------
/AutoNumber/Extensions/DictionaryExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace AutoNumber.Extensions
5 | {
6 | internal static class DictionaryExtensions
7 | {
8 | internal static TValue GetValue(
9 | this IDictionary dictionary,
10 | TKey key,
11 | object dictionaryLock,
12 | Func valueInitializer)
13 | {
14 | TValue value;
15 | var found = dictionary.TryGetValue(key, out value);
16 | if (found) return value;
17 |
18 | lock (dictionaryLock)
19 | {
20 | found = dictionary.TryGetValue(key, out value);
21 | if (found) return value;
22 |
23 | value = valueInitializer();
24 |
25 | dictionary.Add(key, value);
26 | }
27 |
28 | return value;
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Install dependencies
17 | run: dotnet restore
18 | - name: Build
19 | run: dotnet build --configuration Release --no-restore
20 | - name: Running Azurite
21 | run: docker run -d -p 10000:10000 -p 10001:10001 mcr.microsoft.com/azure-storage/azurite
22 | - name: Test
23 | run: dotnet test --no-restore
24 | - name: Create the package
25 | run: dotnet pack --configuration Release
26 | - name: Publish the package to NUGET
27 | env: # Set the secret as an input
28 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
29 | if: ${{ github.ref == 'refs/heads/main' }}
30 | run: dotnet nuget push AutoNumber/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key $NUGET_API_KEY
31 |
--------------------------------------------------------------------------------
/IntegrationTests/File.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using AutoNumber;
4 | using AutoNumber.Interfaces;
5 | using NUnit.Framework;
6 |
7 | namespace IntegrationTests.cs
8 | {
9 | [TestFixture]
10 | public class File : Scenarios
11 | {
12 | protected override TestScope BuildTestScope()
13 | {
14 | return new TestScope();
15 | }
16 |
17 | protected override IOptimisticDataStore BuildStore(TestScope scope)
18 | {
19 | return new DebugOnlyFileDataStore(scope.DirectoryPath);
20 | }
21 |
22 | public class TestScope : ITestScope
23 | {
24 | public TestScope()
25 | {
26 | var ticks = DateTime.UtcNow.Ticks;
27 | IdScopeName = string.Format("AutoNumbertest{0}", ticks);
28 |
29 | DirectoryPath = Path.Combine(Path.GetTempPath(), IdScopeName);
30 | Directory.CreateDirectory(DirectoryPath);
31 | }
32 |
33 | public string DirectoryPath { get; }
34 |
35 | public string IdScopeName { get; }
36 |
37 | public string ReadCurrentPersistedValue()
38 | {
39 | var filePath = Path.Combine(DirectoryPath, string.Format("{0}.txt", IdScopeName));
40 | return System.IO.File.ReadAllText(filePath);
41 | }
42 |
43 | public void Dispose()
44 | {
45 | if (Directory.Exists(DirectoryPath))
46 | Directory.Delete(DirectoryPath, true);
47 | }
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/AutoNumber/DebugOnlyFileDataStore.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using AutoNumber.Interfaces;
5 |
6 | namespace AutoNumber
7 | {
8 | public class DebugOnlyFileDataStore : IOptimisticDataStore
9 | {
10 | private const string SeedValue = "1";
11 |
12 | private readonly string directoryPath;
13 |
14 | public DebugOnlyFileDataStore(string directoryPath)
15 | {
16 | this.directoryPath = directoryPath;
17 | }
18 |
19 | public string GetData(string blockName)
20 | {
21 | var blockPath = Path.Combine(directoryPath, $"{blockName}.txt");
22 | try
23 | {
24 | return File.ReadAllText(blockPath);
25 | }
26 | catch (FileNotFoundException)
27 | {
28 | var file = File.Create(blockPath);
29 |
30 | using (var streamWriter = new StreamWriter(file))
31 | {
32 | streamWriter.Write(SeedValue);
33 | }
34 |
35 | return SeedValue;
36 | }
37 | }
38 |
39 | public Task GetDataAsync(string blockName)
40 | {
41 | throw new NotImplementedException();
42 | }
43 |
44 | public Task InitAsync()
45 | {
46 | return Task.FromResult(true);
47 | }
48 |
49 | public bool Init()
50 | {
51 | return true;
52 | }
53 |
54 | public bool TryOptimisticWrite(string blockName, string data)
55 | {
56 | var blockPath = Path.Combine(directoryPath, $"{blockName}.txt");
57 | File.WriteAllText(blockPath, data);
58 | return true;
59 | }
60 |
61 | public Task TryOptimisticWriteAsync(string blockName, string data)
62 | {
63 | throw new NotImplementedException();
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/UnitTests/UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | 8.0.30703
4 | AutoNumber.UnitTests
5 | AutoNumber.UnitTests
6 | ..\
7 | UnitTests
8 | Microsoft
9 | UnitTests
10 | Copyright © Microsoft 2011
11 | bin\$(Configuration)\
12 | net6.0;net8.0
13 |
14 |
15 | full
16 |
17 |
18 | pdbonly
19 |
20 |
21 |
22 |
23 |
24 |
25 | all
26 | runtime; build; native; contentfiles; analyzers; buildtransitive
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | all
36 | runtime; build; native; contentfiles; analyzers; buildtransitive
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/AutoNumber/AutoNumber.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0;netstandard2.0;netstandard2.1
4 | 8.0.30703
5 | ..\
6 | SnowMaker
7 | AzureAutoNumber
8 | High performance, distributed unique id generator for Azure environments.
9 | 1.4.0
10 | 1.5.0.0
11 | 1.5.0.0
12 | bin\$(Configuration)\
13 | Ali Bahraminezhad
14 | MS-PL
15 | https://github.com/0x414c49/AzureAutoNumber
16 | Azure
17 | AutoNumber
18 | AutoNumber
19 | AutoNumber
20 | true
21 | * .NET 8
22 | * All azure and other nuget dependencies upgraded
23 | AzureAutoNumber
24 | 1.5.0
25 | False
26 |
27 |
28 | full
29 |
30 |
31 | pdbonly
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/IntegrationTests/IntegrationTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | 8.0.30703
4 | IntegrationTests.cs
5 | IntegrationTests.cs
6 | ..\
7 | IntegrationTests.cs
8 | Microsoft
9 | IntegrationTests.cs
10 | Copyright © Microsoft 2011
11 | bin\$(Configuration)\
12 | net6.0;net8.0
13 |
14 |
15 | full
16 |
17 |
18 | pdbonly
19 |
20 |
21 |
22 |
23 |
24 |
25 | all
26 | runtime; build; native; contentfiles; analyzers; buildtransitive
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | all
36 | runtime; build; native; contentfiles; analyzers; buildtransitive
37 |
38 |
39 |
40 |
41 | Always
42 |
43 |
44 |
--------------------------------------------------------------------------------
/IntegrationTests/Azure.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using AutoNumber;
5 | using AutoNumber.Interfaces;
6 | using Azure.Storage.Blobs;
7 | using Azure.Storage.Blobs.Specialized;
8 | using NUnit.Framework;
9 |
10 | namespace IntegrationTests.cs
11 | {
12 | [TestFixture]
13 | public class Azure : Scenarios
14 | {
15 | private readonly BlobServiceClient blobServiceClient = new BlobServiceClient("UseDevelopmentStorage=true");
16 |
17 | protected override TestScope BuildTestScope()
18 | {
19 | return new TestScope(new BlobServiceClient("UseDevelopmentStorage=true"));
20 | }
21 |
22 | protected override IOptimisticDataStore BuildStore(TestScope scope)
23 | {
24 | var blobOptimisticDataStore = new BlobOptimisticDataStore(blobServiceClient, scope.ContainerName);
25 | blobOptimisticDataStore.Init();
26 | return blobOptimisticDataStore;
27 | }
28 | }
29 |
30 | public sealed class TestScope : ITestScope
31 | {
32 | private readonly BlobServiceClient blobServiceClient;
33 |
34 | public TestScope(BlobServiceClient blobServiceClient)
35 | {
36 | var ticks = DateTime.UtcNow.Ticks;
37 | IdScopeName = string.Format("autonumbertest{0}", ticks);
38 | ContainerName = string.Format("autonumbertest{0}", ticks);
39 |
40 | this.blobServiceClient = blobServiceClient;
41 | }
42 |
43 | public string ContainerName { get; }
44 |
45 | public string IdScopeName { get; }
46 |
47 | public string ReadCurrentPersistedValue()
48 | {
49 | var blobContainer = blobServiceClient.GetBlobContainerClient(ContainerName);
50 | var blob = blobContainer.GetBlockBlobClient(IdScopeName);
51 | using (var stream = new MemoryStream())
52 | {
53 | blob.DownloadToAsync(stream).GetAwaiter().GetResult();
54 | return Encoding.UTF8.GetString(stream.ToArray());
55 | }
56 | }
57 |
58 | public void Dispose()
59 | {
60 | var blobContainer = blobServiceClient.GetBlobContainerClient(ContainerName);
61 | blobContainer.DeleteAsync().GetAwaiter().GetResult();
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/AutoNumber/Extensions/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AutoNumber.Interfaces;
3 | using AutoNumber.Options;
4 | using Azure.Storage.Blobs;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 |
8 | namespace AutoNumber
9 | {
10 | public static class ServiceCollectionExtensions
11 | {
12 | private const string AutoNumber = "AutoNumber";
13 |
14 | [Obsolete("This method is deprecated, please use AddAutoNumber with options builder.", false)]
15 | public static IServiceCollection AddAutoNumber(this IServiceCollection services)
16 | {
17 | services.AddOptions()
18 | .Configure((settings, configuration)
19 | => configuration.GetSection(AutoNumber).Bind(settings));
20 |
21 | services.AddSingleton();
22 | services.AddSingleton();
23 |
24 | return services;
25 | }
26 |
27 | public static IServiceCollection AddAutoNumber(this IServiceCollection services, IConfiguration configuration,
28 | Func builder)
29 | {
30 | if (builder == null)
31 | throw new ArgumentNullException(nameof(builder));
32 |
33 | var builderOptions = new AutoNumberOptionsBuilder(configuration);
34 | var options = builder(builderOptions);
35 |
36 | services.AddSingleton(x =>
37 | {
38 | BlobServiceClient blobServiceClient = null;
39 |
40 | if (options.BlobServiceClient != null)
41 | blobServiceClient = options.BlobServiceClient;
42 | else if (options.StorageAccountConnectionString == null)
43 | blobServiceClient = x.GetService();
44 | else
45 | blobServiceClient = new BlobServiceClient(options.StorageAccountConnectionString);
46 |
47 | return new BlobOptimisticDataStore(blobServiceClient, options.StorageContainerName);
48 | });
49 |
50 | services.AddSingleton(x
51 | => new UniqueIdGenerator(x.GetService(), options));
52 |
53 | return services;
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/AutoNumber.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.32228.430
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoNumber", "AutoNumber\AutoNumber.csproj", "{0AD4FCE5-6968-4B85-9159-50F63D168907}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7E1BB869-9152-48ED-9D43-D846800D7DBF}"
9 | ProjectSection(SolutionItems) = preProject
10 | .github\workflows\dotnet.yml = .github\workflows\dotnet.yml
11 | README.md = README.md
12 | EndProjectSection
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "UnitTests\UnitTests.csproj", "{2D578C72-24A1-4EE0-A5E8-30D76FC297B8}"
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "IntegrationTests\IntegrationTests.csproj", "{697EEC17-828B-4AD0-B4BD-4F7422D406F7}"
17 | EndProject
18 | Global
19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
20 | Debug|Any CPU = Debug|Any CPU
21 | Release|Any CPU = Release|Any CPU
22 | EndGlobalSection
23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
24 | {0AD4FCE5-6968-4B85-9159-50F63D168907}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {0AD4FCE5-6968-4B85-9159-50F63D168907}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {0AD4FCE5-6968-4B85-9159-50F63D168907}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {0AD4FCE5-6968-4B85-9159-50F63D168907}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {2D578C72-24A1-4EE0-A5E8-30D76FC297B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {2D578C72-24A1-4EE0-A5E8-30D76FC297B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {2D578C72-24A1-4EE0-A5E8-30D76FC297B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {2D578C72-24A1-4EE0-A5E8-30D76FC297B8}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {697EEC17-828B-4AD0-B4BD-4F7422D406F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {697EEC17-828B-4AD0-B4BD-4F7422D406F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {697EEC17-828B-4AD0-B4BD-4F7422D406F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {697EEC17-828B-4AD0-B4BD-4F7422D406F7}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {558B1269-7F34-4FBE-89CD-BAC6EB325753}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Microsoft Public License (MS-PL)
2 | This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.
3 |
4 | 1. Definitions
5 | The terms "reproduce," "reproduction," "derivative works," and "distribution" have the
6 | same meaning here as under U.S. copyright law.
7 |
8 | A "contribution" is the original software, or any additions or changes to the software.
9 |
10 | A "contributor" is any person that distributes its contribution under this license.
11 |
12 | "Licensed patents" are a contributor's patent claims that read directly on its contribution.
13 |
14 | 2. Grant of Rights
15 |
16 | (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
17 |
18 | (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
19 |
20 | 3. Conditions and Limitations
21 |
22 | (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
23 |
24 | (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
25 |
26 | (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
27 |
28 | (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
29 |
30 | (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.
--------------------------------------------------------------------------------
/UnitTests/DictionaryExtentionsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading;
3 | using AutoNumber.Extensions;
4 | using NUnit.Framework;
5 |
6 | namespace AutoNumber.UnitTests
7 | {
8 | [TestFixture]
9 | public class DictionaryExtentionsTests
10 | {
11 | private static bool IsLockedOnCurrentThread(object lockObject)
12 | {
13 | var reset = new ManualResetEvent(false);
14 | var couldLockBeAcquiredOnOtherThread = false;
15 | new Thread(() =>
16 | {
17 | couldLockBeAcquiredOnOtherThread = Monitor.TryEnter(lockObject, 0);
18 | reset.Set();
19 | }).Start();
20 | reset.WaitOne();
21 | return !couldLockBeAcquiredOnOtherThread;
22 | }
23 |
24 | [Test]
25 | public void GetValueShouldCallTheValueInitializerWithinTheLockIfTheKeyDoesntExist()
26 | {
27 | var dictionary = new Dictionary
28 | {
29 | {"foo", "bar"}
30 | };
31 |
32 | var dictionaryLock = new object();
33 |
34 | // Act
35 | dictionary.GetValue(
36 | "bar",
37 | dictionaryLock,
38 | () =>
39 | {
40 | // Assert
41 | Assert.That(IsLockedOnCurrentThread(dictionaryLock), Is.True);
42 | return "qak";
43 | });
44 | }
45 |
46 | [Test]
47 | public void GetValueShouldReturnExistingValueWithoutUsingTheLock()
48 | {
49 | var dictionary = new Dictionary
50 | {
51 | {"foo", "bar"}
52 | };
53 |
54 | // Act
55 | // null can't be used as a lock and will throw an exception if attempted
56 | var value = dictionary.GetValue("foo", null, null);
57 |
58 | // Assert
59 | Assert.That("bar", Is.EqualTo(value));
60 | }
61 |
62 | [Test]
63 | public void GetValueShouldStoreNewValuesAfterCallingTheValueInitializerOnce()
64 | {
65 | var dictionary = new Dictionary
66 | {
67 | {"foo", "bar"}
68 | };
69 |
70 | var dictionaryLock = new object();
71 |
72 | // Arrange
73 | dictionary.GetValue("bar", dictionaryLock, () => "qak");
74 |
75 | // Act
76 | dictionary.GetValue(
77 | "bar",
78 | dictionaryLock,
79 | () =>
80 | {
81 | // Assert
82 | Assert.Fail("Value initializer should not have been called a second time.");
83 | return null;
84 | });
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Azure AutoNumber
2 |
3 | ---
4 |
5 |
6 | [](https://github.com/0x414c49/AzureAutoNumber/actions)
7 | []()
8 | [](https://www.nuget.org/packages/AzureAutoNumber/)
9 |
10 | High performance, distributed unique thread-safe id generator for Azure.
11 |
12 | - Human-friendly generated ids (number)
13 | - High performant and fast
14 | - 100% guarantee that won't cause any duplicate ids
15 |
16 | ## How to use
17 |
18 | The project is rely on Azure Blob Storage. `AutoNumber` package will generate ids by using a single text file on the Azure Blob Storage.
19 |
20 |
21 | ```
22 | var blobServiceClient = new BlobServiceClient(connectionString);
23 |
24 | var blobOptimisticDataStore = new BlobOptimisticDataStore(blobServiceClient, "unique-ids");
25 |
26 | var idGen = new UniqueIdGenerator(blobOptimisticDataStore);
27 |
28 | // generate ids with different scopes
29 |
30 | var id = idGen.NextId("urls");
31 | var id2 = idGen.NextId("orders");
32 | ```
33 |
34 | ### With Microsoft DI
35 | The project has an extension method to add it and its dependencies to Microsoft ASP.NET DI. ~~The only caveat is you need to registry type of `BlobServiceClient` in DI before registring `AutoNumber`.~~
36 |
37 |
38 | Use options builder to configure the service, take into account the default settings will read from `appsettings.json`.
39 |
40 | ```
41 | services.AddAutoNumber(Configuration, x =>
42 | {
43 | return x.UseContainerName("container-name")
44 | .UseStorageAccount("connection-string-or-connection-string-name")
45 | //.UseBlobServiceClient(blobServiceClient)
46 | .SetBatchSize(10)
47 | .SetMaxWriteAttempts(100)
48 | .Options;
49 | });
50 | ```
51 |
52 |
53 | #### Deprecated way to register the service:
54 |
55 |
56 | ```
57 | // configure the services
58 | // you need to register an instane of CloudStorageAccount before using this
59 | serviceCollection.AddAutoNumber();
60 | ```
61 |
62 | #### Inject `IUniqueIdGenerator` in constructor
63 |
64 | ```
65 | public class Foo
66 | {
67 | public Foo(IUniqueIdGenerator idGenerator)
68 | {
69 | _idGenerator = idGenerator;
70 | }
71 | }
72 | ```
73 |
74 | ### Configuration
75 | These are default configuration for `AutoNumber`. If you prefer registering AutoNumber with `AddAddNumber` method, these options can be set via `appsettings.json`.
76 |
77 | ```
78 | {
79 | "AutoNumber": {
80 | "BatchSize": 50,
81 | "MaxWriteAttempts": 25,
82 | "StorageContainerName": "unique-urls"
83 | }
84 | }
85 | ```
86 | ### Support
87 | Support this proejct and me via [paypal](https://paypal.me/alibahraminezhad)
88 |
89 |
90 | ## Credits
91 | Most of the credits of this library goes to [Tatham Oddie](https://tatham.blog/2011/07/14/released-snowmaker-a-unique-id-generator-for-azure-or-any-other-cloud-hosting-environment/) for making SnowMaker. I forked his work and made lots of change to make it available on .NET Standard (2.0 and 2.1). SnowMaker is out-dated and is using very old version of Azure Packages.
92 |
--------------------------------------------------------------------------------
/AutoNumber/Options/AutoNumberOptionsBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Azure.Storage.Blobs;
3 | using Microsoft.Extensions.Configuration;
4 |
5 | namespace AutoNumber.Options
6 | {
7 | public class AutoNumberOptionsBuilder
8 | {
9 | private const string DefaultContainerName = "unique-urls";
10 | private const string AutoNumber = "AutoNumber";
11 | private readonly IConfiguration _configuration;
12 |
13 | public AutoNumberOptionsBuilder(IConfiguration configuration)
14 | {
15 | _configuration = configuration;
16 | configuration.GetSection(AutoNumber).Bind(Options);
17 | }
18 |
19 | public AutoNumberOptions Options { get; } = new AutoNumberOptions();
20 |
21 | ///
22 | /// Uses the default StorageAccount already defined in dependency injection
23 | ///
24 | public AutoNumberOptionsBuilder UseDefaultStorageAccount()
25 | {
26 | Options.StorageAccountConnectionString = null;
27 | return this;
28 | }
29 |
30 | ///
31 | /// Uses an Azure StorageAccount connection string to init the blob storage
32 | ///
33 | ///
34 | public AutoNumberOptionsBuilder UseStorageAccount(string connectionStringOrName)
35 | {
36 | if (string.IsNullOrEmpty(connectionStringOrName))
37 | throw new ArgumentNullException(nameof(connectionStringOrName));
38 |
39 | Options.StorageAccountConnectionString =
40 | _configuration.GetConnectionString(connectionStringOrName) ?? connectionStringOrName;
41 |
42 | return this;
43 | }
44 |
45 | public AutoNumberOptionsBuilder UseBlobServiceClient(BlobServiceClient blobServiceClient)
46 | {
47 | Options.BlobServiceClient = blobServiceClient
48 | ?? throw new ArgumentNullException(nameof(blobServiceClient));
49 |
50 | return this;
51 | }
52 |
53 | ///
54 | /// Default container name to store latest generated id on Azure blob storage
55 | ///
56 | public AutoNumberOptionsBuilder UseDefaultContainerName()
57 | {
58 | Options.StorageContainerName = DefaultContainerName;
59 | return this;
60 | }
61 |
62 | ///
63 | /// Container name for storing latest generated id on Azure blob storage
64 | ///
65 | ///
66 | public AutoNumberOptionsBuilder UseContainerName(string containerName)
67 | {
68 | Options.StorageContainerName = containerName;
69 | return this;
70 | }
71 |
72 | ///
73 | /// Max retrying to generate unique id
74 | ///
75 | ///
76 | public AutoNumberOptionsBuilder SetMaxWriteAttempts(int attempts = 100)
77 | {
78 | Options.MaxWriteAttempts = attempts;
79 | return this;
80 | }
81 |
82 | ///
83 | /// BatchSize for id generation, higher the value more losing unused id
84 | ///
85 | ///
86 | public AutoNumberOptionsBuilder SetBatchSize(int batchSize = 100)
87 | {
88 | Options.BatchSize = batchSize;
89 | return this;
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/IntegrationTests/DependencyInjectionTest.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using AutoNumber.Interfaces;
3 | using AutoNumber.Options;
4 | using Azure.Storage.Blobs;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Options;
8 | using NUnit.Framework;
9 |
10 | namespace AutoNumber.IntegrationTests
11 | {
12 | [TestFixture]
13 | public class DependencyInjectionTest
14 | {
15 | public IConfigurationRoot Configuration = new ConfigurationBuilder()
16 | .SetBasePath(Directory.GetCurrentDirectory())
17 | .AddJsonFile("appsettings.json", true, true).Build();
18 |
19 | private ServiceProvider GenerateServiceProvider()
20 | {
21 | var serviceCollection = new ServiceCollection();
22 | serviceCollection.AddSingleton(new BlobServiceClient("UseDevelopmentStorage=true"));
23 | serviceCollection.AddSingleton(Configuration);
24 | serviceCollection.AddAutoNumber();
25 | return serviceCollection.BuildServiceProvider();
26 | }
27 |
28 | [Test]
29 | public void OptionsBuilderShouldGenerateOptions()
30 | {
31 | var serviceProvider = GenerateServiceProvider();
32 | var optionsBuilder = new AutoNumberOptionsBuilder(serviceProvider.GetService());
33 |
34 | optionsBuilder.SetBatchSize(5);
35 | Assert.AreEqual(5, optionsBuilder.Options.BatchSize);
36 |
37 | optionsBuilder.SetMaxWriteAttempts(10);
38 | Assert.AreEqual(10, optionsBuilder.Options.MaxWriteAttempts);
39 |
40 | optionsBuilder.UseDefaultContainerName();
41 | Assert.AreEqual("unique-urls", optionsBuilder.Options.StorageContainerName);
42 |
43 | optionsBuilder.UseContainerName("test");
44 | Assert.AreEqual("test", optionsBuilder.Options.StorageContainerName);
45 |
46 | optionsBuilder.UseDefaultStorageAccount();
47 | Assert.AreEqual(null, optionsBuilder.Options.StorageAccountConnectionString);
48 |
49 | optionsBuilder.UseStorageAccount("test");
50 | Assert.AreEqual("test123", optionsBuilder.Options.StorageAccountConnectionString);
51 |
52 | optionsBuilder.UseStorageAccount("test-22");
53 | Assert.AreEqual("test-22", optionsBuilder.Options.StorageAccountConnectionString);
54 | }
55 |
56 | [Test]
57 | public void ShouldCraeteUniqueIdGenerator()
58 | {
59 | var serviceProvider = GenerateServiceProvider();
60 |
61 | var uniqueId = serviceProvider.GetService();
62 |
63 | Assert.NotNull(uniqueId);
64 | }
65 |
66 | [Test]
67 | public void ShouldOptionsContainsDefaultValues()
68 | {
69 | var serviceProvider = GenerateServiceProvider();
70 |
71 | var options = serviceProvider.GetService>();
72 |
73 | Assert.NotNull(options.Value);
74 | Assert.AreEqual(25, options.Value.MaxWriteAttempts);
75 | Assert.AreEqual(50, options.Value.BatchSize);
76 | Assert.AreEqual("unique-urls", options.Value.StorageContainerName);
77 | }
78 |
79 | [Test]
80 | public void ShouldResolveUniqueIdGenerator()
81 | {
82 | var serviceCollection = new ServiceCollection();
83 | serviceCollection.AddSingleton(new BlobServiceClient("UseDevelopmentStorage=true"));
84 |
85 | serviceCollection.AddAutoNumber(Configuration, x =>
86 | {
87 | return x.UseContainerName("ali")
88 | .UseDefaultStorageAccount()
89 | .SetBatchSize(10)
90 | .SetMaxWriteAttempts()
91 | .Options;
92 | });
93 |
94 | var service = serviceCollection.BuildServiceProvider()
95 | .GetService();
96 |
97 | Assert.NotNull(service);
98 | }
99 | }
100 | }
--------------------------------------------------------------------------------
/AutoNumber/UniqueIdGenerator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Threading;
5 | using AutoNumber.Exceptions;
6 | using AutoNumber.Extensions;
7 | using AutoNumber.Interfaces;
8 | using AutoNumber.Options;
9 | using Microsoft.Extensions.Options;
10 |
11 | namespace AutoNumber
12 | {
13 | ///
14 | /// Generate a new incremental id regards the scope name
15 | ///
16 | public class UniqueIdGenerator : IUniqueIdGenerator
17 | {
18 | ///
19 | /// Generate a new incremental id regards the scope name
20 | ///
21 | ///
22 | ///
23 | public long NextId(string scopeName)
24 | {
25 | var state = GetScopeState(scopeName);
26 |
27 | lock (state.IdGenerationLock)
28 | {
29 | if (state.LastId == state.HighestIdAvailableInBatch)
30 | UpdateFromSyncStore(scopeName, state);
31 |
32 | return Interlocked.Increment(ref state.LastId);
33 | }
34 | }
35 |
36 | private ScopeState GetScopeState(string scopeName)
37 | {
38 | return states.GetValue(
39 | scopeName,
40 | statesLock,
41 | () => new ScopeState());
42 | }
43 |
44 | private void UpdateFromSyncStore(string scopeName, ScopeState state)
45 | {
46 | var writesAttempted = 0;
47 |
48 | while (writesAttempted < MaxWriteAttempts)
49 | {
50 | var data = optimisticDataStore.GetData(scopeName);
51 |
52 | if (!long.TryParse(data, out var nextId))
53 | throw new UniqueIdGenerationException(
54 | $"The id seed returned from storage for scope '{scopeName}' was corrupt, and could not be parsed as a long. The data returned was: {data}");
55 |
56 | state.LastId = nextId - 1;
57 | state.HighestIdAvailableInBatch = nextId - 1 + BatchSize;
58 | var firstIdInNextBatch = state.HighestIdAvailableInBatch + 1;
59 |
60 | if (optimisticDataStore.TryOptimisticWrite(scopeName,
61 | firstIdInNextBatch.ToString(CultureInfo.InvariantCulture)))
62 | return;
63 |
64 | writesAttempted++;
65 | }
66 |
67 | throw new UniqueIdGenerationException(
68 | $"Failed to update the data store after {writesAttempted} attempts. This likely represents too much contention against the store. Increase the batch size to a value more appropriate to your generation load.");
69 | }
70 |
71 | #region fields
72 |
73 | private readonly IOptimisticDataStore optimisticDataStore;
74 | private readonly IDictionary states = new Dictionary();
75 | private readonly object statesLock = new object();
76 | private int maxWriteAttempts = 25;
77 |
78 | #endregion
79 |
80 | #region properties
81 |
82 | public int BatchSize { get; set; } = 100;
83 |
84 | public int MaxWriteAttempts
85 | {
86 | get => maxWriteAttempts;
87 | set
88 | {
89 | if (value < 1)
90 | throw new ArgumentOutOfRangeException(nameof(value), value,
91 | "MaxWriteAttempts must be a positive number.");
92 |
93 | maxWriteAttempts = value;
94 | }
95 | }
96 |
97 | #endregion
98 |
99 | #region ctor
100 |
101 | public UniqueIdGenerator(IOptimisticDataStore optimisticDataStore)
102 | {
103 | this.optimisticDataStore = optimisticDataStore;
104 | optimisticDataStore.Init();
105 | }
106 |
107 | public UniqueIdGenerator(IOptimisticDataStore optimisticDataStore, IOptions options)
108 | : this(optimisticDataStore)
109 | {
110 | BatchSize = options.Value.BatchSize;
111 | MaxWriteAttempts = options.Value.MaxWriteAttempts;
112 | }
113 |
114 | public UniqueIdGenerator(IOptimisticDataStore optimisticDataStore, AutoNumberOptions options)
115 | : this(optimisticDataStore)
116 | {
117 | BatchSize = options.BatchSize;
118 | MaxWriteAttempts = options.MaxWriteAttempts;
119 | }
120 |
121 | #endregion
122 | }
123 | }
--------------------------------------------------------------------------------
/UnitTests/UniqueIdGeneratorTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AutoNumber.Exceptions;
3 | using AutoNumber.Interfaces;
4 | using NSubstitute;
5 | using NUnit.Framework;
6 | using NUnit.Framework.Legacy;
7 |
8 | namespace AutoNumber.UnitTests
9 | {
10 | [TestFixture]
11 | public class UniqueIdGeneratorTest
12 | {
13 | [Test]
14 | public void ConstructorShouldNotRetrieveDataFromStore()
15 | {
16 | var store = Substitute.For();
17 | // ReSharper disable once ObjectCreationAsStatement
18 | new UniqueIdGenerator(store);
19 | store.DidNotReceiveWithAnyArgs().GetData(null);
20 | }
21 |
22 | [Test]
23 | public void MaxWriteAttemptsShouldThrowArgumentOutOfRangeExceptionWhenValueIsNegative()
24 | {
25 | var store = Substitute.For();
26 | Assert.That(() =>
27 | // ReSharper disable once ObjectCreationAsStatement
28 | new UniqueIdGenerator(store)
29 | {
30 | MaxWriteAttempts = -1
31 | }
32 | , Throws.TypeOf());
33 | }
34 |
35 | [Test]
36 | public void MaxWriteAttemptsShouldThrowArgumentOutOfRangeExceptionWhenValueIsZero()
37 | {
38 | var store = Substitute.For();
39 | Assert.That(() =>
40 | // ReSharper disable once ObjectCreationAsStatement
41 | new UniqueIdGenerator(store)
42 | {
43 | MaxWriteAttempts = 0
44 | }
45 | , Throws.TypeOf());
46 | }
47 |
48 | [Test]
49 | public void NextIdShouldReturnNumbersSequentially()
50 | {
51 | var store = Substitute.For();
52 | store.GetData("test").Returns("0", "250");
53 | store.TryOptimisticWrite("test", "3").Returns(true);
54 |
55 | var subject = new UniqueIdGenerator(store)
56 | {
57 | BatchSize = 3
58 | };
59 |
60 | Assert.That(subject.NextId("test"), Is.EqualTo(0));
61 | Assert.That(subject.NextId("test"), Is.EqualTo(1));
62 | Assert.That(subject.NextId("test"), Is.EqualTo(2));
63 | }
64 |
65 | [Test]
66 | public void NextIdShouldRollOverToNewBlockWhenCurrentBlockIsExhausted()
67 | {
68 | var store = Substitute.For();
69 | store.GetData("test").Returns("0", "250");
70 | store.TryOptimisticWrite("test", "3").Returns(true);
71 | store.TryOptimisticWrite("test", "253").Returns(true);
72 |
73 | var subject = new UniqueIdGenerator(store)
74 | {
75 | BatchSize = 3
76 | };
77 |
78 | Assert.That(subject.NextId("test"), Is.EqualTo(0));
79 | Assert.That(subject.NextId("test"), Is.EqualTo(1));
80 | Assert.That(subject.NextId("test"), Is.EqualTo(2) );
81 | Assert.That(subject.NextId("test"), Is.EqualTo(250));
82 | Assert.That(subject.NextId("test"), Is.EqualTo(251));
83 | Assert.That(subject.NextId("test"), Is.EqualTo(252));
84 | }
85 |
86 | [Test]
87 | public void NextIdShouldThrowExceptionOnCorruptData()
88 | {
89 | var store = Substitute.For();
90 | store.GetData("test").Returns("abc");
91 |
92 | Assert.That(() =>
93 | {
94 | var generator = new UniqueIdGenerator(store);
95 | generator.NextId("test");
96 | }
97 | , Throws.TypeOf());
98 | }
99 |
100 | [Test]
101 | public void NextIdShouldThrowExceptionOnNullData()
102 | {
103 | var store = Substitute.For();
104 | store.GetData("test").Returns((string) null);
105 |
106 | Assert.That(() =>
107 | {
108 | var generator = new UniqueIdGenerator(store);
109 | generator.NextId("test");
110 | }
111 | , Throws.TypeOf());
112 | }
113 |
114 | [Test]
115 | public void NextIdShouldThrowExceptionWhenRetriesAreExhausted()
116 | {
117 | var store = Substitute.For();
118 | store.GetData("test").Returns("0");
119 | store.TryOptimisticWrite("test", "3").Returns(false, false, false, true);
120 |
121 | var generator = new UniqueIdGenerator(store)
122 | {
123 | MaxWriteAttempts = 3
124 | };
125 |
126 | try
127 | {
128 | generator.NextId("test");
129 | }
130 | catch (Exception ex)
131 | {
132 | StringAssert.StartsWith("Failed to update the data store after 3 attempts.", ex.Message);
133 | return;
134 | }
135 |
136 | Assert.Fail("NextId should have thrown and been caught in the try block");
137 | }
138 | }
139 | }
--------------------------------------------------------------------------------
/AutoNumber/BlobOptimisticDataStore.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.IO;
3 | using System.Net;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using AutoNumber.Extensions;
7 | using AutoNumber.Interfaces;
8 | using AutoNumber.Options;
9 | using Azure;
10 | using Azure.Storage.Blobs;
11 | using Azure.Storage.Blobs.Models;
12 | using Azure.Storage.Blobs.Specialized;
13 | using Microsoft.Extensions.Options;
14 |
15 | namespace AutoNumber
16 | {
17 | public class BlobOptimisticDataStore : IOptimisticDataStore
18 | {
19 | private const string SeedValue = "1";
20 | private readonly BlobContainerClient blobContainer;
21 | private readonly ConcurrentDictionary blobReferences;
22 | private readonly object blobReferencesLock = new object();
23 |
24 | public BlobOptimisticDataStore(BlobServiceClient blobServiceClient, string containerName)
25 | {
26 | blobContainer = blobServiceClient.GetBlobContainerClient(containerName.ToLower());
27 | blobReferences = new ConcurrentDictionary();
28 | }
29 |
30 | public BlobOptimisticDataStore(BlobServiceClient blobServiceClient, IOptions options)
31 | : this(blobServiceClient, options.Value.StorageContainerName)
32 | {
33 | }
34 |
35 | public string GetData(string blockName)
36 | {
37 | var blobReference = GetBlobReference(blockName);
38 |
39 | using (var stream = new MemoryStream())
40 | {
41 | blobReference.DownloadTo(stream);
42 | return Encoding.UTF8.GetString(stream.ToArray());
43 | }
44 | }
45 |
46 | public async Task GetDataAsync(string blockName)
47 | {
48 | var blobReference = GetBlobReference(blockName);
49 |
50 | using (var stream = new MemoryStream())
51 | {
52 | await blobReference.DownloadToAsync(stream).ConfigureAwait(false);
53 | return Encoding.UTF8.GetString(stream.ToArray());
54 | }
55 | }
56 |
57 | public async Task InitAsync()
58 | {
59 | var result = await blobContainer.CreateIfNotExistsAsync().ConfigureAwait(false);
60 | return result == null || result.Value != null;
61 | }
62 |
63 | public bool Init()
64 | {
65 | var result = blobContainer.CreateIfNotExists();
66 | return result == null || result.Value != null;
67 | }
68 |
69 | public bool TryOptimisticWrite(string blockName, string data)
70 | {
71 | var blobReference = GetBlobReference(blockName);
72 | try
73 | {
74 | var blobRequestCondition = new BlobRequestConditions
75 | {
76 | IfMatch = (blobReference.GetProperties()).Value.ETag
77 | };
78 | UploadText(
79 | blobReference,
80 | data,
81 | blobRequestCondition);
82 | }
83 | catch (RequestFailedException exc)
84 | {
85 | if (exc.Status == (int)HttpStatusCode.PreconditionFailed)
86 | return false;
87 |
88 | throw;
89 | }
90 |
91 | return true;
92 | }
93 |
94 | public async Task TryOptimisticWriteAsync(string blockName, string data)
95 | {
96 | var blobReference = GetBlobReference(blockName);
97 | try
98 | {
99 | var blobRequestCondition = new BlobRequestConditions
100 | {
101 | IfMatch = (await blobReference.GetPropertiesAsync()).Value.ETag
102 | };
103 | await UploadTextAsync(
104 | blobReference,
105 | data,
106 | blobRequestCondition).ConfigureAwait(false);
107 | }
108 | catch (RequestFailedException exc)
109 | {
110 | if (exc.Status == (int)HttpStatusCode.PreconditionFailed)
111 | return false;
112 |
113 | throw;
114 | }
115 |
116 | return true;
117 | }
118 |
119 | private BlockBlobClient GetBlobReference(string blockName)
120 | {
121 | return blobReferences.GetValue(
122 | blockName,
123 | blobReferencesLock,
124 | () => InitializeBlobReference(blockName));
125 | }
126 |
127 | private BlockBlobClient InitializeBlobReference(string blockName)
128 | {
129 | var blobReference = blobContainer.GetBlockBlobClient(blockName);
130 |
131 | if (blobReference.Exists())
132 | return blobReference;
133 |
134 | try
135 | {
136 | var blobRequestCondition = new BlobRequestConditions
137 | {
138 | IfNoneMatch = ETag.All
139 | };
140 | UploadText(blobReference, SeedValue, blobRequestCondition);
141 | }
142 | catch (RequestFailedException uploadException)
143 | {
144 | if (uploadException.Status != (int)HttpStatusCode.Conflict)
145 | throw;
146 | }
147 |
148 | return blobReference;
149 | }
150 |
151 | private async Task UploadTextAsync(BlockBlobClient blob, string text, BlobRequestConditions accessCondition)
152 | {
153 | var header = new BlobHttpHeaders
154 | {
155 | ContentType = "text/plain"
156 | };
157 |
158 | using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(text)))
159 | {
160 | await blob.UploadAsync(stream, header, null, accessCondition, null, null).ConfigureAwait(false);
161 | }
162 | }
163 |
164 | private void UploadText(BlockBlobClient blob, string text, BlobRequestConditions accessCondition)
165 | {
166 | var header = new BlobHttpHeaders
167 | {
168 | ContentType = "text/plain"
169 | };
170 |
171 | using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(text)))
172 | {
173 | blob.Upload(stream, header, null, accessCondition, null, null);
174 | }
175 | }
176 | }
177 | }
--------------------------------------------------------------------------------
/IntegrationTests/Scenarios.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Linq;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using AutoNumber;
6 | using AutoNumber.Interfaces;
7 | using NUnit.Framework;
8 |
9 | namespace IntegrationTests.cs
10 | {
11 | public abstract class Scenarios where TTestScope : ITestScope
12 | {
13 | protected abstract IOptimisticDataStore BuildStore(TTestScope scope);
14 | protected abstract TTestScope BuildTestScope();
15 |
16 | [Test]
17 | public void ShouldReturnOneForFirstIdInNewScope()
18 | {
19 | // Arrange
20 | using (var testScope = BuildTestScope())
21 | {
22 | var store = BuildStore(testScope);
23 | var generator = new UniqueIdGenerator(store) {BatchSize = 3};
24 |
25 | // Act
26 | var generatedId = generator.NextId(testScope.IdScopeName);
27 |
28 | // Assert
29 | Assert.AreEqual(1, generatedId);
30 | }
31 | }
32 |
33 | [Test]
34 | public void ShouldInitializeBlobForFirstIdInNewScope()
35 | {
36 | // Arrange
37 | using (var testScope = BuildTestScope())
38 | {
39 | var store = BuildStore(testScope);
40 | var generator = new UniqueIdGenerator(store) {BatchSize = 3};
41 |
42 | // Act
43 | generator.NextId(testScope.IdScopeName); //1
44 |
45 | // Assert
46 | Assert.AreEqual("4", testScope.ReadCurrentPersistedValue());
47 | }
48 | }
49 |
50 | [Test]
51 | public void ShouldNotUpdateBlobAtEndOfBatch()
52 | {
53 | // Arrange
54 | using (var testScope = BuildTestScope())
55 | {
56 | var store = BuildStore(testScope);
57 | var generator = new UniqueIdGenerator(store) {BatchSize = 3};
58 |
59 | // Act
60 | generator.NextId(testScope.IdScopeName); //1
61 | generator.NextId(testScope.IdScopeName); //2
62 | generator.NextId(testScope.IdScopeName); //3
63 |
64 | // Assert
65 | Assert.AreEqual("4", testScope.ReadCurrentPersistedValue());
66 | }
67 | }
68 |
69 | [Test]
70 | public void ShouldUpdateBlobWhenGeneratingNextIdAfterEndOfBatch()
71 | {
72 | // Arrange
73 | using (var testScope = BuildTestScope())
74 | {
75 | var store = BuildStore(testScope);
76 | var generator = new UniqueIdGenerator(store) {BatchSize = 3};
77 |
78 | // Act
79 | generator.NextId(testScope.IdScopeName); //1
80 | generator.NextId(testScope.IdScopeName); //2
81 | generator.NextId(testScope.IdScopeName); //3
82 | generator.NextId(testScope.IdScopeName); //4
83 |
84 | // Assert
85 | Assert.AreEqual("7", testScope.ReadCurrentPersistedValue());
86 | }
87 | }
88 |
89 | [Test]
90 | public void ShouldReturnIdsFromThirdBatchIfSecondBatchTakenByAnotherGenerator()
91 | {
92 | // Arrange
93 | using (var testScope = BuildTestScope())
94 | {
95 | var store1 = BuildStore(testScope);
96 | var generator1 = new UniqueIdGenerator(store1) {BatchSize = 3};
97 | var store2 = BuildStore(testScope);
98 | var generator2 = new UniqueIdGenerator(store2) {BatchSize = 3};
99 |
100 | // Act
101 | generator1.NextId(testScope.IdScopeName); //1
102 | generator1.NextId(testScope.IdScopeName); //2
103 | generator1.NextId(testScope.IdScopeName); //3
104 | generator2.NextId(testScope.IdScopeName); //4
105 | var lastId = generator1.NextId(testScope.IdScopeName); //7
106 |
107 | // Assert
108 | Assert.AreEqual(7, lastId);
109 | }
110 | }
111 |
112 | [Test]
113 | public void ShouldReturnIdsAcrossMultipleGenerators()
114 | {
115 | // Arrange
116 | using (var testScope = BuildTestScope())
117 | {
118 | var store1 = BuildStore(testScope);
119 | var generator1 = new UniqueIdGenerator(store1) {BatchSize = 3};
120 | var store2 = BuildStore(testScope);
121 | var generator2 = new UniqueIdGenerator(store2) {BatchSize = 3};
122 |
123 | // Act
124 | var generatedIds = new[]
125 | {
126 | generator1.NextId(testScope.IdScopeName), //1
127 | generator1.NextId(testScope.IdScopeName), //2
128 | generator1.NextId(testScope.IdScopeName), //3
129 | generator2.NextId(testScope.IdScopeName), //4
130 | generator1.NextId(testScope.IdScopeName), //7
131 | generator2.NextId(testScope.IdScopeName), //5
132 | generator2.NextId(testScope.IdScopeName), //6
133 | generator2.NextId(testScope.IdScopeName), //10
134 | generator1.NextId(testScope.IdScopeName), //8
135 | generator1.NextId(testScope.IdScopeName) //9
136 | };
137 |
138 | // Assert
139 | CollectionAssert.AreEqual(
140 | new[] {1, 2, 3, 4, 7, 5, 6, 10, 8, 9},
141 | generatedIds);
142 | }
143 | }
144 |
145 | [Test]
146 | public void ShouldSupportUsingOneGeneratorFromMultipleThreads()
147 | {
148 | // Arrange
149 | using (var testScope = BuildTestScope())
150 | {
151 | var store = BuildStore(testScope);
152 | var generator = new UniqueIdGenerator(store) {BatchSize = 1000};
153 | const int testLength = 10000;
154 |
155 | // Act
156 | var generatedIds = new ConcurrentQueue();
157 | var threadIds = new ConcurrentQueue();
158 | var scopeName = testScope.IdScopeName;
159 | Parallel.For(
160 | 0,
161 | testLength,
162 | new ParallelOptions {MaxDegreeOfParallelism = 10},
163 | i =>
164 | {
165 | generatedIds.Enqueue(generator.NextId(scopeName));
166 | threadIds.Enqueue(Thread.CurrentThread.ManagedThreadId);
167 | });
168 |
169 | // Assert we generated the right count of ids
170 | Assert.AreEqual(testLength, generatedIds.Count);
171 |
172 | // Assert there were no duplicates
173 | Assert.IsFalse(generatedIds.GroupBy(n => n).Any(g => g.Count() != 1));
174 |
175 | // Assert we used multiple threads
176 | var uniqueThreadsUsed = threadIds.Distinct().Count();
177 | if (uniqueThreadsUsed == 1)
178 | Assert.Inconclusive("The test failed to actually utilize multiple threads");
179 | }
180 | }
181 | }
182 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Coverlet is a free, cross platform Code Coverage Tool
141 | coverage*[.json, .xml, .info]
142 |
143 | # Visual Studio code coverage results
144 | *.coverage
145 | *.coveragexml
146 |
147 | # NCrunch
148 | _NCrunch_*
149 | .*crunch*.local.xml
150 | nCrunchTemp_*
151 |
152 | # MightyMoose
153 | *.mm.*
154 | AutoTest.Net/
155 |
156 | # Web workbench (sass)
157 | .sass-cache/
158 |
159 | # Installshield output folder
160 | [Ee]xpress/
161 |
162 | # DocProject is a documentation generator add-in
163 | DocProject/buildhelp/
164 | DocProject/Help/*.HxT
165 | DocProject/Help/*.HxC
166 | DocProject/Help/*.hhc
167 | DocProject/Help/*.hhk
168 | DocProject/Help/*.hhp
169 | DocProject/Help/Html2
170 | DocProject/Help/html
171 |
172 | # Click-Once directory
173 | publish/
174 |
175 | # Publish Web Output
176 | *.[Pp]ublish.xml
177 | *.azurePubxml
178 | # Note: Comment the next line if you want to checkin your web deploy settings,
179 | # but database connection strings (with potential passwords) will be unencrypted
180 | *.pubxml
181 | *.publishproj
182 |
183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
184 | # checkin your Azure Web App publish settings, but sensitive information contained
185 | # in these scripts will be unencrypted
186 | PublishScripts/
187 |
188 | # NuGet Packages
189 | *.nupkg
190 | # NuGet Symbol Packages
191 | *.snupkg
192 | # The packages folder can be ignored because of Package Restore
193 | **/[Pp]ackages/*
194 | # except build/, which is used as an MSBuild target.
195 | !**/[Pp]ackages/build/
196 | # Uncomment if necessary however generally it will be regenerated when needed
197 | #!**/[Pp]ackages/repositories.config
198 | # NuGet v3's project.json files produces more ignorable files
199 | *.nuget.props
200 | *.nuget.targets
201 |
202 | # Microsoft Azure Build Output
203 | csx/
204 | *.build.csdef
205 |
206 | # Microsoft Azure Emulator
207 | ecf/
208 | rcf/
209 |
210 | # Windows Store app package directories and files
211 | AppPackages/
212 | BundleArtifacts/
213 | Package.StoreAssociation.xml
214 | _pkginfo.txt
215 | *.appx
216 | *.appxbundle
217 | *.appxupload
218 |
219 | # Visual Studio cache files
220 | # files ending in .cache can be ignored
221 | *.[Cc]ache
222 | # but keep track of directories ending in .cache
223 | !?*.[Cc]ache/
224 |
225 | # Others
226 | ClientBin/
227 | ~$*
228 | *~
229 | *.dbmdl
230 | *.dbproj.schemaview
231 | *.jfm
232 | *.pfx
233 | *.publishsettings
234 | orleans.codegen.cs
235 |
236 | # Including strong name files can present a security risk
237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
238 | #*.snk
239 |
240 | # Since there are multiple workflows, uncomment next line to ignore bower_components
241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
242 | #bower_components/
243 |
244 | # RIA/Silverlight projects
245 | Generated_Code/
246 |
247 | # Backup & report files from converting an old project file
248 | # to a newer Visual Studio version. Backup files are not needed,
249 | # because we have git ;-)
250 | _UpgradeReport_Files/
251 | Backup*/
252 | UpgradeLog*.XML
253 | UpgradeLog*.htm
254 | ServiceFabricBackup/
255 | *.rptproj.bak
256 |
257 | # SQL Server files
258 | *.mdf
259 | *.ldf
260 | *.ndf
261 |
262 | # Business Intelligence projects
263 | *.rdl.data
264 | *.bim.layout
265 | *.bim_*.settings
266 | *.rptproj.rsuser
267 | *- [Bb]ackup.rdl
268 | *- [Bb]ackup ([0-9]).rdl
269 | *- [Bb]ackup ([0-9][0-9]).rdl
270 |
271 | # Microsoft Fakes
272 | FakesAssemblies/
273 |
274 | # GhostDoc plugin setting file
275 | *.GhostDoc.xml
276 |
277 | # Node.js Tools for Visual Studio
278 | .ntvs_analysis.dat
279 | node_modules/
280 |
281 | # Visual Studio 6 build log
282 | *.plg
283 |
284 | # Visual Studio 6 workspace options file
285 | *.opt
286 |
287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
288 | *.vbw
289 |
290 | # Visual Studio LightSwitch build output
291 | **/*.HTMLClient/GeneratedArtifacts
292 | **/*.DesktopClient/GeneratedArtifacts
293 | **/*.DesktopClient/ModelManifest.xml
294 | **/*.Server/GeneratedArtifacts
295 | **/*.Server/ModelManifest.xml
296 | _Pvt_Extensions
297 |
298 | # Paket dependency manager
299 | .paket/paket.exe
300 | paket-files/
301 |
302 | # FAKE - F# Make
303 | .fake/
304 |
305 | # CodeRush personal settings
306 | .cr/personal
307 |
308 | # Python Tools for Visual Studio (PTVS)
309 | __pycache__/
310 | *.pyc
311 |
312 | # Cake - Uncomment if you are using it
313 | # tools/**
314 | # !tools/packages.config
315 |
316 | # Tabs Studio
317 | *.tss
318 |
319 | # Telerik's JustMock configuration file
320 | *.jmconfig
321 |
322 | # BizTalk build output
323 | *.btp.cs
324 | *.btm.cs
325 | *.odx.cs
326 | *.xsd.cs
327 |
328 | # OpenCover UI analysis results
329 | OpenCover/
330 |
331 | # Azure Stream Analytics local run output
332 | ASALocalRun/
333 |
334 | # MSBuild Binary and Structured Log
335 | *.binlog
336 |
337 | # NVidia Nsight GPU debugger configuration file
338 | *.nvuser
339 |
340 | # MFractors (Xamarin productivity tool) working folder
341 | .mfractor/
342 |
343 | # Local History for Visual Studio
344 | .localhistory/
345 |
346 | # BeatPulse healthcheck temp database
347 | healthchecksdb
348 |
349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
350 | MigrationBackup/
351 |
352 | # Ionide (cross platform F# VS Code tools) working folder
353 | .ionide/
354 | __blobstorage__
355 | __queuestorage__
356 | __azurite_db_blob__.json
357 | __azurite_db_blob_extent__.json
358 | __azurite_db_queue__.json
359 | __azurite_db_queue_extent__.json
360 |
361 | .idea
362 |
--------------------------------------------------------------------------------