diff --git a/src/Pomelo.EntityFrameworkCore.MySql/ValueGeneration/Internal/MySqlSequentialGuidValueGenerator.cs b/src/Pomelo.EntityFrameworkCore.MySql/ValueGeneration/Internal/MySqlSequentialGuidValueGenerator.cs new file mode 100644 index 000000000..23ccad6c1 --- /dev/null +++ b/src/Pomelo.EntityFrameworkCore.MySql/ValueGeneration/Internal/MySqlSequentialGuidValueGenerator.cs @@ -0,0 +1,68 @@ +using System; +using System.Security.Cryptography; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.ValueGeneration.Internal +{ + public class MySqlSequentialGuidValueGenerator : ValueGenerator + { + + private readonly MySqlScopedTypeMapper _mySqlTypeMapper; + + public MySqlSequentialGuidValueGenerator(MySqlScopedTypeMapper mySqlTypeMapper) + { + _mySqlTypeMapper = mySqlTypeMapper; + } + + private static readonly RandomNumberGenerator Rng = RandomNumberGenerator.Create(); + + /// + /// 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 + /// 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]; + Rng.GetBytes(randomBytes); + var ticks = (ulong) DateTime.UtcNow.Ticks; + + if (_mySqlTypeMapper.ConnectionSettings.OldGuids) + { + var guidBytes = new byte[16]; + var tickBytes = BitConverter.GetBytes(ticks); + if (BitConverter.IsLittleEndian) + Array.Reverse(tickBytes); + + Buffer.BlockCopy(tickBytes, 0, guidBytes, 0, 8); + Buffer.BlockCopy(randomBytes, 0, guidBytes, 8, 8); + + return new Guid(guidBytes); + } + + var guid = new Guid((uint) (ticks >> 32), (ushort) (ticks << 32 >> 48), (ushort) (ticks << 48 >> 48), + randomBytes[0], + randomBytes[1], + randomBytes[2], + randomBytes[3], + randomBytes[4], + randomBytes[5], + randomBytes[6], + randomBytes[7]); + + return guid; + } + + /// + /// 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. + /// + public override bool GeneratesTemporaryValues => false; + + } +} diff --git a/src/Pomelo.EntityFrameworkCore.MySql/ValueGeneration/Internal/MySqlValueGeneratorSelector.cs b/src/Pomelo.EntityFrameworkCore.MySql/ValueGeneration/Internal/MySqlValueGeneratorSelector.cs index ae569f6a2..777e8476f 100644 --- a/src/Pomelo.EntityFrameworkCore.MySql/ValueGeneration/Internal/MySqlValueGeneratorSelector.cs +++ b/src/Pomelo.EntityFrameworkCore.MySql/ValueGeneration/Internal/MySqlValueGeneratorSelector.cs @@ -1,6 +1,8 @@ using System; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Utilities; // ReSharper disable once CheckNamespace @@ -8,11 +10,15 @@ namespace Microsoft.EntityFrameworkCore.ValueGeneration.Internal { public class MySqlValueGeneratorSelector : RelationalValueGeneratorSelector { + private readonly MySqlScopedTypeMapper _mySqlTypeMapper; + public MySqlValueGeneratorSelector( [NotNull] IValueGeneratorCache cache, - [NotNull] IRelationalAnnotationProvider relationalExtensions) + [NotNull] IRelationalAnnotationProvider relationalExtensions, + [NotNull] IRelationalTypeMapper typeMapper) : base(cache, relationalExtensions) { + _mySqlTypeMapper = typeMapper as MySqlScopedTypeMapper; } public override ValueGenerator Create(IProperty property, IEntityType entityType) @@ -24,9 +30,9 @@ public override ValueGenerator Create(IProperty property, IEntityType entityType ? property.ValueGenerated == ValueGenerated.Never || property.MySql().DefaultValueSql != null ? (ValueGenerator)new TemporaryGuidValueGenerator() - : new SequentialGuidValueGenerator() + : new MySqlSequentialGuidValueGenerator(_mySqlTypeMapper) : base.Create(property, entityType); return ret; } } -} \ No newline at end of file +} diff --git a/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Models/GeneratedTypes.cs b/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Models/GeneratedTypes.cs index 749c27af2..9a6d3049f 100644 --- a/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Models/GeneratedTypes.cs +++ b/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Models/GeneratedTypes.cs @@ -45,7 +45,7 @@ public static void OnModelCreating(ModelBuilder modelBuilder) public class GeneratedContact { - public int Id { get; set; } + public Guid Id { get; set; } public string Name { get; set; } diff --git a/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Tests/Models/GeneratedTypesTest.cs b/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Tests/Models/GeneratedTypesTest.cs index 0d6810e78..70a1d4400 100644 --- a/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Tests/Models/GeneratedTypesTest.cs +++ b/test/Pomelo.EntityFrameworkCore.MySql.PerfTests/Tests/Models/GeneratedTypesTest.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using MySql.Data.MySqlClient; using Pomelo.EntityFrameworkCore.MySql.PerfTests.Models; using Xunit; @@ -19,14 +21,22 @@ public async Task TestGeneratedContact() const string zip = "99999"; var addressFormatted = string.Join(", ", address, city, state, zip); - Action testContact = contact => + using (var db = new AppDb()) { - Assert.Equal(email, contact.Email); - Assert.Equal(addressFormatted, contact.Address); - }; + void TestContact(GeneratedContact contact) + { + var csb = new MySqlConnectionStringBuilder(db.Database.GetDbConnection().ConnectionString); + var guidHexStr = csb.OldGuids + ? BitConverter.ToString(contact.Id.ToByteArray().Take(8).ToArray()).Replace("-", "") + : contact.Id.ToString().Replace("-", "").Substring(0, 16); + var guidTicks = Convert.ToInt64("0x" + guidHexStr, 16); + var guidDateTime = new DateTime(guidTicks); + + Assert.InRange(guidDateTime - DateTime.UtcNow, TimeSpan.FromSeconds(-5), TimeSpan.FromSeconds(5)); + Assert.Equal(email, contact.Email); + Assert.Equal(addressFormatted, contact.Address); + } - using (var db = new AppDb()) - { var gen = new GeneratedContact { Names = new JsonObject>(new List {"Bob", "Bobby"}), @@ -43,11 +53,11 @@ public async Task TestGeneratedContact() // test the entity after saving to the db db.GeneratedContacts.Add(gen); await db.SaveChangesAsync(); - testContact(gen); + TestContact(gen); // test the entity after fresh retreival from the database var genDb = await db.GeneratedContacts.FirstOrDefaultAsync(m => m.Id == gen.Id); - testContact(genDb); + TestContact(genDb); } }