Skip to content

Commit f97148f

Browse files
authored
Create a new sequential guid generator based on the UUID version 7 (#254)
* Create a new sequential guid generator based on the UUID version 7
1 parent 2159969 commit f97148f

5 files changed

Lines changed: 130 additions & 2 deletions

File tree

src/EFCore.Jet/ValueGeneration/Internal/JetValueGeneratorSelector.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public JetValueGeneratorSelector(
4242
=> property.ClrType.UnwrapNullableType() == typeof(Guid)
4343
? property.ValueGenerated == ValueGenerated.Never || property.GetDefaultValueSql() != null
4444
? new TemporaryGuidValueGenerator()
45-
: new SequentialGuidValueGenerator()
45+
: new JetSequentialGuidValueGenerator()
4646
: base.FindForType(property, typeBase, clrType);
4747
}
4848
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using EntityFrameworkCore.Jet.Utilities;
2+
using Microsoft.EntityFrameworkCore.ChangeTracking;
3+
using Microsoft.EntityFrameworkCore.ValueGeneration;
4+
using System;
5+
using System.Runtime.InteropServices;
6+
using System.Threading;
7+
8+
namespace EntityFrameworkCore.Jet.ValueGeneration;
9+
10+
/// <summary>
11+
/// Generates sequential <see cref="Guid" /> values according to the UUID version 7 specification.
12+
/// Will be updated to use <see cref="Guid.CreateVersion7"/> when available.
13+
/// </summary>
14+
public class JetSequentialGuidValueGenerator : ValueGenerator<Guid>
15+
{
16+
private long _counter = DateTime.UtcNow.Ticks;
17+
private const byte Variant10xxValue = 0x80;
18+
private const ushort Version7Value = 0x7000;
19+
private const ushort VersionMask = 0xF000;
20+
private const byte Variant10xxMask = 0xC0;
21+
22+
/// <summary>
23+
/// Gets a value to be assigned to a property.
24+
/// </summary>
25+
/// <param name="entry">The change tracking entry of the entity for which the value is being generated.</param>
26+
/// <returns>The value to be assigned to a property.</returns>
27+
public override Guid Next(EntityEntry entry)
28+
{
29+
Span<byte> guidBytes = stackalloc byte[16];
30+
var succeeded = Guid.NewGuid().TryWriteBytes(guidBytes);
31+
var incrementedCounter = Interlocked.Increment(ref _counter);
32+
Span<byte> counterBytes = stackalloc byte[sizeof(long)];
33+
MemoryMarshal.Write(counterBytes, in incrementedCounter);
34+
35+
if (!BitConverter.IsLittleEndian)
36+
{
37+
counterBytes.Reverse();
38+
}
39+
40+
//unix ts ms - 48 bits (6 bytes)
41+
42+
guidBytes[00] = counterBytes[2];
43+
guidBytes[01] = counterBytes[3];
44+
guidBytes[02] = counterBytes[4];
45+
guidBytes[03] = counterBytes[5];
46+
guidBytes[04] = counterBytes[0];
47+
guidBytes[05] = counterBytes[1];
48+
49+
//UIDv7 version - first 4 bits (1/2 byte) of the next 16 bits (2 bytes)
50+
var _c = BitConverter.ToInt16(guidBytes.Slice(6, 2));
51+
_c = (short)((_c & ~VersionMask) | Version7Value);
52+
BitConverter.TryWriteBytes(guidBytes.Slice(6, 2), _c);
53+
54+
//2 bit variant
55+
//first 2 bits of the next 64 bits (8 bytes)
56+
guidBytes[8] = (byte)((guidBytes[8] & ~Variant10xxMask) | Variant10xxValue);
57+
return new Guid(guidBytes);
58+
}
59+
60+
/// <summary>
61+
/// Gets a value indicating whether the values generated are temporary or permanent. This implementation
62+
/// always returns false, meaning the generated values will be saved to the database.
63+
/// </summary>
64+
public override bool GeneratesTemporaryValues
65+
=> false;
66+
}

test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_odbc_x86.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19114,6 +19114,8 @@ EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_does_not_leave_co
1911419114
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: False)
1911519115
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: True)
1911619116
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_explicit_values
19117+
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_sequential_GUID_end_to_end_async
19118+
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.CustomUuid7Test
1911719119
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: False)
1911819120
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: True)
1911919121
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: True, ignoreLoops: False, writeIndented: False)

