├── .editorconfig ├── .gitignore ├── Identities.EntityFramework.IntegrationTests ├── DecimalIdMappingTests.cs ├── Identities.EntityFramework.IntegrationTests.csproj └── TestHelpers │ ├── StronglyTypedTestEntity.cs │ ├── TestDbContext.cs │ └── TestEntity.cs ├── Identities.EntityFramework ├── DecimalIdConvention.cs ├── DecimalIdConverter.cs ├── DecimalIdMappingExtensions.cs └── Identities.EntityFramework.csproj ├── Identities.Example ├── ExampleDbContext.cs ├── Identities.Example.csproj ├── Program.cs └── UserEntityWithAmbientContext.cs ├── Identities.Tests ├── DistributedIds │ ├── CustomDistributedId128GeneratorTests.cs │ ├── CustomDistributedIdGeneratorTests.cs │ ├── DistributedId128GeneratorTests.cs │ ├── DistributedId128Tests.cs │ ├── DistributedIdGeneratorScopeTests.cs │ ├── DistributedIdGeneratorTests.cs │ ├── DistributedIdTests.cs │ ├── IncrementalDistributedId128GeneratorTests.cs │ ├── IncrementalDistributedIdGeneratorTests.cs │ └── RandomSequence48Tests.cs ├── Encodings │ ├── AlphanumericIdEncoderTests.cs │ ├── Base62EncoderTests.cs │ ├── BinaryIdEncoderTests.cs │ ├── HexadecimalEncoderTests.cs │ ├── HexadecimalIdEncoderTests.cs │ └── IdEncodingExtensionTests.cs ├── Identities.Tests.csproj └── PublicIdentities │ ├── AesPublicIdentityConverterTests.cs │ └── PublicIdentityExtensionsTests.cs ├── Identities.sln ├── Identities ├── DistributedIds │ ├── CustomDistributedId128Generator.cs │ ├── CustomDistributedIdGenerator.cs │ ├── DistributedId.cs │ ├── DistributedId128.cs │ ├── DistributedId128Generator.cs │ ├── DistributedId128GeneratorScope.cs │ ├── DistributedIdGenerator.cs │ ├── DistributedIdGeneratorScope.cs │ ├── IDistributedId128Generator.cs │ ├── IDistributedIdGenerator.cs │ ├── IncrementalDistributedId128Generator.cs │ ├── IncrementalDistributedIdGenerator.cs │ ├── RandomSequence48.cs │ └── RandomSequence75.cs ├── Encodings │ ├── AlphanumericIdEncoder.cs │ ├── Base62Encoder.cs │ ├── BinaryIdEncoder.cs │ ├── DecimalStructure.cs │ ├── HexadecimalEncoder.cs │ ├── HexadecimalIdEncoder.cs │ └── IdEncodingExtensions.cs ├── Identities.csproj ├── InternalsVisibleTo.cs └── PublicIdentities │ ├── AesPublicIdentityConverter.cs │ ├── CustomPublicIdentityConverter.cs │ ├── IPublicIdentityConverter.cs │ ├── PublicIdentityConverterExtensions.cs │ └── PublicIdentityExtensions.cs ├── LICENSE ├── README.md ├── Test ├── CollisionTestResults.txt ├── DistributedIdGenerator.cs ├── Program.cs ├── RandomSequence6.cs └── Test.csproj ├── pipeline-publish-preview-identities-entityframework.yml ├── pipeline-publish-preview-identities.yml ├── pipeline-publish-stable-identities-entityframework.yml ├── pipeline-publish-stable-identities.yml └── pipeline-verify.yml /.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 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ 353 | 354 | 355 | 356 | 357 | *.swp 358 | *.*~ 359 | project.lock.json 360 | .DS_Store 361 | *.pyc 362 | nupkg/ 363 | 364 | # Visual Studio Code 365 | .vscode 366 | 367 | # Rider 368 | .idea 369 | 370 | # User-specific files 371 | *.suo 372 | *.user 373 | *.userosscache 374 | *.sln.docstates 375 | 376 | # Build results 377 | [Dd]ebug/ 378 | [Dd]ebugPublic/ 379 | [Rr]elease/ 380 | [Rr]eleases/ 381 | x64/ 382 | x86/ 383 | build/ 384 | bld/ 385 | [Bb]in/ 386 | [Oo]bj/ 387 | [Oo]ut/ 388 | msbuild.log 389 | msbuild.err 390 | msbuild.wrn 391 | 392 | # Visual Studio 2015 393 | .vs/ -------------------------------------------------------------------------------- /Identities.EntityFramework.IntegrationTests/DecimalIdMappingTests.cs: -------------------------------------------------------------------------------- 1 | using Architect.Identities.EntityFramework.IntegrationTests.TestHelpers; 2 | using Microsoft.EntityFrameworkCore; 3 | using Xunit; 4 | 5 | namespace Architect.Identities.EntityFramework.IntegrationTests 6 | { 7 | public class DecimalIdMappingTests 8 | { 9 | /// 10 | /// Not a requirement, but a baseline for our other tests. 11 | /// Can be changed or deleted if the situation changes. 12 | /// 13 | [Fact] 14 | public void UnconfiguredProperty_WithSqlite_ShouldReturnIncorrectPrecision() 15 | { 16 | using var dbContext = TestDbContext.Create((modelBuilder, dbContext) => 17 | { 18 | modelBuilder.Entity().Ignore(x => x.Id); 19 | modelBuilder.Entity().Property(x => x.Id); 20 | }); 21 | 22 | var entity = new TestEntity(number: 1234567890123456789012345678m); 23 | var loadedEntity = this.SaveAndReload(entity, dbContext); 24 | 25 | Assert.Equal(entity.DoesNotHaveIdSuffix, loadedEntity.DoesNotHaveIdSuffix); 26 | Assert.Equal(65536, GetSignAndScale(loadedEntity.DoesNotHaveIdSuffix)); 27 | } 28 | 29 | [Fact] 30 | public void StoreWithDecimalIdPrecision_WithInMemory_ShouldReturnExpectedPrecision() 31 | { 32 | using var dbContext = TestDbContext.Create(useInMemoryInsteadOfSqlite: true); 33 | 34 | var entity = new TestEntity(); 35 | var loadedEntity = this.SaveAndReload(entity, dbContext); 36 | 37 | Assert.Equal(entity.Id, loadedEntity.Id); 38 | Assert.Equal(0, GetSignAndScale(loadedEntity.Id)); 39 | //Assert.Equal("DECIMAL(28,0)", dbContext.Model.FindEntityType(typeof(TestEntity)).FindProperty(nameof(TestEntity.Id)).GetColumnType()); // Does not work with in-memory provider 40 | } 41 | 42 | [Fact] 43 | public void StoreWithDecimalIdPrecision_WithInMemoryAndDifferentColumnType_ShouldReturnExpectedPrecision() 44 | { 45 | using var dbContext = TestDbContext.Create(useInMemoryInsteadOfSqlite: true, onModelCreating: (modelBuilder, dbContext) => 46 | modelBuilder.Entity(entity => entity.Property(e => e.Id).HasColumnType("DECIMAL(29,1)"))); 47 | 48 | var entity = new TestEntity(); 49 | var loadedEntity = this.SaveAndReload(entity, dbContext); 50 | 51 | Assert.Equal(entity.Id, loadedEntity.Id); 52 | Assert.Equal(0, GetSignAndScale(loadedEntity.Id)); // Truncated 53 | //Assert.Equal("DECIMAL(29,1)", dbContext.Model.FindEntityType(typeof(TestEntity)).FindProperty(nameof(TestEntity.Id)).GetColumnType()); // Does not work with in-memory provider 54 | } 55 | 56 | [Fact] 57 | public void StoreDecimalIdsWithCorrectPrecision_WithSqliteAndPrimitiveId_ShouldReturnExpectedPrecision() 58 | { 59 | using var dbContext = TestDbContext.Create(); 60 | 61 | var entity = new TestEntity(); 62 | var loadedEntity = this.SaveAndReload(entity, dbContext); 63 | 64 | Assert.Equal(entity.Id, loadedEntity.Id); 65 | Assert.Equal(0, GetSignAndScale(loadedEntity.Id)); 66 | } 67 | 68 | [Fact] 69 | public void StoreDecimalIdsWithCorrectPrecision_WithSqliteAndCustomStructId_ShouldReturnExpectedPrecision() 70 | { 71 | using var dbContext = TestDbContext.Create(); 72 | 73 | var entity = new StronglyTypedTestEntity(); 74 | var loadedEntity = this.SaveAndReload(entity, dbContext); 75 | 76 | Assert.Equal(entity.Id, loadedEntity.Id); 77 | Assert.Equal(0, GetSignAndScale(loadedEntity.Id)); 78 | } 79 | 80 | [Theory] 81 | [InlineData(null)] 82 | [InlineData(1)] 83 | public void StoreDecimalIdsWithCorrectPrecision_WithSqliteAndPrimitiveId_ShouldAffectSecondaryIdProperties(int? nullableForeignIdValue) 84 | { 85 | using var dbContext = TestDbContext.Create(); 86 | 87 | var entity = new TestEntity() 88 | { 89 | ForeignID = nullableForeignIdValue, 90 | }; 91 | var loadedEntity = this.SaveAndReload(entity, dbContext); 92 | 93 | Assert.Equal(entity.ForeignId, loadedEntity.ForeignId); 94 | Assert.Equal(entity.ForeignID, loadedEntity.ForeignID); 95 | Assert.Equal(0, GetSignAndScale(loadedEntity.ForeignId)); 96 | if (nullableForeignIdValue is not null) 97 | Assert.Equal(0, GetSignAndScale(loadedEntity.ForeignID.Value)); 98 | } 99 | 100 | [Theory] 101 | [InlineData(null)] 102 | [InlineData(1)] 103 | public void StoreDecimalIdsWithCorrectPrecision_WithSqliteAndCustomStructId_ShouldAffectSecondaryIdProperties(int? nullableForeignIdValue) 104 | { 105 | using var dbContext = TestDbContext.Create(); 106 | 107 | var entity = new StronglyTypedTestEntity() 108 | { 109 | ForeignID = (TestEntityId?)nullableForeignIdValue, 110 | }; 111 | var loadedEntity = this.SaveAndReload(entity, dbContext); 112 | 113 | Assert.Equal(entity.ForeignId, loadedEntity.ForeignId); 114 | Assert.Equal(entity.ForeignID, loadedEntity.ForeignID); 115 | Assert.Equal(0, GetSignAndScale(loadedEntity.ForeignId)); 116 | if (nullableForeignIdValue is not null) 117 | Assert.Equal(0, GetSignAndScale(loadedEntity.ForeignID.Value)); 118 | } 119 | 120 | [Fact] 121 | public void StoreDecimalIdsWithCorrectPrecision_WithSqlite_ShouldNotAffectNonIdDecimals() 122 | { 123 | using var dbContext = TestDbContext.Create(); 124 | 125 | var entity = new TestEntity(); 126 | var loadedEntity = this.SaveAndReload(entity, dbContext); 127 | 128 | Assert.Equal(entity.Number, loadedEntity.Number); 129 | Assert.Equal(entity.DoesNotHaveIdSuffix, loadedEntity.DoesNotHaveIdSuffix); 130 | Assert.Equal(entity.DoesNotHaveIdSuffixEither, loadedEntity.DoesNotHaveIdSuffixEither); 131 | Assert.NotEqual(0, GetSignAndScale(loadedEntity.Number)); 132 | Assert.NotEqual(0, GetSignAndScale(loadedEntity.DoesNotHaveIdSuffix)); 133 | Assert.NotEqual(0, GetSignAndScale(loadedEntity.DoesNotHaveIdSuffixEither)); 134 | } 135 | 136 | private TestEntity SaveAndReload(TestEntity entity, TestDbContext dbContext) 137 | { 138 | dbContext.Entities.Add(entity); 139 | dbContext.SaveChanges(); 140 | 141 | dbContext.Entry(entity).State = EntityState.Detached; 142 | 143 | var loadedEntity = dbContext.Entities.Single(); 144 | return loadedEntity; 145 | } 146 | 147 | private StronglyTypedTestEntity SaveAndReload(StronglyTypedTestEntity entity, TestDbContext dbContext) 148 | { 149 | dbContext.StronglyTypedEntities.Add(entity); 150 | dbContext.SaveChanges(); 151 | 152 | dbContext.Entry(entity).State = EntityState.Detached; 153 | 154 | var loadedEntity = dbContext.StronglyTypedEntities.Single(); 155 | return loadedEntity; 156 | } 157 | 158 | private static int GetSignAndScale(decimal dec) => Decimal.GetBits(dec)[3]; // Decimal.GetBits() contains the sign-and-scale portion of a decimal 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Identities.EntityFramework.IntegrationTests/Identities.EntityFramework.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Architect.Identities.EntityFramework.IntegrationTests 6 | Architect.Identities.EntityFramework.IntegrationTests 7 | Enable 8 | False 9 | 10 | 11 | 12 | 13 | IDE0290 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | all 32 | runtime; build; native; contentfiles; analyzers; buildtransitive 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Identities.EntityFramework.IntegrationTests/TestHelpers/StronglyTypedTestEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Architect.Identities.EntityFramework.IntegrationTests.TestHelpers 2 | { 3 | public sealed class StronglyTypedTestEntity 4 | { 5 | public TestEntityId Id { get; } 6 | public string Name { get; } 7 | public decimal Number { get; } 8 | public decimal ForeignId { get; } 9 | public TestEntityId? ForeignID { get; init; } // Deliberate spelling 10 | public decimal DoesNotHaveIdSuffix { get; } 11 | 12 | public StronglyTypedTestEntity(string name = "TestName", decimal number = 0.1234567890123456789012345678m) 13 | { 14 | this.Id = DistributedId.CreateId(); 15 | 16 | this.Name = name; 17 | this.Number = number; 18 | this.ForeignId = 1234567890123456789012345678m; 19 | this.ForeignID = 1234567890123456789012345678m; 20 | this.DoesNotHaveIdSuffix = number; 21 | } 22 | 23 | /// 24 | /// EF constructor. 25 | /// 26 | private StronglyTypedTestEntity() 27 | { 28 | } 29 | } 30 | 31 | public readonly struct TestEntityId : IEquatable, IComparable 32 | { 33 | public decimal Value { get; } 34 | 35 | public TestEntityId(decimal value) 36 | { 37 | this.Value = value; 38 | } 39 | public override string ToString() 40 | { 41 | return this.Value.ToString(); 42 | } 43 | 44 | public override int GetHashCode() 45 | { 46 | return this.Value.GetHashCode(); 47 | } 48 | 49 | public override bool Equals(object other) 50 | { 51 | return other is TestEntityId otherId && this.Equals(otherId); 52 | } 53 | 54 | public bool Equals(TestEntityId other) 55 | { 56 | return this.Value == other.Value; 57 | } 58 | 59 | public int CompareTo(TestEntityId other) 60 | { 61 | return this.Value.CompareTo(other.Value); 62 | } 63 | 64 | public static bool operator ==(TestEntityId left, TestEntityId right) => left.Equals(right); 65 | public static bool operator !=(TestEntityId left, TestEntityId right) => !(left == right); 66 | public static bool operator >(TestEntityId left, TestEntityId right) => left.CompareTo(right) > 0; 67 | public static bool operator <(TestEntityId left, TestEntityId right) => left.CompareTo(right) < 0; 68 | public static bool operator >=(TestEntityId left, TestEntityId right) => left.CompareTo(right) >= 0; 69 | public static bool operator <=(TestEntityId left, TestEntityId right) => left.CompareTo(right) <= 0; 70 | 71 | public static implicit operator TestEntityId(decimal value) => new TestEntityId(value); 72 | public static implicit operator decimal(TestEntityId id) => id.Value; 73 | 74 | public static implicit operator TestEntityId?(decimal? value) => value is null ? null : new TestEntityId(value.Value); 75 | public static implicit operator decimal?(TestEntityId? id) => id?.Value; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Identities.EntityFramework.IntegrationTests/TestHelpers/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Reflection.Emit; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Architect.Identities.EntityFramework.IntegrationTests.TestHelpers 6 | { 7 | public class TestDbContext : DbContext // Must be public, unsealed for Create method's reflection 8 | { 9 | private static ModuleBuilder ModuleBuilder { get; } = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("UniqueTestDbContextAssembly"), AssemblyBuilderAccess.Run) 10 | .DefineDynamicModule("UniqueTestDbContextModule"); 11 | 12 | public DbSet Entities { get; protected set; } 13 | public DbSet StronglyTypedEntities { get; protected set; } 14 | public Action OnModelCreatingAction { get; } 15 | 16 | public static TestDbContext Create(Action onModelCreating = null, bool useInMemoryInsteadOfSqlite = false) 17 | { 18 | // Must construct a runtime subtype, because otherwise EF caches the result of OnModelCreating 19 | 20 | var typeBuilder = ModuleBuilder.DefineType($"{nameof(TestDbContext)}_{Guid.NewGuid()}", 21 | TypeAttributes.Sealed | TypeAttributes.Class | TypeAttributes.Public, typeof(TestDbContext)); 22 | 23 | var baseConstructor = typeof(TestDbContext).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, binder: null, 24 | [typeof(Action), typeof(bool)], modifiers: null); 25 | var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, [typeof(Action), typeof(bool)]); 26 | var ilGenerator = ctorBuilder.GetILGenerator(); 27 | ilGenerator.Emit(OpCodes.Ldarg_0); 28 | ilGenerator.Emit(OpCodes.Ldarg_1); 29 | ilGenerator.Emit(OpCodes.Ldarg_2); 30 | ilGenerator.Emit(OpCodes.Call, baseConstructor); 31 | ilGenerator.Emit(OpCodes.Ret); 32 | 33 | var type = typeBuilder.CreateType(); 34 | 35 | try 36 | { 37 | var instance = (TestDbContext)Activator.CreateInstance(type, [onModelCreating, useInMemoryInsteadOfSqlite,]); 38 | return instance; 39 | } 40 | catch (TargetInvocationException e) 41 | { 42 | throw e.InnerException; 43 | } 44 | } 45 | 46 | protected TestDbContext(Action onModelCreating = null, bool useInMemoryInsteadOfSqlite = false) 47 | : base(useInMemoryInsteadOfSqlite 48 | ? new DbContextOptionsBuilder().UseInMemoryDatabase(Guid.NewGuid().ToString("N")).Options 49 | : new DbContextOptionsBuilder().UseSqlite("Filename=:memory:").Options) 50 | { 51 | this.OnModelCreatingAction = onModelCreating ?? ((modelBuilder, dbContext) => { }); 52 | 53 | if (!useInMemoryInsteadOfSqlite) 54 | this.Database.OpenConnection(); 55 | 56 | this.Database.EnsureCreated(); 57 | } 58 | 59 | protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) 60 | { 61 | base.ConfigureConventions(configurationBuilder); 62 | 63 | configurationBuilder.ConfigureDecimalIdTypes(modelAssemblies: this.GetType().Assembly); 64 | } 65 | 66 | protected override void OnModelCreating(ModelBuilder modelBuilder) 67 | { 68 | base.OnModelCreating(modelBuilder); 69 | 70 | modelBuilder.Entity(entity => 71 | { 72 | entity.Property(e => e.Id); 73 | 74 | entity.Property(e => e.Name); 75 | 76 | entity.Property(e => e.Number); 77 | 78 | entity.Property(e => e.ForeignId) 79 | .HasColumnName("ForeignId1"); 80 | 81 | entity.Property(e => e.ForeignID) 82 | .HasColumnName("ForeignId2"); 83 | 84 | entity.Property(e => e.DoesNotHaveIdSuffix); 85 | 86 | entity.Property(e => e.DoesNotHaveIdSuffixEither) 87 | .HasConversion(codeValue => (decimal)codeValue, dbValue => (TestEntityId)dbValue); 88 | 89 | entity.HasKey(e => e.Id); 90 | }); 91 | 92 | modelBuilder.Entity(entity => 93 | { 94 | entity.Property(e => e.Id); 95 | 96 | entity.Property(e => e.Name); 97 | 98 | entity.Property(e => e.Number); 99 | 100 | entity.Property(e => e.ForeignId) 101 | .HasColumnName("ForeignId1"); 102 | 103 | entity.Property(e => e.ForeignID) 104 | .HasColumnName("ForeignId2"); 105 | 106 | entity.Property(e => e.DoesNotHaveIdSuffix); 107 | 108 | entity.HasKey(e => e.Id); 109 | }); 110 | 111 | this.OnModelCreatingAction(modelBuilder, this); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Identities.EntityFramework.IntegrationTests/TestHelpers/TestEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Architect.Identities.EntityFramework.IntegrationTests.TestHelpers 2 | { 3 | public sealed class TestEntity 4 | { 5 | public decimal Id { get; } 6 | public string Name { get; } 7 | public decimal Number { get; } 8 | public decimal ForeignId { get; } 9 | public decimal? ForeignID { get; init; } // Deliberate spelling 10 | public decimal DoesNotHaveIdSuffix { get; } 11 | public TestEntityId DoesNotHaveIdSuffixEither { get; } 12 | 13 | public TestEntity(string name = "TestName", decimal number = 0.1234567890123456789012345678m) 14 | { 15 | this.Id = DistributedId.CreateId(); 16 | 17 | this.Name = name; 18 | this.Number = number; 19 | this.ForeignId = 1234567890123456789012345678m; 20 | this.ForeignID = 1234567890123456789012345678m; 21 | this.DoesNotHaveIdSuffix = number; 22 | this.DoesNotHaveIdSuffixEither = number; 23 | } 24 | 25 | /// 26 | /// EF constructor. 27 | /// 28 | private TestEntity() 29 | { 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Identities.EntityFramework/DecimalIdConvention.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | using Microsoft.EntityFrameworkCore.Metadata.Conventions; 5 | 6 | namespace Architect.Identities.EntityFramework 7 | { 8 | /// 9 | /// 10 | /// An that maps decimal-like ID properties by casting them to/from decimal. 11 | /// 12 | /// 13 | /// For a property to match, its name must be *Id or *ID. 14 | /// Additionally, it must either be of type decimal, or be implicitly convertible to decimal and (implicitly or explicitly) convertible from decimal. 15 | /// 16 | /// 17 | /// Beware that property mappings alone do not cover scenarios such as where Entity Framework writes calls to CAST(). 18 | /// can fix such scenarios. 19 | /// 20 | /// 21 | public sealed class DecimalIdConvention : IPropertyAddedConvention 22 | { 23 | public void ProcessPropertyAdded(IConventionPropertyBuilder propertyBuilder, IConventionContext context) 24 | { 25 | // ID properties only 26 | if (!propertyBuilder.Metadata.Name.EndsWith("Id") && !propertyBuilder.Metadata.Name.EndsWith("ID")) 27 | return; 28 | 29 | var type = propertyBuilder.Metadata.ClrType; 30 | 31 | // Decimal-like types only 32 | if (!DecimalIdMappingExtensions.IsDecimalConvertible(type)) 33 | return; 34 | 35 | propertyBuilder.HasConverter(typeof(DecimalIdConverter<>).MakeGenericType(type), fromDataAnnotation: true); 36 | propertyBuilder.HasPrecision(28); 37 | propertyBuilder.HasScale(0); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Identities.EntityFramework/DecimalIdConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | 6 | namespace Architect.Identities.EntityFramework 7 | { 8 | /// 9 | /// Mostly similar to with the provider type set to . 10 | /// Additionally, this type also truncates the provider value before casting it to a model value. 11 | /// This solves an issue where the SQLite provider introduces an undesirable decimal place. 12 | /// 13 | internal sealed class DecimalIdConverter : ValueConverter 14 | { 15 | public DecimalIdConverter() 16 | : base( 17 | convertToProviderExpression: CreateConversionExpression(), 18 | convertFromProviderExpression: CreateConversionExpression()) 19 | { 20 | } 21 | 22 | private static Expression> CreateConversionExpression() 23 | { 24 | var param = Expression.Parameter(typeof(TIn), "value"); 25 | 26 | // Truncate decimals before converting them 27 | Expression value = (typeof(TIn) == typeof(decimal)) 28 | ? Expression.Call(typeof(DecimalIdConverter).GetMethod(nameof(TruncateIfLossless), BindingFlags.Static | BindingFlags.NonPublic)!, param) 29 | : param; 30 | 31 | var result = Expression.Lambda>( 32 | Expression.Convert(value, typeof(TOut)), 33 | param); 34 | 35 | return result; 36 | } 37 | 38 | /// 39 | /// 40 | /// Returns the truncated input value if its value is equal, or the input value otherwise. 41 | /// 42 | /// 43 | /// This method can be used to remove needless decimal places, such as from 123.0. 44 | /// SQLite typically causes such values. 45 | /// 46 | /// 47 | private static decimal TruncateIfLossless(decimal value) 48 | { 49 | var result = Decimal.Truncate(value); 50 | return result == value 51 | ? result 52 | : value; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Identities.EntityFramework/DecimalIdMappingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Architect.Identities.EntityFramework 7 | { 8 | /// 9 | /// Provides extensions for mapping decimal ID properties using EntityFramework. 10 | /// 11 | public static class DecimalIdMappingExtensions 12 | { 13 | private static readonly Type[] ParameterListWithSingleDecimal = new[] { typeof(decimal) }; 14 | 15 | /// 16 | /// 17 | /// Configures the mapping of decimal ID types to DECIMAL(28,0). 18 | /// 19 | /// 20 | /// Primarily, this method registers a , which configures properties named *Id or *ID, provided that they are of type decimal or have appropriate conversions to and from decimal. 21 | /// 22 | /// 23 | /// Additionaly, for any given assemblies, this method configures the default type mapping for any of their types named *Id or *ID that are convertible to and from decimal. 24 | /// The default type mapping is used when types occur in queries outside of properties, such as when Entity Framework writes calls to CAST(). 25 | /// 26 | /// 27 | /// Any assemblies containing types mapped to tables. For example, if domain objects are mapped directly, the domain layer's assembly should be passed here. 28 | public static ModelConfigurationBuilder ConfigureDecimalIdTypes(this ModelConfigurationBuilder modelConfigurationBuilder, params Assembly[] modelAssemblies) 29 | { 30 | // Configure decimal-like ID properties 31 | modelConfigurationBuilder.Conventions.Add(_ => new DecimalIdConvention()); 32 | 33 | // Configure decimal-like types outside of properties (e.g. in CAST(), SUM(), AVG(), etc.) 34 | foreach (var decimalIdType in modelAssemblies.SelectMany(assembly => assembly.GetTypes().Where(type => 35 | type.Name.EndsWith("Id") && 36 | IsDecimalConvertible(type)))) 37 | { 38 | modelConfigurationBuilder.DefaultTypeMapping(decimalIdType) 39 | .HasConversion(typeof(DecimalIdConverter<>).MakeGenericType(decimalIdType)) 40 | .HasPrecision(28, 0); 41 | } 42 | 43 | return modelConfigurationBuilder; 44 | } 45 | 46 | /// 47 | /// Determines whether the given type is a type (or nullable wrapper thereof) that is or is convertible to and from it. 48 | /// 49 | internal static bool IsDecimalConvertible(Type type) 50 | { 51 | // Dig through nullable 52 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) 53 | type = type.GenericTypeArguments[0]; 54 | 55 | // Short-circuit for decimals 56 | if (type == typeof(decimal)) 57 | return true; 58 | 59 | // Must have explicit OR implicit conversion from decimal 60 | if (type.GetMethod("op_Explicit", genericParameterCount: 0, ParameterListWithSingleDecimal) is null && // Compiler enforces that return type is the type itself 61 | type.GetMethod("op_Implicit", genericParameterCount: 0, ParameterListWithSingleDecimal) is null) // Compiler enforces that return type is the type itself 62 | return false; 63 | 64 | // Must have implicit conversion to decimal 65 | if (!type.GetMethods(BindingFlags.Static | BindingFlags.Public) 66 | .Any(method => method.Name == "op_Implicit" && method.ReturnType == typeof(decimal))) // Compiler enforces single parameter of the type itself 67 | return false; 68 | 69 | return true; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Identities.EntityFramework/Identities.EntityFramework.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Architect.Identities.EntityFramework 6 | Architect.Identities.EntityFramework 7 | Enable 8 | 11 9 | True 10 | False 11 | 12 | 13 | 14 | Architect.Identities.EntityFramework.IntegrationTests 15 | 16 | 17 | 18 | 2.1.1 19 | 20 | 21 | Entity Framework extensions for the Architect.Identities package. 22 | 23 | https://github.com/TheArchitectDev/Architect.Identities 24 | 25 | Release notes: 26 | 27 | 2.1.1: 28 | - Enhancement: Upgraded package versions. 29 | 30 | 2.1.0: 31 | - The ConfigureDecimalIdTypes() extension method now truncates needless trailing zeros received from the database provider (`123.0` => `123`), as is the case with SQLite. 32 | 33 | 2.0.0: 34 | - BREAKING: Now targeting .NET 6+, to support new EF Core APIs. 35 | - BREAKING: Now using EF Core 7.0.0. 36 | - BREAKING: No longer referencing the Identities package. (The current package is still considered an addition to it, but the hard link is gone.) 37 | - BREAKING: Removed ApplicationInstanceIds. (See Identities 2.0.0.) 38 | - BREAKING: Decimal IDs in SQLite may get reconstituted with a one (inadvertent) decimal place, e.g. "1234567890123456789012345678" => "1234567890123456789012345678.0". Detecting and fixing for SQLite is a hassle. 39 | - BREAKING: DecimalIdMappingExtensions's methods have been replaced by modelConfigurationBuilder.ConfigureDecimalIdTypes(). This should be called from DbContext.ConfigureConventions(). 40 | - The new extension method handles decimal-convertible types (i.e. value objects) in addition to plain decimals. 41 | - The new extension method also sets DefaultTypeMapping, to achieve appropriate behavior when EF writes things like CAST(). Property mappings alone do not cover such scenarios. 42 | 43 | 1.0.2: 44 | - Fixed an incompatibility with EF Core 6.0.0+ (caused by a breaking change in EF itself). 45 | - Now using AmbientContexts 1.1.1, which fixes extremely rare bugs and improves performance. 46 | 47 | 1.0.1: 48 | - Now using AmbientContexts 1.1.0, for a performance improvement. 49 | 50 | The Architect 51 | The Architect 52 | TheArchitectDev, Timovzl 53 | https://github.com/TheArchitectDev/Architect.Identities 54 | Git 55 | LICENSE 56 | Entity, Framework, EntityFramework, EF, Core, EfCore, ID, IDs, identity, identities, DistributedId, distributed, locally, unique, locally-unique, generator, generation, IdGenerator, UUID, GUID, auto-increment, primary, key, entity, entities, PublicIdentities 57 | 58 | 59 | 60 | 61 | True 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | all 70 | runtime; build; native; contentfiles; analyzers; buildtransitive 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /Identities.Example/ExampleDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Architect.Identities.Example 4 | { 5 | internal sealed class ExampleDbContext : DbContext 6 | { 7 | public ExampleDbContext(DbContextOptions options) 8 | : base(options) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Identities.Example/Identities.Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | Architect.Identities.Example 7 | Architect.Identities.Example 8 | Enable 9 | False 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Identities.Example/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Architect.Identities.Example 2 | { 3 | /// 4 | /// Demonstrates some uses of the Identities package: DistributedIds, and the Flexible, Locally-Unique ID (Fluid) generator. 5 | /// 6 | internal static class Program 7 | { 8 | private static void Main() 9 | { 10 | // Demo some code (without needing any registrations) 11 | Console.WriteLine("Demonstrating DistributedId as a drop-in replacement for GUID:"); 12 | CreateDistributedIds(); 13 | 14 | // Demo some code that uses the registrations 15 | Console.WriteLine("Demonstrating entities that use DistributedIds:"); 16 | CreateUsersWithAmbientContext(); 17 | 18 | // Demo Inversion of Control (IoC) 19 | using (new DistributedIdGeneratorScope(new IncrementalDistributedIdGenerator())) 20 | { 21 | Console.WriteLine("Registered an incremental ID generator for test purposes:"); 22 | CreateUsersWithAmbientContext(); 23 | 24 | using (new DistributedIdGeneratorScope(new CustomDistributedIdGenerator(id: 0m))) 25 | { 26 | Console.WriteLine("Registered a fixed ID generator for test purposes:"); 27 | CreateUsersWithAmbientContext(); 28 | } 29 | } 30 | 31 | Console.WriteLine("Once the generators have gone out of scope, the default behavior is restored:"); 32 | CreateUsersWithAmbientContext(); 33 | 34 | Console.ReadKey(intercept: true); 35 | } 36 | 37 | private static void CreateDistributedIds() 38 | { 39 | // Like GUIDs, these IDs can be generated from anywhere, without any registrations whatsoever 40 | var id1 = DistributedId.CreateId(); 41 | var id2 = DistributedId.CreateId(); 42 | var id1Alphanumeric = id1.ToAlphanumeric(); // IdEncoder can decode 43 | var id2Alphanumeric = id2.ToAlphanumeric(); // IdEncoder can decode 44 | Console.WriteLine($"Here is a DistributedId generated much like a GUID: {id1} (alphanumeric form: {id1Alphanumeric})"); 45 | Console.WriteLine($"Here is a DistributedId generated much like a GUID: {id2} (alphanumeric form: {id2Alphanumeric})"); 46 | Console.WriteLine(); 47 | } 48 | 49 | private static void CreateUsersWithAmbientContext() 50 | { 51 | // These users access the IIdGenerator "out of thin air", through the ambient context pattern 52 | var user1 = new UserEntityWithAmbientContext("JohnDoe", "John Doe"); 53 | var user2 = new UserEntityWithAmbientContext("JaneDoe", "Jane Doe"); 54 | Console.WriteLine(user1); 55 | Console.WriteLine(user2); 56 | Console.WriteLine(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Identities.Example/UserEntityWithAmbientContext.cs: -------------------------------------------------------------------------------- 1 | namespace Architect.Identities.Example 2 | { 3 | /// 4 | /// 5 | /// This implementation of a user entity determines its own ID, by depending on an ID generator "plucked out of thin air". 6 | /// 7 | /// 8 | /// The entity remains easy-to-create, as other types without access to services/dependencies remain able to create it. 9 | /// Also, the entity remains responsible for choosing its ID generation strategy. 10 | /// 11 | /// 12 | /// Inversion of Control (IoC) remains possible thanks to the Ambient Context pattern. 13 | /// A test method that wants to see ID "0000000000000000000000000001" can simply run the code within a block of "using (new DistributedIdGeneratorScope(new IncrementalDistributedIdGenerator()))". 14 | /// 15 | /// 16 | public sealed class UserEntityWithAmbientContext 17 | { 18 | public override string ToString() => $"{{User easily created with 'new': UserName={{{this.UserName}}} FullName={{{this.FullName}}} Id={this.Id}}}"; 19 | 20 | /// 21 | /// Guarantueed to be unique within the Bounded Context, regardless which replica of which application on which server created it. 22 | /// 23 | public decimal Id { get; } 24 | 25 | public string UserName { get; } 26 | public string FullName { get; private set; } 27 | 28 | /// 29 | /// Constructs a new instance representing the given data. 30 | /// 31 | public UserEntityWithAmbientContext(string userName, string fullName) 32 | { 33 | // DistributedId.CreateId() uses the ambient DistributedIdGeneratorScope.CurrentGenerator to generate a new ID 34 | this.Id = DistributedId.CreateId(); 35 | 36 | this.UserName = userName ?? throw new ArgumentNullException(nameof(userName)); 37 | this.FullName = fullName ?? throw new ArgumentNullException(nameof(fullName)); 38 | } 39 | 40 | /// 41 | /// The domain operation of changing the user's full name. 42 | /// 43 | public void ChangeFullName(string fullName) 44 | { 45 | this.FullName = fullName ?? throw new ArgumentNullException(nameof(fullName)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/CustomDistributedId128GeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Architect.Identities.Tests.DistributedIds 4 | { 5 | public sealed class CustomDistributedId128GeneratorTests 6 | { 7 | [Fact] 8 | public void CreateId_ConstructedWithFixedUInt128_ShouldReturnExpectedResult() 9 | { 10 | var generator = new CustomDistributedId128Generator(5); 11 | 12 | var idOne = generator.CreateId(); 13 | var idTwo = generator.CreateId(); 14 | 15 | Assert.Equal((UInt128)5, idOne); 16 | Assert.Equal((UInt128)5, idTwo); 17 | } 18 | 19 | [Fact] 20 | public void CreateId_ConstructedWithUInt128Func_ShouldReturnExpectedResult() 21 | { 22 | var result = default(UInt128); 23 | var generator = new CustomDistributedId128Generator(() => ++result); 24 | 25 | var idOne = generator.CreateId(); 26 | var idTwo = generator.CreateId(); 27 | 28 | Assert.Equal((UInt128)1, idOne); 29 | Assert.Equal((UInt128)2, idTwo); 30 | } 31 | 32 | [Fact] 33 | public void CreateGuid_ConstructedWithFixedGuid_ShouldReturnExpectedResult() 34 | { 35 | var guid = Guid.NewGuid(); 36 | var generator = new CustomDistributedId128Generator(guid); 37 | 38 | var idOne = generator.CreateGuid(); 39 | var idTwo = generator.CreateGuid(); 40 | 41 | Assert.Equal(guid, idOne); 42 | Assert.Equal(guid, idTwo); 43 | 44 | generator = new CustomDistributedId128Generator(guid); 45 | 46 | Assert.Equal(idOne, generator.CreateId().ToGuid()); 47 | Assert.Equal(idTwo, generator.CreateId().ToGuid()); 48 | } 49 | 50 | [Fact] 51 | public void CreateId_ConstructedWithGuidFunc_ShouldReturnExpectedResult() 52 | { 53 | var guid1 = Guid.NewGuid(); 54 | var guid2 = Guid.NewGuid(); 55 | var invocationCount = 0; 56 | var generator = new CustomDistributedId128Generator(() => invocationCount++ == 0 ? guid1 : guid2); 57 | 58 | var idOne = generator.CreateGuid(); 59 | var idTwo = generator.CreateGuid(); 60 | 61 | Assert.Equal(guid1, idOne); 62 | Assert.Equal(guid2, idTwo); 63 | 64 | invocationCount = 0; 65 | 66 | Assert.Equal(idOne, generator.CreateId().ToGuid()); 67 | Assert.Equal(idTwo, generator.CreateId().ToGuid()); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/CustomDistributedIdGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Architect.Identities.Tests.DistributedIds 4 | { 5 | public sealed class CustomDistributedIdGeneratorTests 6 | { 7 | [Fact] 8 | public void CreateId_ConstructedWithFixedDecimal_ShouldReturnExpectedResult() 9 | { 10 | var generator = new CustomDistributedIdGenerator(5m); 11 | 12 | var idOne = generator.CreateId(); 13 | var idTwo = generator.CreateId(); 14 | 15 | Assert.Equal(5m, idOne); 16 | Assert.Equal(5m, idTwo); 17 | } 18 | 19 | [Fact] 20 | public void CreateId_ConstructedWithFunc_ShouldReturnExpectedResult() 21 | { 22 | var result = 0m; 23 | var generator = new CustomDistributedIdGenerator(() => ++result); 24 | 25 | var idOne = generator.CreateId(); 26 | var idTwo = generator.CreateId(); 27 | 28 | Assert.Equal(1m, idOne); 29 | Assert.Equal(2m, idTwo); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/DistributedId128Tests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Architect.Identities.Tests.DistributedIds 4 | { 5 | public sealed class DistributedId128Tests 6 | { 7 | [Theory] 8 | [InlineData("9223372036854775808")] // 2^63 9 | [InlineData("18446744073709551615")] // UInt64.MaxValue 10 | [InlineData("170141183460469231731687303715884105728")] // 2^127 11 | [InlineData("340282366920938463463374607431768211455")] // UInt128.MaxValue 12 | public void Split_WithTopBitOfAnyHalfSet_ShouldThrow(string uint128IdString) 13 | { 14 | var id = UInt128.Parse(uint128IdString); 15 | Assert.Throws(() => DistributedId128.Split(id)); 16 | } 17 | 18 | [Theory] 19 | [InlineData("0", 0L, 0L)] 20 | [InlineData("1", 0L, 1L)] 21 | [InlineData("2147483647", 0L, Int32.MaxValue)] // Int32.MaxValue 22 | [InlineData("18446744073709551616", 1L, 0L)] // UInt64.MaxValue + 1 23 | [InlineData("9223372036854775807", 0L, Int64.MaxValue)] // 2^63 - 1 24 | [InlineData("170141183460469231713240559642174554112", Int64.MaxValue, 0L)] // (2^63 - 1) << 64 25 | [InlineData("170141183460469231722463931679029329919", Int64.MaxValue, Int64.MaxValue)] // (2^63 - 1) << 64 + (2^63 - 1) 26 | public void Split_WithTopBitsOfHalvesUnset_ShouldReturnExpectedResult(string uint128IdString, long expectedUpper, long expectedLower) 27 | { 28 | var id = UInt128.Parse(uint128IdString); 29 | 30 | var (upper, lower) = DistributedId128.Split(id); 31 | 32 | Assert.Equal(expectedUpper, upper); 33 | Assert.Equal(expectedLower, lower); 34 | } 35 | 36 | [Theory] 37 | [InlineData(-1L, 0L)] 38 | [InlineData(0L, -1L)] 39 | [InlineData(Int32.MinValue, 0L)] 40 | [InlineData(0L, Int32.MinValue)] 41 | [InlineData(-1L, -1L)] 42 | [InlineData(Int32.MinValue, Int32.MinValue)] 43 | public void Join_WithNegativeValue_ShouldThrow(long upper, long lower) 44 | { 45 | Assert.Throws(() => DistributedId128.Join(upper, lower)); 46 | } 47 | 48 | [Theory] 49 | [InlineData("0", 0L, 0L)] 50 | [InlineData("1", 0L, 1L)] 51 | [InlineData("2147483647", 0L, Int32.MaxValue)] // Int32.MaxValue 52 | [InlineData("18446744073709551616", 1L, 0L)] // UInt64.MaxValue + 1 53 | [InlineData("9223372036854775807", 0L, Int64.MaxValue)] // 2^63 - 1 54 | [InlineData("170141183460469231713240559642174554112", Int64.MaxValue, 0L)] // (2^63 - 1) << 64 55 | [InlineData("170141183460469231722463931679029329919", Int64.MaxValue, Int64.MaxValue)] // (2^63 - 1) << 64 + (2^63 - 1) 56 | public void Join_AfterSplit_ShouldReverse(string expectedUint128IdString, long upper, long lower) 57 | { 58 | var expectedResult = UInt128.Parse(expectedUint128IdString); 59 | 60 | var result = DistributedId128.Join(upper, lower); 61 | 62 | Assert.Equal(expectedResult, result); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/DistributedIdGeneratorScopeTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Architect.Identities.Tests.DistributedIds 4 | { 5 | public sealed class DistributedIdGeneratorScopeTests 6 | { 7 | [Fact] 8 | public void CurrentGenerator_Regularly_ShouldReturnDefaultGenerator() 9 | { 10 | var result = DistributedIdGeneratorScope.CurrentGenerator; 11 | 12 | Assert.IsType(result); 13 | } 14 | 15 | [Fact] 16 | public void CurrentGenerator_WithAmbientGenerator_ShouldReturnThatGenerator() 17 | { 18 | var generator = new NineIdGenerator(); 19 | 20 | IDistributedIdGenerator result; 21 | 22 | using (new DistributedIdGeneratorScope(generator)) 23 | result = DistributedIdGeneratorScope.CurrentGenerator; 24 | 25 | Assert.Equal(generator, result); 26 | } 27 | 28 | [Fact] 29 | public void CurrentGenerator_WithNestedAmbientGenerators_ShouldReturnInnermostGenerator() 30 | { 31 | var outerGenerator = new NineIdGenerator(); 32 | var innerGenerator = new EightIdGenerator(); 33 | 34 | IDistributedIdGenerator result; 35 | 36 | using (new DistributedIdGeneratorScope(outerGenerator)) 37 | using (new DistributedIdGeneratorScope(innerGenerator)) 38 | result = DistributedIdGeneratorScope.CurrentGenerator; 39 | 40 | Assert.Equal(innerGenerator, result); 41 | } 42 | 43 | [Fact] 44 | public void CurrentGenerator_WithAmbientGeneratorAfterDisposingInnerOne_ShouldReturnExpectedGenerator() 45 | { 46 | var outerGenerator = new NineIdGenerator(); 47 | var innerGenerator = new EightIdGenerator(); 48 | 49 | IDistributedIdGenerator result; 50 | 51 | using (new DistributedIdGeneratorScope(outerGenerator)) 52 | { 53 | using (new DistributedIdGeneratorScope(innerGenerator)) 54 | { 55 | } 56 | result = DistributedIdGeneratorScope.CurrentGenerator; 57 | } 58 | 59 | Assert.Equal(outerGenerator, result); 60 | } 61 | 62 | [Fact] 63 | public void CurrentGenerator_AfterDisposingNestedAmbientGenerators_ShouldReturnDefaultGenerator() 64 | { 65 | using (new DistributedIdGeneratorScope(new NineIdGenerator())) 66 | using (new DistributedIdGeneratorScope(new EightIdGenerator())) 67 | { 68 | } 69 | 70 | var result = DistributedIdGeneratorScope.CurrentGenerator; 71 | 72 | Assert.IsType(result); 73 | } 74 | 75 | private sealed class EightIdGenerator : IDistributedIdGenerator 76 | { 77 | /// 78 | /// Generates value 8. 79 | /// 80 | public decimal CreateId() => 8m; 81 | } 82 | 83 | private sealed class NineIdGenerator : IDistributedIdGenerator 84 | { 85 | /// 86 | /// Generates value 9. 87 | /// 88 | public decimal CreateId() => 9m; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/DistributedIdTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Architect.Identities.Tests.DistributedIds 4 | { 5 | /// 6 | /// The static subject under test merely acts as a wrapper, rather than implementing the functionality by itself. 7 | /// This test class merely confirms that its methods succeed, covering them. All other assertions are done in tests on the implementing types. 8 | /// 9 | public sealed class DistributedIdTests 10 | { 11 | [Fact] 12 | public void CreateId_WithNoCustomScope_ShouldSucceed() 13 | { 14 | _ = DistributedId.CreateId(); 15 | } 16 | 17 | [Fact] 18 | public void CreateId_WithScope_ShouldUseGeneratorFromScope() 19 | { 20 | decimal id; 21 | 22 | using (new DistributedIdGeneratorScope(new TenIdGenerator())) 23 | id = DistributedId.CreateId(); 24 | 25 | Assert.Equal(10m, id); 26 | } 27 | 28 | private sealed class TenIdGenerator : IDistributedIdGenerator 29 | { 30 | /// 31 | /// Generates value 10. 32 | /// 33 | public decimal CreateId() => 10m; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/IncrementalDistributedId128GeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Xunit; 3 | 4 | namespace Architect.Identities.Tests.DistributedIds 5 | { 6 | public sealed class IncrementalDistributedId128GeneratorTests 7 | { 8 | private IncrementalDistributedId128Generator Generator { get; } = new IncrementalDistributedId128Generator(); 9 | 10 | [Fact] 11 | public void CreateId_Initially_ShouldReturnExpectedResult() 12 | { 13 | var id = this.Generator.CreateId(); 14 | 15 | Assert.Equal((UInt128)1, id); 16 | } 17 | 18 | [Fact] 19 | public void CreateGuid_Initially_ShouldReturnExpectedResult() 20 | { 21 | var id = this.Generator.CreateGuid(); 22 | 23 | Assert.Equal(((UInt128)1).ToGuid(), id); 24 | } 25 | 26 | [Fact] 27 | public void CreateId_Repeatedly_ShouldReturnExpectedResult() 28 | { 29 | var idOne = this.Generator.CreateId(); 30 | var idTwo = this.Generator.CreateId(); 31 | var idThree = this.Generator.CreateId(); 32 | 33 | Assert.Equal((UInt128)1, idOne); 34 | Assert.Equal((UInt128)2, idTwo); 35 | Assert.Equal((UInt128)3, idThree); 36 | } 37 | 38 | /// 39 | /// The results should be the expected ones, confirming that the type is thread-safe, although the order can be anything. 40 | /// 41 | [Fact] 42 | public void CreateId_InParallel_ShouldReturnExpectedResult() 43 | { 44 | var expectedResults = Enumerable.Range(1, 10).Select(i => (UInt128)i); 45 | var results = new ConcurrentQueue(); 46 | 47 | Parallel.For(0, 10, _ => results.Enqueue(this.Generator.CreateId())); 48 | 49 | Assert.Equal(expectedResults, results.OrderBy(id => id)); 50 | } 51 | 52 | /// 53 | /// The generators should be independent and each keep their own count. 54 | /// 55 | [Fact] 56 | public void CreateId_InParallelWithDifferentGenerators_ShouldReturnExpectedResult() 57 | { 58 | var results = new ConcurrentQueue(); 59 | 60 | Parallel.For(0, 10, _ => results.Enqueue(new IncrementalDistributedId128Generator().CreateId())); 61 | 62 | Assert.All(results, result => Assert.Equal((UInt128)1, result)); 63 | } 64 | 65 | [Fact] 66 | public void CreateId_RepeatedlyWithCustomUInt128StartingValue_ShouldReturnExpectedResult() 67 | { 68 | var generator = new IncrementalDistributedId128Generator(firstId: 0UL); 69 | 70 | var result1 = generator.CreateId(); 71 | var result2 = generator.CreateId(); 72 | 73 | Assert.Equal((UInt128)0, result1); 74 | Assert.Equal((UInt128)1, result2); 75 | 76 | generator = new IncrementalDistributedId128Generator(firstId: 0UL); 77 | 78 | var resultA = generator.CreateGuid(); 79 | var resultB = generator.CreateGuid(); 80 | 81 | Assert.Equal(result1, resultA.ToUInt128()); 82 | Assert.Equal(result2, resultB.ToUInt128()); 83 | 84 | generator = new IncrementalDistributedId128Generator(firstId: UInt64.MaxValue); 85 | 86 | result1 = generator.CreateId(); 87 | result2 = generator.CreateId(); 88 | 89 | Assert.Equal((UInt128)UInt64.MaxValue, result1); 90 | Assert.Equal((UInt128)UInt64.MaxValue + 1, result2); 91 | 92 | generator = new IncrementalDistributedId128Generator(firstId: UInt64.MaxValue); 93 | 94 | resultA = generator.CreateGuid(); 95 | resultB = generator.CreateGuid(); 96 | 97 | Assert.Equal(result1, resultA.ToUInt128()); 98 | Assert.Equal(result2, resultB.ToUInt128()); 99 | } 100 | 101 | [Fact] 102 | public void CreateId_RepeatedlyWithCustomGuidStartingValue_ShouldReturnExpectedResult() 103 | { 104 | var guidZero = ((UInt128)0UL).ToGuid(); 105 | var guidUlongMax = ((UInt128)UInt64.MaxValue).ToGuid(); 106 | 107 | var generator = new IncrementalDistributedId128Generator(firstId: guidZero); 108 | 109 | var result1 = generator.CreateId(); 110 | var result2 = generator.CreateId(); 111 | 112 | Assert.Equal((UInt128)0, result1); 113 | Assert.Equal((UInt128)1, result2); 114 | 115 | generator = new IncrementalDistributedId128Generator(firstId: guidZero); 116 | 117 | var resultA = generator.CreateGuid(); 118 | var resultB = generator.CreateGuid(); 119 | 120 | Assert.Equal(result1, resultA.ToUInt128()); 121 | Assert.Equal(result2, resultB.ToUInt128()); 122 | Assert.Equal(guidZero, resultA); 123 | 124 | generator = new IncrementalDistributedId128Generator(firstId: guidUlongMax); 125 | 126 | result1 = generator.CreateId(); 127 | result2 = generator.CreateId(); 128 | 129 | Assert.Equal((UInt128)UInt64.MaxValue, result1); 130 | Assert.Equal((UInt128)UInt64.MaxValue + 1, result2); 131 | 132 | generator = new IncrementalDistributedId128Generator(firstId: guidUlongMax); 133 | 134 | resultA = generator.CreateGuid(); 135 | resultB = generator.CreateGuid(); 136 | 137 | Assert.Equal(result1, resultA.ToUInt128()); 138 | Assert.Equal(result2, resultB.ToUInt128()); 139 | Assert.Equal(guidUlongMax, resultA); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/IncrementalDistributedIdGeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Xunit; 3 | 4 | namespace Architect.Identities.Tests.DistributedIds 5 | { 6 | public sealed class IncrementalDistributedIdGeneratorTests 7 | { 8 | private IncrementalDistributedIdGenerator Generator { get; } = new IncrementalDistributedIdGenerator(); 9 | 10 | [Fact] 11 | public void CreateId_Initially_ShouldReturnExpectedResult() 12 | { 13 | var id = this.Generator.CreateId(); 14 | 15 | Assert.Equal(1m, id); 16 | } 17 | 18 | [Fact] 19 | public void CreateId_Repeatedly_ShouldReturnExpectedResult() 20 | { 21 | var idOne = this.Generator.CreateId(); 22 | var idTwo = this.Generator.CreateId(); 23 | var idThree = this.Generator.CreateId(); 24 | 25 | Assert.Equal(1m, idOne); 26 | Assert.Equal(2m, idTwo); 27 | Assert.Equal(3m, idThree); 28 | } 29 | 30 | /// 31 | /// The results should be the expected ones, confirming that the type is thread-safe, although the order can be anything. 32 | /// 33 | [Fact] 34 | public void CreateId_InParallel_ShouldReturnExpectedResult() 35 | { 36 | var expectedResults = Enumerable.Range(1, 10).Select(i => (decimal)i); 37 | var results = new ConcurrentQueue(); 38 | 39 | Parallel.For(0, 10, _ => results.Enqueue(this.Generator.CreateId())); 40 | 41 | Assert.Equal(expectedResults, results.OrderBy(id => id)); 42 | } 43 | 44 | /// 45 | /// The generators should be independent and each keep their own count. 46 | /// 47 | [Fact] 48 | public void CreateId_InParallelWithDifferentGenerators_ShouldReturnExpectedResult() 49 | { 50 | var results = new ConcurrentQueue(); 51 | 52 | Parallel.For(0, 10, _ => results.Enqueue(new IncrementalDistributedIdGenerator().CreateId())); 53 | 54 | foreach (var result in results) 55 | Assert.Equal(1m, result); 56 | } 57 | 58 | [Fact] 59 | public void CreateId_RepeatedlyWithCustomStartingValue_ShouldReturnExpectedResult() 60 | { 61 | var generator = new IncrementalDistributedIdGenerator(firstId: 0L); 62 | 63 | var result1 = generator.CreateId(); 64 | var result2 = generator.CreateId(); 65 | 66 | Assert.Equal(0m, result1); 67 | Assert.Equal(1m, result2); 68 | 69 | generator = new IncrementalDistributedIdGenerator(UInt64.MaxValue); 70 | 71 | result1 = generator.CreateId(); 72 | result2 = generator.CreateId(); 73 | 74 | Assert.Equal(UInt64.MaxValue, result1); 75 | Assert.Equal(UInt64.MaxValue + 1m, result2); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Identities.Tests/DistributedIds/RandomSequence48Tests.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using Xunit; 3 | 4 | namespace Architect.Identities.Tests.DistributedIds 5 | { 6 | public sealed class RandomSequence48Tests 7 | { 8 | private static readonly int SafeRateLimitPerTimestamp = DistributedIdGenerator.AverageRateLimitPerTimestamp / 2; 9 | 10 | #pragma warning disable CS0618 // Type or member is obsolete -- Obsolete intended to protect against non-test usage 11 | private static RandomSequence48 SimulateRandomSequenceWithValue(ulong value) => RandomSequence48.CreatedSimulated(value); 12 | #pragma warning restore CS0618 // Type or member is obsolete 13 | 14 | [Fact] 15 | public void Create_Regularly_ShouldBeNonzero() 16 | { 17 | var result = RandomSequence48.Create(); 18 | 19 | Assert.NotEqual(0UL, result); 20 | } 21 | 22 | /// 23 | /// Unfortunately non-deterministic. 24 | /// 25 | [Fact] 26 | public void Create_Regularly_ShouldHaveHighEntropyInLow5Bytes() 27 | { 28 | var results = new List(); 29 | for (var i = 0; i < 1000; i++) 30 | results.Add(RandomSequence48.Create()); 31 | 32 | var sumValuesPerByte = new int[5]; 33 | Span bytes = stackalloc byte[8]; 34 | foreach (var result in results) 35 | { 36 | BinaryPrimitives.WriteUInt64BigEndian(bytes, result); 37 | for (var i = 0; i < sumValuesPerByte.Length; i++) 38 | sumValuesPerByte[i] += bytes[8 - sumValuesPerByte.Length + i]; 39 | } 40 | 41 | var averageValuesPerByte = new int[sumValuesPerByte.Length]; 42 | for (var i = 0; i < sumValuesPerByte.Length; i++) 43 | averageValuesPerByte[i] = sumValuesPerByte[i] / results.Count; 44 | 45 | // Each byte should have an average value close to 127 46 | foreach (var value in averageValuesPerByte) 47 | Assert.True(value >= 102 && value <= 152); 48 | 49 | // The average of the averages should be very close to 127 50 | var totalAverage = averageValuesPerByte.Average(); 51 | Assert.True(totalAverage >= 120 && totalAverage <= 134); 52 | } 53 | 54 | [Fact] 55 | public void Create_Regularly_ShouldLeaveHigh16BitsZero() 56 | { 57 | var result = RandomSequence48.Create(); 58 | 59 | Assert.Equal(0UL, result >> (64 - 16)); 60 | } 61 | 62 | [Fact] 63 | public void CastToUlong_Regularly_ShouldLeave2HighBytesZero() 64 | { 65 | var result = SimulateRandomSequenceWithValue(UInt64.MaxValue >> 16); 66 | 67 | var ulongValue = (ulong)result; 68 | 69 | Assert.Equal(0UL, ulongValue >> (64 - 16)); 70 | } 71 | 72 | [Fact] 73 | public void CastToUlong_Regularly_ShouldMatchRandomData() 74 | { 75 | var randomValue = (ulong)UInt32.MaxValue >> 16; 76 | var result = SimulateRandomSequenceWithValue(randomValue); 77 | 78 | Assert.Equal(randomValue, result); 79 | } 80 | 81 | [Theory] 82 | [InlineData(UInt64.MaxValue >> 32, UInt32.MaxValue, (ulong)UInt32.MaxValue + UInt32.MaxValue)] 83 | [InlineData(UInt64.MaxValue >> 32, 5, (ulong)UInt32.MaxValue + 5)] 84 | [InlineData((UInt64.MaxValue >> 32) - 101, UInt32.MaxValue, (ulong)UInt32.MaxValue + UInt32.MaxValue - 101)] 85 | [InlineData((UInt64.MaxValue >> 32) - 101, 500, (ulong)UInt32.MaxValue + 500 - 101)] 86 | public void TryAddRandomBits_WithoutOverflow_ShouldProduceExpectedResult(ulong initialValue, uint increment, ulong expectedResult) 87 | { 88 | var left = SimulateRandomSequenceWithValue(initialValue); 89 | var right = SimulateRandomSequenceWithValue(increment); 90 | var didSucceed = left.TryAddRandomBits(right, out var result); 91 | 92 | Assert.True(didSucceed); 93 | Assert.Equal(expectedResult, result); 94 | } 95 | 96 | [Fact] 97 | public void TryAddRandomBits_WithOverflow_ShouldReturnFalse() 98 | { 99 | var left = SimulateRandomSequenceWithValue((UInt64.MaxValue >> 16) - 2UL); 100 | var right = SimulateRandomSequenceWithValue(3UL); 101 | var didSucceed = left.TryAddRandomBits(right, out _); 102 | 103 | Assert.False(didSucceed); 104 | } 105 | 106 | [Fact] 107 | public void TryAddRandomBits_WithinRateLimitTimesWithoutOverflow_ShouldReturnDistinctResults() 108 | { 109 | var results = new List(); 110 | 111 | for (var x = 0; x < 20; x++) // Because probability 112 | { 113 | var left = SimulateRandomSequenceWithValue(UInt64.MaxValue >> 32); 114 | var right = SimulateRandomSequenceWithValue(UInt32.MaxValue); 115 | 116 | results.Add(left); 117 | 118 | var didAddBits = true; 119 | 120 | for (var i = 0; i < SafeRateLimitPerTimestamp && didAddBits; i++) 121 | { 122 | didAddBits = left.TryAddRandomBits(right, out left); 123 | results.Add(left); 124 | } 125 | 126 | if (didAddBits) break; 127 | } 128 | 129 | Assert.Equal(1 + SafeRateLimitPerTimestamp, results.Distinct().Count()); 130 | } 131 | 132 | [Fact] 133 | public void TryAddRandomBits_WithinRateLimitTimesWithoutOverflow_ShouldReturnIncrementalResults() 134 | { 135 | var results = new List(); 136 | 137 | for (var x = 0; x < 20; x++) // Because probability 138 | { 139 | var left = SimulateRandomSequenceWithValue(UInt64.MaxValue >> 32); 140 | var right = SimulateRandomSequenceWithValue(UInt32.MaxValue); 141 | 142 | results.Add(left); 143 | 144 | var didAddBits = true; 145 | 146 | for (var i = 0; i < SafeRateLimitPerTimestamp && didAddBits; i++) 147 | { 148 | didAddBits = left.TryAddRandomBits(right, out left); 149 | results.Add(left); 150 | } 151 | 152 | if (didAddBits) break; 153 | } 154 | 155 | Assert.Equal(1 + SafeRateLimitPerTimestamp, results.Count); 156 | 157 | for (var i = 1; i < results.Count; i++) 158 | Assert.True(results[i] > results[i - 1]); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Identities.Tests/Encodings/Base62EncoderTests.cs: -------------------------------------------------------------------------------- 1 | using Architect.Identities.Encodings; 2 | using Xunit; 3 | 4 | namespace Architect.Identities.Tests.Encodings 5 | { 6 | public sealed class Base62EncoderTests 7 | { 8 | [Theory] 9 | [InlineData("12345678", "4DruweP3xQ8")] 10 | [InlineData("12345679", "4DruweP3xQ9")] 11 | public void ToBase62Chars8_Regularly_ShouldReturnExpectedOutput(string text, string base62String) 12 | { 13 | var bytes = System.Text.Encoding.UTF8.GetBytes(text); 14 | Span outputChars = stackalloc byte[base62String.Length]; 15 | Base62Encoder.ToBase62Chars8(bytes, outputChars); 16 | var outputString = System.Text.Encoding.UTF8.GetString(outputChars); 17 | 18 | Assert.Equal(base62String, outputString); 19 | } 20 | 21 | [Theory] 22 | [InlineData("12345678", "4DruweP3xQ8")] 23 | [InlineData("12345679", "4DruweP3xQ9")] 24 | public void ToBase62Chars8_WithOverlappingInputAndOutputSpans_ShouldReturnExpectedOutput(string text, string base62String) 25 | { 26 | Span bytes = stackalloc byte[11]; 27 | System.Text.Encoding.UTF8.GetBytes(text, bytes); 28 | Base62Encoder.ToBase62Chars8(bytes, bytes); 29 | var outputString = System.Text.Encoding.UTF8.GetString(bytes); 30 | 31 | Assert.Equal(base62String, outputString); 32 | } 33 | 34 | [Theory] 35 | [InlineData("12345678" + "12345678", "4DruweP3xQ8" + "4DruweP3xQ8")] 36 | [InlineData("12345679" + "12345679", "4DruweP3xQ9" + "4DruweP3xQ9")] 37 | [InlineData("12345678" + "12345679", "4DruweP3xQ8" + "4DruweP3xQ9")] 38 | public void ToBase62Chars16_Regularly_ShouldReturnExpectedOutput(string text, string base62String) 39 | { 40 | var bytes = System.Text.Encoding.UTF8.GetBytes(text); 41 | Span outputChars = stackalloc byte[base62String.Length]; 42 | Base62Encoder.ToBase62Chars16(bytes, outputChars); 43 | var outputString = System.Text.Encoding.UTF8.GetString(outputChars); 44 | 45 | Assert.Equal(base62String, outputString); 46 | } 47 | 48 | [Theory] 49 | [InlineData("12345678", "4DruweP3xQ8")] 50 | [InlineData("12345679", "4DruweP3xQ9")] 51 | public void FromBase62Chars11_Regularly_ShouldReturnExpectedOutput(string text, string base62String) 52 | { 53 | var chars = System.Text.Encoding.UTF8.GetBytes(base62String); 54 | Span outputBytes = stackalloc byte[text.Length]; 55 | Base62Encoder.FromBase62Chars11(chars, outputBytes); 56 | var originalString = System.Text.Encoding.UTF8.GetString(outputBytes); 57 | 58 | Assert.Equal(text, originalString); 59 | } 60 | 61 | [Theory] 62 | [InlineData("12345678", "4DruweP3xQ8")] 63 | [InlineData("12345679", "4DruweP3xQ9")] 64 | public void FromBase62Chars11_WithOverlappingInputAndOutputSpans_ShouldReturnExpectedOutput(string text, string base62String) 65 | { 66 | Span bytes = stackalloc byte[11]; 67 | System.Text.Encoding.UTF8.GetBytes(base62String, bytes); 68 | Base62Encoder.FromBase62Chars11(bytes, bytes); 69 | var originalString = System.Text.Encoding.UTF8.GetString(bytes[..8]); 70 | 71 | Assert.Equal(text, originalString); 72 | } 73 | 74 | [Theory] 75 | [InlineData("12345678", "4DruweP3xQ8")] 76 | [InlineData("12345679", "4DruweP3xQ9")] 77 | public void FromBase62Chars11_AfterToBase62Chars_ShouldReturnOriginalInput(string text, string base62String) 78 | { 79 | var bytes = System.Text.Encoding.UTF8.GetBytes(text); 80 | Span outputChars = stackalloc byte[base62String.Length]; 81 | Base62Encoder.ToBase62Chars8(bytes, outputChars); 82 | Span decodedBytes = stackalloc byte[bytes.Length]; 83 | Base62Encoder.FromBase62Chars11(outputChars, decodedBytes); 84 | var originalString = System.Text.Encoding.UTF8.GetString(decodedBytes); 85 | 86 | Assert.Equal(text, originalString); 87 | } 88 | 89 | [Theory] 90 | [InlineData("12345678" + "12345678", "4DruweP3xQ8" + "4DruweP3xQ8")] 91 | [InlineData("12345679" + "12345679", "4DruweP3xQ9" + "4DruweP3xQ9")] 92 | [InlineData("12345678" + "12345679", "4DruweP3xQ8" + "4DruweP3xQ9")] 93 | public void FromBase62Chars22_Regularly_ShouldReturnExpectedOutput(string text, string base62String) 94 | { 95 | var chars = System.Text.Encoding.UTF8.GetBytes(base62String); 96 | Span outputBytes = stackalloc byte[text.Length]; 97 | Base62Encoder.FromBase62Chars22(chars, outputBytes); 98 | var originalString = System.Text.Encoding.UTF8.GetString(outputBytes); 99 | 100 | Assert.Equal(text, originalString); 101 | } 102 | 103 | [Theory] 104 | [InlineData("12345678" + "12345678", "4DruweP3xQ8" + "4DruweP3xQ8")] 105 | [InlineData("12345679" + "12345679", "4DruweP3xQ9" + "4DruweP3xQ9")] 106 | [InlineData("12345678" + "12345679", "4DruweP3xQ8" + "4DruweP3xQ9")] 107 | public void FromBase62Chars22_WithOverlappingInputAndOutputSpans_ShouldReturnExpectedOutput(string text, string base62String) 108 | { 109 | Span bytes = stackalloc byte[22]; 110 | System.Text.Encoding.UTF8.GetBytes(base62String, bytes); 111 | Base62Encoder.FromBase62Chars22(bytes, bytes); 112 | var originalString = System.Text.Encoding.UTF8.GetString(bytes[..16]); 113 | 114 | Assert.Equal(text, originalString); 115 | } 116 | 117 | [Theory] 118 | [InlineData("12345678" + "12345678", "4DruweP3xQ8" + "4DruweP3xQ8")] 119 | [InlineData("12345679" + "12345679", "4DruweP3xQ9" + "4DruweP3xQ9")] 120 | [InlineData("12345678" + "12345679", "4DruweP3xQ8" + "4DruweP3xQ9")] 121 | public void FromBase62Chars22_AfterToBase62Chars_ShouldReturnOriginalInput(string text, string base62String) 122 | { 123 | var bytes = System.Text.Encoding.UTF8.GetBytes(text); 124 | Span outputChars = stackalloc byte[base62String.Length]; 125 | Base62Encoder.ToBase62Chars16(bytes, outputChars); 126 | Span decodedBytes = stackalloc byte[bytes.Length]; 127 | Base62Encoder.FromBase62Chars22(outputChars, decodedBytes); 128 | var originalString = System.Text.Encoding.UTF8.GetString(decodedBytes); 129 | 130 | Assert.Equal(text, originalString); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Identities.Tests/Encodings/HexadecimalEncoderTests.cs: -------------------------------------------------------------------------------- 1 | using Architect.Identities.Encodings; 2 | using Xunit; 3 | 4 | namespace Architect.Identities.Tests.Encodings 5 | { 6 | public sealed class HexadecimalEncoderTests 7 | { 8 | [Theory] 9 | [InlineData("12345678" + "12345678", "3132333435363738" + "3132333435363738")] 10 | [InlineData("12345679" + "12345679", "3132333435363739" + "3132333435363739")] 11 | [InlineData("12345678" + "12345679", "3132333435363738" + "3132333435363739")] 12 | [InlineData("ZZZZZZZZ" + "ZZZZZZZZ", "5A5A5A5A5A5A5A5A" + "5A5A5A5A5A5A5A5A")] 13 | [InlineData("zzzzzzzz" + "zzzzzzzz", "7A7A7A7A7A7A7A7A" + "7A7A7A7A7A7A7A7A")] 14 | public void ToHexChars_Regularly_ShouldReturnExpectedOutput(string text, string hexString) 15 | { 16 | var bytes = System.Text.Encoding.UTF8.GetBytes(text); 17 | Span outputChars = stackalloc byte[hexString.Length]; 18 | HexadecimalEncoder.ToHexChars(bytes, outputChars, inputByteCount: 16); 19 | var outputString = System.Text.Encoding.UTF8.GetString(outputChars); 20 | 21 | Assert.Equal(hexString, outputString); 22 | } 23 | 24 | [Theory] 25 | [InlineData("12345678" + "12345678", "3132333435363738" + "3132333435363738")] 26 | [InlineData("12345679" + "12345679", "3132333435363739" + "3132333435363739")] 27 | [InlineData("12345678" + "12345679", "3132333435363738" + "3132333435363739")] 28 | [InlineData("ZZZZZZZZ" + "ZZZZZZZZ", "5A5A5A5A5A5A5A5A" + "5A5A5A5A5A5A5A5A")] 29 | [InlineData("zzzzzzzz" + "zzzzzzzz", "7A7A7A7A7A7A7A7A" + "7A7A7A7A7A7A7A7A")] 30 | [InlineData("zzzzzzzz" + "zzzzzzzz", "7a7a7a7a7a7a7a7a" + "7a7a7a7a7a7a7a7a")] 31 | public void FromHexChars_Regularly_ShouldReturnExpectedOutput(string text, string hexString) 32 | { 33 | var chars = System.Text.Encoding.UTF8.GetBytes(hexString); 34 | Span outputBytes = stackalloc byte[text.Length]; 35 | HexadecimalEncoder.FromHexChars(chars, outputBytes, inputByteCount: 32); 36 | var originalString = System.Text.Encoding.UTF8.GetString(outputBytes); 37 | 38 | Assert.Equal(text, originalString); 39 | } 40 | 41 | [Theory] 42 | [InlineData("12345678" + "12345678", "3132333435363738" + "3132333435363738")] 43 | [InlineData("12345679" + "12345679", "3132333435363739" + "3132333435363739")] 44 | [InlineData("12345678" + "12345679", "3132333435363738" + "3132333435363739")] 45 | [InlineData("ZZZZZZZZ" + "ZZZZZZZZ", "5A5A5A5A5A5A5A5A" + "5A5A5A5A5A5A5A5A")] 46 | [InlineData("zzzzzzzz" + "zzzzzzzz", "7A7A7A7A7A7A7A7A" + "7A7A7A7A7A7A7A7A")] 47 | public void FromHexChars_AfterToHexChars_ShouldReturnOriginalInput(string text, string hexString) 48 | { 49 | var bytes = System.Text.Encoding.UTF8.GetBytes(text); 50 | Span outputChars = stackalloc byte[hexString.Length]; 51 | HexadecimalEncoder.ToHexChars(bytes, outputChars, inputByteCount: 16); 52 | Span decodedBytes = stackalloc byte[bytes.Length]; 53 | HexadecimalEncoder.FromHexChars(outputChars, decodedBytes, inputByteCount: 32); 54 | var originalString = System.Text.Encoding.UTF8.GetString(decodedBytes); 55 | 56 | Assert.Equal(text, originalString); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Identities.Tests/Encodings/IdEncodingExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Architect.Identities.Tests.Encodings 4 | { 5 | public sealed class IdEncodingExtensionTests 6 | { 7 | [Fact] 8 | public void ToAlphanumeric_WithNegativeLong_ShouldThrow() 9 | { 10 | Assert.Throws(() => (-1L).ToAlphanumeric()); 11 | } 12 | 13 | [Fact] 14 | public void ToAlphanumeric_WithNegativeDecimal_ShouldThrow() 15 | { 16 | Assert.Throws(() => (-1m).ToAlphanumeric()); 17 | } 18 | 19 | [Theory] 20 | [InlineData(0)] 21 | [InlineData(1)] 22 | [InlineData(Int32.MaxValue)] 23 | [InlineData(Int64.MaxValue)] 24 | public void ToAlphanumeric_WithLong_ShouldMatchIdEncoderResult(long id) 25 | { 26 | var expectedResult = AlphanumericIdEncoder.Encode(id); 27 | 28 | var result = id.ToAlphanumeric(); 29 | 30 | Assert.Equal(expectedResult, result); 31 | } 32 | 33 | [Theory] 34 | [InlineData(0)] 35 | [InlineData(1)] 36 | [InlineData(Int32.MaxValue)] 37 | [InlineData(Int64.MaxValue)] 38 | public void ToAlphanumeric_WithLongAndByteOutput_ShouldMatchIdEncoderResult(long id) 39 | { 40 | Span expectedOutput = stackalloc byte[11]; 41 | AlphanumericIdEncoder.Encode(id, expectedOutput); 42 | 43 | Span output = stackalloc byte[11]; 44 | id.ToAlphanumeric(output); 45 | 46 | Assert.True(output.SequenceEqual(expectedOutput)); 47 | } 48 | 49 | [Theory] 50 | [InlineData(0)] 51 | [InlineData(1)] 52 | [InlineData(Int32.MaxValue)] 53 | [InlineData(Int64.MaxValue)] 54 | [InlineData(UInt64.MaxValue)] 55 | public void ToAlphanumeric_WithUlong_ShouldMatchIdEncoderResult(ulong id) 56 | { 57 | var expectedResult = AlphanumericIdEncoder.Encode(id); 58 | 59 | var result = id.ToAlphanumeric(); 60 | 61 | Assert.Equal(expectedResult, result); 62 | } 63 | 64 | [Theory] 65 | [InlineData(0)] 66 | [InlineData(1)] 67 | [InlineData(Int32.MaxValue)] 68 | [InlineData(Int64.MaxValue)] 69 | [InlineData(UInt64.MaxValue)] 70 | public void ToAlphanumeric_WithUlongAndByteOutput_ShouldMatchIdEncoderResult(ulong id) 71 | { 72 | Span expectedOutput = stackalloc byte[11]; 73 | AlphanumericIdEncoder.Encode(id, expectedOutput); 74 | 75 | Span output = stackalloc byte[11]; 76 | id.ToAlphanumeric(output); 77 | 78 | Assert.True(output.SequenceEqual(expectedOutput)); 79 | } 80 | 81 | [Theory] 82 | [InlineData(0)] 83 | [InlineData(1)] 84 | [InlineData(Int32.MaxValue)] 85 | [InlineData(Int64.MaxValue)] 86 | [InlineData(UInt64.MaxValue)] 87 | public void ToAlphanumeric_WithDecimal_ShouldMatchIdEncoderResult(decimal id) 88 | { 89 | var expectedResult = AlphanumericIdEncoder.Encode(id); 90 | 91 | var result = id.ToAlphanumeric(); 92 | 93 | Assert.Equal(expectedResult, result); 94 | } 95 | 96 | [Theory] 97 | [InlineData(0)] 98 | [InlineData(1)] 99 | [InlineData(Int32.MaxValue)] 100 | [InlineData(Int64.MaxValue)] 101 | [InlineData(UInt64.MaxValue)] 102 | public void ToAlphanumeric_WithDecimalAndByteOutput_ShouldMatchIdEncoderResult(decimal id) 103 | { 104 | Span expectedOutput = stackalloc byte[16]; 105 | AlphanumericIdEncoder.Encode(id, expectedOutput); 106 | 107 | Span output = stackalloc byte[16]; 108 | id.ToAlphanumeric(output); 109 | 110 | Assert.True(output.SequenceEqual(expectedOutput)); 111 | } 112 | 113 | [Theory] 114 | [InlineData(0)] 115 | [InlineData(1)] 116 | [InlineData(Int32.MaxValue)] 117 | [InlineData(Int64.MaxValue)] 118 | [InlineData(UInt64.MaxValue)] 119 | public void ToAlphanumeric_WithGuid_ShouldMatchIdEncoderResult(decimal id) 120 | { 121 | var guid = AlphanumericIdEncoderTests.Guid(id); 122 | 123 | var expectedResult = AlphanumericIdEncoder.Encode(guid); 124 | 125 | var result = guid.ToAlphanumeric(); 126 | 127 | Assert.Equal(expectedResult, result); 128 | } 129 | 130 | [Theory] 131 | [InlineData(0)] 132 | [InlineData(1)] 133 | [InlineData(Int32.MaxValue)] 134 | [InlineData(Int64.MaxValue)] 135 | [InlineData(UInt64.MaxValue)] 136 | public void ToAlphanumeric_WithGuidAndByteOutput_ShouldMatchIdEncoderResult(decimal id) 137 | { 138 | var guid = AlphanumericIdEncoderTests.Guid(id); 139 | 140 | Span expectedOutput = stackalloc byte[22]; 141 | AlphanumericIdEncoder.Encode(guid, expectedOutput); 142 | 143 | Span output = stackalloc byte[22]; 144 | guid.ToAlphanumeric(output); 145 | 146 | Assert.True(output.SequenceEqual(expectedOutput)); 147 | } 148 | 149 | [Theory] 150 | [InlineData(0)] 151 | [InlineData(1)] 152 | [InlineData(Int32.MaxValue)] 153 | [InlineData(Int64.MaxValue)] 154 | public void ToHexadecimal_Regularly_ShouldMatchIdEncoderResult(decimal id) 155 | { 156 | var expectedLongResult = HexadecimalIdEncoder.Encode((long)id); 157 | var expectedUlongResult = HexadecimalIdEncoder.Encode((ulong)id); 158 | var expectedDecimalResult = HexadecimalIdEncoder.Encode(id); 159 | var expectedGuidResult = HexadecimalIdEncoder.Encode(AlphanumericIdEncoderTests.Guid(id)); 160 | 161 | var longResult = ((long)id).ToHexadecimal(); 162 | var ulongResult = ((ulong)id).ToHexadecimal(); 163 | var decimalResult = id.ToHexadecimal(); 164 | var guidResult = AlphanumericIdEncoderTests.Guid(id).ToHexadecimal(); 165 | 166 | Assert.Equal(expectedLongResult, longResult); 167 | Assert.Equal(expectedUlongResult, ulongResult); 168 | Assert.Equal(expectedDecimalResult, decimalResult); 169 | Assert.Equal(expectedGuidResult, guidResult); 170 | 171 | var longResultBytes = new byte[16]; 172 | var ulongResultBytes = new byte[16]; 173 | var decimalResultBytes = new byte[26]; 174 | var guidResultBytes = new byte[32]; 175 | ((long)id).ToHexadecimal(longResultBytes); 176 | ((ulong)id).ToHexadecimal(ulongResultBytes); 177 | id.ToHexadecimal(decimalResultBytes); 178 | AlphanumericIdEncoderTests.Guid(id).ToHexadecimal(guidResultBytes); 179 | 180 | Assert.Equal(expectedLongResult.Select(chr => (byte)chr), longResultBytes); 181 | Assert.Equal(expectedUlongResult.Select(chr => (byte)chr), ulongResultBytes); 182 | Assert.Equal(expectedDecimalResult.Select(chr => (byte)chr), decimalResultBytes); 183 | Assert.Equal(expectedGuidResult.Select(chr => (byte)chr), guidResultBytes); 184 | } 185 | 186 | [Theory] 187 | [InlineData("0")] 188 | [InlineData("1")] 189 | [InlineData("2147483647")] // Int32.MaxValue 190 | [InlineData("18446744073709551615")] // UInt64.MaxValue 191 | [InlineData("18446744073709551616")] // UInt64.MaxValue + 1 192 | [InlineData("340282366920938463463374607431768211455")] // UInt128.MaxValue 193 | public void ToUInt128_Regularly_ShouldReverseToGuid(string uint128IdString) 194 | { 195 | var id = UInt128.Parse(uint128IdString); 196 | 197 | var guid = IdEncodingExtensions.ToGuid(id); 198 | var reversed = IdEncodingExtensions.ToUInt128(guid); 199 | 200 | Assert.Equal(id, reversed); 201 | } 202 | 203 | [Theory] 204 | [InlineData("0")] 205 | [InlineData("1")] 206 | [InlineData("2147483647")] // Int32.MaxValue 207 | [InlineData("18446744073709551615")] // UInt64.MaxValue 208 | [InlineData("18446744073709551616")] // UInt64.MaxValue + 1 209 | [InlineData("340282366920938463463374607431768211455")] // UInt128.MaxValue 210 | public void ToGuid_Regularly_ShouldHaveSameBinaryEncoding(string uint128IdString) 211 | { 212 | var id = UInt128.Parse(uint128IdString); 213 | var guid = IdEncodingExtensions.ToGuid(id); 214 | 215 | var expectedBinaryRepresentation = BinaryIdEncoder.Encode(id); 216 | var binaryRepresentation = BinaryIdEncoder.Encode(guid); 217 | 218 | Assert.Equal(expectedBinaryRepresentation, binaryRepresentation); 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Identities.Tests/Identities.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Architect.Identities.Tests 6 | Architect.Identities.Tests 7 | Enable 8 | False 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Identities.Tests/PublicIdentities/PublicIdentityExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Xunit; 4 | 5 | namespace Architect.Identities.Tests.PublicIdentities 6 | { 7 | public sealed class PublicIdentityExtensionsTests 8 | { 9 | [Fact] 10 | public void AddPublicIdentities_WithSameKey_ShouldGenerateSameResults() 11 | { 12 | var key1 = new byte[32]; 13 | key1[0] = 1; 14 | var key2 = new byte[32]; 15 | key1.AsSpan().CopyTo(key2); 16 | 17 | var hostBuilder1 = new HostBuilder(); 18 | hostBuilder1.ConfigureServices(services => services.AddPublicIdentities(identities => identities.Key(key1))); 19 | using var host1 = hostBuilder1.Build(); 20 | var guid1 = host1.Services.GetRequiredService().GetPublicRepresentation(0UL); 21 | 22 | var hostBuilder2 = new HostBuilder(); 23 | hostBuilder2.ConfigureServices(services => services.AddPublicIdentities(identities => identities.Key(key2))); 24 | using var host2 = hostBuilder2.Build(); 25 | var guid2 = host2.Services.GetRequiredService().GetPublicRepresentation(0UL); 26 | 27 | Assert.Equal(guid1, guid2); // A second custom key gives the same result as an identical first custom key 28 | } 29 | 30 | [Fact] 31 | public void AddPublicIdentities_WithKey_ShouldGenerateResultBasedOnThatKey() 32 | { 33 | var key1 = new byte[32]; 34 | key1[0] = 1; 35 | var key2 = new byte[32]; 36 | key2[31] = 2; 37 | 38 | var guid0 = new CustomPublicIdentityConverter().GetPublicRepresentation(0UL); 39 | 40 | var hostBuilder1 = new HostBuilder(); 41 | hostBuilder1.ConfigureServices(services => services.AddPublicIdentities(identities => identities.Key(key1))); 42 | using var host1 = hostBuilder1.Build(); 43 | var guid1 = host1.Services.GetRequiredService().GetPublicRepresentation(0UL); 44 | 45 | var hostBuilder2 = new HostBuilder(); 46 | hostBuilder2.ConfigureServices(services => services.AddPublicIdentities(identities => identities.Key(key2))); 47 | using var host2 = hostBuilder2.Build(); 48 | var guid2 = host2.Services.GetRequiredService().GetPublicRepresentation(0UL); 49 | 50 | Assert.NotEqual(guid0, guid1); // A custom key gives a different result than the default unit test key 51 | Assert.NotEqual(guid1, guid2); // A second custom key gives a different result than the first custom key 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Identities.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identities.Example", "Identities.Example\Identities.Example.csproj", "{FEE561A3-FE36-4F52-ACD8-BEA4E3409A21}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identities", "Identities\Identities.csproj", "{81DD7BFB-A472-458E-9184-678518FF65CA}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identities.Tests", "Identities.Tests\Identities.Tests.csproj", "{0000C4C4-7D1F-43B1-9BA1-806CC6F7DAE9}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{40ADCE6A-4A53-4F2B-8152-140CA2E363C9}" 13 | ProjectSection(SolutionItems) = preProject 14 | .editorconfig = .editorconfig 15 | LICENSE = LICENSE 16 | pipeline-publish-preview-identities-entityframework.yml = pipeline-publish-preview-identities-entityframework.yml 17 | pipeline-publish-preview-identities.yml = pipeline-publish-preview-identities.yml 18 | pipeline-publish-stable-identities-entityframework.yml = pipeline-publish-stable-identities-entityframework.yml 19 | pipeline-publish-stable-identities.yml = pipeline-publish-stable-identities.yml 20 | pipeline-verify.yml = pipeline-verify.yml 21 | README.md = README.md 22 | EndProjectSection 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{0BAEE0E6-1C1D-4480-8A7A-795F5B753AFA}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identities.EntityFramework", "Identities.EntityFramework\Identities.EntityFramework.csproj", "{0DF9B7BB-8C7A-40D6-9B94-F6D7F2987828}" 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Identities.EntityFramework.IntegrationTests", "Identities.EntityFramework.IntegrationTests\Identities.EntityFramework.IntegrationTests.csproj", "{2DA84553-03BB-4876-9EA3-08CE0C5CD461}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Release|Any CPU = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {FEE561A3-FE36-4F52-ACD8-BEA4E3409A21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {FEE561A3-FE36-4F52-ACD8-BEA4E3409A21}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {FEE561A3-FE36-4F52-ACD8-BEA4E3409A21}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {FEE561A3-FE36-4F52-ACD8-BEA4E3409A21}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {81DD7BFB-A472-458E-9184-678518FF65CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {81DD7BFB-A472-458E-9184-678518FF65CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {81DD7BFB-A472-458E-9184-678518FF65CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {81DD7BFB-A472-458E-9184-678518FF65CA}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {0000C4C4-7D1F-43B1-9BA1-806CC6F7DAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {0000C4C4-7D1F-43B1-9BA1-806CC6F7DAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {0000C4C4-7D1F-43B1-9BA1-806CC6F7DAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {0000C4C4-7D1F-43B1-9BA1-806CC6F7DAE9}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {0BAEE0E6-1C1D-4480-8A7A-795F5B753AFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {0BAEE0E6-1C1D-4480-8A7A-795F5B753AFA}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {0BAEE0E6-1C1D-4480-8A7A-795F5B753AFA}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {0BAEE0E6-1C1D-4480-8A7A-795F5B753AFA}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {0DF9B7BB-8C7A-40D6-9B94-F6D7F2987828}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {0DF9B7BB-8C7A-40D6-9B94-F6D7F2987828}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {0DF9B7BB-8C7A-40D6-9B94-F6D7F2987828}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {0DF9B7BB-8C7A-40D6-9B94-F6D7F2987828}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {2DA84553-03BB-4876-9EA3-08CE0C5CD461}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {2DA84553-03BB-4876-9EA3-08CE0C5CD461}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {2DA84553-03BB-4876-9EA3-08CE0C5CD461}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {2DA84553-03BB-4876-9EA3-08CE0C5CD461}.Release|Any CPU.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(ExtensibilityGlobals) = postSolution 65 | SolutionGuid = {BA5A1E43-A4A7-44BF-8A03-A1BF043AB29E} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /Identities/DistributedIds/CustomDistributedId128Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// A manually configured , mainly intended for testing purposes. 8 | /// 9 | public sealed class CustomDistributedId128Generator : IDistributedId128Generator 10 | { 11 | private delegate void IdWriter(Span bytes); 12 | 13 | private IdWriter IdGenerator { get; } 14 | 15 | private CustomDistributedId128Generator(IdWriter idGenerator) 16 | { 17 | this.IdGenerator = idGenerator; 18 | } 19 | 20 | #if NET7_0_OR_GREATER 21 | 22 | /// 23 | /// Constructs a new instance that always returns the given ID. 24 | /// 25 | public CustomDistributedId128Generator(UInt128 id) 26 | : this(bytes => BinaryIdEncoder.Encode(id, bytes)) 27 | { 28 | } 29 | 30 | /// 31 | /// Constructs a new instance that invokes the given generator whenever a new ID is requested. 32 | /// 33 | public CustomDistributedId128Generator(Func idGenerator) 34 | : this(bytes => BinaryIdEncoder.Encode(idGenerator(), bytes)) 35 | { 36 | } 37 | 38 | public UInt128 CreateId() 39 | { 40 | Span bytes = stackalloc byte[16]; 41 | this.IdGenerator(bytes); 42 | BinaryIdEncoder.TryDecodeUInt128(bytes, out var result); 43 | return result; 44 | } 45 | 46 | #endif 47 | 48 | /// 49 | /// Constructs a new instance that always returns the given ID. 50 | /// 51 | public CustomDistributedId128Generator(Guid id) 52 | : this(bytes => BinaryIdEncoder.Encode(id, bytes)) 53 | { 54 | } 55 | 56 | /// 57 | /// Constructs a new instance that invokes the given generator whenever a new ID is requested. 58 | /// 59 | public CustomDistributedId128Generator(Func idGenerator) 60 | : this(bytes => BinaryIdEncoder.Encode(idGenerator(), bytes)) 61 | { 62 | } 63 | 64 | public Guid CreateGuid() 65 | { 66 | Span bytes = stackalloc byte[16]; 67 | this.IdGenerator(bytes); 68 | BinaryIdEncoder.TryDecodeGuid(bytes, out var result); 69 | return result; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Identities/DistributedIds/CustomDistributedIdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// A manually configured , mainly intended for testing purposes. 8 | /// 9 | public sealed class CustomDistributedIdGenerator : IDistributedIdGenerator 10 | { 11 | public Func IdGenerator { get; } 12 | 13 | /// 14 | /// Constructs a new instance that always returns the given ID. 15 | /// 16 | public CustomDistributedIdGenerator(decimal id) 17 | : this(() => id) 18 | { 19 | } 20 | 21 | /// 22 | /// Constructs a new instance that invokes the given generator whenever a new ID is requested. 23 | /// 24 | public CustomDistributedIdGenerator(Func idGenerator) 25 | { 26 | this.IdGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator)); 27 | } 28 | 29 | public decimal CreateId() 30 | { 31 | var id = this.IdGenerator(); 32 | return id; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Identities/DistributedIds/DistributedId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// 8 | /// A replacement that provides values that are unique with extremely high probability, in a distributed fashion. 9 | /// 10 | /// 11 | /// Similarities: Like .NET's built-in version-4 values, created values are hard to guess and extremely unlikely to collide. 12 | /// 13 | /// 14 | /// Benefits: The values have the added benefit of being incremental, intuitive to display and to sort, and slightly more compact to persist. 15 | /// The incremental property generally makes values much more efficient as database/storage keys than random values. 16 | /// 17 | /// 18 | /// Structure: The values are decimals of exactly 28 digits, with 0 decimal places. In SQL databases, the corresponding type is DECIMAL(28, 0). 19 | /// In [Azure] SQL Server and MySQL, this takes 13 bytes of storage, making it about 20% more compact than a . 20 | /// 21 | /// 22 | /// Exposure: Note that the values expose their creation timestamps to some degree. This may be sensitive data in certain contexts. 23 | /// 24 | /// 25 | /// Rate limit: The rate limit per process is 128 values generated per millisecond (i.e. 128K per second) on average, with threads sleeping if necessary. 26 | /// However, about 128K values can be burst generated instantaneously, with the burst capacity recovering quickly during non-exhaustive use. 27 | /// 28 | /// 29 | /// Collisions: Collisions between generated values are extremely unlikely, although worldwide uniqueness is not a guarantee. View the README for more information. 30 | /// 31 | /// 32 | /// Sorting: The type has broad support and predictable implementations, unlike the type. 33 | /// For example, SQL Server sorts values in an unfavorable way, treating some of the middle bytes as the most significant. 34 | /// MySQL has no type, making manual queries cumbersome. 35 | /// Decimals avoid such issues. 36 | /// 37 | /// 38 | public static class DistributedId 39 | { 40 | /// 41 | /// 42 | /// Returns a new ID value of exactly 28 decimal digits, with no decimal places. 43 | /// 44 | /// 45 | /// View the class summary or the README for an extensive description of the ID's properties. 46 | /// 47 | /// 48 | /// The ID generator can be controlled by constructing a new in a using statement. 49 | /// 50 | /// 51 | public static decimal CreateId() 52 | { 53 | var id = DistributedIdGeneratorScope.CurrentGenerator.CreateId(); 54 | return id; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Identities/DistributedIds/DistributedId128.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// 8 | /// A drop-in replacement that provides values that are unique with exceedingly high probability, in a distributed fashion. 9 | /// 10 | /// 11 | /// Application: Although for most applications the is more suitable than the , not all code can tolerate the former's constraints. 12 | /// For example, class libraries can make no assumptions about their host applications with regards to acceptable rate limits and collision probabilities. 13 | /// The offers practically no collisions, practically no rate limit, and the format of a valid version-7 UUID. 14 | /// 15 | /// 16 | /// Similarities: Like .NET's built-in version-4 values, created values are exceedingly hard to guess and exceedingly unlikely to collide. 17 | /// 18 | /// 19 | /// Benefits: The values have the added benefit of being incremental. The incremental property generally makes values much more efficient as database/storage keys than random values. 20 | /// 21 | /// 22 | /// Exposure: Note that the values expose their creation timestamps to some degree. This may be sensitive data in certain contexts. 23 | /// 24 | /// 25 | /// Rate limit: The rate limit per process is over 100K IDs generated per millisecond (i.e. over 1 million per second) on average, with threads sleeping if necessary. 26 | /// 27 | /// 28 | /// Collisions: 29 | /// For OTP, 1 million servers each generating 1 ID at the same millisecond, repeatedly, expect less than 1 collision per 75 quadrillion (75,000,000,000,000,000) IDs. 30 | /// For batch processing, 100K servers each generating 100K IDs at the same millisecond, repeatedly, expect less than 1 collision per 5 trillion (5,000,000,000,000) IDs. 31 | /// 32 | /// 33 | /// Sorting: The sort order of the resulting values should only be relied upon within .NET, since other platforms may sort UUIDs differently. 34 | /// For universally sortable values, call on the values. 35 | /// Alternatively, values can be represented numerically (.NET7+ only). Until at least the year 4000, these fit in a DECIMAL(38) or a pair of signed BIGINTs (with the sign bits unused). 36 | /// 37 | /// 38 | public static class DistributedId128 39 | { 40 | #if NET7_0_OR_GREATER 41 | /// 42 | /// 43 | /// Returns a new ID value, encoded as a , consisting of exactly 38 digits until beyond the year 4000. 44 | /// 45 | /// 46 | /// Note that the numeric (.NET7+ only) and creation methods return identical values, encoded in different formats. 47 | /// Both are incremental. 48 | /// The two can be freely transcoded to one another using the extension methods provided by . 49 | /// 50 | /// 51 | /// View the class summary or the README for an extensive description of the ID's properties. 52 | /// 53 | /// 54 | /// The ID generator can be controlled by constructing a new in a using statement. 55 | /// 56 | /// 57 | public static UInt128 CreateId() 58 | { 59 | var id = DistributedId128GeneratorScope.CurrentGenerator.CreateId(); 60 | return id; 61 | } 62 | #endif 63 | 64 | /// 65 | /// 66 | /// Returns a new ID value, encoded as a version-7 UUID. 67 | /// 68 | /// 69 | /// Note that the numeric (.NET7+ only) and creation methods return identical values, encoded in different formats. 70 | /// Both are incremental. 71 | /// The two can be freely transcoded to one another using the extension methods provided by . 72 | /// 73 | /// 74 | /// View the class summary or the README for an extensive description of the ID's properties. 75 | /// 76 | /// 77 | /// The ID generator can be controlled by constructing a new in a using statement. 78 | /// 79 | /// 80 | public static Guid CreateGuid() 81 | { 82 | var id = DistributedId128GeneratorScope.CurrentGenerator.CreateGuid(); 83 | return id; 84 | } 85 | 86 | #if NET7_0_OR_GREATER 87 | 88 | /// 89 | /// 90 | /// Splits the given value into its upper and lower halves, returned in that order. 91 | /// The halves are positive values. 92 | /// 93 | /// 94 | /// If a value other than one generated by the is passed, this method may throw. 95 | /// It expects the top bit of each half to be unset, to permit the use of two values. 96 | /// 97 | /// 98 | /// An value. 99 | /// A pair consisting of the most significant 8 bytes first and the least significant 8 bytes last. 100 | public static (long, long) Split(UInt128 id) 101 | { 102 | var upper = (long)(id >> 64); 103 | var lower = (long)id; 104 | 105 | if (upper < 0L || lower < 0L) 106 | throw new ArgumentException($"A value generated by the {nameof(DistributedId128Generator)} was expected. The top bit of each half must be unset."); 107 | 108 | return (upper, lower); 109 | } 110 | 111 | /// 112 | /// 113 | /// Reconstitutes a value from its upper and lower halves. 114 | /// 115 | /// 116 | /// If the input values together do not represent a value that could be generated by the , this method may throw. 117 | /// It expects the top bit of each half to be unset, i.e. each half being positive. 118 | /// 119 | /// 120 | /// The most significant 8 bytes. 121 | /// The least significant 8 bytes. 122 | public static UInt128 Join(long upper, long lower) 123 | { 124 | if (upper < 0L || lower < 0L) 125 | throw new ArgumentException($"A value generated by the {nameof(DistributedId128Generator)} was expected. The top bit of each half must be unset."); 126 | 127 | var result = ((UInt128)upper) << 64; 128 | result |= (UInt128)lower; 129 | return result; 130 | } 131 | 132 | #endif 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Identities/DistributedIds/DistributedId128Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Threading; 4 | 5 | #pragma warning disable IDE0130 // Namespace does not match folder structure 6 | namespace Architect.Identities 7 | { 8 | /// 9 | /// Used to implement in a testable way. 10 | /// 11 | internal sealed class DistributedId128Generator : IDistributedId128Generator 12 | { 13 | /// 14 | /// Bits 0000_0111. 15 | /// 16 | private const byte VersionMarkerByte = 0b_0000_0111; 17 | /// 18 | /// Bits 0111 starting at the 48th top bit. 19 | /// 20 | private const ulong VersionMarker = (ulong)VersionMarkerByte << (64 - 48 - 4); // Shift left to move from bit 60 to bit 48 21 | 22 | #if NET7_0_OR_GREATER 23 | /// 24 | /// The maximum ID value to fit in 38 digits. 25 | /// 26 | internal static readonly UInt128 MaxValueToFitInDecimal38 = UInt128.Parse("99999999999999999999999999999999999999"); 27 | #endif 28 | 29 | static DistributedId128Generator() 30 | { 31 | if (!Environment.Is64BitOperatingSystem) 32 | throw new PlatformNotSupportedException($"{nameof(DistributedId)} is not supported on non-64-bit operating systems. It uses 64-bit instructions that must be atomic."); 33 | 34 | if (!BitConverter.IsLittleEndian) 35 | throw new PlatformNotSupportedException($"{nameof(DistributedId)} is not supported on big-endian architectures. The binary conversions have not been tested."); 36 | } 37 | 38 | private static DateTime GetUtcNow() 39 | { 40 | return DateTime.UtcNow; 41 | } 42 | 43 | /// 44 | /// 45 | /// The custom epoch helps ensure 38-digit IDs, avoiding 37-digit ones. 46 | /// 47 | /// 48 | /// The IDs stay at 38 digits until the year 4000+, fitting in a DECIMAL(38). 49 | /// 50 | /// 51 | /// The IDs stay within 127 bits until the year 6000+, fitting in two signed longs that are positive. (The 64th bit is always 0.) 52 | /// 53 | /// 54 | internal static readonly DateTime Epoch = new DateTime(1700, 01, 01, 00, 00, 00, DateTimeKind.Utc); 55 | 56 | /// 57 | /// Can be invoked to get the current UTC datetime. 58 | /// 59 | private Func Clock { get; } 60 | /// 61 | /// Can be invoked to cause the current thread sleep for the given number of milliseconds. 62 | /// 63 | private Action SleepAction { get; } 64 | 65 | /// 66 | /// The previous UTC timestamp (in milliseconds since the epoch) on which an ID was created (or 0 initially). 67 | /// 68 | internal ulong PreviousCreationTimestamp { get; private set; } 69 | /// 70 | /// The random sequence used during the previous ID creation. 71 | /// 72 | internal RandomSequence75 PreviousRandomSequence { get; set; } 73 | 74 | /// 75 | /// A lock object used to govern access to the mutable properties. 76 | /// 77 | private readonly object _lockObject = new object(); 78 | 79 | internal DistributedId128Generator(Func? utcClock = null, Action? sleepAction = null) 80 | { 81 | this.Clock = utcClock ?? GetUtcNow; 82 | this.SleepAction = sleepAction ?? Thread.Sleep; 83 | } 84 | 85 | #if NET7_0_OR_GREATER 86 | public UInt128 CreateId() 87 | { 88 | return this.CreateGuid().ToUInt128(); 89 | } 90 | #endif 91 | 92 | public Guid CreateGuid() 93 | { 94 | var (timestamp, randomSequence) = this.CreateValues(); 95 | return this.CreateCore(timestamp, randomSequence); 96 | } 97 | 98 | /// 99 | /// 100 | /// Locking. 101 | /// 102 | /// 103 | /// Creates the values required to create an ID. 104 | /// 105 | /// 106 | private (ulong Timestamp, RandomSequence75 RandomSequence) CreateValues() 107 | { 108 | var randomSequence = CreateRandomSequence(); 109 | 110 | Start: 111 | 112 | lock (this._lockObject) 113 | { 114 | var timestamp = this.GetCurrentTimestamp(); 115 | 116 | // If the clock has not advanced beyond the last used timestamp, then we must make an effort to continue where we left off 117 | if (timestamp <= this.PreviousCreationTimestamp) 118 | { 119 | // If we succeed in creating another, greater random value to use with the previous timestamp, return that 120 | if (this.TryCreateIncrementalRandomSequence(this.PreviousRandomSequence, randomSequence, out var incrementedRandomSequence)) 121 | { 122 | timestamp = this.PreviousCreationTimestamp; 123 | this.PreviousRandomSequence = incrementedRandomSequence; 124 | return (timestamp, incrementedRandomSequence); 125 | } 126 | 127 | // In the unlikely event that we cannot increase the random portion without overflowing, we must wait for the timestamp to increase 128 | // In the edge case where the clock was turned back by too much, sleeping would take too long, so simply fall through and lose our incremental property, using the new, smaller timestamp 129 | if (timestamp + 1000 > this.PreviousCreationTimestamp) 130 | goto SleepAndRestart; 131 | } 132 | 133 | // Update the last used values 134 | this.PreviousCreationTimestamp = timestamp; 135 | this.PreviousRandomSequence = randomSequence; 136 | 137 | return (timestamp, randomSequence); 138 | } 139 | 140 | SleepAndRestart: 141 | 142 | this.SleepAction(1); // Ideally outside the lock 143 | goto Start; 144 | } 145 | 146 | /// 147 | /// Returns the UTC timestamp in milliseconds since some epoch. 148 | /// 149 | private ulong GetCurrentTimestamp() 150 | { 151 | var utcNow = this.Clock(); 152 | var millisecondsSinceEpoch = (ulong)(utcNow - Epoch).TotalMilliseconds; 153 | 154 | return millisecondsSinceEpoch; 155 | } 156 | 157 | /// 158 | /// 159 | /// Pure function (although the random number generator may use locking internally). 160 | /// 161 | /// 162 | /// Returns a new 75-bit random sequence. 163 | /// 164 | /// 165 | private static RandomSequence75 CreateRandomSequence() 166 | { 167 | return RandomSequence75.Create(); 168 | } 169 | 170 | /// 171 | /// 172 | /// Pure function. 173 | /// 174 | /// 175 | /// Creates a new 75-bit random sequence based on the given previous one and new one. 176 | /// Adds new randomness while maintaining the incremental property. 177 | /// 178 | /// 179 | /// Returns true on success or false on overflow. 180 | /// 181 | /// 182 | private bool TryCreateIncrementalRandomSequence(RandomSequence75 previousRandomSequence, RandomSequence75 newRandomSequence, out RandomSequence75 incrementedRandomSequence) 183 | { 184 | return previousRandomSequence.TryAddRandomBits(newRandomSequence, out incrementedRandomSequence); 185 | } 186 | 187 | /// 188 | /// 189 | /// Pure function. 190 | /// 191 | /// 192 | /// Creates a new ID based on the given values. 193 | /// 194 | /// 195 | /// The UTC timestamp in milliseconds since the epoch. 196 | internal Guid CreateCore(ulong timestamp, RandomSequence75 randomSequence) 197 | { 198 | Span resultBytes = stackalloc byte[16]; 199 | 200 | // Bits 0-47: Timestamp (48 bits, of which top 1 bit remains 0 until year 6000+, to fit in 2 signed longs) 201 | // Bits 48-51: Version marker (4 bits) 202 | // Bits 52-63: Randomness (12 bits) 203 | var leftHalf = timestamp << (64 - 48); 204 | leftHalf |= VersionMarker; 205 | leftHalf |= randomSequence.GetHigh12Bits(); 206 | 207 | // Bit 64: 0 (1 bit variant indicator, pretending to be legacy Apollo variant (0), to fit in 2 signed longs) 208 | // Bits 65-127: Randomness (63 bits) 209 | var rightHalf = randomSequence.GetLow63Bits(); 210 | 211 | BinaryPrimitives.WriteUInt64BigEndian(resultBytes, leftHalf); 212 | BinaryPrimitives.WriteUInt64BigEndian(resultBytes[8..], rightHalf); 213 | 214 | BinaryIdEncoder.TryDecodeGuid(resultBytes, out var result); 215 | 216 | System.Diagnostics.Debug.Assert(result != default, "A non-default value should have been generated."); 217 | System.Diagnostics.Debug.Assert((leftHalf | rightHalf) != 0UL, "A non-default value should have been generated."); 218 | System.Diagnostics.Debug.Assert(leftHalf >> (64 - 48) == timestamp, "The first component should have been the timestamp."); 219 | System.Diagnostics.Debug.Assert((leftHalf >> (64 - 48 - 4) & 0b_1111UL) == VersionMarkerByte, "The second component should have been the expected version marker."); 220 | System.Diagnostics.Debug.Assert(rightHalf >> 63 == 0UL, "The first variant indicator bit should have been zero."); 221 | 222 | return result; 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /Identities/DistributedIds/DistributedId128GeneratorScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Architect.AmbientContexts; 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Architect.Identities 6 | { 7 | /// 8 | /// 9 | /// Provides access to an through the Ambient Context pattern. 10 | /// 11 | /// 12 | /// This type provides a lightweight Inversion of Control (IoC) mechanism. 13 | /// The mechanism optimizes accessiblity (through a static property) at the cost of transparency, making it suitable for obvious, ubiquitous, rarely-changing dependencies. 14 | /// 15 | /// 16 | /// A default scope is available by default. 17 | /// Changing the scope is intended for testing purposes, to control the IDs generated. 18 | /// 19 | /// 20 | /// Outer code may construct a custom inside a using statement, causing any code within the using block to see that instance. 21 | /// 22 | /// 23 | public sealed class DistributedId128GeneratorScope : AmbientScope 24 | { 25 | static DistributedId128GeneratorScope() 26 | { 27 | var defaultGenerator = new DistributedId128Generator(); 28 | var defaultScope = new DistributedId128GeneratorScope(defaultGenerator, AmbientScopeOption.NoNesting); 29 | SetDefaultScope(defaultScope); 30 | } 31 | 32 | internal static DistributedId128GeneratorScope Current => GetAmbientScope()!; 33 | public static IDistributedId128Generator CurrentGenerator => Current.IdGenerator; 34 | 35 | private IDistributedId128Generator IdGenerator { get; } 36 | 37 | /// 38 | /// Establishes the given as the ambient one until the scope is disposed. 39 | /// 40 | public DistributedId128GeneratorScope(IDistributedId128Generator idGenerator) 41 | : this(idGenerator, AmbientScopeOption.ForceCreateNew) 42 | { 43 | this.Activate(); 44 | } 45 | 46 | private DistributedId128GeneratorScope(IDistributedId128Generator idGenerator, AmbientScopeOption scopeOption) 47 | : base(scopeOption) 48 | { 49 | this.IdGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator)); 50 | } 51 | 52 | protected override void DisposeImplementation() 53 | { 54 | if (this.IdGenerator is IDisposable disposableGenerator) 55 | disposableGenerator.Dispose(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Identities/DistributedIds/DistributedIdGeneratorScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Architect.AmbientContexts; 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Architect.Identities 6 | { 7 | /// 8 | /// 9 | /// Provides access to an through the Ambient Context pattern. 10 | /// 11 | /// 12 | /// This type provides a lightweight Inversion of Control (IoC) mechanism. 13 | /// The mechanism optimizes accessiblity (through a static property) at the cost of transparency, making it suitable for obvious, ubiquitous, rarely-changing dependencies. 14 | /// 15 | /// 16 | /// A default scope is available by default. 17 | /// Changing the scope is intended for testing purposes, to control the IDs generated. 18 | /// 19 | /// 20 | /// Outer code may construct a custom inside a using statement, causing any code within the using block to see that instance. 21 | /// 22 | /// 23 | public sealed class DistributedIdGeneratorScope : AmbientScope 24 | { 25 | static DistributedIdGeneratorScope() 26 | { 27 | var defaultGenerator = new DistributedIdGenerator(); 28 | var defaultScope = new DistributedIdGeneratorScope(defaultGenerator, AmbientScopeOption.NoNesting); 29 | SetDefaultScope(defaultScope); 30 | } 31 | 32 | internal static DistributedIdGeneratorScope Current => GetAmbientScope()!; 33 | public static IDistributedIdGenerator CurrentGenerator => Current.IdGenerator; 34 | 35 | private IDistributedIdGenerator IdGenerator { get; } 36 | 37 | /// 38 | /// Establishes the given as the ambient one until the scope is disposed. 39 | /// 40 | public DistributedIdGeneratorScope(IDistributedIdGenerator idGenerator) 41 | : this(idGenerator, AmbientScopeOption.ForceCreateNew) 42 | { 43 | this.Activate(); 44 | } 45 | 46 | private DistributedIdGeneratorScope(IDistributedIdGenerator idGenerator, AmbientScopeOption scopeOption) 47 | : base(scopeOption) 48 | { 49 | this.IdGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator)); 50 | } 51 | 52 | protected override void DisposeImplementation() 53 | { 54 | if (this.IdGenerator is IDisposable disposableGenerator) 55 | disposableGenerator.Dispose(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Identities/DistributedIds/IDistributedId128Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// Generates 128-bit ID values in a distributed way, with no synchronization between generators. 8 | /// 9 | public interface IDistributedId128Generator 10 | { 11 | #if NET7_0_OR_GREATER 12 | /// 13 | /// 14 | /// Returns a new ID value, encoded as a . 15 | /// 16 | /// 17 | /// Note that the numeric (.NET7+ only) and creation methods return identical values, encoded in different formats. 18 | /// Both are incremental. 19 | /// The two can be freely transcoded to one another using the extension methods provided by . 20 | /// 21 | /// 22 | UInt128 CreateId(); 23 | #endif 24 | 25 | /// 26 | /// 27 | /// Returns a new ID value, encoded as a . 28 | /// 29 | /// 30 | /// Note that the numeric (.NET7+ only) and creation methods return identical values, encoded in different formats. 31 | /// Both are incremental. 32 | /// The two can be freely transcoded to one another using the extension methods provided by . 33 | /// 34 | /// 35 | Guid CreateGuid(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Identities/DistributedIds/IDistributedIdGenerator.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // Namespace does not match folder structure 2 | namespace Architect.Identities 3 | { 4 | /// 5 | /// Generates decimal ID values in a distributed way, with no synchronization between generators. 6 | /// 7 | public interface IDistributedIdGenerator 8 | { 9 | /// 10 | /// Returns a new ID value. 11 | /// 12 | decimal CreateId(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Identities/DistributedIds/IncrementalDistributedId128Generator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Architect.Identities 6 | { 7 | /// 8 | /// 9 | /// A simple incremental , intended for testing purposes. 10 | /// 11 | /// 12 | /// Generates ID values equivalent to 1, 2, 3, and so on. 13 | /// 14 | /// 15 | /// Although this type is thread-safe, single-threaded use additionally allows the generated IDs to be predicted. 16 | /// 17 | /// 18 | public sealed class IncrementalDistributedId128Generator : IDistributedId128Generator 19 | { 20 | #if NET7_0_OR_GREATER 21 | private readonly UInt128 _firstId; 22 | #endif 23 | private long _previousIncrement = -1; 24 | 25 | public IncrementalDistributedId128Generator() 26 | { 27 | #if NET7_0_OR_GREATER 28 | this._firstId = 1; 29 | #else 30 | this._previousIncrement = 0; 31 | #endif 32 | } 33 | 34 | #if NET7_0_OR_GREATER 35 | 36 | public IncrementalDistributedId128Generator(Guid firstId) 37 | : this(firstId.ToUInt128()) 38 | { 39 | } 40 | 41 | public IncrementalDistributedId128Generator(UInt128 firstId) 42 | { 43 | this._firstId = firstId; 44 | } 45 | 46 | public UInt128 CreateId() 47 | { 48 | return this._firstId + (ulong)Interlocked.Increment(ref this._previousIncrement); 49 | } 50 | 51 | #endif 52 | 53 | public Guid CreateGuid() 54 | { 55 | #if NET7_0_OR_GREATER 56 | var result = this.CreateId().ToGuid(); 57 | return result; 58 | #else 59 | var increment = (ulong)Interlocked.Increment(ref this._previousIncrement); 60 | 61 | Span bytes = stackalloc byte[16]; 62 | BinaryIdEncoder.Encode(increment, bytes[8..]); 63 | BinaryIdEncoder.TryDecodeGuid(bytes, out var result); 64 | return result; 65 | #endif 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Identities/DistributedIds/IncrementalDistributedIdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// 8 | /// A simple incremental , intended for testing purposes. 9 | /// 10 | /// 11 | /// Generates ID values 1, 2, 3, and so on. 12 | /// 13 | /// 14 | /// Although this type is thread-safe, single-threaded use additionally allows the generated IDs to be predicted. 15 | /// 16 | /// 17 | public sealed class IncrementalDistributedIdGenerator : IDistributedIdGenerator 18 | { 19 | private readonly ulong _firstId; 20 | private long _previousIncrement = -1; 21 | 22 | public IncrementalDistributedIdGenerator() 23 | : this(firstId: 1) 24 | { 25 | } 26 | 27 | public IncrementalDistributedIdGenerator(ulong firstId = 1) 28 | { 29 | this._firstId = firstId; 30 | } 31 | 32 | public decimal CreateId() 33 | { 34 | var id = this.GenerateId(); 35 | return id; 36 | } 37 | 38 | private decimal GenerateId() 39 | { 40 | return this._firstId + (decimal)Interlocked.Increment(ref this._previousIncrement); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Identities/DistributedIds/RandomSequence48.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Security.Cryptography; 4 | 5 | #pragma warning disable IDE0130 // Namespace does not match folder structure 6 | namespace Architect.Identities 7 | { 8 | /// 9 | /// 10 | /// A sequence of 48 bits (6 bytes) of pseudorandom data. 11 | /// This type is castable to a ulong with the high 16 bits set to zero. 12 | /// 13 | /// 14 | /// This type supports the creation of new values by adding bits of pseudorandom data to an existing value. 15 | /// As such, statistically, values may lean towards higher values, and entropy may not be the full 48 bits, depending on use. 16 | /// 17 | /// 18 | /// A newly created instance will have 48 bits of random data. 19 | /// By calling , a value with most of the parameter value's bits added is created. 20 | /// 21 | /// 22 | /// The random data originates from a cryptographically-secure pseudorandom number generator (CSPRNG). 23 | /// 24 | /// 25 | /// Although technically an instance can be created using the default constructor, 26 | /// all public operations (e.g. cast or ) will throw for such an instance. 27 | /// 28 | /// 29 | internal readonly struct RandomSequence48 30 | { 31 | internal const ulong MaxValue = UInt64.MaxValue >> 16; 32 | /// 33 | /// The number of bits added by the operation. 34 | /// 35 | internal const int AdditionalBitCount = 41; 36 | /// 37 | /// A mask to keep the low bits of a ulong, in order to add additional random bits to an existing value. 38 | /// 39 | private const ulong AdditionalBitMask = UInt64.MaxValue >> (64 - AdditionalBitCount); 40 | 41 | /// 42 | /// A pseudorandom value with the most significant 2 bytes set to zero. 43 | /// 44 | private ulong Value => this._value == 0UL 45 | ? ThrowCreateOnlyThroughCreateMethodException() 46 | : this._value; 47 | private readonly ulong _value; 48 | 49 | private static ulong ThrowCreateOnlyThroughCreateMethodException() => throw new InvalidOperationException($"Create this only through {nameof(RandomSequence48)}.{nameof(Create)}."); 50 | 51 | /// 52 | /// Constructs a new randomized instance. 53 | /// 54 | /// A dummy parameter to distinguish this from the struct's mandatory default constructor. 55 | private RandomSequence48(byte _) 56 | { 57 | Span bytes = stackalloc byte[8]; 58 | var low6Bytes = bytes[..6]; // Fill the low 6 bytes from little-endian perspective (i.e. the left 6) 59 | RandomNumberGenerator.Fill(low6Bytes); 60 | 61 | // Use little endian to ensure that the 2 zero bytes on the right are considered the most significant 62 | this._value = BinaryPrimitives.ReadUInt64LittleEndian(bytes); 63 | 64 | // Avoid 0, which we use to protect against incorrectly created instances 65 | if (this._value == 0UL) 66 | this._value = 1UL; 67 | 68 | System.Diagnostics.Debug.Assert(this._value != 0UL, "The data does not look randomized."); 69 | System.Diagnostics.Debug.Assert(bytes.EndsWith(new byte[2]), "The high 2 bytes should have been zero."); 70 | System.Diagnostics.Debug.Assert(this._value >> (64 - 16) == 0UL, "The high 16 bits should have been zero."); 71 | } 72 | 73 | /// 74 | /// Constructs a new instance that contains the given value. 75 | /// 76 | private RandomSequence48(ulong value) 77 | { 78 | if (value == 0UL || value >> (64 - 16) != 0UL) 79 | throw new ArgumentException("The value must be a randomized, non-zero value with the high 2 bytes set to zero."); 80 | 81 | this._value = value; 82 | } 83 | 84 | /// 85 | /// Generates a new 48-bit pseudorandom value. 86 | /// 87 | public static RandomSequence48 Create() 88 | { 89 | return new RandomSequence48(_: default); 90 | } 91 | 92 | /// 93 | /// Simulates an instance with the given value. 94 | /// For testing purposes only. 95 | /// 96 | /// A value with the high 2 bytes set to zero. 97 | [Obsolete("For testing purposes only.")] 98 | internal static RandomSequence48 CreatedSimulated(ulong value) 99 | { 100 | return new RandomSequence48(value); 101 | } 102 | 103 | /// 104 | /// 105 | /// Returns true and outputs a new instance that contains the current one's value with random data from the given one added to it. 106 | /// If the result would overflow, this method returns false instead. 107 | /// 108 | /// 109 | public bool TryAddRandomBits(RandomSequence48 additionalRandomSource, out RandomSequence48 result) 110 | { 111 | var value = this.Value; 112 | var randomIncrement = additionalRandomSource.Value; 113 | 114 | randomIncrement &= AdditionalBitMask; 115 | 116 | // Avoid incrementing by 0, which would introduce a collision 117 | if (randomIncrement == 0UL) 118 | randomIncrement = 1UL; 119 | 120 | if (randomIncrement > MaxValue - value) // Addition would overflow our intended maximum 121 | { 122 | result = this; 123 | return false; 124 | } 125 | 126 | unchecked // Cannot overflow UInt64 here anyway 127 | { 128 | value += randomIncrement; 129 | } 130 | 131 | result = new RandomSequence48(value); 132 | return true; 133 | } 134 | 135 | /// 136 | /// Converts the struct to a ulong filled with pseudorandom data, except that the high 2 bytes are set to zero. 137 | /// 138 | public static implicit operator ulong(RandomSequence48 sequence) => sequence.Value; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Identities/DistributedIds/RandomSequence75.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Security.Cryptography; 4 | using Architect.Identities.Encodings; 5 | using Binary128 = 6 | #if NET7_0_OR_GREATER 7 | System.UInt128; 8 | #else 9 | System.Decimal; 10 | #endif 11 | 12 | #pragma warning disable IDE0130 // Namespace does not match folder structure 13 | namespace Architect.Identities 14 | { 15 | /// 16 | /// 17 | /// A sequence of 75 bits of pseudorandom data. 18 | /// 19 | /// 20 | /// This type supports the creation of new values by adding bits of pseudorandom data to an existing value. 21 | /// As such, statistically, values may lean towards higher values, and entropy may not be the full 75 bits, depending on use. 22 | /// 23 | /// 24 | /// A newly created instance will have 75 bits of random data. 25 | /// By calling , a value with most of the parameter value's bits added is created. 26 | /// 27 | /// 28 | /// The random data originates from a cryptographically-secure pseudorandom number generator (CSPRNG). 29 | /// 30 | /// 31 | /// Although technically an instance can be created using the default constructor, 32 | /// all public operations (e.g. cast or ) will throw for such an instance. 33 | /// 34 | /// 35 | internal readonly struct RandomSequence75 36 | { 37 | internal const ulong MaxHighValue = UInt64.MaxValue >> (64 - 11); 38 | internal static readonly Binary128 MaxValue = 39 | #if NET7_0_OR_GREATER 40 | new Binary128(upper: UInt64.MaxValue >> (128 - 75), lower: UInt64.MaxValue); 41 | #else 42 | new decimal(lo: ~0, mid: ~0, hi: (int)MaxHighValue, isNegative: false, scale: 0); 43 | #endif 44 | 45 | /// 46 | /// The number of bits added by the operation. 47 | /// 48 | internal const int AdditionalBitCount = 58; 49 | /// 50 | /// A mask to keep the low bits of a ulong, in order to add additional random bits to an existing value. 51 | /// 52 | private static readonly ulong AdditionalBitMask = UInt64.MaxValue >> (64 - AdditionalBitCount); 53 | 54 | /// 55 | /// Contains the value's high 11 bits as its LSB. 56 | /// 57 | private ulong High { get; } 58 | /// 59 | /// Contains the value's low 64 bits. 60 | /// 61 | private ulong Low { get; } 62 | 63 | private static ulong ThrowCreateOnlyThroughCreateMethodException() => throw new InvalidOperationException($"Create this only through {nameof(RandomSequence75)}.{nameof(Create)}."); 64 | 65 | /// 66 | /// Constructs a new randomized instance. 67 | /// 68 | /// A dummy parameter to distinguish this from the struct's mandatory default constructor. 69 | private RandomSequence75(byte _) 70 | { 71 | Span bytes = stackalloc byte[16]; 72 | RandomNumberGenerator.Fill(bytes); 73 | 74 | var ulongs = MemoryMarshal.Cast(bytes); 75 | var high = ulongs[0] >> (64 - 11); // 11 LSB populated 76 | var low = ulongs[1]; // All 64 bits populated 77 | 78 | // Avoid 0, which we use to protect against incorrectly created instances 79 | if ((high | low) == 0UL) 80 | low = 1UL; 81 | 82 | this.High = high; 83 | this.Low = low; 84 | 85 | System.Diagnostics.Debug.Assert(this.High != 0UL || this.Low != 0UL, "The data does not look randomized."); 86 | System.Diagnostics.Debug.Assert(this.High >> 11 == 0UL, "The high 53 bits should have been zero."); 87 | } 88 | 89 | /// 90 | /// Constructs a new instance that contains the given value. 91 | /// 92 | private RandomSequence75(ulong high, ulong low) 93 | { 94 | if ((high | low) == 0UL || high > MaxHighValue) 95 | throw new ArgumentException("The value must be a randomized, non-zero value with the high 53 bits set to zero."); 96 | 97 | this.High = high; 98 | this.Low = low; 99 | } 100 | 101 | /// 102 | /// Generates a new 75-bit pseudorandom value. 103 | /// 104 | public static RandomSequence75 Create() 105 | { 106 | return new RandomSequence75(_: default); 107 | } 108 | 109 | /// 110 | /// Simulates an instance with the given value. 111 | /// For testing purposes only. 112 | /// 113 | /// A value with the high 53 bits set to zero. 114 | [Obsolete("For testing purposes only.")] 115 | internal static RandomSequence75 CreatedSimulated(Binary128 value) 116 | { 117 | var (high, low) = GetHighAndLow(value); 118 | return CreatedSimulated(high: high, low: low); 119 | } 120 | 121 | /// 122 | /// Simulates an instance with the given value. 123 | /// For testing purposes only. 124 | /// 125 | /// A value with the high 53 bits set to zero. 126 | [Obsolete("For testing purposes only.")] 127 | internal static RandomSequence75 CreatedSimulated(ulong high, ulong low) 128 | { 129 | return new RandomSequence75(high: high, low: low); 130 | } 131 | 132 | private Binary128 GetValue() 133 | { 134 | if ((this.High | this.Low) == 0UL) 135 | ThrowCreateOnlyThroughCreateMethodException(); 136 | 137 | #if NET7_0_OR_GREATER 138 | return new Binary128(upper: this.High, lower: this.Low); 139 | #else 140 | return new Binary128( 141 | lo: (int)(this.Low & UInt32.MaxValue), 142 | mid: (int)(this.Low >> 32), 143 | hi: (int)this.High, 144 | isNegative: false, 145 | scale: 0); 146 | #endif 147 | } 148 | 149 | private static (ulong, ulong) GetHighAndLow(Binary128 value) 150 | { 151 | #if NET7_0_OR_GREATER 152 | var high = (ulong)(value >> 64); 153 | var low = (ulong)value; 154 | return (high, low); 155 | #else 156 | var decimals = MemoryMarshal.CreateSpan(ref value, length: 1); 157 | var ints = MemoryMarshal.Cast(decimals); 158 | var lo = (ulong)DecimalStructure.GetLo(ints); 159 | var mid = (ulong)DecimalStructure.GetMid(ints); 160 | var hi = (ulong)DecimalStructure.GetHi(ints); 161 | var low = (mid << 32) | lo; 162 | return (hi, low); 163 | #endif 164 | } 165 | 166 | public ulong GetHigh12Bits() 167 | { 168 | if ((this.High | this.Low) == 0UL) 169 | ThrowCreateOnlyThroughCreateMethodException(); 170 | 171 | var result = this.High << 1; // 11 bits 172 | result |= this.Low >> 63; // +1 bit 173 | return result; 174 | } 175 | 176 | public ulong GetLow63Bits() 177 | { 178 | if ((this.High | this.Low) == 0UL) 179 | ThrowCreateOnlyThroughCreateMethodException(); 180 | 181 | var result = this.Low & (UInt64.MaxValue >> 1); // 63 bits 182 | return result; 183 | } 184 | 185 | /// 186 | /// 187 | /// Returns true and outputs a new instance that contains the current one's value with random data from the given one added to it. 188 | /// If the result would overflow, this method returns false instead. 189 | /// 190 | /// 191 | public bool TryAddRandomBits(RandomSequence75 additionalRandomSource, out RandomSequence75 result) 192 | { 193 | var value = this.GetValue(); 194 | var randomIncrement = additionalRandomSource.Low; 195 | 196 | randomIncrement &= AdditionalBitMask; 197 | 198 | // Avoid incrementing by 0, which would introduce a collision 199 | if (randomIncrement == 0UL) 200 | randomIncrement = 1UL; 201 | 202 | if (randomIncrement > MaxValue - value) // Addition would overflow our intended maximum 203 | { 204 | result = this; 205 | return false; 206 | } 207 | 208 | unchecked // Cannot overflow here anyway 209 | { 210 | value += randomIncrement; 211 | } 212 | 213 | var (high, low) = GetHighAndLow(value); 214 | result = new RandomSequence75(high: high, low: low); 215 | return true; 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Identities/Encodings/Base62Encoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace Architect.Identities.Encodings 6 | { 7 | /// 8 | /// A limited base62 encoder, aimed at simplicity, efficiency, and useful endianness. 9 | /// 10 | internal static class Base62Encoder 11 | { 12 | private static Base62Alphabet DefaultAlphabet { get; } = new Base62Alphabet(Encoding.ASCII.GetBytes("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")); 13 | 14 | /// 15 | /// 16 | /// Converts the given 8 bytes to 11 base62 chars. 17 | /// 18 | /// 19 | public static void ToBase62Chars8(ReadOnlySpan bytes, Span chars) 20 | { 21 | System.Diagnostics.Debug.Assert(bytes.Length >= 8); 22 | System.Diagnostics.Debug.Assert(chars.Length >= 11); 23 | 24 | var forwardAlphabet = DefaultAlphabet.ForwardAlphabet; 25 | 26 | EncodeBlock(forwardAlphabet, bytes, chars); 27 | } 28 | 29 | /// 30 | /// 31 | /// Converts the given 16 bytes to 22 base62 chars. 32 | /// 33 | /// 34 | /// The input and output spans must not overlap. This is asserted in debug mode. 35 | /// 36 | /// 37 | public static void ToBase62Chars16(ReadOnlySpan bytes, Span chars) 38 | { 39 | System.Diagnostics.Debug.Assert(bytes.Length >= 16); 40 | System.Diagnostics.Debug.Assert(chars.Length >= 22); 41 | System.Diagnostics.Debug.Assert(!bytes.Overlaps(chars), "The input and output spans must not overlap, as the first block's output will overwrite the second block of input."); 42 | 43 | var forwardAlphabet = DefaultAlphabet.ForwardAlphabet; 44 | 45 | EncodeBlock(forwardAlphabet, bytes, chars); 46 | bytes = bytes[8..]; 47 | chars = chars[11..]; 48 | EncodeBlock(forwardAlphabet, bytes, chars); 49 | } 50 | 51 | private static void EncodeBlock(ReadOnlySpan alphabet, ReadOnlySpan bytes, Span chars) 52 | { 53 | System.Diagnostics.Debug.Assert(alphabet.Length == 62); 54 | System.Diagnostics.Debug.Assert(bytes.Length >= 8); 55 | System.Diagnostics.Debug.Assert(chars.Length >= 11); 56 | 57 | var ulongValue = 0UL; 58 | for (var i = 0; i < 8; i++) ulongValue = (ulongValue << 8) | bytes[i]; 59 | 60 | // Can encode 8 bytes as 11 chars 61 | for (var i = 11 - 1; i >= 0; i--) 62 | { 63 | var quotient = ulongValue / 62UL; 64 | var remainder = ulongValue - 62UL * quotient; 65 | ulongValue = quotient; 66 | chars[i] = alphabet[(int)remainder]; 67 | } 68 | } 69 | 70 | /// 71 | /// 72 | /// Converts the given 11 base62 chars to 8 bytes. 73 | /// 74 | /// 75 | /// Throws an on invalid input. 76 | /// 77 | /// 78 | public static void FromBase62Chars11(ReadOnlySpan chars, Span bytes) 79 | { 80 | System.Diagnostics.Debug.Assert(chars.Length >= 11); 81 | System.Diagnostics.Debug.Assert(bytes.Length >= 8); 82 | 83 | var reverseAlphabet = DefaultAlphabet.ReverseAlphabet; 84 | 85 | DecodeBlock(reverseAlphabet, chars, bytes); 86 | } 87 | 88 | /// 89 | /// 90 | /// Converts the given 22 base62 chars to 16 bytes. 91 | /// 92 | /// 93 | /// Throws an on invalid input. 94 | /// 95 | /// 96 | public static void FromBase62Chars22(ReadOnlySpan chars, Span bytes) 97 | { 98 | System.Diagnostics.Debug.Assert(chars.Length >= 22); 99 | System.Diagnostics.Debug.Assert(bytes.Length >= 16); 100 | 101 | var reverseAlphabet = DefaultAlphabet.ReverseAlphabet; 102 | 103 | DecodeBlock(reverseAlphabet, chars, bytes); 104 | chars = chars[11..]; 105 | bytes = bytes[8..]; 106 | DecodeBlock(reverseAlphabet, chars, bytes); 107 | } 108 | 109 | private static void DecodeBlock(ReadOnlySpan reverseAlphabet, ReadOnlySpan chars11, Span bytes) 110 | { 111 | System.Diagnostics.Debug.Assert(reverseAlphabet.Length == 256); 112 | System.Diagnostics.Debug.Assert(chars11.Length >= 11); 113 | System.Diagnostics.Debug.Assert(bytes.Length >= 8); 114 | 115 | // Can decode 11 chars back into 8 bytes 116 | var ulongValue = 0UL; 117 | for (var i = 0; i < 11; i++) 118 | { 119 | var chr = chars11[i]; 120 | var value = (ulong)reverseAlphabet[chr]; // -1 (invalid character) becomes UInt64.MaxValue 121 | if (value >= 62) throw new ArgumentException("The input encoding is invalid."); 122 | 123 | ulongValue = ulongValue * 62 + value; 124 | } 125 | 126 | for (var i = 8 - 1; i >= 0; i--) 127 | { 128 | bytes[i] = (byte)ulongValue; 129 | ulongValue >>= 8; 130 | } 131 | } 132 | } 133 | 134 | internal sealed class Base62Alphabet 135 | { 136 | public override bool Equals(object? obj) => obj is Base62Alphabet other && other.ForwardAlphabet.SequenceEqual(this.ForwardAlphabet); 137 | public override int GetHashCode() => this.ForwardAlphabet[0].GetHashCode() ^ this.ForwardAlphabet[61].GetHashCode(); 138 | 139 | public ReadOnlySpan ForwardAlphabet => this._alphabet; 140 | private readonly byte[] _alphabet; 141 | 142 | public ReadOnlySpan ReverseAlphabet => this._reverseAlphabet.AsSpan(); 143 | private readonly sbyte[] _reverseAlphabet; 144 | 145 | /// 146 | /// Constructs a Base62 alphabet, including its reverse representation. 147 | /// The result should be cached for reuse. 148 | /// 149 | public Base62Alphabet(ReadOnlySpan alphabet) 150 | { 151 | if (alphabet.Length != 62) throw new ArgumentException("Expected an alphabet of length 62."); 152 | 153 | this._alphabet = alphabet.ToArray(); 154 | 155 | if (this._alphabet.Any(chr => chr == 0)) 156 | throw new ArgumentException("The NULL character is not allowed."); 157 | if (this._alphabet.Any(chr => chr > 127)) 158 | throw new ArgumentException("Non-ASCII characters are not allowed."); 159 | if (this._alphabet.Distinct().Count() != this._alphabet.Length) 160 | throw new ArgumentException("All characters in the alphabet must be distinct."); 161 | 162 | this._reverseAlphabet = GetReverseAlphabet(this.ForwardAlphabet); 163 | 164 | System.Diagnostics.Debug.Assert(this.ReverseAlphabet.Length == 256); 165 | } 166 | 167 | /// 168 | /// 169 | /// Creates a reverse alphabet for the given alphabet. 170 | /// 171 | /// 172 | /// When indexing into the slot matching a character's numeric value, the result is the value between 0 and 61 (inclusive) represented by the character. 173 | /// (Slots not related to any of the alphabet's characters contain -1.) 174 | /// 175 | /// 176 | /// The result should be cached for reuse. 177 | /// 178 | /// 179 | internal static sbyte[] GetReverseAlphabet(ReadOnlySpan alphabet) 180 | { 181 | if (alphabet.Length != 62) throw new ArgumentException("Expected an alphabet of length 62."); 182 | 183 | var result = new sbyte[256]; 184 | Array.Fill(result, (sbyte)-1); 185 | for (sbyte i = 0; i < alphabet.Length; i++) result[alphabet[i]] = i; 186 | return result; 187 | } 188 | } 189 | } 190 | 191 | -------------------------------------------------------------------------------- /Identities/Encodings/DecimalStructure.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Architect.Identities.Encodings 6 | { 7 | /// 8 | /// Provides operations related to the binary layout of decimals. 9 | /// 10 | internal static class DecimalStructure 11 | { 12 | /// 13 | /// Throws if the binary layout of decimals is different then expected, i.e. if it has changed for the current runtime since the time of writing. 14 | /// 15 | public static void ThrowIfDecimalStructureIsUnexpected() 16 | { 17 | // Fill a decimal's bits according to its current structure (yes, decimal composition is weird) 18 | Span decimals = stackalloc decimal[1]; 19 | var ints = MemoryMarshal.Cast(decimals); 20 | ints[0] = 0; // Sign and scale 21 | ints[1] = 3; // Hi 22 | ints[2] = 1; // Lo 23 | ints[3] = 2; // Mid 24 | 25 | // Confirm that it interprets them as expected 26 | var components = Decimal.GetBits(decimals[0]); // Lo, mid, hi, sign-and-scale 27 | if (components[0] != 1 || 28 | components[1] != 2 || 29 | components[2] != 3 || 30 | components[3] != 0) 31 | { 32 | throw new NotSupportedException("The binary structure of decimals has changed. An updated package version is needed to avoid handling them incorrectly."); 33 | } 34 | } 35 | 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public static int GetSignAndScale(Span decimalComponents) => decimalComponents[0]; 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public static int GetSignAndScale(ReadOnlySpan decimalComponents) => decimalComponents[0]; 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | public static int GetLo(Span decimalComponents) => decimalComponents[2]; 43 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 44 | public static int GetLo(ReadOnlySpan decimalComponents) => decimalComponents[2]; 45 | 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public static int GetMid(Span decimalComponents) => decimalComponents[3]; 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public static int GetMid(ReadOnlySpan decimalComponents) => decimalComponents[3]; 50 | 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | public static int GetHi(Span decimalComponents) => decimalComponents[1]; 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | public static int GetHi(ReadOnlySpan decimalComponents) => decimalComponents[1]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Identities/Encodings/HexadecimalEncoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace Architect.Identities.Encodings 6 | { 7 | /// 8 | /// A simple hexadecimal encoder, aimed at simplicity and efficiency. 9 | /// 10 | internal static class HexadecimalEncoder 11 | { 12 | private static readonly HexAlphabet DefaultAlphabet = new HexAlphabet(Encoding.ASCII.GetBytes("0123456789ABCDEF")); 13 | 14 | /// 15 | /// 16 | /// Converts the given bytes to hexadecimal chars. 17 | /// 18 | /// 19 | public static void ToHexChars(ReadOnlySpan bytes, Span chars, int inputByteCount) 20 | { 21 | System.Diagnostics.Debug.Assert(bytes.Length >= inputByteCount); 22 | System.Diagnostics.Debug.Assert(chars.Length >= 2 * inputByteCount); 23 | 24 | var alphabet = DefaultAlphabet.ForwardAlphabet; 25 | 26 | // E.g. if inputByteCount = 16, write the first two characters at position (16 * 2) - 1 = 31 and the one before it 27 | for (var i = (inputByteCount << 1) - 1; i >= 0; i -= 2) 28 | { 29 | var byteValue = bytes[i >> 1]; 30 | 31 | chars[i - 1] = alphabet[byteValue >> 4]; 32 | chars[i] = alphabet[byteValue & 15]; 33 | } 34 | } 35 | 36 | /// 37 | /// 38 | /// Converts the given hexadecimal chars to bytes. 39 | /// 40 | /// 41 | /// Throws an on invalid input. 42 | /// 43 | /// 44 | public static void FromHexChars(ReadOnlySpan chars, Span bytes, int inputByteCount) 45 | { 46 | System.Diagnostics.Debug.Assert(inputByteCount % 1 == 0, "An even number of input bytes is expected."); 47 | System.Diagnostics.Debug.Assert(chars.Length >= inputByteCount); 48 | System.Diagnostics.Debug.Assert(bytes.Length >= inputByteCount / 2); 49 | 50 | var alphabet = DefaultAlphabet.ReverseAlphabet; 51 | 52 | for (var i = 0; i < inputByteCount; i += 2) 53 | { 54 | int leftValue = alphabet[chars[i]]; 55 | int rightValue = alphabet[chars[i + 1]]; 56 | 57 | if (leftValue < 0 || rightValue < 0) throw new ArgumentException("The input encoding is invalid."); 58 | 59 | var value = leftValue << 4 | rightValue; 60 | 61 | System.Diagnostics.Debug.Assert(value <= Byte.MaxValue); 62 | 63 | bytes[i >> 1] = (byte)value; 64 | } 65 | } 66 | } 67 | 68 | internal sealed class HexAlphabet 69 | { 70 | public override bool Equals(object? obj) => obj is HexAlphabet other && other.ForwardAlphabet.SequenceEqual(this.ForwardAlphabet); 71 | public override int GetHashCode() => this.ForwardAlphabet[0].GetHashCode() ^ this.ForwardAlphabet[15].GetHashCode(); 72 | 73 | public ReadOnlySpan ForwardAlphabet => this._alphabet; 74 | private readonly byte[] _alphabet; 75 | 76 | public ReadOnlySpan ReverseAlphabet => this._reverseAlphabet.AsSpan(); 77 | private readonly sbyte[] _reverseAlphabet; 78 | 79 | /// 80 | /// Constructs a Hexadecimal alphabet, including its reverse representation. 81 | /// The result should be cached for reuse. 82 | /// 83 | public HexAlphabet(ReadOnlySpan alphabet) 84 | { 85 | if (alphabet.Length != 16) throw new ArgumentException("Expected an alphabet of length 16."); 86 | 87 | this._alphabet = alphabet.ToArray(); 88 | 89 | if (this._alphabet.Any(chr => chr == 0)) 90 | throw new ArgumentException("The NULL character is not allowed."); 91 | if (this._alphabet.Any(chr => chr > 127)) 92 | throw new ArgumentException("Non-ASCII characters are not allowed."); 93 | if (this._alphabet.Distinct().Count() != this._alphabet.Length) 94 | throw new ArgumentException("All characters in the alphabet must be distinct."); 95 | 96 | this._reverseAlphabet = GetReverseAlphabet(this.ForwardAlphabet); 97 | 98 | System.Diagnostics.Debug.Assert(this.ReverseAlphabet.Length == 256); 99 | } 100 | 101 | /// 102 | /// 103 | /// Creates a reverse alphabet for the given alphabet. 104 | /// 105 | /// 106 | /// When indexing into the slot matching a character's numeric value, the result is the value between 0 and 15 (inclusive) represented by the character. 107 | /// (Slots not related to any of the alphabet's characters contain -1.) 108 | /// 109 | /// 110 | /// The result should be cached for reuse. 111 | /// 112 | /// 113 | /// An uppercase alphabet. For each uppercase letter, its lowercase equivalent will be mapped to the same binary value. 114 | internal static sbyte[] GetReverseAlphabet(ReadOnlySpan alphabet) 115 | { 116 | if (alphabet.Length != 16) throw new ArgumentException("Expected an alphabet of length 16."); 117 | 118 | var result = new sbyte[256]; 119 | 120 | Array.Fill(result, (sbyte)-1); 121 | 122 | for (sbyte i = 0; i < alphabet.Length; i++) 123 | { 124 | // Map the character to its represented binary value 125 | var charValue = alphabet[i]; 126 | 127 | // If the character is an uppercase letter, map its lowercase equivalent to the same represented binary value 128 | result[charValue] = i; 129 | if (charValue >= 'A' && charValue <= 'Z') 130 | result[charValue + 32] = i; 131 | } 132 | 133 | return result; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Identities/Encodings/IdEncodingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | public static class IdEncodingExtensions 7 | { 8 | /// 9 | /// 10 | /// Returns an 11-character alphanumeric representation of the given ID. 11 | /// 12 | /// 13 | /// The positive ID to encode. 14 | public static string ToAlphanumeric(this long id) => AlphanumericIdEncoder.Encode(id); 15 | /// 16 | /// 17 | /// Returns an 11-character alphanumeric representation of the given ID. 18 | /// 19 | /// 20 | /// The ID to encode. 21 | public static string ToAlphanumeric(this ulong id) => AlphanumericIdEncoder.Encode(id); 22 | /// 23 | /// 24 | /// Returns a 16-character alphanumeric representation of the given ID. 25 | /// 26 | /// 27 | /// A positive decimal with 0 decimal places, consisting of no more than 28 digits, such as a value generated using . 28 | public static string ToAlphanumeric(this decimal id) => AlphanumericIdEncoder.Encode(id); 29 | /// 30 | /// 31 | /// Returns a 22-character alphanumeric representation of the given ID. 32 | /// 33 | /// 34 | /// Any sequence of bytes stored in a . 35 | public static string ToAlphanumeric(this Guid id) => AlphanumericIdEncoder.Encode(id); 36 | 37 | /// 38 | /// 39 | /// Outputs an 11-character alphanumeric representation of the given ID. 40 | /// 41 | /// 42 | /// The positive ID to encode. 43 | /// At least 11 bytes, to write the alphanumeric representation to. 44 | public static void ToAlphanumeric(this long id, Span bytes) => AlphanumericIdEncoder.Encode(id, bytes); 45 | /// 46 | /// 47 | /// Outputs an 11-character alphanumeric representation of the given ID. 48 | /// 49 | /// 50 | /// The ID to encode. 51 | /// At least 11 bytes, to write the alphanumeric representation to. 52 | public static void ToAlphanumeric(this ulong id, Span bytes) => AlphanumericIdEncoder.Encode(id, bytes); 53 | /// 54 | /// 55 | /// Outputs a 16-character alphanumeric representation of the given ID. 56 | /// 57 | /// 58 | /// A positive decimal with 0 decimal places, consisting of no more than 28 digits, such as a value generated using . 59 | /// At least 16 bytes, to write the alphanumeric representation to. 60 | public static void ToAlphanumeric(this decimal id, Span bytes) => AlphanumericIdEncoder.Encode(id, bytes); 61 | /// 62 | /// 63 | /// Outputs a 22-character alphanumeric representation of the given ID. 64 | /// 65 | /// 66 | /// Any sequence of bytes stored in a . 67 | /// At least 22 bytes, to write the alphanumeric representation to. 68 | public static void ToAlphanumeric(this Guid id, Span bytes) => AlphanumericIdEncoder.Encode(id, bytes); 69 | 70 | /// 71 | /// 72 | /// Returns a 16-character hexadecimal representation of the given ID. 73 | /// 74 | /// 75 | /// The positive ID to encode. 76 | public static string ToHexadecimal(this long id) => HexadecimalIdEncoder.Encode(id); 77 | /// 78 | /// 79 | /// Returns a 16-character hexadecimal representation of the given ID. 80 | /// 81 | /// 82 | /// The ID to encode. 83 | public static string ToHexadecimal(this ulong id) => HexadecimalIdEncoder.Encode(id); 84 | /// 85 | /// 86 | /// Returns a 26-character hexadecimal representation of the given ID. 87 | /// 88 | /// 89 | /// A positive decimal with 0 decimal places, consisting of no more than 28 digits, such as a value generated using . 90 | public static string ToHexadecimal(this decimal id) => HexadecimalIdEncoder.Encode(id); 91 | /// 92 | /// 93 | /// Returns a 32-character hexadecimal representation of the given ID. 94 | /// 95 | /// 96 | /// Any sequence of bytes stored in a . 97 | public static string ToHexadecimal(this Guid id) => HexadecimalIdEncoder.Encode(id); 98 | 99 | /// 100 | /// 101 | /// Outputs a 16-character hexadecimal representation of the given ID. 102 | /// 103 | /// 104 | /// The positive ID to encode. 105 | /// At least 16 bytes, to write the hexadecimal representation to. 106 | public static void ToHexadecimal(this long id, Span bytes) => HexadecimalIdEncoder.Encode(id, bytes); 107 | /// 108 | /// 109 | /// Outputs a 16-character hexadecimal representation of the given ID. 110 | /// 111 | /// 112 | /// The ID to encode. 113 | /// At least 16 bytes, to write the hexadecimal representation to. 114 | public static void ToHexadecimal(this ulong id, Span bytes) => HexadecimalIdEncoder.Encode(id, bytes); 115 | /// 116 | /// 117 | /// Outputs a 26-character hexadecimal representation of the given ID. 118 | /// 119 | /// 120 | /// A positive decimal with 0 decimal places, consisting of no more than 28 digits, such as a value generated using . 121 | /// At least 26 bytes, to write the hexadecimal representation to. 122 | public static void ToHexadecimal(this decimal id, Span bytes) => HexadecimalIdEncoder.Encode(id, bytes); 123 | /// 124 | /// 125 | /// Outputs a 32-character hexadecimal representation of the given ID. 126 | /// 127 | /// 128 | /// Any sequence of bytes stored in a . 129 | /// At least 32 bytes, to write the hexadecimal representation to. 130 | public static void ToHexadecimal(this Guid id, Span bytes) => HexadecimalIdEncoder.Encode(id, bytes); 131 | 132 | #region Transcoding 133 | 134 | #if NET7_0_OR_GREATER 135 | 136 | /// 137 | /// 138 | /// Transcodes the given into a , retaining the lexicographical ordering. 139 | /// 140 | /// 141 | /// Input values and their respective output values have the same relative ordering. 142 | /// The same is true of their string representations. 143 | /// The same is also true of their binary representations obtained through . 144 | /// 145 | /// 146 | public static UInt128 ToUInt128(this Guid id) 147 | { 148 | Span bytes = stackalloc byte[16]; 149 | BinaryIdEncoder.Encode(id, bytes); 150 | BinaryIdEncoder.TryDecodeUInt128(bytes, out var result); 151 | return result; 152 | } 153 | 154 | /// 155 | /// 156 | /// Transcodes the given into a , retaining the lexicographical ordering. 157 | /// 158 | /// 159 | /// Input values and their respective output values have the same relative ordering. 160 | /// The same is true of their string representations. 161 | /// The same is also true of their binary representations obtained through . 162 | /// 163 | /// 164 | public static Guid ToGuid(this UInt128 id) 165 | { 166 | Span bytes = stackalloc byte[16]; 167 | BinaryIdEncoder.Encode(id, bytes); 168 | BinaryIdEncoder.TryDecodeGuid(bytes, out var result); 169 | return result; 170 | } 171 | 172 | #endif 173 | 174 | #endregion 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Identities/Identities.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net7.0;net6.0;net5.0;netcoreapp3.1; 5 | Architect.Identities 6 | Architect.Identities 7 | Enable 8 | 10 9 | True 10 | True 11 | 12 | 13 | 14 | 2.1.1 15 | 16 | Reliable unique ID generation and management for distributed applications. 17 | 18 | Auto-increment IDs reveal sensitive information. UUIDs (also known as GUIDs) are inefficient as primary keys in a database. Having two different IDs is cumbersome and counterintuitive. We can do better. 19 | 20 | - For a 93-bit UUID replacement that is efficient as a primary key, use the DistributedId. 21 | - For a 128-bit 128-bit UUID replacement with the advantages of the DistributedId and practically no rate limits or collisions, at the cost of more space, use the DistributedId128. 22 | - To expose IDs externally in a sensitive environment where zero metadata must be leaked, transform them with PublicIdentities. 23 | 24 | https://github.com/TheArchitectDev/Architect.Identities 25 | 26 | Release notes: 27 | 28 | 2.1.1: 29 | - Enhancement: Upgraded package versions. 30 | 31 | 2.1.0: 32 | - Semi-breaking: IPublicIdentityConverter now features additional methods, although the type is generally not user-implemented. 33 | - Semi-breaking: HexadecimalIdEncoder's char-based parameters have been renamed from "bytes" to "chars". 34 | - Added DistributedId128, a 128-bit (Guid/UInt128) DistributedId variant with practically no rate limits or collisions that also doubles as a version-7 UUID. 35 | - Added encoding methods for UInt128. 36 | - Added extension methods to transcode between UInt128 and Guid. 37 | - Added public identity conversions for UInt128 and Guid. 38 | - DistributedIdGeneratorScope and DistributedId128GeneratorScope now expose the CurrentGenerator property, which helps when implementing generators that need to piggyback on the encapsulated generator. 39 | 40 | 2.0.0: 41 | - BREAKING: Removed Fluid. Ambient scopes with startup configuration are now considered undesirable. 42 | - BREAKING: Removed ApplicationInstanceId. Ambient scopes with startup configuration are now considered undesirable. 43 | - BREAKING: Removed ambient access to IPublicIdentityConverter. Ambient scopes with startup configuration are now considered undesirable. 44 | - BREAKING: IdEncoder has been reworked into BinaryIdEncoder, AphanumericIdEncoder, and HexadecimalIdEncoder. 45 | - BREAKING: ID decoding methods now throw if the input is too long. This is specially relevant for strings (such as query parameters) where 0123456789123456 and 0123456789123456aaaa should not produce the same ID. 46 | - BREAKING: IPublicIdentityConverter now throws on big-endian architectures, instead of risking silent portability issues between architectures. 47 | - BREAKING: Now using AmbientContexts 2.0.0. 48 | - Semi-breaking: DistributedIds are now always 28 digits, to avoid a change from 27 to 28 digits in the future. Newly generated IDs will be significantly greater than before. Avoid downgrading after upgrading. 49 | - DistributedIds can now burst-generate ~128,000 IDs at once before the ~128 IDs per millisecond throttling kicks in. This makes throttling much less likely to be encountered. 50 | - DistributedId now stays incremental even under clock adjustments of up to 1 second. (Note that the UTC clock remains unaffected by DST.) 51 | - Hexadecimal ID encodings are now supported. 52 | - IPublicIdentityConverter now comes with a TestPublicIdentityConverter implementation for unit tests. 53 | - Added UnsupportedOSPlatform("browser") to PublicIdentities, due to continuing lack of AES support. 54 | 55 | 1.0.2: 56 | - Now using AmbientContexts 1.1.1, which fixes extremely rare bugs and improves performance. 57 | 58 | 1.0.1: 59 | - Now using AmbientContexts 1.1.0, for a performance improvement. 60 | 61 | The Architect 62 | The Architect 63 | TheArchitectDev, Timovzl 64 | https://github.com/TheArchitectDev/Architect.Identities 65 | Git 66 | LICENSE 67 | ID, IDs, identity, identities, DistributedId, distributed, locally, unique, locally-unique, generator, generation, IdGenerator, UUID, GUID, auto-increment, primary, key, entity, entities, PublicIdentities 68 | 69 | 70 | 71 | 72 | True 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /Identities/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly:InternalsVisibleTo("Architect.Identities.Tests")] 4 | [assembly: InternalsVisibleTo("Architect.Identities.IntegrationTests")] 5 | -------------------------------------------------------------------------------- /Identities/PublicIdentities/CustomPublicIdentityConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// 8 | /// A custom , mainly intended for testing purposes. 9 | /// 10 | /// 11 | /// The inner workings and resulting values of this implementation are subject to change. 12 | /// 13 | /// 14 | public sealed class CustomPublicIdentityConverter : IPublicIdentityConverter 15 | { 16 | private AesPublicIdentityConverter InternalConverter { get; } 17 | 18 | /// 19 | /// Constructs a default instance using a zero key. 20 | /// 21 | public CustomPublicIdentityConverter() 22 | : this(new AesPublicIdentityConverter(aesKey: new byte[16])) 23 | { 24 | } 25 | 26 | /// 27 | /// Constructs an instance using the given key. 28 | /// 29 | public CustomPublicIdentityConverter(ReadOnlySpan keyBytes) 30 | : this(new AesPublicIdentityConverter(keyBytes)) 31 | { 32 | } 33 | 34 | /// 35 | /// Constructs an instance using the given base64-encoded key. 36 | /// 37 | public CustomPublicIdentityConverter(string base64Key) 38 | : this(new AesPublicIdentityConverter(Convert.FromBase64String(base64Key ?? throw new ArgumentNullException(nameof(base64Key))))) 39 | { 40 | } 41 | 42 | private CustomPublicIdentityConverter(AesPublicIdentityConverter internalConverter) 43 | { 44 | this.InternalConverter = internalConverter; 45 | } 46 | 47 | public void Dispose() 48 | { 49 | this.InternalConverter.Dispose(); 50 | } 51 | 52 | public Guid GetPublicRepresentation(ulong id) 53 | { 54 | return this.InternalConverter.GetPublicRepresentation(id); 55 | } 56 | 57 | public Guid GetPublicRepresentation(decimal id) 58 | { 59 | return this.InternalConverter.GetPublicRepresentation(id); 60 | } 61 | 62 | #if NET7_0_OR_GREATER 63 | public Guid GetPublicRepresentation(UInt128 id) 64 | { 65 | return this.InternalConverter.GetPublicRepresentation(id); 66 | } 67 | #endif 68 | 69 | public Guid GetPublicRepresentation(Guid id) 70 | { 71 | return this.InternalConverter.GetPublicRepresentation(id); 72 | } 73 | 74 | public bool TryGetUlong(Guid publicId, out ulong id) 75 | { 76 | return this.InternalConverter.TryGetUlong(publicId, out id); 77 | } 78 | 79 | public bool TryGetDecimal(Guid publicId, out decimal id) 80 | { 81 | return this.InternalConverter.TryGetDecimal(publicId, out id); 82 | } 83 | 84 | #if NET7_0_OR_GREATER 85 | public bool TryGetUInt128(Guid publicId, out UInt128 id) 86 | { 87 | return this.InternalConverter.TryGetUInt128(publicId, out id); 88 | } 89 | #endif 90 | 91 | public bool TryGetGuid(Guid publicId, out Guid id) 92 | { 93 | return this.InternalConverter.TryGetGuid(publicId, out id); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Identities/PublicIdentities/IPublicIdentityConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable IDE0130 // Namespace does not match folder structure 4 | namespace Architect.Identities 5 | { 6 | /// 7 | /// Provides deterministic conversions between local and public IDs. 8 | /// This allows local IDs to be kept hidden, with public IDs directly based on them, without the bookkeeping that comes with unrelated public IDs. 9 | /// 10 | public interface IPublicIdentityConverter : IDisposable 11 | { 12 | /// 13 | /// 14 | /// Returns a 16-byte public representation of the given ID. 15 | /// 16 | /// 17 | /// The public representation is shaped much like a and is indistinguishable from random noise. 18 | /// Only with possession of the configured key can it be converted back to the original ID. 19 | /// 20 | /// 21 | /// The various ID encoders in the package provide methods to encode the resulting object in various ways, such as in binary, alphanumeric, or hexadecimal form. 22 | /// 23 | /// 24 | public Guid GetPublicRepresentation(long id) 25 | { 26 | if (id < 0) throw new ArgumentOutOfRangeException(nameof(id)); 27 | return this.GetPublicRepresentation((ulong)id); 28 | } 29 | /// 30 | /// 31 | /// Returns a 16-byte public representation of the given ID. 32 | /// 33 | /// 34 | /// The public representation is shaped much like a and is indistinguishable from random noise. 35 | /// Only with possession of the configured key can it be converted back to the original ID. 36 | /// 37 | /// 38 | /// The various ID encoders in the package provide methods to encode the resulting object in various ways, such as in binary, alphanumeric, or hexadecimal form. 39 | /// 40 | /// 41 | public Guid GetPublicRepresentation(ulong id); 42 | /// 43 | /// 44 | /// Returns a 16-byte public representation of the given ID. 45 | /// 46 | /// 47 | /// The public representation is shaped much like a and is indistinguishable from random noise. 48 | /// Only with possession of the configured key can it be converted back to the original ID. 49 | /// 50 | /// 51 | /// The various ID encoders in the package provide methods to encode the resulting object in various ways, such as in binary, alphanumeric, or hexadecimal form. 52 | /// 53 | /// 54 | /// A positive decimal with 0 decimal places, consisting of no more than 28 digits, such as a value generated using . 55 | public Guid GetPublicRepresentation(decimal id); 56 | #if NET7_0_OR_GREATER 57 | /// 58 | /// 59 | /// Returns a 16-byte public representation of the given ID. 60 | /// 61 | /// 62 | /// The public representation is shaped much like a and is indistinguishable from random noise. 63 | /// Only with possession of the configured key can it be converted back to the original ID. 64 | /// 65 | /// 66 | /// The various ID encoders in the package provide methods to encode the resulting object in various ways, such as in binary, alphanumeric, or hexadecimal form. 67 | /// 68 | /// 69 | /// Any unsigned 128-bit numeric ID. 70 | public Guid GetPublicRepresentation(UInt128 id); 71 | #endif 72 | /// 73 | /// 74 | /// Returns a 16-byte public representation of the given ID. 75 | /// 76 | /// 77 | /// The public representation is shaped much like a and is indistinguishable from random noise. 78 | /// Only with possession of the configured key can it be converted back to the original ID. 79 | /// 80 | /// 81 | /// The various ID encoders in the package provide methods to encode the resulting object in various ways, such as in binary, alphanumeric, or hexadecimal form. 82 | /// 83 | /// 84 | /// Any 128-bit ID. 85 | public Guid GetPublicRepresentation(Guid id); 86 | 87 | /// 88 | /// 89 | /// Outputs the original ID represented by the given public ID. 90 | /// 91 | /// 92 | /// This method returns false if the input value was not created by the same converter using the same configuration. 93 | /// 94 | /// 95 | public bool TryGetLong(Guid publicId, out long id) 96 | { 97 | if (!this.TryGetUlong(publicId, out var ulongId) || ulongId > Int64.MaxValue) 98 | { 99 | id = default; 100 | return false; 101 | } 102 | id = (long)ulongId; 103 | return true; 104 | } 105 | /// 106 | /// 107 | /// Outputs the original ID represented by the given public ID. 108 | /// 109 | /// 110 | /// This method returns false if the input value was not created by the same converter using the same configuration. 111 | /// 112 | /// 113 | public bool TryGetUlong(Guid publicId, out ulong id); 114 | /// 115 | /// 116 | /// Outputs the original ID represented by the given public ID. 117 | /// 118 | /// 119 | /// This method returns false if the input value was not created by the same converter using the same configuration. 120 | /// 121 | /// 122 | public bool TryGetDecimal(Guid publicId, out decimal id); 123 | #if NET7_0_OR_GREATER 124 | /// 125 | /// 126 | /// Outputs the original ID represented by the given public ID. 127 | /// 128 | /// 129 | /// This method always returns true. It follows the "Try*" API shape for consistency with other overloads. 130 | /// 131 | /// 132 | public bool TryGetUInt128(Guid publicId, out UInt128 id); 133 | #endif 134 | /// 135 | /// 136 | /// Outputs the original ID represented by the given public ID. 137 | /// 138 | /// 139 | /// This method always returns true. It follows the "Try*" API shape for consistency with other overloads. 140 | /// 141 | /// 142 | public bool TryGetGuid(Guid publicId, out Guid id); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Identities/PublicIdentities/PublicIdentityConverterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Architect.Identities 4 | { 5 | /// 6 | /// Provides additional methods on . 7 | /// 8 | public static class PublicIdentityConverterExtensions 9 | { 10 | /// 11 | /// 12 | /// Returns the original ID represented by the given public ID. 13 | /// 14 | /// 15 | /// This method returns null if the input value was not created by the same converter using the same configuration. 16 | /// 17 | /// 18 | public static long? GetLongOrDefault(this IPublicIdentityConverter converter, Guid publicId) 19 | { 20 | if (converter is null) throw new ArgumentNullException(nameof(converter)); 21 | return converter.TryGetLong(publicId, out var id) ? id : null; 22 | } 23 | /// 24 | /// 25 | /// Returns the original ID represented by the given public ID. 26 | /// 27 | /// 28 | /// This method returns null if the input value was not created by the same converter using the same configuration. 29 | /// 30 | /// 31 | public static ulong? GetUlongOrDefault(this IPublicIdentityConverter converter, Guid publicId) 32 | { 33 | if (converter is null) throw new ArgumentNullException(nameof(converter)); 34 | return converter.TryGetUlong(publicId, out var id) ? id : null; 35 | } 36 | /// 37 | /// 38 | /// Returns the original ID represented by the given public ID. 39 | /// 40 | /// 41 | /// This method returns null if the input value was not created by the same converter using the same configuration. 42 | /// 43 | /// 44 | public static decimal? GetDecimalOrDefault(this IPublicIdentityConverter converter, Guid publicId) 45 | { 46 | if (converter is null) throw new ArgumentNullException(nameof(converter)); 47 | return converter.TryGetDecimal(publicId, out var id) ? id : null; 48 | } 49 | #if NET7_0_OR_GREATER 50 | /// 51 | /// 52 | /// Returns the original ID represented by the given public ID. 53 | /// 54 | /// 55 | /// This method never returns null. It follows the "*OrDefault" API shape for consistency with other overloads. 56 | /// 57 | /// 58 | public static UInt128? GetUInt128OrDefault(this IPublicIdentityConverter converter, Guid publicId) 59 | { 60 | if (converter is null) throw new ArgumentNullException(nameof(converter)); 61 | return converter.TryGetUInt128(publicId, out var id) ? id : null; 62 | } 63 | #endif 64 | /// 65 | /// 66 | /// Returns the original ID represented by the given public ID. 67 | /// 68 | /// 69 | /// This method never returns null. It follows the "*OrDefault" API shape for consistency with other overloads. 70 | /// 71 | /// 72 | public static Guid? GetGuidOrDefault(this IPublicIdentityConverter converter, Guid publicId) 73 | { 74 | if (converter is null) throw new ArgumentNullException(nameof(converter)); 75 | return converter.TryGetGuid(publicId, out var id) ? id : null; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Identities/PublicIdentities/PublicIdentityExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Architect.Identities 6 | { 7 | #if NET5_0_OR_GREATER 8 | [System.Runtime.Versioning.UnsupportedOSPlatform("browser")] 9 | #endif 10 | public static class PublicIdentityExtensions 11 | { 12 | /// 13 | /// 14 | /// Registers an implementation, providing deterministic conversion between local and public IDs. 15 | /// This allows local IDs to be kept hidden, with public IDs directly based on them, without the bookkeeping that comes with unrelated public IDs. 16 | /// 17 | /// 18 | /// Use the options to specify the key. 19 | /// 20 | /// 21 | /// Public IDs are deterministic under the key, but otherwise indistinguishable from random noise. 22 | /// 23 | /// 24 | /// When decoded, public IDs are validated, with a chance of guessing a valid (and even then likely nonexistent) ID of 1/2^64 at best. 25 | /// 26 | /// 27 | public static IServiceCollection AddPublicIdentities(this IServiceCollection services, Action identitiesOptions) 28 | { 29 | if (identitiesOptions is null) throw new ArgumentNullException(nameof(identitiesOptions)); 30 | 31 | var optionsObject = new Options(services); 32 | identitiesOptions(optionsObject); 33 | 34 | if (optionsObject.Key is null) throw new ArgumentException("Use the options to specify the key."); 35 | 36 | services.AddSingleton(new AesPublicIdentityConverter(optionsObject.Key)); 37 | return services; 38 | } 39 | 40 | public sealed class Options 41 | { 42 | internal IServiceCollection Services { get; } 43 | 44 | internal byte[]? Key { get; set; } 45 | 46 | internal Options(IServiceCollection services) 47 | { 48 | this.Services = services ?? throw new ArgumentNullException(nameof(services)); 49 | } 50 | } 51 | 52 | /// 53 | /// Sets the key used in the conversion between local and public IDs. 54 | /// 55 | public static Options Key(this Options options, string base64Key) 56 | { 57 | options.Key = Convert.FromBase64String(base64Key); 58 | return options; 59 | } 60 | 61 | /// 62 | /// Sets the key used in the conversion between local and public IDs. 63 | /// 64 | public static Options Key(this Options options, ReadOnlySpan key) 65 | { 66 | options.Key = key.ToArray(); 67 | return options; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Test/CollisionTestResults.txt: -------------------------------------------------------------------------------- 1 | Collision test results 2 | ====================== 3 | 4 | The first number is the bit count of the initial random sequence each timestamp. 5 | The second number is the bit count of the increment for subsequent random sequences on the same timestamp. 6 | Note that the INCREMENT bit count is leading. (Only adding a bit to the increment is almost as effective as adding a bit to both. But only adding a bit to the initial is not.) 7 | 8 | 2 simultaneous application instances: 9 | 24/18: 406K 10 | 26/20: 1.6M *4 (+2 bits) 11 | 28/22: 6.3M *4 (+2 bits) 12 | 30/24: 28M *4 (+2 bits) 13 | 34/28: 416M *16 (+4 bits) 14 | 48/42: 6800B (INTERPOLATED ONLY) 15 | 35/28: 500M 16 | 48/41: 4000B (INTERPOLATED ONLY) 17 | 18 | 10 simultaneous application instances: 19 | 24/18: 44K 20 | 26/20: 198K *4 (+2 bits) 21 | 28/22: 707K *4 (+2 bits) 22 | 30/24: 3M *4 (+2 bits) 23 | 34/28: 46M *16 (+4 bits) 24 | 48/42: 738B 25 | 48/42: After 1502 minutes: 524692289 iterations, 626.132.225.770 IDs, 1 collisions: 1 in 626.132.225.770 26 | 48/42: After 2665 minutes: 932835926 iterations, 1.113.183.436.850 IDs, 1 collisions: 1 in 1.113.183.436.850 27 | 37/30: 170M 28 | 39/32: 700M *4 (+2 bits) 29 | 48/41: 358B (INTERPOLATED ONLY) 30 | 31 | 100 simultaneous application instances: 32 | 24/18: 4K *4 33 | 26/20: 16K *4 (+2 bits) 34 | 28/22: 63K *4 (+2 bits) 35 | 30/24: 256K *4 (+2 bits) 36 | 34/28: 4.3M *16 (+4 bits) 37 | 48/41: 34B 38 | 48/41: (9 in ~315B: 1 in 35B) 39 | 48/42: 67B 40 | 48/42: After 855 minutes: 51304977 iterations, 331.775.677.658 IDs, 3 collisions: 1 in 110.591.892.552 41 | 48/42: After 2210 minutes: 79902785 iterations, 953.505.581.604 IDs, 14 collisions: 1 in 68.107.541.543 42 | 48/42: After 2310 minutes: 83800893 iterations, 1.000.023.086.681 IDs, 14 collisions: 1 in 71.430.220.000 43 | (With double rate, double collisions:) 44 | (After 533 minutes: 19057726 iterations, 242.564.619.371 IDs, 6 collisions: 1 in 40.427.436.561) 45 | -------------------------------------------------------------------------------- /Test/DistributedIdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using Architect.Identities; 3 | 4 | #pragma warning disable IDE0130 // Namespace does not match folder structure 5 | namespace Test 6 | { 7 | /// 8 | /// Used to implement in a testable way. 9 | /// 10 | internal sealed class DistributedIdGenerator 11 | { 12 | /// 13 | /// The maximum ID value that can be generated, and the maximum value to fit in 28 digits. 14 | /// 15 | internal const decimal MaxValue = 99999_99999_99999_99999_99999_999m; 16 | 17 | static DistributedIdGenerator() 18 | { 19 | if (!Environment.Is64BitOperatingSystem) 20 | throw new NotSupportedException($"{nameof(DistributedId)} is not supported on non-64-bit operating systems. It uses 64-bit instructions that must be atomic."); 21 | 22 | if (!BitConverter.IsLittleEndian) 23 | throw new NotSupportedException($"{nameof(DistributedId)} is not supported on big-endian architectures. The decimal-binary conversions have not been tested."); 24 | } 25 | 26 | private static DateTime GetUtcNow() 27 | { 28 | return DateTime.UtcNow; 29 | } 30 | 31 | /// 32 | /// A single application instance will aim to create no more than this many IDs on a single timestamp. 33 | /// 34 | internal const uint RateLimitPerTimestamp = 225; 35 | 36 | private Func Clock { get; } 37 | /// 38 | /// Can be invoked to cause the current thread sleep for the given number of milliseconds. 39 | /// 40 | private Action SleepAction { get; } 41 | 42 | /// 43 | /// The previous UTC timestamp (in milliseconds since the epoch) on which an ID was created (or 0 initially). 44 | /// 45 | private ulong PreviousCreationTimestamp { get; set; } 46 | /// 47 | /// The number of contiguous IDs created thus far on the . 48 | /// 49 | private uint CurrentTimestampCreationCount { get; set; } 50 | /// 51 | /// The random sequence used during the previous ID creation. 52 | /// 53 | private RandomSequence6 PreviousRandomSequence { get; set; } 54 | 55 | /// 56 | /// A lock object used to govern access to the mutable properties. 57 | /// 58 | private readonly object _lockObject = new object(); 59 | 60 | internal DistributedIdGenerator(Func? utcClock = null, Action? sleepAction = null) 61 | { 62 | this.Clock = utcClock ?? GetUtcNow; 63 | this.SleepAction = sleepAction ?? Thread.Sleep; 64 | } 65 | 66 | public decimal CreateId() 67 | { 68 | var (timestamp, randomSequence) = this.CreateValues(); 69 | return this.CreateCore(timestamp, randomSequence); 70 | } 71 | 72 | /// 73 | /// 74 | /// Locking. 75 | /// 76 | /// 77 | /// Creates the values required to create an ID. 78 | /// 79 | /// 80 | private (ulong Timestamp, RandomSequence6 RandomSequence) CreateValues() 81 | { 82 | // The random number generator is likely to lock, so doing this outside of our own lock is likely to increase throughput 83 | var randomSequence = this.CreateRandomSequence(); 84 | 85 | lock (this._lockObject) 86 | { 87 | var timestamp = this.GetTimestamp(); 88 | 89 | // If the clock has not advanced since the previous invocation 90 | if (timestamp == this.PreviousCreationTimestamp) 91 | { 92 | // If we can create more contiguous values, advance the count and create the next value 93 | if (this.TryCreateIncrementalRandomSequence(this.PreviousRandomSequence, randomSequence, out var largerRandomSequence)) 94 | { 95 | this.PreviousRandomSequence = largerRandomSequence; 96 | this.CurrentTimestampCreationCount++; 97 | return (timestamp, largerRandomSequence); 98 | } 99 | // Otherwise, sleep until the clock has advanced 100 | else 101 | { 102 | timestamp = this.AwaitUpdatedClockValue(); 103 | } 104 | } 105 | 106 | // Update the previous timestamp and reset the counter 107 | this.PreviousCreationTimestamp = timestamp; 108 | this.CurrentTimestampCreationCount = 1U; 109 | this.PreviousRandomSequence = randomSequence; 110 | 111 | return (timestamp, randomSequence); 112 | } 113 | } 114 | 115 | /// 116 | /// Returns the UTC timestamp in milliseconds since the epoch. 117 | /// 118 | private ulong GetTimestamp() 119 | { 120 | var utcNow = this.Clock(); 121 | var millisecondsSinceEpoch = (ulong)(utcNow - DateTime.UnixEpoch).TotalMilliseconds; 122 | return millisecondsSinceEpoch; 123 | } 124 | 125 | /// 126 | /// 127 | /// Sleeps until the clock has changed onto another millisecond and then returns that timestamp. 128 | /// 129 | /// 130 | /// May cause the current thread to sleep. 131 | /// 132 | /// 133 | /// Intended for use inside lock. Reads object state, but does not mutate it. 134 | /// 135 | /// 136 | internal ulong AwaitUpdatedClockValue() 137 | { 138 | ulong timestamp; 139 | do 140 | { 141 | this.SleepAction(1); 142 | } while ((timestamp = this.GetTimestamp()) == this.PreviousCreationTimestamp); 143 | return timestamp; 144 | } 145 | 146 | /// 147 | /// 148 | /// Pure function (although the random number generator may use locking internally). 149 | /// 150 | /// 151 | /// Returns a new 48-bit (6-byte) random sequence. 152 | /// 153 | /// 154 | private RandomSequence6 CreateRandomSequence() 155 | { 156 | return RandomSequence6.Create(); 157 | } 158 | 159 | private bool TryCreateIncrementalRandomSequence(RandomSequence6 previousRandomSequence, RandomSequence6 newRandomSequence, out RandomSequence6 largerRandomSequence) 160 | { 161 | return previousRandomSequence.TryAddRandomBits(newRandomSequence, out largerRandomSequence); 162 | } 163 | 164 | /// 165 | /// 166 | /// Pure function. 167 | /// 168 | /// 169 | /// Creates a new ID based on the given values. 170 | /// 171 | /// 172 | /// The UTC timestamp in milliseconds since the epoch. 173 | /// A random sequence whose 2 high bytes are zeros. This is checked to ensure that the caller has understood what will be used. 174 | internal decimal CreateCore(ulong timestamp, RandomSequence6 randomSequence) 175 | { 176 | // 93 bits fit into 28 decimals 177 | // 96 bits: [3 unused bits] [45 time bits] [48 random bits] 178 | 179 | Span bytes = stackalloc byte[2 + 12 + 2]; // Bits: 16 padding (to treat the left half as ulong) + 96 useful + 16 padding (to treat the right half as ulong) 180 | 181 | // Populate the left half with the timestamp 182 | { 183 | // The 64-bit timestamp's 19 high bits must be zero, leaving the low 45 bits to be used 184 | if (timestamp >> 45 != 0UL) 185 | throw new InvalidOperationException($"{nameof(DistributedId)} has run out of available time bits."); // Year 3084 186 | 187 | // Write the time component into the first 8 bytes (64 bits: 16 padding to write a ulong, 3 unused, 45 used) 188 | BinaryPrimitives.WriteUInt64BigEndian(bytes, timestamp); 189 | } 190 | 191 | bytes = bytes[2..]; // Disregard the left padding 192 | 193 | // Populate the right half with the random data 194 | { 195 | var randomSequenceWithHighPadding = (ulong)randomSequence; 196 | System.Diagnostics.Debug.Assert(randomSequenceWithHighPadding >> (64 - 16) == 0, "The high 2 bytes should have been zero."); 197 | var randomSequenceWithLowPadding = randomSequenceWithHighPadding << 16; 198 | System.Diagnostics.Debug.Assert((ushort)randomSequenceWithLowPadding == 0, "The low 2 bytes should have been zero."); 199 | 200 | BinaryPrimitives.WriteUInt64BigEndian(bytes[^8..], randomSequenceWithLowPadding); 201 | } 202 | 203 | bytes = bytes[..^2]; // Disregard the right padding 204 | 205 | var id = new decimal( 206 | lo: BinaryPrimitives.ReadInt32BigEndian(bytes[8..12]), 207 | mid: BinaryPrimitives.ReadInt32BigEndian(bytes[4..8]), 208 | hi: BinaryPrimitives.ReadInt32BigEndian(bytes[0..4]), 209 | isNegative: false, 210 | scale: 0); 211 | 212 | System.Diagnostics.Debug.Assert(id <= MaxValue, "Overflowed the expected decimal digits."); // 2^93 (93 bits) fits in 10^28 (28 decimal digits) 213 | 214 | return id; 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Test 4 | { 5 | /// 6 | /// This program is used to empirically test for collisions. 7 | /// 8 | internal static class Program 9 | { 10 | /// 11 | /// The number of simultaneously working application instances. 12 | /// Both are simulated to repeatedly generate the IDs at the maximum rate on the same millisecond. 13 | /// 14 | private const ushort Parallelism = 10; 15 | /// 16 | /// The rate used to be fixed, but is now dynamic. This should be greater, to provide enough array space for the generated IDs. 17 | /// 18 | private const ushort RateLimit = 256; 19 | /// 20 | /// The interval at which to log to the console. 21 | /// 22 | private static readonly TimeSpan LogInterval = TimeSpan.FromSeconds(10); 23 | 24 | private static List Generators { get; } 25 | private static List GenerationCounts { get; } 26 | private static List Arrays { get; } 27 | private static int IterationCount = 0; 28 | private static long IdCount = 0; 29 | 30 | private static HashSet DistinctValues { get; } = new HashSet(capacity: 1571); 31 | private static List Collisions { get; } = []; 32 | 33 | static Program() 34 | { 35 | GenerationCounts = new List(Parallelism); 36 | for (var i = 0; i < Parallelism; i++) GenerationCounts.Add(0); 37 | Generators = new List(capacity: Parallelism); 38 | Arrays = Enumerable.Range(0, Parallelism).Select(_ => new decimal[RateLimit]).ToList(); 39 | 40 | for (var i = 0; i < Parallelism; i++) 41 | { 42 | var iCopy = i; 43 | Generators.Add(new DistributedIdGenerator(() => DateTime.UnixEpoch.AddMilliseconds(IterationCount + (GenerationCounts[iCopy] < 0 ? -1 : 0)), sleepAction: _ => GenerationCounts[iCopy] *= -1)); 44 | } 45 | } 46 | 47 | private static void Main() 48 | { 49 | /* 50 | // Attempt to calculate probabilities 51 | { 52 | const int bits = 42; 53 | const int servers = 100; 54 | const int rate = 64; 55 | 56 | // Probability on one (rate-exhausted) timestamp for one server to have NO collisions with one other server 57 | var prob = 1.0; 58 | for (var i = 0UL; i < rate; i++) 59 | { 60 | var probForI = ((1UL << bits) - rate - i) / (double)((1UL << bits) - i); 61 | prob *= probForI; 62 | } 63 | 64 | // To the power of the number of distinct server pairs 65 | // Gives us the probability that there are no collisions on that timestamp among all of the servers 66 | prob = Math.Pow(prob, servers * (servers - 1) / 2); 67 | 68 | // Probability of one or more collisions on one (rate-exhausted) timestamp 69 | // We will pretend this is the probability of just one collision, although it is technically one OR MORE 70 | var collisionProb = 1 - prob; 71 | 72 | var collisionsPerId = collisionProb / (servers * Rate); 73 | 74 | Console.WriteLine(collisionsPerId); 75 | var idsPerCollision = 1 / collisionsPerId; 76 | 77 | Console.WriteLine($"Calculated 1 collision in {(ulong)idsPerCollision:#,##0} IDs."); 78 | }*/ 79 | 80 | // Calculate average maximum generation rate 81 | { 82 | var tempResults = new List(); 83 | for (var i = 0; i < 100; i++) 84 | { 85 | var rate = 1; 86 | var previousValue = RandomSequence6.Create(); 87 | while (previousValue.TryAddRandomBits(RandomSequence6.Create(), out previousValue)) 88 | rate++; 89 | 90 | tempResults.Add(rate); 91 | } 92 | var lowRate = tempResults.Min(); 93 | var highRate = tempResults.Max(); 94 | var avgRate = tempResults.Average(); 95 | Console.WriteLine($"Low {lowRate}, high {highRate}, avg {avgRate}"); 96 | } 97 | 98 | var logInterval = TimeSpan.FromSeconds(10); 99 | 100 | var sw = Stopwatch.StartNew(); 101 | 102 | _ = LogAtIntervals(sw); // Unawaited task 103 | 104 | while (true) 105 | { 106 | IterationCount++; 107 | Parallel.For(0, Parallelism, ProcessArray); 108 | FindDuplicates(); 109 | ResetGenerationCounts(); 110 | } 111 | } 112 | 113 | private static void ProcessArray(int index) 114 | { 115 | var generator = Generators[index]; 116 | var array = Arrays[index]; 117 | 118 | var i = 0; 119 | do 120 | { 121 | GenerationCounts[index]++; 122 | var id = generator.CreateId(); // This makes the generation count negative as soon as it can go no further 123 | array[i++] = id; 124 | } while (GenerationCounts[index] >= 0 && i < RateLimit); 125 | 126 | // Make positive again, and subtract the last one, which was a false ID 127 | if (GenerationCounts[index] < 0) 128 | GenerationCounts[index] = GenerationCounts[index] * -1 - 1; 129 | } 130 | 131 | private static void FindDuplicates() 132 | { 133 | Debug.Assert(DistinctValues.Count == 0); 134 | 135 | for (var index = 0; index < Arrays.Count; index++) 136 | { 137 | var generationCount = GenerationCounts[index]; 138 | IdCount += generationCount; 139 | 140 | var array = Arrays[index]; 141 | for (var i = 0; i < generationCount; i++) 142 | { 143 | if (!DistinctValues.Add(array[i])) 144 | Collisions.Add(array[i]); 145 | } 146 | } 147 | 148 | DistinctValues.Clear(); 149 | } 150 | 151 | private static void ResetGenerationCounts() 152 | { 153 | for (var i = 0; i < GenerationCounts.Count; i++) 154 | GenerationCounts[i] = 0; 155 | } 156 | 157 | private static async Task LogAtIntervals(Stopwatch sw) 158 | { 159 | while (true) 160 | { 161 | await Task.Delay(LogInterval); 162 | 163 | var collisionCount = (ulong)GetCollisionCount(); 164 | var ids = (ulong)IdCount; 165 | Console.WriteLine($"{(int)sw.Elapsed.TotalMinutes:000}: {IterationCount} iterations, {ids:#,##0} IDs, {collisionCount} collisions: 1 in {ids/Math.Max(1, collisionCount):#,##0}"); 166 | } 167 | } 168 | 169 | private static int GetCollisionCount() 170 | { 171 | return Collisions.Count; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Test/RandomSequence6.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers.Binary; 2 | using System.Security.Cryptography; 3 | 4 | namespace Test 5 | { 6 | /// 7 | /// 8 | /// A sequence of 6 bytes containing 45 bits of pseudorandom data, optionally increased to up to 48 bits by repeatedly adding another 40 random bits. 9 | /// This type is castable to a ulong with the high 16-19 bits set to zero. 10 | /// 11 | /// 12 | /// A newly created instance will have 45 bits of random data. 13 | /// By calling [up to times], 40 bits are added repeatedly. 14 | /// 15 | /// 16 | /// The random data originates from a cryptographically-secure pseudorandom number generator (CSPRNG). 17 | /// 18 | /// 19 | /// Although technically an instance can be created using the default constructor, 20 | /// all public operations (e.g. cast or ) will throw for such an instance. 21 | /// 22 | /// 23 | internal readonly struct RandomSequence6 24 | { 25 | private const ulong MaxValue = UInt64.MaxValue >> (64 - InitialBitCount); 26 | /// 27 | /// The number of bits added by the operation. 28 | /// 29 | private const int AdditionalBitCount = 41; 30 | private const int InitialBitCount = 48; 31 | /// 32 | /// A mask to keep the low bits of a ulong, in order to add additional random bits to an existing value. 33 | /// 34 | private const ulong AdditionalBitMask = UInt64.MaxValue >> (64 - AdditionalBitCount); 35 | 36 | /// 37 | /// A pseudorandom value with the most significant 2 bytes set to zero. 38 | /// 39 | private ulong Value => this._value == 0UL 40 | ? ThrowCreateOnlyThroughCreateMethodException() 41 | : this._value; 42 | private readonly ulong _value; 43 | 44 | private static ulong ThrowCreateOnlyThroughCreateMethodException() => throw new InvalidOperationException($"Create this only through {nameof(RandomSequence6)}.{nameof(Create)}."); 45 | 46 | /// 47 | /// Constructs a new randomized instance. 48 | /// 49 | /// A dummy parameter to distinguish this from the struct's mandatory default constructor. 50 | private RandomSequence6(byte _) 51 | { 52 | Span bytes = stackalloc byte[8]; 53 | var low6Bytes = bytes[..6]; // Fill the low 6 bytes from little-endian perspective (i.e. the left 6) 54 | RandomNumberGenerator.Fill(low6Bytes); 55 | 56 | // Use little endian to ensure that the 2 zero bytes on the right are considered the most significant 57 | this._value = BinaryPrimitives.ReadUInt64LittleEndian(bytes); 58 | 59 | this._value &= ~0UL >> (64 - InitialBitCount); 60 | 61 | if (this._value == 0UL) this._value = 1UL; // Avoid 0, which would result in exeptions 62 | 63 | System.Diagnostics.Debug.Assert(this.Value != 0UL, "The data does not look randomized."); 64 | System.Diagnostics.Debug.Assert(bytes.EndsWith(new byte[2]), "The high 2 bytes should have been zero."); 65 | System.Diagnostics.Debug.Assert(this.Value >> (64 - 16) == 0UL, "The high 16 bits should have been zero."); 66 | } 67 | 68 | /// 69 | /// Constructs a new instance that contains the given value. 70 | /// 71 | private RandomSequence6(ulong value) 72 | { 73 | if (value == 0UL || value >> (64 - 16) != 0UL) 74 | throw new ArgumentException("The value must be a randomized, non-zero value with the high 2 bytes set to zero."); 75 | 76 | this._value = value; 77 | } 78 | 79 | /// 80 | /// Generates a new 6-byte pseudorandom value. 81 | /// 82 | public static RandomSequence6 Create() 83 | { 84 | return new RandomSequence6(_: default); 85 | } 86 | 87 | /// 88 | /// Simulates an instance with the given value. 89 | /// For testing purposes only. 90 | /// 91 | /// A value with the high 2 bytes set to zero. 92 | [Obsolete("For testing purposes only.")] 93 | internal static RandomSequence6 CreatedSimulated(ulong value) 94 | { 95 | return new RandomSequence6(value); 96 | } 97 | 98 | /// 99 | /// 100 | /// Returns true and outputs a new instance that contains the current one's value with random data from the given one added to it. 101 | /// If the result would overflow, this method returns false instead. 102 | /// 103 | /// 104 | public bool TryAddRandomBits(RandomSequence6 additionalRandomSource, out RandomSequence6 result) 105 | { 106 | var value = this.Value; 107 | var randomIncrement = additionalRandomSource.Value; 108 | 109 | randomIncrement &= AdditionalBitMask; 110 | 111 | if (randomIncrement == 0UL) randomIncrement = 1UL; // Avoid incrementing by 0, which would introduce a collision 112 | 113 | if (randomIncrement > MaxValue - value) // Addition would cause overflow 114 | { 115 | result = this; 116 | return false; 117 | } 118 | 119 | unchecked // Cannot overflow UInt64 here anyway 120 | { 121 | value += randomIncrement; 122 | } 123 | 124 | result = new RandomSequence6(value); 125 | return true; 126 | } 127 | 128 | /// 129 | /// Converts the struct to a ulong filled with pseudorandom data, except that the high 2 bytes are set to zero. 130 | /// 131 | public static implicit operator ulong(RandomSequence6 sequence) => sequence.Value; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | Enable 7 | Enable 8 | False 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /pipeline-publish-preview-identities-entityframework.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | pool: 5 | vmImage: 'windows-2022' 6 | 7 | steps: 8 | 9 | # Explicit restore helps avoid the issue described here: 10 | # https://developercommunity.visualstudio.com/content/problem/983843/dotnet-build-task-does-not-use-nugetorg-for-one-pr.html 11 | - task: DotNetCoreCLI@2 12 | displayName: 'DotNet Restore' 13 | inputs: 14 | command: 'restore' 15 | includeNugetOrg: true 16 | projects: | 17 | **/*.csproj 18 | !**/*Tests*.csproj 19 | 20 | #- task: DotNetCoreCLI@2 21 | # displayName: 'DotNet Build' 22 | # inputs: 23 | # command: 'build' 24 | # arguments: '/WarnAsError --no-restore --configuration Release' 25 | # projects: | 26 | # **/*.csproj 27 | # 28 | #- task: DotNetCoreCLI@2 29 | # displayName: 'DotNet Test' 30 | # inputs: 31 | # command: 'test' 32 | # arguments: '--no-restore --no-build --configuration Release' 33 | # projects: | 34 | # **/*Tests*.csproj 35 | 36 | # DotNet Pack needs to be run from a script in order to use --version-suffix 37 | - script: dotnet pack $(Build.SourcesDirectory)/Identities.EntityFramework/Identities.EntityFramework.csproj --no-restore --configuration Release --version-suffix "preview-$(Build.BuildNumber)" -o $(Build.ArtifactStagingDirectory) 38 | displayName: 'DotNet Pack Identities.EntityFramework' 39 | 40 | - task: NuGetCommand@2 41 | displayName: 'NuGet Push' 42 | inputs: 43 | command: 'push' 44 | nuGetFeedType: 'external' 45 | publishFeedCredentials: 'NuGet' 46 | -------------------------------------------------------------------------------- /pipeline-publish-preview-identities.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | pr: none 4 | 5 | pool: 6 | vmImage: 'windows-2022' 7 | 8 | steps: 9 | 10 | # Explicit restore helps avoid the issue described here: 11 | # https://developercommunity.visualstudio.com/content/problem/983843/dotnet-build-task-does-not-use-nugetorg-for-one-pr.html 12 | - task: DotNetCoreCLI@2 13 | displayName: 'DotNet Restore' 14 | inputs: 15 | command: 'restore' 16 | includeNugetOrg: true 17 | projects: | 18 | **/*.csproj 19 | !**/*Tests*.csproj 20 | 21 | #- task: DotNetCoreCLI@2 22 | # displayName: 'DotNet Build' 23 | # inputs: 24 | # command: 'build' 25 | # arguments: '/WarnAsError --no-restore --configuration Release' 26 | # projects: | 27 | # **/*.csproj 28 | # 29 | #- task: DotNetCoreCLI@2 30 | # displayName: 'DotNet Test' 31 | # inputs: 32 | # command: 'test' 33 | # arguments: '--no-restore --no-build --configuration Release' 34 | # projects: | 35 | # **/*Tests*.csproj 36 | 37 | # DotNet Pack needs to be run from a script in order to use --version-suffix 38 | - script: dotnet pack $(Build.SourcesDirectory)/Identities/Identities.csproj --no-restore --configuration Release --version-suffix "preview-$(Build.BuildNumber)" -o $(Build.ArtifactStagingDirectory) 39 | displayName: 'DotNet Pack Identities' 40 | 41 | - task: NuGetCommand@2 42 | displayName: 'NuGet Push' 43 | inputs: 44 | command: 'push' 45 | nuGetFeedType: 'external' 46 | publishFeedCredentials: 'NuGet' 47 | -------------------------------------------------------------------------------- /pipeline-publish-stable-identities-entityframework.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | pool: 5 | vmImage: 'windows-2022' 6 | 7 | steps: 8 | 9 | # Explicit restore helps avoid the issue described here: 10 | # https://developercommunity.visualstudio.com/content/problem/983843/dotnet-build-task-does-not-use-nugetorg-for-one-pr.html 11 | - task: DotNetCoreCLI@2 12 | displayName: 'DotNet Restore' 13 | inputs: 14 | command: 'restore' 15 | includeNugetOrg: true 16 | projects: | 17 | **/*.csproj 18 | 19 | - task: DotNetCoreCLI@2 20 | displayName: 'DotNet Build' 21 | inputs: 22 | command: 'build' 23 | arguments: '/WarnAsError --no-restore --configuration Release' 24 | projects: | 25 | **/*.csproj 26 | 27 | - task: DotNetCoreCLI@2 28 | displayName: 'DotNet Test' 29 | inputs: 30 | command: 'test' 31 | arguments: '--no-restore --no-build --configuration Release' 32 | projects: | 33 | **/*Tests*.csproj 34 | 35 | - script: dotnet pack $(Build.SourcesDirectory)/Identities.EntityFramework/Identities.EntityFramework.csproj /WarnAsError --no-restore --configuration Release -o $(Build.ArtifactStagingDirectory) 36 | displayName: 'DotNet Pack' 37 | 38 | - task: NuGetCommand@2 39 | displayName: 'NuGet Push' 40 | inputs: 41 | command: 'push' 42 | nuGetFeedType: 'external' 43 | publishFeedCredentials: 'NuGet' 44 | -------------------------------------------------------------------------------- /pipeline-publish-stable-identities.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: none 3 | 4 | pool: 5 | vmImage: 'windows-2022' 6 | 7 | steps: 8 | 9 | # Explicit restore helps avoid the issue described here: 10 | # https://developercommunity.visualstudio.com/content/problem/983843/dotnet-build-task-does-not-use-nugetorg-for-one-pr.html 11 | - task: DotNetCoreCLI@2 12 | displayName: 'DotNet Restore' 13 | inputs: 14 | command: 'restore' 15 | includeNugetOrg: true 16 | projects: | 17 | **/*.csproj 18 | 19 | - task: DotNetCoreCLI@2 20 | displayName: 'DotNet Build' 21 | inputs: 22 | command: 'build' 23 | arguments: '/WarnAsError --no-restore --configuration Release' 24 | projects: | 25 | **/*.csproj 26 | 27 | - task: DotNetCoreCLI@2 28 | displayName: 'DotNet Test' 29 | inputs: 30 | command: 'test' 31 | arguments: '--no-restore --no-build --configuration Release' 32 | projects: | 33 | **/*Tests*.csproj 34 | 35 | - script: dotnet pack $(Build.SourcesDirectory)/Identities/Identities.csproj /WarnAsError --no-restore --configuration Release -o $(Build.ArtifactStagingDirectory) 36 | displayName: 'DotNet Pack' 37 | 38 | - task: NuGetCommand@2 39 | displayName: 'NuGet Push' 40 | inputs: 41 | command: 'push' 42 | nuGetFeedType: 'external' 43 | publishFeedCredentials: 'NuGet' 44 | -------------------------------------------------------------------------------- /pipeline-verify.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | pr: 3 | - master 4 | 5 | pool: 6 | vmImage: 'windows-2022' 7 | 8 | steps: 9 | 10 | # Explicit restore helps avoid the issue described here: 11 | # https://developercommunity.visualstudio.com/content/problem/983843/dotnet-build-task-does-not-use-nugetorg-for-one-pr.html 12 | - task: DotNetCoreCLI@2 13 | displayName: 'DotNet Restore' 14 | inputs: 15 | command: 'restore' 16 | includeNugetOrg: true 17 | projects: | 18 | **/*.csproj 19 | 20 | - task: DotNetCoreCLI@2 21 | displayName: 'DotNet Test' 22 | inputs: 23 | command: 'test' 24 | arguments: '/WarnAsError --no-restore --configuration Release' 25 | projects: | 26 | **/*Tests*.csproj 27 | --------------------------------------------------------------------------------