diff --git a/src/EFCore.MySql/Storage/Internal/MySqlConnectionSettings.cs b/src/EFCore.MySql/Storage/Internal/MySqlConnectionSettings.cs
index 0d30c0187..b4594abb3 100644
--- a/src/EFCore.MySql/Storage/Internal/MySqlConnectionSettings.cs
+++ b/src/EFCore.MySql/Storage/Internal/MySqlConnectionSettings.cs
@@ -35,8 +35,8 @@ public MySqlConnectionSettings(string connectionString)
TreatTinyAsBoolean = csb.TreatTinyAsBoolean;
}
- public MySqlGuidFormat GuidFormat { get; }
- public bool TreatTinyAsBoolean { get; }
+ public virtual MySqlGuidFormat GuidFormat { get; }
+ public virtual bool TreatTinyAsBoolean { get; }
protected bool Equals(MySqlConnectionSettings other)
{
diff --git a/src/EFCore.MySql/ValueGeneration/Internal/MySqlSequentialGuidValueGenerator.cs b/src/EFCore.MySql/ValueGeneration/Internal/MySqlSequentialGuidValueGenerator.cs
index 7dbbcf54e..1ec7d622a 100644
--- a/src/EFCore.MySql/ValueGeneration/Internal/MySqlSequentialGuidValueGenerator.cs
+++ b/src/EFCore.MySql/ValueGeneration/Internal/MySqlSequentialGuidValueGenerator.cs
@@ -22,16 +22,39 @@ public MySqlSequentialGuidValueGenerator(IMySqlOptions options)
///
/// Gets a value to be assigned to a property.
/// Creates a GUID where the first 8 bytes are the current UTC date/time (in ticks)
- /// and the last 8 bytes are cryptographically random. This allows for better performance
+ /// and the rest are cryptographically random. This allows for better performance
/// in clustered index scenarios.
///
/// The change tracking entry of the entity for which the value is being generated.
/// The value to be assigned to a property.
public override Guid Next(EntityEntry entry)
{
- var randomBytes = new byte[8];
+ return Next();
+ }
+
+ public Guid Next()
+ {
+ return Next(DateTimeOffset.UtcNow);
+ }
+
+ public Guid Next(DateTimeOffset timeNow)
+ {
+ // According to RFC 4122:
+ // dddddddd-dddd-Mddd-Ndrr-rrrrrrrrrrrr
+ // - M = RFC version, in this case '4' for random UUID
+ // - N = RFC variant (plus other bits), in this case 0b1000 for variant 1
+ // - d = nibbles based on UTC date/time in ticks
+ // - r = nibbles based on random bytes
+
+ var randomBytes = new byte[7];
_rng.GetBytes(randomBytes);
- var ticks = (ulong) DateTime.UtcNow.Ticks;
+ var ticks = (ulong) timeNow.Ticks;
+
+ var uuidVersion = (ushort) 4;
+ var uuidVariant = (ushort) 0b1000;
+
+ var ticksAndVersion = (ushort)((ticks << 48 >> 52) | (ushort)(uuidVersion << 12));
+ var ticksAndVariant = (byte) ((ticks << 60 >> 60) | (byte) (uuidVariant << 4));
if (_options.ConnectionSettings.GuidFormat == MySqlGuidFormat.LittleEndianBinary16)
{
@@ -42,21 +65,24 @@ public override Guid Next(EntityEntry entry)
Array.Reverse(tickBytes);
}
- Buffer.BlockCopy(tickBytes, 0, guidBytes, 0, 8);
- Buffer.BlockCopy(randomBytes, 0, guidBytes, 8, 8);
+ Buffer.BlockCopy(tickBytes, 0, guidBytes, 0, 6);
+ guidBytes[6] = (byte)(ticksAndVersion << 8 >> 8);
+ guidBytes[7] = (byte)(ticksAndVersion >> 8);
+ guidBytes[8] = ticksAndVariant;
+ Buffer.BlockCopy(randomBytes, 0, guidBytes, 9, 7);
return new Guid(guidBytes);
}
- var guid = new Guid((uint) (ticks >> 32), (ushort) (ticks << 32 >> 48), (ushort) (ticks << 48 >> 48),
+ var guid = new Guid((uint) (ticks >> 32), (ushort) (ticks << 32 >> 48), ticksAndVersion,
+ ticksAndVariant,
randomBytes[0],
randomBytes[1],
randomBytes[2],
randomBytes[3],
randomBytes[4],
randomBytes[5],
- randomBytes[6],
- randomBytes[7]);
+ randomBytes[6]);
return guid;
}
diff --git a/test/EFCore.MySql.Tests/ValueGeneration/Internal/MysqlSequentialGuidValueGeneratorTest.cs b/test/EFCore.MySql.Tests/ValueGeneration/Internal/MysqlSequentialGuidValueGeneratorTest.cs
new file mode 100644
index 000000000..06ad7c1b8
--- /dev/null
+++ b/test/EFCore.MySql.Tests/ValueGeneration/Internal/MysqlSequentialGuidValueGeneratorTest.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Linq;
+using Moq;
+using MySql.Data.MySqlClient;
+using Pomelo.EntityFrameworkCore.MySql.Internal;
+using Pomelo.EntityFrameworkCore.MySql.Storage.Internal;
+using Pomelo.EntityFrameworkCore.MySql.ValueGeneration.Internal;
+using Xunit;
+
+namespace Pomelo.EntityFrameworkCore.MySql.Tests.ValueGeneration.Internal
+{
+ public class MysqlSequentialGuidValueGeneratorTest
+ {
+ private MySqlSequentialGuidValueGenerator GetGenerator(bool mysqlGuidFormatIsLittleEndianBinary16) {
+ var connectionSettings = new Mock();
+ connectionSettings.SetupGet(x => x.GuidFormat).Returns(mysqlGuidFormatIsLittleEndianBinary16 ? MySqlGuidFormat.LittleEndianBinary16 : MySqlGuidFormat.None);
+
+ var options = new Mock();
+ options.SetupGet(x => x.ConnectionSettings).Returns(connectionSettings.Object);
+
+ return new MySqlSequentialGuidValueGenerator(options.Object);
+ }
+
+ [Fact]
+ public void Next_returns_new_Guid()
+ {
+ var guid = GetGenerator(false).Next();
+ Assert.NotEqual(Guid.Empty, guid);
+ }
+
+ [Fact]
+ public void Next_returns_new_Guid_LittleEndianBinary16()
+ {
+ var guid = GetGenerator(true).Next();
+ Assert.NotEqual(Guid.Empty, guid);
+ }
+
+ [Fact]
+ public void Next_returns_loosely_sorted_Guid()
+ {
+ // GUIDs are loosely sorted on time. When one GUID is generated at
+ // least one tick (100 ns) after the last, it is sorted higher than
+ // the previous one. This property gives us a performance benefit
+ // on sorting in the database.
+ var generator = GetGenerator(false);
+
+ var timeNow = DateTimeOffset.UtcNow;
+ var lastGuid = generator.Next(timeNow);
+ for(var i = 0; i < 100; ++i) {
+ timeNow = timeNow.AddTicks(1);
+ var guid = generator.Next(timeNow);
+ Assert.True(string.Compare(guid.ToString(), lastGuid.ToString()) > 0);
+ lastGuid = guid;
+ }
+ }
+
+ [Fact]
+ public void Next_returns_loosely_sorted_Guid_LittleEndianBinary16()
+ {
+ // GUIDs are loosely sorted on time. When one GUID is generated at
+ // least one tick (100 ns) after the last, it is sorted higher than
+ // the previous one. This property gives us a performance benefit
+ // on sorting in the database.
+ var generator = GetGenerator(true);
+
+ var timeNow = DateTimeOffset.UtcNow;
+ var lastGuid = generator.Next(timeNow);
+ for(var i = 0; i < 100; ++i) {
+ timeNow = timeNow.AddTicks(1);
+ var guid = generator.Next(timeNow);
+ Assert.True(string.Compare(guid.ToString(), lastGuid.ToString()) > 0);
+ lastGuid = guid;
+ }
+ }
+
+ [Fact]
+ public void Next_returns_valid_Rfc4122_random_Guid()
+ {
+ var generator = GetGenerator(false);
+
+ // Since the values are based on randomness and time, perform this test
+ // multiple times to increase the chance of finding incorrect values.
+ for(var i = 0; i < 100; ++i) {
+ var guid = generator.Next();
+ var bytes = guid.ToByteArray();
+ // Version, indicated by the 13th nibble, must be 4
+ var version = bytes[7] >> 4;
+ Assert.Equal(4, version);
+ // Variant, indicated by the first bits of the 17th nibble, must be 0b10xx
+ var variant = (bytes[8] >> 4) & 0b1100;
+ Assert.Equal(0b1000, variant);
+ }
+ }
+
+ [Fact]
+ public void Next_returns_valid_Rfc4122_random_Guid_LittleEndianBinary16()
+ {
+ var generator = GetGenerator(true);
+
+ // Since the values are based on randomness and time, perform this test
+ // multiple times to increase the chance of finding incorrect values.
+ for(var i = 0; i < 100; ++i) {
+ var guid = generator.Next();
+ var bytes = guid.ToByteArray();
+ // Version, indicated by the 13th nibble, must be 4
+ var version = bytes[7] >> 4;
+ Assert.Equal(4, version);
+ // Variant, indicated by the first bits of the 17th nibble, must be 0b10xx
+ var variant = (bytes[8] >> 4) & 0b1100;
+ Assert.Equal(0b1000, variant);
+ }
+ }
+
+ [Fact]
+ public void Next_returns_unique_Guid()
+ {
+ var generator = GetGenerator(false);
+
+ var guids = Enumerable.Range(0, 100).Select(_ => generator.Next()).ToArray();
+ Assert.Equal(guids.Distinct().Count(), guids.Length);
+ }
+
+ [Fact]
+ public void Next_returns_unique_Guid_LittleEndianBinary16()
+ {
+ var generator = GetGenerator(true);
+
+ var guids = Enumerable.Range(0, 100).Select(_ => generator.Next()).ToArray();
+ Assert.Equal(guids.Distinct().Count(), guids.Length);
+ }
+ }
+}