Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that generated GUIDs conform to the RFC 4122 standard. #969

Merged
merged 4 commits into from
Dec 9, 2019
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
4 changes: 2 additions & 2 deletions src/EFCore.MySql/Storage/Internal/MySqlConnectionSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public MySqlConnectionSettings(string connectionString)
TreatTinyAsBoolean = csb.TreatTinyAsBoolean;
}

public MySqlGuidFormat GuidFormat { get; }
public bool TreatTinyAsBoolean { get; }
public virtual MySqlGuidFormat GuidFormat { get; }
sgielen marked this conversation as resolved.
Show resolved Hide resolved
public virtual bool TreatTinyAsBoolean { get; }

protected bool Equals(MySqlConnectionSettings other)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,39 @@ public MySqlSequentialGuidValueGenerator(IMySqlOptions options)
/// <summary>
/// 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.
/// </summary>
/// <para>The change tracking entry of the entity for which the value is being generated.</para>
/// <returns> The value to be assigned to a property. </returns>
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)
{
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MySqlConnectionSettings>();
connectionSettings.SetupGet(x => x.GuidFormat).Returns(mysqlGuidFormatIsLittleEndianBinary16 ? MySqlGuidFormat.LittleEndianBinary16 : MySqlGuidFormat.None);

var options = new Mock<MySqlOptions>();
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);
}
}
}