Error when inserting into temporal table using Entity Framework Core 2.1
Asked Answered
A

3

4

I'm getting the below error when trying to insert into a temporal table using Entity Framework Core 2.1.

Cannot insert an explicit value into a GENERATED ALWAYS column in table 'db_test.dbo.Department'. Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT into GENERATED ALWAYS column.

Below is my table schema

CREATE TABLE Department   
(    
     DeptID int NOT NULL PRIMARY KEY CLUSTERED, 
     DeptName varchar(50) NOT NULL,
     ManagerID INT  NULL,
     ParentDeptID int NULL,
     SysStartTime datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
     SysEndTime datetime2 GENERATED ALWAYS AS ROW END NOT NULL, 
     PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)     
)   
WITH    
   (   
      SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.DepartmentHistory)   
   )   
;

And my DbContext looks like this:

modelBuilder.Entity<Department>(entity =>
            {
                entity.HasKey(e => e.DeptId);

                entity.Property(e => e.DeptId)
                    .HasColumnName("DeptID")
                    .ValueGeneratedNever();

                entity.Property(e => e.DeptName)
                    .IsRequired()
                    .HasMaxLength(50)
                    .IsUnicode(false);

                entity.Property(e => e.ManagerId).HasColumnName("ManagerID");

                entity.Property(e => e.ParentDeptId).HasColumnName("ParentDeptID");
            });

Please take a look and let me know how we can fix this. I'm not a fan of modifying the db context class as I generate it via nuget console using DBScaffold and I cannot update it manually each time when we re-generate it.

Acromion answered 4/10, 2018 at 20:23 Comment(0)
A
2

Making the Period start column(SysStartTime) and Period end column(SysEndTime) should fix this issue. We can do this by

ALTER TABLE [dbo].[Department] ALTER COLUMN [SysStartTime] ADD HIDDEN;
ALTER TABLE [dbo].[Department] ALTER COLUMN [SysEndTime] ADD HIDDEN;

We can see the settings for hidden against these columns in the sys.columns table

SELECT * FROM sys.columns WHERE is_hidden = 1 
Acromion answered 8/11, 2018 at 20:29 Comment(0)
H
2

TL;DR:

  • Add entity.Property(e => e.Modified).HasComputedColumnSql("GENERATED ALWAYS AS ROW START"); for your "valid from" column in your OnModelCreating code.
  • You do not need to do anything for your "valid to" column.

Background:

I'm using .NET Core 2.2 with EntityFrameworkCore 3.0 Preview 6.

Rather than adding the HIDDEN attribute to those columns in @VPP's answer, I found that adding HasComputedColumnSql does the trick and it also means EF retrieves the column after each UPDATE.

Here's my CREATE TABLE:

CREATE TABLE [dbo].[Contacts] (
    [ContactId]             INT           NOT NULL IDENTITY, 
    [TenantId]              INT           NOT NULL, 
    [Created]               DATETIME2 (7)                               NOT NULL,
    [Modified]              DATETIME2 (7) GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo]               DATETIME2 (7) GENERATED ALWAYS AS ROW END   NOT NULL CONSTRAINT [DF_Contacts_ValidTo] DEFAULT ('9999-12-31 23:59:59.9999999'),
    [Name]                  NVARCHAR(255) NOT NULL, 
    [PhoneNumber]           NVARCHAR(255) NOT NULL, 
    [HasAMulletHaircut]     BIT           NOT NULL, 
    [ListensToDevo]         BIT           NOT NULL, 
    [Status]                INT           NOT NULL, 

    CONSTRAINT PK_Contacts PRIMARY KEY ([ContactId], [TenantId]), 
    CONSTRAINT [FK_Contacts_Tenants] FOREIGN KEY ([TenantId]) REFERENCES dbo.Tenants( TenantId ),

    PERIOD FOR SYSTEM_TIME ([Modified], [ValidTo])
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE=[dbo].[Contacts_History], DATA_CONSISTENCY_CHECK=ON));

Here's what my Model Builder code looks like (originally generated by dotnet ef dbcontext scaffold, with my modifications labelled):

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Contact>(entity =>
        {
            entity.HasKey(e => new { e.ContactId, e.TenantId });

            // I added this:
            entity.Property(e => e.Modified).HasComputedColumnSql("GENERATED ALWAYS AS ROW START");
            // I added this, then commented it out because properties cannot have both `HasComputedColumnSql` and `HasDefaultValueSql`. But it works anyway.
            //entity.Property(e => e.ValidTo).HasComputedColumnSql("GENERATED ALWAYS AS ROW END");

            entity.Property(e => e.ValidTo).HasDefaultValueSql("('9999-12-31 23:59:59.9999999')");

            entity.HasOne(d => d.Tenant)
                .WithMany(p => p.Contacts)
                .HasForeignKey(d => d.TenantId)
                .OnDelete(DeleteBehavior.ClientSetNull)
                .HasConstraintName("FK_Contacts_Tenants");
        });

In short:

  • Add entity.Property(e => e.Modified).HasComputedColumnSql("GENERATED ALWAYS AS ROW START"); for your "valid from" column.
  • You do not need to do anything for your "valid to" column.

I ran tests and INSERT and UPDATE statements work fine (note that my code does not modify my "valid from" or "valid to" properties at all).

As mentioned above, the advantage of this approach is that after you call SaveChanges/SaveChangesAsync, EF will fetch the updated/new values for the "valid from" column automatically.

Hama answered 1/7, 2019 at 19:56 Comment(0)
M
0

You can set the temporal date valid to/from columns to Is Hidden = True. Then EF does not care and your temporal version works without any extra work. When EF supports it (3.0ish) you should be able to use .FromSql($"SELECT * FROM dbo.Products FOR SYSTEM_TIME AS OF {{0}}", date.ToUniversalTime()) to filter that data but most front ends only care about the recent data so it might not be an issue for most of us.

Magistery answered 29/7, 2019 at 16:34 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.