// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities; using EntityFrameworkCore.Jet.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; using Xunit; namespace EntityFrameworkCore.Jet.FunctionalTests; #nullable disable public abstract class JetValueGenerationScenariosTestBase { //protected static readonly GeometryFactory GeometryFactory = NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326); protected abstract string DatabaseName { get; } protected abstract Guid GuidSentinel { get; } protected abstract int IntSentinel { get; } protected abstract uint UIntSentinel { get; } protected abstract IntKey IntKeySentinel { get; } //protected abstract ULongKey ULongKeySentinel { get; } protected abstract int? NullableIntSentinel { get; } protected abstract string StringSentinel { get; } protected abstract DateTime DateTimeSentinel { get; } protected abstract NeedsConverter NeedsConverterSentinel { get; } //protected abstract GeometryCollection GeometryCollectionSentinel { get; } protected abstract byte[] TimestampSentinel { get; } // Positive cases [ConditionalFact] public async Task Insert_with_Identity_column() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextIdentity(testStore.Name, OnModelCreating)) { context.Database.EnsureCreatedResiliently(); context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); context.SaveChanges(); } await using (var context = new BlogContextIdentity(testStore.Name, OnModelCreating)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(1, blogs[0].Id); Assert.Equal(2, blogs[1].Id); } } public class BlogContextIdentity(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder); [ConditionalFact] public async Task Insert_with_default_value_from_sequence() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextDefaultValue(testStore.Name, OnModelCreating)) { context.Database.EnsureCreatedResiliently(); context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); context.SaveChanges(); } await using (var context = new BlogContextDefaultValue(testStore.Name, OnModelCreating)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(0, blogs[0].Id); Assert.Equal(1, blogs[1].Id); } await using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name, OnModelCreating)) { context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); context.SaveChanges(); } await using (var context = new BlogContextDefaultValueNoMigrations(testStore.Name, OnModelCreating)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(0, blogs[0].Id); Assert.Equal(1, blogs[1].Id); Assert.Equal(2, blogs[2].Id); Assert.Equal(3, blogs[3].Id); } } public class BlogContextDefaultValue(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .HasSequence("MySequence") .StartsAt(0); modelBuilder .Entity() .Property(e => e.Id) .HasDefaultValueSql("next value for MySequence"); } } public class BlogContextDefaultValueNoMigrations(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity() .Property(e => e.Id) .HasDefaultValue(); } } [ConditionalFact] public async Task Insert_with_default_string_value_from_sequence() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextStringDefaultValue(testStore.Name, OnModelCreating, StringSentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new BlogWithStringKey { Id = StringSentinel, Name = "One Unicorn" }, new BlogWithStringKey { Id = StringSentinel, Name = "Two Unicorns" }); context.SaveChanges(); } await using (var context = new BlogContextStringDefaultValue(testStore.Name, OnModelCreating, StringSentinel)) { var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); Assert.Equal("i77", blogs[0].Id); Assert.Equal("i78", blogs[1].Id); } } public class BlogContextStringDefaultValue(string databaseName, Action modelBuilder, string stringSentinel) : ContextBase(databaseName, modelBuilder) { private readonly string _stringSentinel = stringSentinel; public DbSet StringyBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .HasSequence("MyStringSequence") .StartsAt(77); modelBuilder .Entity() .Property(e => e.Id) .HasDefaultValueSql("'i' + CAST((NEXT VALUE FOR MyStringSequence) AS VARCHAR(20))") .HasSentinel(_stringSentinel); } } public class BlogWithStringKey { public string Id { get; set; } public string Name { get; set; } } [ConditionalFact] public async Task Insert_with_key_default_value_from_sequence() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name, OnModelCreating)) { context.Database.EnsureCreatedResiliently(); context.AddRange(CreateBlog("One Unicorn"), CreateBlog("Two Unicorns")); context.SaveChanges(); } await using (var context = new BlogContextKeyColumnWithDefaultValue(testStore.Name, OnModelCreating)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(77, blogs[0].Id); Assert.Equal(78, blogs[1].Id); } } public class BlogContextKeyColumnWithDefaultValue(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .HasSequence("MySequence") .StartsAt(77); modelBuilder .Entity() .Property(e => e.Id) .HasDefaultValueSql("next value for MySequence") .Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); } } [ConditionalFact] public async Task Insert_uint_to_Identity_column_using_value_converter() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name, OnModelCreating, UIntSentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new BlogWithUIntKey { Id = UIntSentinel, Name = "One Unicorn" }, new BlogWithUIntKey { Id = UIntSentinel, Name = "Two Unicorns" }); context.SaveChanges(); } await using (var context = new BlogContextUIntToIdentityUsingValueConverter(testStore.Name, OnModelCreating, UIntSentinel)) { var blogs = context.UnsignedBlogs.OrderBy(e => e.Id).ToList(); Assert.Equal((uint)1, blogs[0].Id); Assert.Equal((uint)2, blogs[1].Id); } } public class BlogContextUIntToIdentityUsingValueConverter(string databaseName, Action modelBuilder, uint uintSentinel) : ContextBase(databaseName, modelBuilder) { private readonly uint _uintSentinel = uintSentinel; public DbSet UnsignedBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity() .Property(e => e.Id) .HasConversion() .HasSentinel(_uintSentinel); } } public class BlogWithUIntKey { public uint Id { get; set; } public string Name { get; set; } } [ConditionalFact] public async Task Insert_int_enum_to_Identity_column() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextIntEnumToIdentity(testStore.Name, OnModelCreating, IntKeySentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new BlogWithIntEnumKey { Id = IntKeySentinel, Name = "One Unicorn" }, new BlogWithIntEnumKey { Id = IntKeySentinel, Name = "Two Unicorns" }); context.SaveChanges(); } using (var context = new BlogContextIntEnumToIdentity(testStore.Name, OnModelCreating, IntKeySentinel)) { var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList(); Assert.Equal(1, (int)blogs[0].Id); Assert.Equal(2, (int)blogs[1].Id); } } public class BlogContextIntEnumToIdentity(string databaseName, Action modelBuilder, IntKey sentinel) : ContextBase(databaseName, modelBuilder) { private readonly IntKey _sentinel = sentinel; public DbSet EnumBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedOnAdd() .HasSentinel(_sentinel); } } public class BlogWithIntEnumKey { public IntKey Id { get; set; } public string Name { get; set; } } public enum IntKey { Zero, One, SixSixSeven, } //Does not support ulong as identity /*[ConditionalFact] public async Task Insert_ulong_enum_to_Identity_column() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextULongEnumToIdentity(testStore.Name, OnModelCreating, ULongKeySentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new BlogWithULongEnumKey { Id = ULongKeySentinel, Name = "One Unicorn" }, new BlogWithULongEnumKey { Id = ULongKeySentinel, Name = "Two Unicorns" }); context.SaveChanges(); } await using (var context = new BlogContextULongEnumToIdentity(testStore.Name, OnModelCreating, ULongKeySentinel)) { var blogs = context.EnumBlogs.OrderBy(e => e.Id).ToList(); Assert.Equal(1, (int)blogs[0].Id); Assert.Equal(2, (int)blogs[1].Id); } } public class BlogContextULongEnumToIdentity(string databaseName, Action modelBuilder, ULongKey sentinel) : ContextBase(databaseName, modelBuilder) { private readonly ULongKey _sentinel = sentinel; public DbSet EnumBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedOnAdd() .HasSentinel(_sentinel); } } public class BlogWithULongEnumKey { public ULongKey Id { get; set; } public string Name { get; set; } } public enum ULongKey : ulong { Zero, Sentinel }*/ [ConditionalFact] public async Task Insert_string_to_Identity_column_using_value_converter() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name, OnModelCreating, StringSentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new BlogWithStringKey { Id = StringSentinel, Name = "One Unicorn" }, new BlogWithStringKey { Id = StringSentinel, Name = "Two Unicorns" }); context.SaveChanges(); } await using (var context = new BlogContextStringToIdentityUsingValueConverter(testStore.Name, OnModelCreating, StringSentinel)) { var blogs = context.StringyBlogs.OrderBy(e => e.Id).ToList(); Assert.Equal("1", blogs[0].Id); Assert.Equal("2", blogs[1].Id); } } public class BlogContextStringToIdentityUsingValueConverter(string databaseName, Action modelBuilder, string sentinel) : ContextBase(databaseName, modelBuilder) { private readonly string _sentinel = sentinel; public DbSet StringyBlogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); Guid guid; modelBuilder .Entity() .Property(e => e.Id) .HasValueGenerator() .HasConversion( v => Guid.TryParse(v, out guid) ? default : int.Parse(v), v => v.ToString()) .ValueGeneratedOnAdd() .HasSentinel(_sentinel); } } [ConditionalFact] public async Task Insert_with_explicit_non_default_keys() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextNoKeyGeneration(testStore.Name, OnModelCreating)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = 66, Name = "One Unicorn" }, new Blog { Id = 67, Name = "Two Unicorns" }); context.SaveChanges(); } await using (var context = new BlogContextNoKeyGeneration(testStore.Name, OnModelCreating)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(66, blogs[0].Id); Assert.Equal(67, blogs[1].Id); } } public class BlogContextNoKeyGeneration(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedNever(); } } [ConditionalFact] public async Task Insert_with_explicit_with_default_keys() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name, OnModelCreating, NullableIntSentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new NullableKeyBlog { Id = 0, Name = "One Unicorn" }, new NullableKeyBlog { Id = 1, Name = "Two Unicorns" }); context.SaveChanges(); } await using (var context = new BlogContextNoKeyGenerationNullableKey(testStore.Name, OnModelCreating, NullableIntSentinel)) { var blogs = context.NullableKeyBlogs.OrderBy(e => e.Id).ToList(); Assert.Equal(0, blogs[0].Id); Assert.Equal(1, blogs[1].Id); } } public class BlogContextNoKeyGenerationNullableKey(string databaseName, Action modelBuilder, int? sentinel) : ContextBase(databaseName, modelBuilder) { private readonly int? _sentinel = sentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedNever() .HasSentinel(_sentinel); } } [ConditionalFact] public async Task Insert_with_non_key_default_value() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating)) { context.Database.EnsureCreatedResiliently(); var blogs = new List { new() { Id = IntSentinel, Name = "One Unicorn", CreatedOn = DateTimeSentinel, NeedsConverter = NeedsConverterSentinel }, new() { Id = IntSentinel, Name = "Two Unicorns", CreatedOn = new DateTime(1969, 8, 3, 0, 10, 0), NeedsConverter = new NeedsConverter(111) } }; context.AddRange(blogs); context.SaveChanges(); Assert.NotEqual(new DateTime(), blogs[0].CreatedOn); Assert.NotEqual(new DateTime(), blogs[1].CreatedOn); Assert.Equal(111, blogs[1].NeedsConverter.Value); } await using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating)) { 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[1].NeedsConverter = new NeedsConverter(222); blogs[2].Name = "Y Unicorns"; blogs[2].NeedsConverter = new NeedsConverter(333); context.SaveChanges(); } await using (var context = new BlogContextNonKeyDefaultValue(testStore.Name, OnModelCreating)) { 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(222, blogs[1].NeedsConverter.Value); Assert.Equal(new DateTime(1974, 8, 3, 0, 10, 0), blogs[2].CreatedOn); Assert.Equal(333, blogs[2].NeedsConverter.Value); } } /*[ConditionalFact] public async Task Insert_with_non_key_default_spatial_value() { using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextNonKeyDefaultSpatialValue(testStore.Name, OnModelCreating)) { context.Database.EnsureCreatedResiliently(); var blogs = new List { new() { Id = IntSentinel, Name = "One Unicorn", GeometryCollection = GeometryCollectionSentinel }, new() { Id = IntSentinel, Name = "Two Unicorns", GeometryCollection = GeometryFactory.CreateGeometryCollection( [GeometryFactory.CreatePoint(new Coordinate(1, 3))]) } }; context.AddRange(blogs); context.SaveChanges(); var point = ((Point)blogs[1].GeometryCollection.Geometries[0]); Assert.Equal(1, point.X); Assert.Equal(3, point.Y); } using (var context = new BlogContextNonKeyDefaultSpatialValue(testStore.Name, OnModelCreating)) { var blogs = context.SpatialBlogs.OrderBy(e => e.Name).ToList(); Assert.Equal(3, blogs.Count); var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]); Assert.Equal(1, point1.X); Assert.Equal(3, point1.Y); var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]); Assert.Equal(1, point2.X); Assert.Equal(2, point2.Y); blogs[1].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 11)); blogs[2].GeometryCollection.Geometries[0] = GeometryFactory.CreatePoint(new Coordinate(1, 22)); context.SaveChanges(); } using (var context = new BlogContextNonKeyDefaultSpatialValue(testStore.Name, OnModelCreating)) { var blogs = context.SpatialBlogs.OrderBy(e => e.Name).ToList(); Assert.Equal(3, blogs.Count); var point1 = ((Point)blogs[1].GeometryCollection.Geometries[0]); Assert.Equal(1, point1.X); Assert.Equal(11, point1.Y); var point2 = ((Point)blogs[2].GeometryCollection.Geometries[0]); Assert.Equal(1, point2.X); Assert.Equal(22, point2.Y); } }*/ public class BlogContextNonKeyDefaultValue(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(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), NeedsConverter = new NeedsConverter(111), }); }); } } /*public class BlogContextNonKeyDefaultSpatialValue(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity( b => { b.Property(e => e.GeometryCollection).HasDefaultValue(GeometryFactory.CreateGeometryCollection()); b.HasData( new BlogWithSpatial { Id = 9979, Name = "W Unicorns", GeometryCollection = GeometryFactory.CreateGeometryCollection( [GeometryFactory.CreatePoint(new Coordinate(1, 2))]) }); }); } }*/ [ConditionalFact] public async Task Insert_with_non_key_default_value_readonly() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = IntSentinel, Name = "One Unicorn", CreatedOn = DateTimeSentinel }, new Blog { Id = IntSentinel, Name = "Two Unicorns", CreatedOn = DateTimeSentinel }); context.SaveChanges(); Assert.NotEqual(new DateTime(), context.Blogs.ToList()[0].CreatedOn); } DateTime dateTime0; using (var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel)) { 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, OnModelCreating, IntSentinel, DateTimeSentinel)) { 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( string databaseName, Action modelBuilder, int intSentinel, DateTime dateTimeSentinel) : ContextBase(databaseName, modelBuilder) { private readonly int _intSentinel = intSentinel; private readonly DateTime _dateTimeSentinel = dateTimeSentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity( b => { b.Property(e => e.Id).HasSentinel(_intSentinel); var property = b.Property(e => e.CreatedOn).HasDefaultValueSql("NOW()").HasSentinel(_dateTimeSentinel); property.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); }); } } [ConditionalFact] public async Task Insert_and_update_with_computed_column() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { context.Database.EnsureCreatedResiliently(); var blog = context.Add( new FullNameBlog { Id = IntSentinel, FirstName = "One", LastName = "Unicorn", FullName = StringSentinel }).Entity; context.SaveChanges(); Assert.Equal("One Unicorn", blog.FullName); } using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { 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(string databaseName, Action modelBuilder, int intSentinel, string stringSentinel) : ContextBase(databaseName, modelBuilder) { private readonly int _intSentinel = intSentinel; private readonly string _stringSentinel = stringSentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity( b => { b.Property(e => e.Id).HasSentinel(_intSentinel); var property = b.Property(e => e.FullName) .HasComputedColumnSql("FirstName + ' ' + LastName") .HasSentinel(_stringSentinel) .Metadata; property.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); property.SetAfterSaveBehavior(PropertySaveBehavior.Throw); }); } } public class BlogContextComputedColumnWithTriggerMetadata( string databaseName, Action modelBuilder, int intSentinel, string stringSentinel) : BlogContextComputedColumn(databaseName, modelBuilder, intSentinel, stringSentinel) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity().ToTable(tb => tb.HasTrigger("SomeTrigger")); } } /*// #6044 [ConditionalFact] public async Task Insert_and_update_with_computed_column_with_function() { using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { context.Database.ExecuteSqlRaw ( @"CREATE FUNCTION [dbo].[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, OnModelCreating, IntSentinel, StringSentinel)) { var blog = context.Add( new FullNameBlog { Id = IntSentinel, FirstName = "One", LastName = "Unicorn", FullName = StringSentinel }).Entity; context.SaveChanges(); Assert.Equal("OneUnicorn", blog.FullName); } using (var context = new BlogContextComputedColumnWithFunction(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { var blog = context.FullNameBlogs.Single(); Assert.Equal("OneUnicorn", blog.FullName); blog.LastName = "Pegasus"; context.SaveChanges(); Assert.Equal("OnePegasus", blog.FullName); } } public class BlogContextComputedColumnWithFunction( string databaseName, Action modelBuilder, int intSentinel, string stringSentinel) : ContextBase(databaseName, modelBuilder) { private readonly int _intSentinel = intSentinel; private readonly string _stringSentinel = stringSentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity( b => { b.Property(e => e.Id).HasSentinel(_intSentinel); var property = modelBuilder.Entity() .Property(e => e.FullName) .HasComputedColumnSql("[dbo].[GetFullName]([FirstName], [LastName])") .HasSentinel(_stringSentinel) .Metadata; property.SetAfterSaveBehavior(PropertySaveBehavior.Throw); }); } } // #6044 [ConditionalFact] public async Task Insert_and_update_with_computed_column_with_querying_function() { JetTestStore testStore = null; try { testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); using (var context = new BlogContextComputedColumnWithTriggerMetadata( testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { context.GetService().CreateTables(); context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); context.Database.ExecuteSqlRaw( @"CREATE FUNCTION [dbo].[GetFullName](@Id int) RETURNS nvarchar(max) WITH SCHEMABINDING AS BEGIN DECLARE @FullName nvarchar(max); SELECT @FullName = [FirstName] + [LastName] FROM [dbo].[FullNameBlogs] WHERE [Id] = @Id; RETURN @FullName END"); context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs ADD FullName AS [dbo].[GetFullName]([Id]); "); } using (var context = new BlogContextComputedColumnWithTriggerMetadata( testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { var blog = context.Add( new FullNameBlog { Id = IntSentinel, FirstName = "One", LastName = "Unicorn", FullName = StringSentinel }).Entity; context.SaveChanges(); Assert.Equal("OneUnicorn", blog.FullName); } using (var context = new BlogContextComputedColumnWithTriggerMetadata( testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { var blog = context.FullNameBlogs.Single(); Assert.Equal("OneUnicorn", blog.FullName); blog.LastName = "Pegasus"; context.SaveChanges(); Assert.Equal("OnePegasus", blog.FullName); } using (var context = new BlogContextComputedColumnWithTriggerMetadata( testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { var blog1 = context.Add( new FullNameBlog { Id = IntSentinel, FirstName = "Hank", LastName = "Unicorn", FullName = StringSentinel }).Entity; var blog2 = context.Add( new FullNameBlog { Id = IntSentinel, FirstName = "Jeff", LastName = "Unicorn", FullName = StringSentinel }).Entity; context.SaveChanges(); Assert.Equal("HankUnicorn", blog1.FullName); Assert.Equal("JeffUnicorn", blog2.FullName); } } finally { using var context = new BlogContextComputedColumnWithTriggerMetadata( DatabaseName, OnModelCreating, IntSentinel, StringSentinel); context.Database.ExecuteSqlRaw("ALTER TABLE dbo.FullNameBlogs DROP COLUMN FullName;"); context.Database.ExecuteSqlRaw("DROP FUNCTION [dbo].[GetFullName];"); testStore?.Dispose(); } }*/ [ConditionalFact] public async Task Insert_with_client_generated_GUID_key() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); Guid afterSave; await using (var context = new BlogContextClientGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) { context.Database.EnsureCreatedResiliently(); var blog = context.Add( new GuidBlog { Id = GuidSentinel, Name = "One Unicorn", NotId = GuidSentinel }).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); } await using (var context = new BlogContextClientGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) { Assert.Equal(afterSave, context.GuidBlogs.Single().Id); } } public class BlogContextClientGuidKey(string databaseName, Action modelBuilder, Guid sentinel) : ContextBase(databaseName, modelBuilder) { private readonly Guid _sentinel = sentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity( eb => { eb.HasAlternateKey(e => e.NotId); eb.Property(e => e.NotId).ValueGeneratedOnAdd().HasSentinel(_sentinel); eb.Property(e => e.Id).HasSentinel(_sentinel); }); } } [ConditionalFact] public async Task Insert_with_ValueGeneratedOnAdd_GUID_nonkey_property_throws() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using var context = new BlogContextClientGuidNonKey(testStore.Name, OnModelCreating, GuidSentinel); context.Database.EnsureCreatedResiliently(); var blog = context.Add( new GuidBlog { Id = GuidSentinel, Name = "One Unicorn", NotId = GuidSentinel }).Entity; Assert.Equal(GuidSentinel, blog.NotId); // No value set on a required column var updateException = Assert.Throws(() => context.SaveChanges()); Assert.Single(updateException.Entries); } public class BlogContextClientGuidNonKey(string databaseName, Action modelBuilder, Guid sentinel) : ContextBase(databaseName, modelBuilder) { private readonly Guid _sentinel = sentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity( b => { b.Property(e => e.Id).HasSentinel(_sentinel); b.Property(e => e.NotId).ValueGeneratedOnAdd().HasSentinel(_sentinel); }); } } [ConditionalFact(Skip = "Jet can't return server generated guid's when it is the key. Currently using client geerated guid's")] public async Task Insert_with_server_generated_GUID_key() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); Guid afterSave; await using (var context = new BlogContextServerGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) { context.Database.EnsureCreatedResiliently(); var blog = context.Add( new GuidBlog { Id = GuidSentinel, Name = "One Unicorn", NotId = GuidSentinel }).Entity; var beforeSave = blog.Id; var beforeSaveNotId = blog.NotId; Assert.Equal(GuidSentinel, beforeSave); Assert.Equal(GuidSentinel, beforeSaveNotId); context.SaveChanges(); afterSave = blog.Id; var afterSaveNotId = blog.NotId; Assert.NotEqual(GuidSentinel, afterSave); Assert.NotEqual(GuidSentinel, afterSaveNotId); Assert.NotEqual(beforeSave, afterSave); Assert.NotEqual(beforeSaveNotId, afterSaveNotId); } using (var context = new BlogContextServerGuidKey(testStore.Name, OnModelCreating, GuidSentinel)) { Assert.Equal(afterSave, context.GuidBlogs.Single().Id); } } public class BlogContextServerGuidKey(string databaseName, Action modelBuilder, Guid sentinel) : ContextBase(databaseName, modelBuilder) { private readonly Guid _sentinel = sentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity( eb => { eb.Property(e => e.Id).HasDefaultValueSql("GenGUID()").HasSentinel(_sentinel); eb.Property(e => e.NotId).HasDefaultValueSql("GenGUID()").HasSentinel(_sentinel); }); } } // Negative cases [ConditionalFact] public async Task Insert_with_explicit_non_default_keys_by_default() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using var context = new BlogContext(testStore.Name, OnModelCreating); 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 => Assert.Throws(() => c.SaveChanges())); } [ConditionalFact] public async Task Insert_with_explicit_default_keys() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using var context = new BlogContext(testStore.Name, OnModelCreating); context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = IntSentinel, 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(string databaseName, Action modelBuilder) : ContextBase(databaseName, modelBuilder); [ConditionalFact] public async Task Insert_with_implicit_default_keys() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name, OnModelCreating, IntSentinel)) { context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = 0, Name = "One Unicorn" }, new Blog { Id = 667, Name = "Two Unicorns" }); context.SaveChanges(); } await using (var context = new BlogContextSpecifyKeysUsingDefault(testStore.Name, OnModelCreating, IntSentinel)) { var blogs = context.Blogs.OrderBy(e => e.Id).ToList(); Assert.Equal(0, blogs[0].Id); Assert.Equal(667, blogs[1].Id); } } public class BlogContextSpecifyKeysUsingDefault(string databaseName, Action modelBuilder, int sentinel) : ContextBase(databaseName, modelBuilder) { private readonly int _sentinel = sentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .Entity() .Property(e => e.Id) .ValueGeneratedNever() .HasSentinel(_sentinel); } } [ConditionalFact] public async Task Insert_explicit_value_throws_when_readonly_sequence_before_save() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using var context = new BlogContextReadOnlySequenceKeyColumnWithDefaultValue(testStore.Name, OnModelCreating, IntSentinel); context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = 1, Name = "One Unicorn" }, new Blog { Id = IntSentinel, Name = "Two Unicorns" }); // The property 'Id' 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("Id", "Blog"), Assert.Throws(() => context.SaveChanges()).Message); } public class BlogContextReadOnlySequenceKeyColumnWithDefaultValue(string databaseName, Action modelBuilder, int sentinel) : ContextBase(databaseName, modelBuilder) { private readonly int _sentinel = sentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasSequence("MySequence"); var property = modelBuilder .Entity() .Property(e => e.Id) .HasDefaultValueSql("next value for MySequence") .HasSentinel(_sentinel); property.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); } } [ConditionalFact] public async Task Insert_explicit_value_throws_when_readonly_before_save() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using var context = new BlogContextNonKeyReadOnlyDefaultValue(testStore.Name, OnModelCreating, IntSentinel, DateTimeSentinel); context.Database.EnsureCreatedResiliently(); context.AddRange( new Blog { Id = IntSentinel, Name = "One Unicorn", CreatedOn = DateTimeSentinel }, new Blog { Id = IntSentinel, Name = "Two Unicorns", CreatedOn = new DateTime(1973, 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 async Task Insert_explicit_value_into_computed_column() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel); context.Database.EnsureCreatedResiliently(); context.Add( new FullNameBlog { Id = IntSentinel, 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 async Task Update_explicit_value_in_computed_column() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { context.Database.EnsureCreatedResiliently(); context.Add( new FullNameBlog { Id = IntSentinel, FirstName = "One", LastName = "Unicorn", FullName = StringSentinel }); context.SaveChanges(); } await using (var context = new BlogContextComputedColumn(testStore.Name, OnModelCreating, IntSentinel, StringSentinel)) { 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 async Task Resolve_concurrency() { await using var testStore = await JetTestStore.CreateInitializedAsync(DatabaseName); await using var context = new BlogContextConcurrencyWithRowversion(testStore.Name, OnModelCreating, IntSentinel, TimestampSentinel); context.Database.EnsureCreatedResiliently(); var blog = context.Add( new ConcurrentBlog { Id = IntSentinel, Name = "One Unicorn", Timestamp = TimestampSentinel }).Entity; context.SaveChanges(); await using var innerContext = new BlogContextConcurrencyWithRowversion(testStore.Name, OnModelCreating, IntSentinel, TimestampSentinel); 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( string databaseName, Action modelBuilder, int intSentinel, byte[] timestampSentinel) : ContextBase(databaseName, modelBuilder) { private readonly int _intSentinel = intSentinel; private readonly byte[] _timestampSentinel = timestampSentinel; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity( b => { b.Property(e => e.Id).HasSentinel(_intSentinel); b.Property(e => e.Timestamp) .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken() .HasSentinel(_timestampSentinel); }); } } protected Blog CreateBlog(string name) => new() { Id = IntSentinel, Name = name, CreatedOn = DateTimeSentinel, NeedsConverter = NeedsConverterSentinel, OtherId = NullableIntSentinel }; public class Blog { public int Id { get; set; } public string Name { get; set; } public DateTime CreatedOn { get; set; } public NeedsConverter NeedsConverter { get; set; } public int? OtherId { get; set; } } /*protected BlogWithSpatial CreateBlogWithSpatial(string name) => new() { Id = IntSentinel, Name = name, GeometryCollection = GeometryCollectionSentinel, }; public class BlogWithSpatial { public int Id { get; set; } public string Name { get; set; } public GeometryCollection GeometryCollection { get; set; } }*/ public class NeedsConverter(int value) { public int Value { get; } = value; public override bool Equals(object obj) => throw new InvalidOperationException(); public override int GetHashCode() => throw new InvalidOperationException(); } 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; } } protected virtual void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity() .Property(e => e.NeedsConverter) .HasConversion( v => v.Value, v => new NeedsConverter(v), new ValueComparer( (l, r) => (l == null && r == null) || (l != null && r != null && l.Value == r.Value), v => v.Value.GetHashCode(), v => new NeedsConverter(v.Value))) .HasDefaultValue(new NeedsConverter(999)); public abstract class ContextBase(string databaseName, Action builder) : DbContext { public DbSet Blogs { get; set; } //public DbSet SpatialBlogs { 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 OnModelCreating(ModelBuilder modelBuilder) => builder(modelBuilder); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .EnableServiceProviderCaching(false) .UseJet( JetTestStore.CreateConnectionString(databaseName), b => b.ApplyConfiguration()); } public static IEnumerable IsAsyncData = [[false], [true]]; }