EF Core 6.0 temporal tables - Add-Migration - Period property 'Comment.PeriodStart' must be a shadow property
Asked Answered
B

1

5

We have recently upgraded our project to Microsoft.EntityFrameworkCore 6.0.0. This release enables SQL Server temporal tables out of the box.

https://devblogs.microsoft.com/dotnet/prime-your-flux-capacitor-sql-server-temporal-tables-in-ef-core-6-0/

https://mcmap.net/q/139110/-net-core-entity-framework-and-sql-server-temporal-tables-automatic-scaffolding

We have used temporal tables since Entity Framework Core 3.1 using custom migrations as described here:

https://mcmap.net/q/139017/-entity-framework-core-3-1-with-temporal-tables-access-sysstarttime-and-sysendtime

https://mcmap.net/q/139112/-audit-trail-with-entity-framework-core

Simply following Microsofts guide will of course not work since default column names are PeriodStart and PeriodEnd instead of our SysStartTime and SysEndTime. History table name does not match either.

modelBuilder
    .Entity<Comment>()
    .ToTable("Comments", b => b.IsTemporal());

Migration created:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Comments")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "CommentsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Comments",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Comments",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

Creating a custom conversion should fix this as described below:

modelBuilder
    .Entity<Comment>()
    .ToTable("Comments", tb => tb.IsTemporal(temp =>
    {
        temp.UseHistoryTable("Comments", "History");
        temp.HasPeriodStart("SysStartTime");
        temp.HasPeriodEnd("SysEndTime");
    }));

https://coderedirect.com/questions/540979/how-can-i-use-system-versioned-temporal-table-with-entity-framework

However when doing it like this I get the following error on Add-Migration command:

Period property 'Comment.SysStartTime' must be a shadow property.

To verify there was nothing wrong with any other code I had I reverted to:

modelBuilder
    .Entity<Comment>()
    .ToTable("Comments", b => b.IsTemporal());

And then added public DateTime PeriodStart { get; set; } to Comment.

I then received the error:

Period property 'Comment.PeriodStart' must be a shadow property.

Is there any way to get around this? We use our SysStartTime as a last modified/last updated value and it works really well. Having to include it via EF.Property<DateTime>(comment, "SysStartTime")) seems very unnecessary since the column is present both in temporal table and the original table.

Brandenbrandenburg answered 10/12, 2021 at 11:35 Comment(0)
B
5

Not possible to fix in EF Core 6.0.

From @ajcvickers, Engineering manager for Entity Framework:

Unfortunately, there isn't any workaround for this that allows both the use of the new temporal table features, and mapping the period columns to non-shadow properties.

https://github.com/dotnet/efcore/issues/26960#issuecomment-991867756

Vote below if you want to see this feature in EF Core 7.0:

https://github.com/dotnet/efcore/issues/26463

For new entities using SQL Server temporal tables with EF Core we do use IsTemporal() so that we can use the built in support for querying historical data.

However instead of specifying a table name like this ToTable("Customers"):

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Customer>()
        .ToTable("Customers", b => b.IsTemporal());
}

https://devblogs.microsoft.com/dotnet/prime-your-flux-capacitor-sql-server-temporal-tables-in-ef-core-6-0/

We simply set it like this:

modelBuilder.Entity<Customer>().ToTable(tb => tb.IsTemporal());

In order to not bloat OnModelCreating if you have a lot of entities a private method can be used like this:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    SetTemportalTables(modelBuilder);
}
        
private void SetTemportalTables(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>().ToTable(tb => tb.IsTemporal());
    modelBuilder.Entity<Product>().ToTable(tb => tb.IsTemporal());
    modelBuilder.Entity<Orders>().ToTable(tb => tb.IsTemporal());
}

If you want every entity that implements an interface like IEntity to have SQL Server temporal tables we solved it like this:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var property in modelBuilder.Model.GetEntityTypes())
    {
        if (property.ClrType.IsAssignableTo(typeof(IEntity)))
        {
            property.SetIsTemporal(true);
        }
    }
}
Brandenbrandenburg answered 5/1, 2022 at 22:41 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.