Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public JetValueGeneratorSelector(
=> property.ClrType.UnwrapNullableType() == typeof(Guid)
? property.ValueGenerated == ValueGenerated.Never || property.GetDefaultValueSql() != null
? new TemporaryGuidValueGenerator()
: new SequentialGuidValueGenerator()
: new JetSequentialGuidValueGenerator()
: base.FindForType(property, typeBase, clrType);
}
}
66 changes: 66 additions & 0 deletions src/EFCore.Jet/ValueGeneration/JetSequentialGuidValueGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using EntityFrameworkCore.Jet.Utilities;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration;
using System;
using System.Runtime.InteropServices;
using System.Threading;

namespace EntityFrameworkCore.Jet.ValueGeneration;

/// <summary>
/// Generates sequential <see cref="Guid" /> values according to the UUID version 7 specification.
/// Will be updated to use <see cref="Guid.CreateVersion7"/> when available.
/// </summary>
public class JetSequentialGuidValueGenerator : ValueGenerator<Guid>
{
private long _counter = DateTime.UtcNow.Ticks;
private const byte Variant10xxValue = 0x80;
private const ushort Version7Value = 0x7000;
private const ushort VersionMask = 0xF000;
private const byte Variant10xxMask = 0xC0;

/// <summary>
/// Gets a value to be assigned to a property.
/// </summary>
/// <param name="entry">The change tracking entry of the entity for which the value is being generated.</param>
/// <returns>The value to be assigned to a property.</returns>
public override Guid Next(EntityEntry entry)
{
Span<byte> guidBytes = stackalloc byte[16];
var succeeded = Guid.NewGuid().TryWriteBytes(guidBytes);
var incrementedCounter = Interlocked.Increment(ref _counter);
Span<byte> counterBytes = stackalloc byte[sizeof(long)];
MemoryMarshal.Write(counterBytes, in incrementedCounter);

if (!BitConverter.IsLittleEndian)
{
counterBytes.Reverse();
}

//unix ts ms - 48 bits (6 bytes)

guidBytes[00] = counterBytes[2];
guidBytes[01] = counterBytes[3];
guidBytes[02] = counterBytes[4];
guidBytes[03] = counterBytes[5];
guidBytes[04] = counterBytes[0];
guidBytes[05] = counterBytes[1];

//UIDv7 version - first 4 bits (1/2 byte) of the next 16 bits (2 bytes)
var _c = BitConverter.ToInt16(guidBytes.Slice(6, 2));
_c = (short)((_c & ~VersionMask) | Version7Value);
BitConverter.TryWriteBytes(guidBytes.Slice(6, 2), _c);

//2 bit variant
//first 2 bits of the next 64 bits (8 bytes)
guidBytes[8] = (byte)((guidBytes[8] & ~Variant10xxMask) | Variant10xxValue);
return new Guid(guidBytes);
}

/// <summary>
/// Gets a value indicating whether the values generated are temporary or permanent. This implementation
/// always returns false, meaning the generated values will be saved to the database.
/// </summary>
public override bool GeneratesTemporaryValues
=> false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19114,6 +19114,8 @@ EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_does_not_leave_co
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: False)
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: True)
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_explicit_values
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_sequential_GUID_end_to_end_async
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.CustomUuid7Test
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: False)
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: True)
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: True, ignoreLoops: False, writeIndented: False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22747,6 +22747,8 @@ EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_does_not_leave_co
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: False)
EntityFrameworkCore.Jet.FunctionalTests.SeedingJetTest.Seeding_keyless_entity_throws_exception(async: True)
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_explicit_values
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.Can_use_sequential_GUID_end_to_end_async
EntityFrameworkCore.Jet.FunctionalTests.SequentialGuidEndToEndTest.CustomUuid7Test
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: False)
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: False, ignoreLoops: False, writeIndented: True)
EntityFrameworkCore.Jet.FunctionalTests.SerializationJetTest.Can_round_trip_through_JSON(useNewtonsoft: True, ignoreLoops: False, writeIndented: False)
Expand Down
60 changes: 59 additions & 1 deletion test/EFCore.Jet.FunctionalTests/SequentialGuidEndToEndTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
using System.Collections.Generic;
using EntityFrameworkCore.Jet.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ValueGeneration;
using System.Diagnostics.Metrics;
using System.Runtime.InteropServices;
#nullable disable
// ReSharper disable InconsistentNaming
namespace EntityFrameworkCore.Jet.FunctionalTests
Expand All @@ -29,7 +34,7 @@ public async Task Can_use_sequential_GUID_end_to_end_async()

for (var i = 0; i < 50; i++)
{
context.Add(
await context.AddAsync(
new Pegasus { Name = "Rainbow Dash " + i });
}

Expand Down Expand Up @@ -115,5 +120,58 @@ public Task DisposeAsync()
TestStore.Dispose();
return Task.CompletedTask;
}

[ConditionalFact]
public void CustomUuid7Test()
{
DateTimeOffset dtoNow = DateTimeOffset.UtcNow;
Guid net9internal = Guid.CreateVersion7(dtoNow);
Guid custom = Next(dtoNow);
var bytenet9 = net9internal.ToByteArray().AsSpan(0, 6);
var bytecustom = custom.ToByteArray().AsSpan(0,6);
Assert.Equal(bytenet9,bytecustom);
Assert.Equal(net9internal.Version,custom.Version);
var t1 = net9internal.Variant & Variant10xxMask;
var t2 = BitConverter.GetBytes(custom.Variant);
Assert.InRange(net9internal.Variant,8,0xB);
Assert.InRange(custom.Variant, 8, 0xB);
}

private const byte Variant10xxValue = 0x80;
private const ushort Version7Value = 0x7000;
private const ushort VersionMask = 0xF000;
private const byte Variant10xxMask = 0xC0;

private Guid Next(DateTimeOffset timeStamp)
{
Span<byte> guidBytes = stackalloc byte[16];
var succeeded = Guid.NewGuid().TryWriteBytes(guidBytes);
var unixms = timeStamp.ToUnixTimeMilliseconds();
Span<byte> counterBytes = stackalloc byte[sizeof(long)];
MemoryMarshal.Write(counterBytes, in unixms);

if (!BitConverter.IsLittleEndian)
{
counterBytes.Reverse();
}

//unix ts ms - 48 bits (6 bytes)
guidBytes[00] = counterBytes[2];
guidBytes[01] = counterBytes[3];
guidBytes[02] = counterBytes[4];
guidBytes[03] = counterBytes[5];
guidBytes[04] = counterBytes[0];
guidBytes[05] = counterBytes[1];

//UIDv7 version - first 4 bits (1/2 byte) of the next 16 bits (2 bytes)
var _c = BitConverter.ToInt16(guidBytes.Slice(6, 2));
_c = (short)((_c & ~VersionMask) | Version7Value);
BitConverter.TryWriteBytes(guidBytes.Slice(6, 2), _c);

//2 bit variant
//first 2 bits of the next 64 bits (8 bytes)
guidBytes[8] = (byte)((guidBytes[8] & ~Variant10xxMask) | Variant10xxValue);
return new Guid(guidBytes);
}
}
}