// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using EntityFrameworkCore.Jet.Data; using System.Linq; using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; using NetTopologySuite.Geometries; // ReSharper disable InconsistentNaming namespace EntityFrameworkCore.Jet.FunctionalTests { public class JetValueGenerationScenariosTest { private const string DatabaseName = "JetValueGenerationScenariosTest"; // Positive cases [ConditionalFact] public void Insert_with_Identity_column() { using var testStore = JetTestStore.CreateInitialized(DatabaseName); using (var context = new BlogContextIdentity(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); context.SaveChanges(); } using (var context = new BlogContextIdentity(testStore.Name)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(1, blogs[0].Id); Assert.Equal(2, blogs[1].Id); } } public class BlogContextIdentity : ContextBase { public BlogContextIdentity(string databaseName) : base(databaseName) { } } [ConditionalFact] public void Insert_with_explicit_non_default_keys() { using var testStore = JetTestStore.CreateInitialized(DatabaseName); using (var context = new BlogContextNoKeyGeneration(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = 66, Name = "One Unicorn" }, new Blog { Id = 67, Name = "Two Unicorns" }); context.SaveChanges(); } using (var context = new BlogContextNoKeyGeneration(testStore.Name)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(66, blogs[0].Id); Assert.Equal(67, blogs[1].Id); } } public class BlogContextNoKeyGeneration : ContextBase { public BlogContextNoKeyGeneration(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedNever(); } } [ConditionalFact] public void Insert_with_explicit_with_default_keys() { using var testStore = JetTestStore.CreateInitialized(DatabaseName); using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new NullableKeyBlog { Id = 0, Name = "One Unicorn" }, new NullableKeyBlog { Id = 1, Name = "Two Unicorns" }); context.SaveChanges(); } using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name)) { var blogs = context.NullableKeyBlogs.OrderBy(e => e.Id).ToList(); Assert.Equal(0, blogs[0].Id); Assert.Equal(1, blogs[1].Id); } } public class BlogContextNoKeyGenerationNullableKey : ContextBase { public BlogContextNoKeyGenerationNullableKey(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedNever(); } } [ConditionalFact] public void Insert_with_non_key_default_value() { using var testStore = JetTestStore.CreateInitialized(DatabaseName); using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) { context.Database.EnsureCreatedResiliently(); var blogs = new List { new() { Name = "One Unicorn" }, new() { Name = "Two Unicorns", CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0) } }; context.AddRange(blogs); context.SaveChanges(); Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); } using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) { var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); Assert.Equal(3, blogs.Count); Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn); blogs[0].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); blogs[1].Name = "X Unicorns"; blogs[2].Name = "Y Unicorns"; context.SaveChanges(); } using (var context = new BlogContextNonKeyDefaultValue(testStore.Name)) { var blogs = context.Blogs.OrderBy(e => e.Name).ToList(); Assert.Equal(3, blogs.Count); Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[0].CreatedOn); Assert.Equal(new DateTime(1969, 8, 3, 0, 10, 0), blogs[1].CreatedOn); Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn); } } public class BlogContextNonKeyDefaultValue : ContextBase { public BlogContextNonKeyDefaultValue(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity( b => { b.Property(e => e.CreatedOn) .HasDefaultValueSql("now()"); b.HasData( new Blog { Id = 9979, Name = "W Unicorns", CreatedOn = new DateTime(1974, 8, 3, 0, 10, 0) }); }); } } [ConditionalFact] public void Insert_with_non_key_default_value_readonly() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns" }); context.SaveChanges(); Assert.NotEqual(new DateTime(), context.Blogs.ToList()[0].CreatedOn); } DateTime dateTime0; using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); dateTime0 = blogs[0].CreatedOn; Assert.NotEqual(new DateTime(), dateTime0); Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); blogs[0].Name = "One Pegasus"; blogs[1].CreatedOn = new DateTime(1973, 9, 3, 0, 10, 0); context.SaveChanges(); } using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(dateTime0, blogs[0].CreatedOn); Assert.Equal(new DateTime(1973, 9, 3, 0, 10, 0), blogs[1].CreatedOn); } } } public class BlogContextNonKeyReadOnlyDefaultValue : ContextBase { public BlogContextNonKeyReadOnlyDefaultValue(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(e => e.CreatedOn) .HasDefaultValueSql("now()") .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); } } [ConditionalFact] public void Insert_and_update_with_computed_column() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextComputedColumn(testStore.Name)) { context.Database.EnsureCreatedResiliently(); var blog = context.Add( new FullNameBlog { FirstName = "One", LastName = "Unicorn" }).Entity; context.SaveChanges(); Assert.Equal("One Unicorn", blog.FullName); } using (var context = new BlogContextComputedColumn(testStore.Name)) { var blog = context.FullNameBlogs.Single(); Assert.Equal("One Unicorn", blog.FullName); blog.LastName = "Pegasus"; context.SaveChanges(); Assert.Equal("One Pegasus", blog.FullName); } } } public class BlogContextComputedColumn : ContextBase { public BlogContextComputedColumn(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { var property = modelBuilder.Entity() .Property(e => e.FullName) .HasComputedColumnSql("FirstName + ' ' + LastName") .Metadata; property.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); property.SetAfterSaveBehavior(PropertySaveBehavior.Throw); } } // #6044 [ConditionalFact] public void Insert_and_update_with_computed_column_with_function() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextComputedColumnWithFunction(testStore.Name)) { context.Database.ExecuteSqlRaw ( @"CREATE FUNCTION `GetFullName`(@First NVARCHAR(MAX), @Second NVARCHAR(MAX)) RETURNS NVARCHAR(MAX) WITH SCHEMABINDING AS BEGIN RETURN @First + @Second END"); context.GetService().CreateTables(); } using (var context = new BlogContextComputedColumnWithFunction(testStore.Name)) { var blog = context.Add( new FullNameBlog { FirstName = "One", LastName = "Unicorn" }).Entity; context.SaveChanges(); Assert.Equal("OneUnicorn", blog.FullName); } using (var context = new BlogContextComputedColumnWithFunction(testStore.Name)) { var blog = context.FullNameBlogs.Single(); Assert.Equal("OneUnicorn", blog.FullName); blog.LastName = "Pegasus"; context.SaveChanges(); Assert.Equal("OnePegasus", blog.FullName); } } } public class BlogContextComputedColumnWithFunction : ContextBase { public BlogContextComputedColumnWithFunction(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(e => e.FullName) .HasComputedColumnSql("`GetFullName`(`FirstName`, `LastName`)") .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Throw); } } // #6044 [ConditionalFact] public void Insert_and_update_with_computed_column_with_querying_function() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextComputedColumn(testStore.Name)) { context.GetService().CreateTables(); context.Database.ExecuteSqlRaw("ALTER TABLE FullNameBlogs DROP COLUMN FullName;"); context.Database.ExecuteSqlRaw( @"CREATE FUNCTION `GetFullName`(@Id int) RETURNS NVARCHAR(MAX) WITH SCHEMABINDING AS BEGIN DECLARE @FullName NVARCHAR(MAX); SELECT @FullName = `FirstName` + `LastName` FROM `FullNameBlogs` WHERE `Id` = @Id; RETURN @FullName END"); context.Database.ExecuteSqlRaw("ALTER TABLE FullNameBlogs ADD FullName AS `GetFullName`(`Id`); "); } try { using (var context = new BlogContextComputedColumn(testStore.Name)) { var blog = context.Add( new FullNameBlog { FirstName = "One", LastName = "Unicorn" }).Entity; context.SaveChanges(); Assert.Equal("OneUnicorn", blog.FullName); } using (var context = new BlogContextComputedColumn(testStore.Name)) { var blog = context.FullNameBlogs.Single(); Assert.Equal("OneUnicorn", blog.FullName); blog.LastName = "Pegasus"; context.SaveChanges(); Assert.Equal("OnePegasus", blog.FullName); } using (var context = new BlogContextComputedColumn(testStore.Name)) { var blog1 = context.Add( new FullNameBlog { FirstName = "Hank", LastName = "Unicorn" }).Entity; var blog2 = context.Add( new FullNameBlog { FirstName = "Jeff", LastName = "Unicorn" }).Entity; context.SaveChanges(); Assert.Equal("HankUnicorn", blog1.FullName); Assert.Equal("JeffUnicorn", blog2.FullName); } } finally { using (var context = new BlogContextComputedColumn(testStore.Name)) { context.Database.ExecuteSqlRaw("ALTER TABLE FullNameBlogs DROP COLUMN FullName;"); context.Database.ExecuteSqlRaw("DROP FUNCTION `GetFullName`;"); } } } } [ConditionalFact] public void Insert_with_client_generated_GUID_key() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { Guid afterSave; using (var context = new BlogContextClientGuidKey(testStore.Name)) { context.Database.EnsureCreatedResiliently(); var blog = context.Add( new GuidBlog { Name = "One Unicorn" }).Entity; var beforeSave = blog.Id; var beforeSaveNotId = blog.NotId; Assert.NotEqual(default, beforeSave); Assert.NotEqual(default, beforeSaveNotId); context.SaveChanges(); afterSave = blog.Id; var afterSaveNotId = blog.NotId; Assert.Equal(beforeSave, afterSave); Assert.Equal(beforeSaveNotId, afterSaveNotId); } using (var context = new BlogContextClientGuidKey(testStore.Name)) { Assert.Equal(afterSave, context.GuidBlogs.Single().Id); } } } public class BlogContextClientGuidKey : ContextBase { public BlogContextClientGuidKey(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity( eb => { eb.HasAlternateKey(e => e.NotId); eb.Property(e => e.NotId).ValueGeneratedOnAdd(); }); } [ConditionalFact] public void Insert_with_ValueGeneratedOnAdd_GUID_nonkey_property_throws() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextClientGuidNonKey(testStore.Name)) { context.Database.EnsureCreatedResiliently(); var blog = context.Add( new GuidBlog { Name = "One Unicorn" }).Entity; Assert.Equal(default, blog.NotId); // No value set on a required column Assert.Throws(() => context.SaveChanges()); } } } public class BlogContextClientGuidNonKey : ContextBase { public BlogContextClientGuidNonKey(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity().Property(e => e.NotId).ValueGeneratedOnAdd(); } [ConditionalFact] public void Insert_with_server_generated_GUID_key() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { Guid afterSave; using (var context = new BlogContextServerGuidKey(testStore.Name)) { context.Database.EnsureCreatedResiliently(); var blog = context.Add( new GuidBlog { Name = "One Unicorn" }).Entity; var beforeSave = blog.Id; var beforeSaveNotId = blog.NotId; Assert.Equal(default, beforeSave); Assert.Equal(default, beforeSaveNotId); context.SaveChanges(); afterSave = blog.Id; var afterSaveNotId = blog.NotId; Assert.NotEqual(default, afterSave); Assert.NotEqual(default, afterSaveNotId); Assert.NotEqual(beforeSave, afterSave); Assert.NotEqual(beforeSaveNotId, afterSaveNotId); } using (var context = new BlogContextServerGuidKey(testStore.Name)) { Assert.Equal(afterSave, context.GuidBlogs.Single().Id); } } } public class BlogContextServerGuidKey : ContextBase { public BlogContextServerGuidKey(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity( eb => { eb.Property(e => e.Id) .HasDefaultValueSql("GenGUID()"); eb.Property(e => e.NotId) .HasDefaultValueSql("GenGUID()"); }); } } // Negative cases [ConditionalFact] public void Insert_with_explicit_non_default_keys_by_default() { using var testStore = JetTestStore.CreateInitialized(DatabaseName); using var context = new BlogContext(testStore.Name); context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Id = 2, Name = "Two Unicorns" }); // DbUpdateException : An error occurred while updating the entries. See the // inner exception for details. // SqlException : Cannot insert explicit value for identity column in table // 'Blog' when IDENTITY_INSERT is set to OFF. context.Database.CreateExecutionStrategy().Execute( context, c => { var updateException = Assert.Throws(() => c.SaveChanges()); Assert.Single(updateException.Entries); }); } [ConditionalFact] public void Insert_with_explicit_default_keys() { using var testStore = JetTestStore.CreateInitialized(DatabaseName); using var context = new BlogContext(testStore.Name); context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = 0, Name = "One Unicorn" }, new Blog { Id = 1, Name = "Two Unicorns" }); // DbUpdateException : An error occurred while updating the entries. See the // inner exception for details. // SqlException : Cannot insert explicit value for identity column in table // 'Blog' when IDENTITY_INSERT is set to OFF. var updateException = Assert.Throws(() => context.SaveChanges()); Assert.Single(updateException.Entries); } public class BlogContext : ContextBase { public BlogContext(string databaseName) : base(databaseName) { } } [ConditionalFact] public void Insert_with_implicit_default_keys() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = 0, Name = "One Unicorn" }, new Blog { Id = 1, Name = "Two Unicorns" }); context.SaveChanges(); } using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(0, blogs[0].Id); Assert.Equal(1, blogs[1].Id); } } } public class BlogContextSpecifyKeysUsingDefault : ContextBase { public BlogContextSpecifyKeysUsingDefault(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedNever(); } } [ConditionalFact] public void Insert_explicit_value_throws_when_readonly_before_save() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Name = "One Unicorn" }, new Blog { Name = "Two Unicorns", CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0) }); // The property 'CreatedOn' on entity type 'Blog' is defined to be read-only before it is // saved, but its value has been set to something other than a temporary or default value. Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave("CreatedOn", "Blog"), Assert.Throws(() => context.SaveChanges()).Message); } } } [ConditionalFact] public void Insert_explicit_value_into_computed_column() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextComputedColumn(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.Add( new FullNameBlog { FirstName = "One", LastName = "Unicorn", FullName = "Gerald" }); // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only before it is // saved, but its value has been set to something other than a temporary or default value. Assert.Equal( CoreStrings.PropertyReadOnlyBeforeSave("FullName", "FullNameBlog"), Assert.Throws(() => context.SaveChanges()).Message); } } } [ConditionalFact] public void Update_explicit_value_in_computed_column() { using (var testStore = JetTestStore.CreateInitialized(DatabaseName)) { using (var context = new BlogContextComputedColumn(testStore.Name)) { context.Database.EnsureCreatedResiliently(); context.Add( new FullNameBlog { FirstName = "One", LastName = "Unicorn" }); context.SaveChanges(); } using (var context = new BlogContextComputedColumn(testStore.Name)) { var blog = context.FullNameBlogs.Single(); blog.FullName = "The Gorilla"; // The property 'FullName' on entity type 'FullNameBlog' is defined to be read-only after it has been saved, // but its value has been modified or marked as modified. Assert.Equal( CoreStrings.PropertyReadOnlyAfterSave("FullName", "FullNameBlog"), Assert.Throws(() => context.SaveChanges()).Message); } } } // Concurrency [ConditionalFact] public void Resolve_concurrency() { using var testStore = JetTestStore.CreateInitialized(DatabaseName); using var context = new BlogContextConcurrencyWithRowversion(testStore.Name); context.Database.EnsureCreatedResiliently(); var blog = context.Add( new ConcurrentBlog { Name = "One Unicorn" }).Entity; context.SaveChanges(); using var innerContext = new BlogContextConcurrencyWithRowversion(testStore.Name); var updatedBlog = innerContext.ConcurrentBlogs.Single(); updatedBlog.Name = "One Pegasus"; innerContext.SaveChanges(); var currentTimestamp = updatedBlog.Timestamp.ToArray(); try { blog.Name = "One Earth Pony"; context.SaveChanges(); } catch (DbUpdateConcurrencyException) { // Update original values (and optionally any current values) // Would normally do this with just one method call context.Entry(blog).Property(e => e.Id).OriginalValue = updatedBlog.Id; context.Entry(blog).Property(e => e.Name).OriginalValue = updatedBlog.Name; context.Entry(blog).Property(e => e.Timestamp).OriginalValue = updatedBlog.Timestamp; context.SaveChanges(); Assert.NotEqual(blog.Timestamp, currentTimestamp); } } public class BlogContextConcurrencyWithRowversion : ContextBase { public BlogContextConcurrencyWithRowversion(string databaseName) : base(databaseName) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .Property(e => e.Timestamp) .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken(); } } public class Blog { public int Id { get; set; } public string Name { get; set; } public DateTime CreatedOn { get; set; } public int? OtherId { get; set; } } public class NullableKeyBlog { public int? Id { get; set; } public string Name { get; set; } public DateTime CreatedOn { get; set; } } public class FullNameBlog { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get; set; } } public class GuidBlog { public Guid Id { get; set; } public string Name { get; set; } public Guid NotId { get; set; } } public class ConcurrentBlog { public int Id { get; set; } public string Name { get; set; } public byte[] Timestamp { get; set; } } public abstract class ContextBase : DbContext { private readonly string _databaseName; protected ContextBase(string databaseName) { _databaseName = databaseName; } public DbSet Blogs { get; set; } public DbSet NullableKeyBlogs { get; set; } public DbSet FullNameBlogs { get; set; } public DbSet GuidBlogs { get; set; } public DbSet ConcurrentBlogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .EnableServiceProviderCaching(false) .UseJet( JetTestStore.CreateConnectionString(_databaseName), TestEnvironment.DataAccessProviderFactory, b => b.ApplyConfiguration()); } } }