test/EFCore.Jet.FunctionalTests/GreenTests/ace_2010_oledb_x86.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22747,6 +22747,8 @@ EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_does_not_leave_co
2274722747
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: False)
2274822748
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: True)
2274922749
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_explicit_values
22750+
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_sequential_GUID_end_to_end_async
22751+
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.CustomUuid7Test
2275022752
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: False)
2275122753
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: True)
2275222754
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: True, ignoreLoops: False, writeIndented: False)

test/EFCore.Jet.FunctionalTests/SequentialGuidEndToEndTest.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
using System.Collections.Generic;
55
using EntityFrameworkCore.Jet.Data;
66
using System.Linq;
7+
using System.Threading;
78
using System.Threading.Tasks;
89
using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities;
910
using Microsoft.EntityFrameworkCore;
1011
using Microsoft.EntityFrameworkCore.TestUtilities;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using Xunit;
14+
using Microsoft.EntityFrameworkCore.ChangeTracking;
15+
using Microsoft.EntityFrameworkCore.ValueGeneration;
16+
using System.Diagnostics.Metrics;
17+
using System.Runtime.InteropServices;
1318
#nullable disable
1419
// ReSharper disable InconsistentNaming
1520
namespace EntityFrameworkCore.Jet.FunctionalTests
@@ -29,7 +34,7 @@ public async Task Can_use_sequential_GUID_end_to_end_async()
2934

3035
for (var i = 0; i < 50; i++)
3136
{
32-
context.Add(
37+
await context.AddAsync(
3338
new Pegasus { Name = "Rainbow Dash " + i });
3439
}
3540

@@ -115,5 +120,58 @@ public Task DisposeAsync()
115120
TestStore.Dispose();
116121
return Task.CompletedTask;
117122
}
123+
124+
[ConditionalFact]
125+
public void CustomUuid7Test()
126+
{
127+
DateTimeOffset dtoNow = DateTimeOffset.UtcNow;
128+
Guid net9internal = Guid.CreateVersion7(dtoNow);
129+
Guid custom = Next(dtoNow);
130+
var bytenet9 = net9internal.ToByteArray().AsSpan(0, 6);
131+
var bytecustom = custom.ToByteArray().AsSpan(0,6);
132+
Assert.Equal(bytenet9,bytecustom);
133+
Assert.Equal(net9internal.Version,custom.Version);
134+
var t1 = net9internal.Variant & Variant10xxMask;
135+
var t2 = BitConverter.GetBytes(custom.Variant);
136+
Assert.InRange(net9internal.Variant,8,0xB);
137+
Assert.InRange(custom.Variant, 8, 0xB);
138+
}
139+
140+
private const byte Variant10xxValue = 0x80;
141+
private const ushort Version7Value = 0x7000;
142+
private const ushort VersionMask = 0xF000;
143+
private const byte Variant10xxMask = 0xC0;
144+
145+
private Guid Next(DateTimeOffset timeStamp)
146+
{
147+
Span<byte> guidBytes = stackalloc byte[16];
148+
var succeeded = Guid.NewGuid().TryWriteBytes(guidBytes);
149+
var unixms = timeStamp.ToUnixTimeMilliseconds();
150+
Span<byte> counterBytes = stackalloc byte[sizeof(long)];
151+
MemoryMarshal.Write(counterBytes, in unixms);
152+
153+
if (!BitConverter.IsLittleEndian)
154+
{
155+
counterBytes.Reverse();
156+
}
157+
158+
//unix ts ms - 48 bits (6 bytes)
159+
guidBytes[00] = counterBytes[2];
160+
guidBytes[01] = counterBytes[3];
161+
guidBytes[02] = counterBytes[4];
162+
guidBytes[03] = counterBytes[5];
163+
guidBytes[04] = counterBytes[0];
164+
guidBytes[05] = counterBytes[1];
165+
166+
//UIDv7 version - first 4 bits (1/2 byte) of the next 16 bits (2 bytes)
167+
var _c = BitConverter.ToInt16(guidBytes.Slice(6, 2));
168+
_c = (short)((_c & ~VersionMask) | Version7Value);
169+
BitConverter.TryWriteBytes(guidBytes.Slice(6, 2), _c);
170+
171+
//2 bit variant
172+
//first 2 bits of the next 64 bits (8 bytes)
173+
guidBytes[8] = (byte)((guidBytes[8] & ~Variant10xxMask) | Variant10xxValue);
174+
return new Guid(guidBytes);
175+
}
118176
}
119177
}

0 commit comments

Comments
 (0)