├── 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 | [![.NET Build](https://github.com/0x414c49/AzureAutoNumber/actions/workflows/dotnet.yml/badge.svg)](https://github.com/0x414c49/AzureAutoNumber/actions) 7 | [![Build Status](https://img.shields.io/github/license/0x414c49/AzureAutoNumber)]() 8 | [![NuGet version (AzureAutoNumber)](https://img.shields.io/nuget/v/AzureAutoNumber.svg?style=flat-square)](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 | --------------------------------------------------------------------------------