EF Core 2.0.1 AfterSaveBehavior for read-only identity column is set to PropertySaveBehavior.Save instead of PropertySaveBehavior.Throw

by maclaud   Last Updated January 14, 2018 12:26 PM

I noticed that in EF Core 2.0.1 an identity column which is not a primary key, has its IProperty.AfterSaveBehavior equal to PropertySaveBehavior.Save, although it has a read-only behavior. In other words after save behavior implies that an identity column can be modified, but if we try to modify an identity column, we receive a DbUpdateException.

But if we receive runtime exception, when we modify an identity column of an existing record in the database, then IProperty.AfterSaveBehavior shouldn't be equal to PropertySaveBehavior.Throw?

What I mention probably is not very clear without any written code. Therefore I created a small unit test as a proof of concept. I create a xunit test in Visual Studio 2017, by selecting .NET Core -> xUnit Test Project (.NET Core).

In order to execute the unit test, we have to install first, the EF Core 2.0.1 and the MSSQL provider by executing the following commands in package manager console.

Install-Package Microsoft.EntityFrameworkCore.Tools
Install-Package Microsoft.EntityFrameworkCore.SqlServer

The source code of Xunit test is:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using System;
using System.Linq;
using System.Reflection;
using Xunit;


namespace EFCoreIdentityColumn
{
    public class DataRecord
    {
        public int ID { get; set; }
        public int IdentityField { get; set; }
    }

    class IdentityContext : DbContext
    {
        public IdentityContext() { }
        public IdentityContext(DbContextOptions<IdentityContext> options) : base(options) { }
        public DbSet<DataRecord> DataRecords { get; set; }


        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            string conn = "Server=(localdb)\\mssqllocaldb;Database=TESTIDENTITY;Trusted_Connection=True;MultipleActiveResultSets=true";
            optionsBuilder.UseSqlServer(conn);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<DataRecord>()
                .Property(i => i.ID)
                .ValueGeneratedNever();

            modelBuilder.Entity<DataRecord>()
                .Property(i => i.IdentityField)
                .UseSqlServerIdentityColumn();
        }
    }


    public class IdentityColumnXunitTest
    {
        [Fact]
        public void IdentitySaveBehaviorWithMSSQL()
        {
            using (var context = new IdentityContext())
            {
                //examine DataRecord.IdentityField property
                var identityPropInfo = typeof(DataRecord).GetTypeInfo().DeclaredProperties.First(p => p.Name == "IdentityField");
                var identityIProperty = context.Model.FindEntityType(typeof(DataRecord)).FindProperty(identityPropInfo);
                Assert.Equal(ValueGenerated.OnAdd, identityIProperty.ValueGenerated);
                Assert.Equal(PropertySaveBehavior.Save, identityIProperty.BeforeSaveBehavior);
                Assert.Equal(PropertySaveBehavior.Save, identityIProperty.AfterSaveBehavior);
                Assert.False(identityIProperty.IsReadOnlyAfterSave);
                Assert.True(identityIProperty.SqlServer().ValueGenerationStrategy
                    == SqlServerValueGenerationStrategy.IdentityColumn);
                Assert.False(identityIProperty.IsNullable);
            }
        }

        [Fact]
        public void IdentityColumnSaveBehaviorWithMSSQL()
        {
            using (var context = new IdentityContext())
            {
                context.Database.ExecuteSqlCommand("TRUNCATE TABLE DataRecords");

                DataRecord rec1 = new DataRecord();
                var entry1 = context.Add(rec1);
                Assert.Equal(EntityState.Added, entry1.State);
                Assert.Equal(1, context.SaveChanges()); //one record was added
                Assert.Equal(0, context.DataRecords.First().ID);
                Assert.Equal(1, context.DataRecords.First().IdentityField);

                //primary key value is readonly after save and cannot be modified
                rec1.ID = 10;
                Assert.Throws<InvalidOperationException>(() => context.SaveChanges());

            }

            using (var context = new IdentityContext())
            {
                //we cannot supply a value for identity column
                DataRecord rec2 = new DataRecord()
                {
                    ID = 323,
                    IdentityField = 5
                };

                var entry2 = context.Add(rec2);
                Assert.Equal(EntityState.Added, entry2.State);
                Assert.Throws<DbUpdateException>(() => context.SaveChanges());
            }

            using (var context = new IdentityContext())
            {
                DataRecord rec = new DataRecord()
                {
                    ID = 299,
                };

                var entry = context.Add(rec);
                Assert.Equal(EntityState.Added, entry.State);
                Assert.Equal(1, context.SaveChanges()); //one record was added
                Assert.Equal(299, context.DataRecords.First(i => i.ID == rec.ID).ID);
                Assert.Equal(2, context.DataRecords.First(i => i.ID == rec.ID).IdentityField);

                //we cannot update identity column
                Assert.True(context.Entry(rec).State == EntityState.Unchanged);
                rec.IdentityField = 544;
                Assert.True(context.Entry(rec).State == EntityState.Modified);
                Assert.Throws<DbUpdateException>(() => context.SaveChanges());
            }
        }

    }
}

In order to execute the unit test we have to add migration and update database in package manager console.

Add-Migration migration_name
Update-Database

The test confirms that although the identity column is read-only in practice, the AfterSaveBehavior is set to PropertySaveBehavior.Save and IsReadOnlyAfterSave is set to false. There is a contradiction here.



Related Questions


How can I avoid persisting unit test data?

Updated March 21, 2017 14:26 PM

How to Unit Test ActionResult<T> with xUnit?

Updated July 24, 2018 01:26 AM

How to access IConfiguration in test script

Updated June 25, 2017 13:26 PM

What is the `Assert.That` property for?

Updated December 29, 2017 23:26 PM