diff --git a/example/SnowflakeId.Example/Program.cs b/example/SnowflakeId.Example/Program.cs index 3e237e7..ec6db2c 100644 --- a/example/SnowflakeId.Example/Program.cs +++ b/example/SnowflakeId.Example/Program.cs @@ -8,17 +8,33 @@ Build and implemented with love by Mohammed Ahmed Hussien Babiker using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using SnowflakeId.Core; +using SnowflakeId.Core.Events; using SnowflakeId.DependencyInjection; using System; +using System.Threading.Tasks; IHost host = Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => { - services.AddSnowflakeUniqueId(options => + services.AddSnowflakeUniqueId( options => { options.DataCenterId = 7; - }); + options.Events = new SnowflakeIdEvents + { + OnCreatedSnowflakeId = context => + { + Console.WriteLine("OnCreatedSnowflakeId --> The Id is: {0} and is generated At: {1}", context.Id, context.GeneratedDateTime); + return Task.CompletedTask; + }, + OnCreatingSnowflakeId = context => + { + Console.WriteLine("OnCreatingSnowflakeId --> Generating Id at data center has id: {0}", context.DataCenterId); + return Task.CompletedTask; + } + }; + } + ); }).Build(); var idServive = host.Services.GetRequiredService(); diff --git a/src/core/DependencyInjection/SnowflakeIdServiceExtensions.cs b/src/core/DependencyInjection/SnowflakeIdServiceExtensions.cs index d91db16..22f6863 100644 --- a/src/core/DependencyInjection/SnowflakeIdServiceExtensions.cs +++ b/src/core/DependencyInjection/SnowflakeIdServiceExtensions.cs @@ -10,20 +10,21 @@ namespace SnowflakeId.DependencyInjection public static class SnowflakeIdServiceExtensions { public static IServiceCollection AddSnowflakeUniqueId(this IServiceCollection services, - Action setupAction) + Action setupAction = null) { if (services == null) { throw new ArgumentNullException(nameof(services)); } - if (setupAction == null) - { - throw new ArgumentNullException(nameof(setupAction)); - } + services.AddOptions(); + var o = new SnowflakOptions(); + services.AddSingleton(o); + setupAction?.Invoke(o); services.TryAddScoped(); - services.Configure(setupAction); + if (setupAction != null) + services.Configure(setupAction); return services; } } diff --git a/src/core/Events/SnowflakeIdCreatedContext.cs b/src/core/Events/SnowflakeIdCreatedContext.cs new file mode 100644 index 0000000..f394023 --- /dev/null +++ b/src/core/Events/SnowflakeIdCreatedContext.cs @@ -0,0 +1,28 @@ +// Read more about the licenses under the root of the project in the LICENSE.txt file. + +using System; +namespace SnowflakeId.Core.Events; + +public class SnowflakeIdCreatedContext +{ + /// + /// Get ot set snowflakeId. + /// + public long Id { get; set; } + + /// + /// Get or set the generation datetime of the snowflakeId. + /// + public DateTime GeneratedDateTime { get; set; } + + /// + /// Gets or sets the identifier of the data center. + /// + public int DataCenterId { get; set; } + + /// + /// Gets or sets the default epoch (1970 Jan 1st - Unix Time). + /// Default value is 1970 Jan 1st if the is null."/> + /// + public DateTime DefaultEpoch { get; set; } +} diff --git a/src/core/Events/SnowflakeIdCreatingContext.cs b/src/core/Events/SnowflakeIdCreatingContext.cs new file mode 100644 index 0000000..17728ab --- /dev/null +++ b/src/core/Events/SnowflakeIdCreatingContext.cs @@ -0,0 +1,18 @@ +// Read more about the licenses under the root of the project in the LICENSE.txt file. + +using System; +namespace SnowflakeId.Core.Events; + +public class SnowflakeIdCreatingContext +{ + /// + /// Gets or sets the default epoch (1970 Jan 1st - Unix Time). + /// Default value is 1970 Jan 1st if the is null."/> + /// + public DateTime DefaultEpoch { get; set; } + + /// + /// Gets or sets the identifier of the data center. + /// + public int DataCenterId { get; set; } +} diff --git a/src/core/Events/SnowflakeIdEvents.cs b/src/core/Events/SnowflakeIdEvents.cs new file mode 100644 index 0000000..4d31235 --- /dev/null +++ b/src/core/Events/SnowflakeIdEvents.cs @@ -0,0 +1,42 @@ +// Read more about the licenses under the root of the project in the LICENSE.txt file. + +using System; +using System.Threading.Tasks; +namespace SnowflakeId.Core.Events; + +public class SnowflakeIdEvents +{ + /// + /// Invoke before creating a snowflakeId. + /// + public Func OnCreatingSnowflakeId { get; set; } = context => Task.CompletedTask; + + /// + /// Invoke after a snowflakeId has been created. + /// + public Func OnCreatedSnowflakeId { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked before creating a snowflakeId. + /// + public virtual Task CreatingSnowflakeId(SnowflakeIdCreatingContext context) + { + if (OnCreatingSnowflakeId != null) + { + return OnCreatingSnowflakeId.Invoke(context); + } + return Task.CompletedTask; + } + + /// + /// Invoked after a snowflakeId has been created. + /// + public virtual Task CreatedSnowflakeId(SnowflakeIdCreatedContext context) + { + if (OnCreatedSnowflakeId != null) + { + return OnCreatedSnowflakeId.Invoke(context); + } + return Task.CompletedTask; + } +} diff --git a/src/core/SnowflakOptions.cs b/src/core/SnowflakOptions.cs index 06ebf0c..f031d7a 100644 --- a/src/core/SnowflakOptions.cs +++ b/src/core/SnowflakOptions.cs @@ -1,5 +1,8 @@ // Read more about the licenses under the root of the project in the LICENSE.txt file. +using SnowflakeId.Core.Events; +using System; + namespace SnowflakeId.Core { public class SnowflakOptions @@ -9,11 +12,21 @@ public class SnowflakOptions /// or any other unique things in your webfarm /// Be sure to set this property with number greater than zero. /// - public int DataCenterId { get; set; } + public int? DataCenterId { get; set; } /// /// Get or set the value that determined whether using console log or not, the default value is false. /// public bool UseConsoleLog { get; set; } + + /// + /// Get or set the custom epoch, the default value of this property with set to epoch is 1970 Jan 1s ( Unix Time ) if value is null. + /// + public DateTime? CustomEpoch { get; set; } + + /// + /// events + public SnowflakeIdEvents Events { get; set; } = new SnowflakeIdEvents(); + } } diff --git a/src/core/SnowflakeId.Core.csproj b/src/core/SnowflakeId.Core.csproj index b4c66c8..8d477d2 100644 --- a/src/core/SnowflakeId.Core.csproj +++ b/src/core/SnowflakeId.Core.csproj @@ -8,16 +8,13 @@ Mohammed Ahmed Hussien https://github.com/Shoogn/SnowflakeId https://github.com/Shoogn/SnowflakeId - 3.0.1 + 3.2.0 git - SnowflakeId, UniqueSnowflakeId, UniqueId + SnowflakeId, UniqueSnowflakeId, UniqueId, Primary Keys en-SD - This package implements the twitter's snowflakeId algorithm, - the source code is written in C# programming language, - and the main benefits of this library is to help anyone - wroks with Distrbuted Systems to generate a unique Ids for these Systems or - for Primary Keys is the RDMS such as SQL Server, Oracle, MySQL; or for any other situation that reuired unique Ids. + This is an implementation of snowflakeId algorithm in C# programming language, this algorithm is developed by X formally (Twitter), the source code is written in C# programming language, + and the main benefits of this library is to help anyone wroks with Distrbuted Systems to generate a unique Ids for these Systems or for Primary Keys is the [RDMS] such as SQL Server, Oracle, MySQL; or for any other situation that required unique Ids. @@ -42,7 +39,7 @@ - + \ No newline at end of file diff --git a/src/core/SnowflakeIdConfig.cs b/src/core/SnowflakeIdConfig.cs index 4796938..1c0cb72 100644 --- a/src/core/SnowflakeIdConfig.cs +++ b/src/core/SnowflakeIdConfig.cs @@ -29,6 +29,7 @@ internal class SnowflakeIdConfig /// /// 1023 + /// ((1 << MachineIdBits) - 1); /// public static readonly int MaxMachineId = (int)(Math.Pow(2, MachineIdBits) - 1); diff --git a/src/core/SnowflakeIdService.cs b/src/core/SnowflakeIdService.cs index 9ef900e..688d489 100644 --- a/src/core/SnowflakeIdService.cs +++ b/src/core/SnowflakeIdService.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using SnowflakeId.Core.Events; using System; using System.Threading; using System.Threading.Tasks; @@ -13,6 +14,7 @@ public class SnowflakeIdService : ISnowflakeService, IDisposable { // Lock Token private readonly object threadLock = new object(); + private readonly SemaphoreSlim sem = new SemaphoreSlim(1, 1); private long _lastTimestamp = -1L; private long _sequence = 0L; @@ -25,17 +27,27 @@ public class SnowflakeIdService : ISnowflakeService, IDisposable private readonly SnowflakOptions _snowflakOptions; private readonly ILogger _logger; + protected readonly int DataCenterId = 1; + private static readonly DateTime DefaultEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + /// /// When generating the Id I use the Epoch that start at 1970 Jan 1s ( Unix Time ) /// - public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + public readonly DateTime UnixEpoch; private bool _disposed; public SnowflakeIdService(IOptions options, ILogger logger) { _snowflakOptions = options.Value; _logger = logger ?? new NullLogger(); + + UnixEpoch = options.Value.CustomEpoch ?? DefaultEpoch; + _snowflakOptions.DataCenterId ??= DataCenterId; + if (_snowflakOptions.DataCenterId < 0 || _snowflakOptions.DataCenterId > SnowflakeIdConfig.MaxMachineId) + { + throw new ArgumentException(string.Format("DataCenterId must be between 0 and {0}", SnowflakeIdConfig.MaxMachineId)); + } } /// @@ -46,12 +58,20 @@ public virtual long GenerateSnowflakeId() { lock (threadLock) { + var idCreatingContext = new SnowflakeIdCreatingContext + { + DataCenterId = _snowflakOptions.DataCenterId.Value, + DefaultEpoch = UnixEpoch + }; + _snowflakOptions.Events.CreatingSnowflakeId(idCreatingContext).GetAwaiter().GetResult(); + long currentTimestamp = getTimestamp(); if (currentTimestamp < _lastTimestamp) { if (_snowflakOptions.UseConsoleLog) - _logger.LogError("error in the server clock, the current timestamp should be bigger than generated one, current timestamp is: {0}, and the last generated timestamp is: {1}", currentTimestamp, _lastTimestamp); + _logger.LogError("error in the server clock, the current timestamp should be bigger than generated one, " + + "current timestamp is: {CurrentTimestamp}, and the last generated timestamp is: {LastTimestamp}", currentTimestamp, _lastTimestamp); throw new InvalidOperationException("Error_In_The_Server_Clock"); } @@ -74,7 +94,18 @@ public virtual long GenerateSnowflakeId() long result = (currentTimestamp << _timeStampShift) | ((long)_snowflakOptions.DataCenterId << _machaineIdShift) | (_sequence); if (_snowflakOptions.UseConsoleLog) - _logger.LogInformation("the gnerated unique id is {0}", result); + _logger.LogInformation("the gnerated unique id is {UniqueId}", result); + + var idCreatedContext = new SnowflakeIdCreatedContext + { + Id = result, + GeneratedDateTime = GetDateTimeBySecondsSinceUnixEpoch(GetSecondsSinceUnixEpochFromId(result)), + DataCenterId = _snowflakOptions.DataCenterId.Value, + DefaultEpoch = UnixEpoch + }; + _snowflakOptions.Events.CreatedSnowflakeId(idCreatedContext).GetAwaiter().GetResult(); + + return result; } } @@ -85,18 +116,25 @@ public virtual long GenerateSnowflakeId() /// cancellationToken /// A new unique number that has a long type. /// - public virtual Task GenerateSnowflakeIdAsync(CancellationToken cancellationToken = default) + public virtual async Task GenerateSnowflakeIdAsync(CancellationToken cancellationToken = default) { - SemaphoreSlim sem = new SemaphoreSlim(1, 1); try { - sem.Wait(cancellationToken); + await sem.WaitAsync(cancellationToken).ConfigureAwait(false); + var idCreatingContext = new SnowflakeIdCreatingContext + { + DataCenterId = _snowflakOptions.DataCenterId.Value, + DefaultEpoch = UnixEpoch + }; + await _snowflakOptions.Events.CreatingSnowflakeId(idCreatingContext).ConfigureAwait(false); + long currentTimestamp = getTimestamp(); if (currentTimestamp < _lastTimestamp) { if (_snowflakOptions.UseConsoleLog) - _logger.LogError("error in the server clock, the current timestamp should be bigger than generated one, current timestamp is: {0}, and the last generated timestamp is: {1}", currentTimestamp, _lastTimestamp); + _logger.LogError("error in the server clock, the current timestamp should be bigger than generated one, current timestamp is: {CurrentTimestamp}, " + + "and the last generated timestamp is: {LastTimestamp}", currentTimestamp, _lastTimestamp); throw new InvalidOperationException("Error_In_The_Server_Clock"); } @@ -119,8 +157,19 @@ public virtual Task GenerateSnowflakeIdAsync(CancellationToken cancellatio long result = (currentTimestamp << _timeStampShift) | ((long)_snowflakOptions.DataCenterId << _machaineIdShift) | (_sequence); if (_snowflakOptions.UseConsoleLog) - _logger.LogInformation("the gnerated unique id is {0}", result); - return Task.FromResult(result); + _logger.LogInformation("the gnerated unique id is {UniqueId}", result); + + var idCreatedContext = new SnowflakeIdCreatedContext + { + Id = result, + GeneratedDateTime = GetDateTimeBySecondsSinceUnixEpoch(GetSecondsSinceUnixEpochFromId(result)), + DataCenterId = _snowflakOptions.DataCenterId.Value, + DefaultEpoch = UnixEpoch + + }; + await _snowflakOptions.Events.CreatedSnowflakeId(idCreatedContext).ConfigureAwait(false); + + return result; } finally { @@ -194,7 +243,7 @@ public virtual long GetSecondsSinceUnixEpochFromId(long snowflakeId) } // 41 bits of 1s, will shifted left by 22 bits. - long timestampMask = 0x1FFFFFFFFFF; + long timestampMask = 0x1FFFFFFFFFF; long timeStamp = (snowflakeId >> _timeStampShift) & timestampMask; return timeStamp; } @@ -212,7 +261,7 @@ public virtual int GetDataCenterIdBySnowflakeId(long snowflakeId) } // 10 bits mask (0b1111111111) will shifted left by 12 bits. - long dataCenterIdMask = 0x3FF; + long dataCenterIdMask = 0x3FF; long dataCenterId = (snowflakeId >> _machaineIdShift) & dataCenterIdMask; return (int)dataCenterId; diff --git a/tests/SnowflakeIdConfigTest.cs b/tests/SnowflakeIdConfigTest.cs index fa3ba45..0e7a175 100644 --- a/tests/SnowflakeIdConfigTest.cs +++ b/tests/SnowflakeIdConfigTest.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using SnowflakeId.Core; +using SnowflakeId.Core; namespace SnowflakeId.Tests { diff --git a/tests/SnowflakeIdServiceExtensionsTest.cs b/tests/SnowflakeIdServiceExtensionsTest.cs index e74a037..2d0f21f 100644 --- a/tests/SnowflakeIdServiceExtensionsTest.cs +++ b/tests/SnowflakeIdServiceExtensionsTest.cs @@ -17,13 +17,21 @@ namespace SnowflakeId.Tests public class SnowflakeIdServiceExtensionsTest { [Fact] - - public void AddSnowflakeUniqueId_ThrowArgumentNullException_With_Null_SnowflakOptions() + public void Can_Add_SnowflakId_To_Service_Collection_Without_SnowflakeId_Option() { var services = new ServiceCollection(); services.AddLogging(); + services.AddSnowflakeUniqueId(); + + var serviceProvider = services.BuildServiceProvider(); + + var snowflakeId = services.FirstOrDefault(desc => desc.ServiceType == typeof(ISnowflakeService)); + var snowflakeIdOptionGetter = serviceProvider.GetRequiredService>(); + var snowflakeIdOption = snowflakeIdOptionGetter.Value; - Assert.Throws(() => services.AddSnowflakeUniqueId(null)); + Assert.NotNull(snowflakeId); + Assert.Equal(ServiceLifetime.Scoped, snowflakeId.Lifetime); + Assert.Null(snowflakeIdOption.DataCenterId); } [Fact] @@ -57,7 +65,6 @@ public void Can_Replace_SnowflakeId_Default_Registration_By_Creating_Object_That Assert.NotNull(snowflakeId); Assert.Equal(ServiceLifetime.Scoped, snowflakeId.Lifetime); - // Assert.IsType(serviceProvider.GetRequiredService()); } [Fact] diff --git a/tests/SnowflakeIdServiceTest.cs b/tests/SnowflakeIdServiceTest.cs index 34b0ef7..446f00a 100644 --- a/tests/SnowflakeIdServiceTest.cs +++ b/tests/SnowflakeIdServiceTest.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using SnowflakeId.Core; using SnowflakeId.DependencyInjection; -using System.Collections.Generic; namespace SnowflakeId.Tests { @@ -65,5 +64,43 @@ public void Can_Find_Data_CenterId_From_Generated_UniqueId() } + [Fact] + public async Task Can_Genertate_UniqueId_Asynchrony_In_The_Same_Millisecond() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSnowflakeUniqueId(s => s.DataCenterId = 1); + + var serviceProvider = services.BuildServiceProvider(); + var snowflakeService = serviceProvider.GetRequiredService(); + + var cts = new CancellationTokenSource(); + var tasks = new List>(500_000); + for (int i = 0; i < 50_000; i++) + tasks.Add(Task.Run(() => snowflakeService.GenerateSnowflakeIdAsync(cts.Token))); + + var uniqueIds = await Task.WhenAll(tasks); + var listUniqeness = uniqueIds.Length != uniqueIds.Distinct().Count(); + Assert.False(listUniqeness); + } + + + [Fact] + public void DataCenterId_Is_Equal_To_One_When_Registering_SnowflakeId_WithoutOptions() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSnowflakeUniqueId(); + + var serviceProvider = services.BuildServiceProvider(); + var snowflakeService = serviceProvider.GetRequiredService(); + + var id = snowflakeService.GenerateSnowflakeId(); + var dataCenterId = snowflakeService.GetDataCenterIdBySnowflakeId(id); + Assert.IsType(dataCenterId); + Assert.Equal(1, dataCenterId); + } + + } }