diff --git a/src/EFCore.Design/Design/Internal/CSharpHelper.cs b/src/EFCore.Design/Design/Internal/CSharpHelper.cs
index b2d58b724b6..d185ad07de6 100644
--- a/src/EFCore.Design/Design/Internal/CSharpHelper.cs
+++ b/src/EFCore.Design/Design/Internal/CSharpHelper.cs
@@ -1001,7 +1001,28 @@ public virtual string Fragment(MethodCallCodeFragment fragment)
}
private string Fragment(NestedClosureCodeFragment fragment)
- => fragment.Parameter + " => " + fragment.Parameter + Fragment(fragment.MethodCall);
+ {
+ if (fragment.MethodCalls.Count == 1)
+ {
+ return fragment.Parameter + " => " + fragment.Parameter + Fragment(fragment.MethodCalls[0]);
+ }
+
+ var builder = new IndentedStringBuilder();
+ builder.AppendLine(fragment.Parameter + " =>");
+ builder.AppendLine("{");
+ using (builder.Indent())
+ {
+ foreach (var methodCall in fragment.MethodCalls)
+ {
+ builder.Append(fragment.Parameter + Fragment(methodCall));
+ builder.AppendLine(";");
+ }
+ }
+
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
private static bool IsIdentifierStartCharacter(char ch)
{
diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
index b730f74f701..4ac89b407cb 100644
--- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs
@@ -2,7 +2,6 @@
// 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 System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -29,6 +28,21 @@ public static class RelationalEntityTypeBuilderExtensions
string? name)
=> entityTypeBuilder.ToTable(name, (string?)null);
+ ///
+ /// Configures the table that the entity type maps to when targeting a relational database.
+ ///
+ /// The builder for the entity type being configured.
+ /// An action that performs configuration of the table.
+ /// The same builder instance so that multiple calls can be chained.
+ public static EntityTypeBuilder ToTable(
+ this EntityTypeBuilder entityTypeBuilder,
+ Action buildAction)
+ {
+ buildAction(new TableBuilder(null, null, entityTypeBuilder));
+
+ return entityTypeBuilder;
+ }
+
///
/// Configures the table that the entity type maps to when targeting a relational database.
///
@@ -55,6 +69,23 @@ public static class RelationalEntityTypeBuilderExtensions
where TEntity : class
=> entityTypeBuilder.ToTable(name, (string?)null);
+ ///
+ /// Configures the table that the entity type maps to when targeting a relational database.
+ ///
+ /// The entity type being configured.
+ /// The builder for the entity type being configured.
+ /// An action that performs configuration of the table.
+ /// The same builder instance so that multiple calls can be chained.
+ public static EntityTypeBuilder ToTable(
+ this EntityTypeBuilder entityTypeBuilder,
+ Action> buildAction)
+ where TEntity : class
+ {
+ buildAction(new TableBuilder(null, null, entityTypeBuilder));
+
+ return entityTypeBuilder;
+ }
+
///
/// Configures the table that the entity type maps to when targeting a relational database.
///
@@ -119,7 +150,7 @@ public static class RelationalEntityTypeBuilderExtensions
Check.NotNull(name, nameof(name));
Check.NullButNotEmpty(schema, nameof(schema));
- buildAction(new TableBuilder(name, schema, entityTypeBuilder.Metadata));
+ buildAction(new TableBuilder(name, schema, entityTypeBuilder));
entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(schema);
@@ -181,7 +212,7 @@ public static class RelationalEntityTypeBuilderExtensions
Check.NotNull(name, nameof(name));
Check.NullButNotEmpty(schema, nameof(schema));
- buildAction(new TableBuilder(name, schema, entityTypeBuilder.Metadata));
+ buildAction(new TableBuilder(name, schema, entityTypeBuilder));
entityTypeBuilder.Metadata.SetTableName(name);
entityTypeBuilder.Metadata.SetSchema(schema);
diff --git a/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs b/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
index a0b97cff56f..b97e3769bb4 100644
--- a/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
+++ b/src/EFCore.Relational/Metadata/Builders/TableBuilder.cs
@@ -22,12 +22,15 @@ public class TableBuilder
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[EntityFrameworkInternal]
- public TableBuilder(string name, string? schema, IMutableEntityType entityType)
+ public TableBuilder(string? name, string? schema, EntityTypeBuilder entityTypeBuilder)
{
- EntityType = entityType;
+ EntityTypeBuilder = entityTypeBuilder;
}
- private IMutableEntityType EntityType { [DebuggerStepThrough] get; }
+ ///
+ /// The entity type builder for the entity being configured.
+ ///
+ public virtual EntityTypeBuilder EntityTypeBuilder { get; }
///
/// Configures the table to be ignored by migrations.
@@ -36,7 +39,8 @@ public TableBuilder(string name, string? schema, IMutableEntityType entityType)
/// The same builder instance so that multiple calls can be chained.
public virtual TableBuilder ExcludeFromMigrations(bool excluded = true)
{
- EntityType.SetIsTableExcludedFromMigrations(excluded);
+ EntityTypeBuilder.Metadata.SetIsTableExcludedFromMigrations(excluded);
+
return this;
}
diff --git a/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs b/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs
index 8fdef0c8c4f..a28ed85a3f2 100644
--- a/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs
+++ b/src/EFCore.Relational/Metadata/Builders/TableBuilder`.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Microsoft.EntityFrameworkCore.Metadata.Builders
@@ -22,11 +23,19 @@ public class TableBuilder : TableBuilder
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[EntityFrameworkInternal]
- public TableBuilder(string name, string? schema, IMutableEntityType entityType)
- : base(name, schema, entityType)
+ public TableBuilder(string? name, string? schema, EntityTypeBuilder entityTypeBuilder)
+ : base(name, schema, entityTypeBuilder)
{
}
+ ///
+ /// The entity type builder for the entity being configured.
+ ///
+ public new virtual EntityTypeBuilder EntityTypeBuilder
+ {
+ [DebuggerStepThrough] get => (EntityTypeBuilder)base.EntityTypeBuilder;
+ }
+
///
/// Configures the table to be ignored by migrations.
///
diff --git a/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs b/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs
index 9349e8822a6..efdfb82145b 100644
--- a/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs
+++ b/src/EFCore.Relational/Migrations/IMigrationsAnnotationProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
+using System.Linq;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.DependencyInjection;
@@ -101,5 +102,41 @@ public interface IMigrationsAnnotationProvider
/// The check constraint.
/// The annotations.
IEnumerable ForRemove(ICheckConstraint checkConstraint);
+
+ ///
+ /// Gets provider-specific Migrations annotations for the given
+ /// when it is being renamed.
+ ///
+ /// The table.
+ /// The annotations.
+ IEnumerable ForRename(ITable table)
+ => Enumerable.Empty();
+
+ ///
+ /// Gets provider-specific Migrations annotations for the given
+ /// when it is being renamed.
+ ///
+ /// The column.
+ /// The annotations.
+ IEnumerable ForRename(IColumn column)
+ => Enumerable.Empty();
+
+ ///
+ /// Gets provider-specific Migrations annotations for the given
+ /// when it is being renamed.
+ ///
+ /// The index.
+ /// The annotations.
+ IEnumerable ForRename(ITableIndex index)
+ => Enumerable.Empty();
+
+ ///
+ /// Gets provider-specific Migrations annotations for the given
+ /// when it is being renamed.
+ ///
+ /// The sequence.
+ /// The annotations.
+ IEnumerable ForRename(ISequence sequence)
+ => Enumerable.Empty();
}
}
diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
index de046e80639..531f899f816 100644
--- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
+++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs
@@ -429,7 +429,6 @@ public virtual IReadOnlyList GetDifferences(IRelationalModel
IRelationalModel? target)
{
var targetMigrationsAnnotations = target?.GetAnnotations().ToList();
-
if (source == null)
{
if (targetMigrationsAnnotations?.Count > 0)
@@ -603,13 +602,17 @@ protected virtual IEnumerable Remove(string source, DiffCont
if (source.Schema != target.Schema
|| source.Name != target.Name)
{
- yield return new RenameTableOperation
+ var renameTableOperation = new RenameTableOperation
{
Schema = source.Schema,
Name = source.Name,
NewSchema = target.Schema,
NewName = target.Name
};
+
+ renameTableOperation.AddAnnotations(MigrationsAnnotations.ForRename(source));
+
+ yield return renameTableOperation;
}
var sourceMigrationsAnnotations = source.GetAnnotations();
@@ -983,13 +986,17 @@ private static bool EntityTypePathEquals(IEntityType source, IEntityType target,
if (source.Name != target.Name)
{
- yield return new RenameColumnOperation
+ var renameColumnOperation = new RenameColumnOperation
{
Schema = table.Schema,
Table = table.Name,
Name = source.Name,
NewName = target.Name
};
+
+ renameColumnOperation.AddAnnotations(MigrationsAnnotations.ForRename(source));
+
+ yield return renameColumnOperation;
}
var sourceTypeMapping = sourceMapping.TypeMapping;
@@ -1400,13 +1407,17 @@ private bool IndexStructureEquals(ITableIndex source, ITableIndex target, DiffCo
if (sourceName != targetName)
{
- yield return new RenameIndexOperation
+ var renameIndexOperation = new RenameIndexOperation
{
Schema = targetTable.Schema,
Table = targetTable.Name,
Name = sourceName,
NewName = targetName
};
+
+ renameIndexOperation.AddAnnotations(MigrationsAnnotations.ForRename(source));
+
+ yield return renameIndexOperation;
}
}
@@ -1550,13 +1561,17 @@ protected virtual IEnumerable Remove(ICheckConstraint source
if (source.Schema != target.Schema
|| source.Name != target.Name)
{
- yield return new RenameSequenceOperation
+ var renameSequenceOperation = new RenameSequenceOperation
{
Schema = source.Schema,
Name = source.Name,
NewSchema = target.Schema,
NewName = target.Name
};
+
+ renameSequenceOperation.AddAnnotations(MigrationsAnnotations.ForRename(source));
+
+ yield return renameSequenceOperation;
}
if (source.StartValue != target.StartValue)
diff --git a/src/EFCore.Relational/Migrations/MigrationBuilder.cs b/src/EFCore.Relational/Migrations/MigrationBuilder.cs
index ca7ce759cf1..64ad133d5ef 100644
--- a/src/EFCore.Relational/Migrations/MigrationBuilder.cs
+++ b/src/EFCore.Relational/Migrations/MigrationBuilder.cs
@@ -1066,6 +1066,7 @@ public MigrationBuilder(string? activeProvider)
Name = name,
NewName = newName
};
+
Operations.Add(operation);
return new OperationBuilder(operation);
diff --git a/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs b/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs
index 8a9f1972efa..4907e44cfb4 100644
--- a/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs
+++ b/src/EFCore.Relational/Migrations/MigrationsAnnotationProvider.cs
@@ -71,5 +71,21 @@ public virtual IEnumerable ForRemove(ISequence sequence)
///
public virtual IEnumerable ForRemove(ICheckConstraint checkConstraint)
=> Enumerable.Empty();
+
+ ///
+ public virtual IEnumerable ForRename(ITable table)
+ => Enumerable.Empty();
+
+ ///
+ public virtual IEnumerable ForRename(IColumn column)
+ => Enumerable.Empty();
+
+ ///
+ public virtual IEnumerable ForRename(ITableIndex index)
+ => Enumerable.Empty();
+
+ ///
+ public virtual IEnumerable ForRename(ISequence sequence)
+ => Enumerable.Empty();
}
}
diff --git a/src/EFCore.Relational/Migrations/Operations/AddPrimaryKeyOperation.cs b/src/EFCore.Relational/Migrations/Operations/AddPrimaryKeyOperation.cs
index f77e8e092f2..b68e567b506 100644
--- a/src/EFCore.Relational/Migrations/Operations/AddPrimaryKeyOperation.cs
+++ b/src/EFCore.Relational/Migrations/Operations/AddPrimaryKeyOperation.cs
@@ -50,6 +50,7 @@ public static AddPrimaryKeyOperation CreateFrom(IPrimaryKeyConstraint primaryKey
Name = primaryKey.Name,
Columns = primaryKey.Columns.Select(c => c.Name).ToArray()
};
+
operation.AddAnnotations(primaryKey.GetAnnotations());
return operation;
diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
index 12472c0bfda..96fb6770f7c 100644
--- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
+++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
@@ -3,9 +3,12 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
@@ -78,6 +81,91 @@ public SqlServerAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies depe
return fragments;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public override IReadOnlyList GenerateFluentApiCalls(
+ IEntityType entityType,
+ IDictionary annotations)
+ {
+ var result = base.GenerateFluentApiCalls(entityType, annotations);
+
+ if (annotations.TryGetValue(SqlServerAnnotationNames.IsTemporal, out var isTemporalAnnotation)
+ && isTemporalAnnotation.Value as bool? == true)
+ {
+ var historyTableName = annotations[SqlServerAnnotationNames.TemporalHistoryTableName].Value as string;
+ var historyTableSchema = annotations.ContainsKey(SqlServerAnnotationNames.TemporalHistoryTableSchema)
+ ? annotations[SqlServerAnnotationNames.TemporalHistoryTableSchema].Value as string
+ : null;
+
+ var periodStartProperty = entityType.GetProperty(entityType.GetTemporalPeriodStartPropertyName()!);
+ var periodEndProperty = entityType.GetProperty(entityType.GetTemporalPeriodEndPropertyName()!);
+ var periodStartColumnName = periodStartProperty[RelationalAnnotationNames.ColumnName] as string;
+ var periodEndColumnName = periodEndProperty[RelationalAnnotationNames.ColumnName] as string;
+
+ // ttb => ttb.WithHistoryTable("HistoryTable", "schema")
+ var temporalTableBuilderCalls = new List();
+ if (historyTableName != null)
+ {
+ temporalTableBuilderCalls.Add(
+ historyTableSchema != null
+ ? new MethodCallCodeFragment(nameof(TemporalTableBuilder.WithHistoryTable), historyTableName, historyTableSchema)
+ : new MethodCallCodeFragment(nameof(TemporalTableBuilder.WithHistoryTable), historyTableName));
+ }
+
+ // ttb => ttb.HasPeriodStart("Start").HasColumnName("ColumnStart")
+ temporalTableBuilderCalls.Add(
+ periodStartColumnName != null
+ ? new MethodCallCodeFragment(
+ nameof(TemporalTableBuilder.HasPeriodStart),
+ new[] { periodStartProperty.Name },
+ new MethodCallCodeFragment(
+ nameof(TemporalPeriodPropertyBuilder.HasColumnName),
+ periodStartColumnName))
+ : new MethodCallCodeFragment(
+ nameof(TemporalTableBuilder.HasPeriodStart),
+ periodStartProperty.Name));
+
+ // ttb => ttb.HasPeriodEnd("End").HasColumnName("ColumnEnd")
+ temporalTableBuilderCalls.Add(
+ periodEndColumnName != null
+ ? new MethodCallCodeFragment(
+ nameof(TemporalTableBuilder.HasPeriodEnd),
+ new[] { periodEndProperty.Name },
+ new MethodCallCodeFragment(
+ nameof(TemporalPeriodPropertyBuilder.HasColumnName),
+ periodEndColumnName))
+ : new MethodCallCodeFragment(
+ nameof(TemporalTableBuilder.HasPeriodEnd),
+ periodEndProperty.Name));
+
+
+ // ToTable(tb => tb.IsTemporal(ttb => { ... }))
+ var toTemporalTableCall = new MethodCallCodeFragment(
+ nameof(RelationalEntityTypeBuilderExtensions.ToTable),
+ new NestedClosureCodeFragment(
+ "tb",
+ new MethodCallCodeFragment(
+ nameof(SqlServerTableBuilderExtensions.IsTemporal),
+ new NestedClosureCodeFragment(
+ "ttb",
+ temporalTableBuilderCalls))));
+
+ annotations.Remove(SqlServerAnnotationNames.IsTemporal);
+ annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableName);
+ annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema);
+ annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartPropertyName);
+ annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndPropertyName);
+
+ return result.Concat(new[] { toTemporalTableCall }).ToList();
+ }
+
+ return result;
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeBuilderExtensions.cs
index 0523cdbdc04..35d4efc7996 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeBuilderExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeBuilderExtensions.cs
@@ -116,5 +116,216 @@ public static class SqlServerEntityTypeBuilderExtensions
return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.MemoryOptimized, memoryOptimized, fromDataAnnotation);
}
+
+ ///
+ /// Configures the table as temporal.
+ ///
+ /// The builder for the entity being configured.
+ /// A value indicating whether the table is temporal.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionEntityTypeBuilder? IsTemporal(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ bool temporal = true,
+ bool fromDataAnnotation = false)
+ {
+ if (entityTypeBuilder.CanSetIsTemporal(temporal, fromDataAnnotation))
+ {
+ entityTypeBuilder.Metadata.SetIsTemporal(temporal, fromDataAnnotation);
+
+ return entityTypeBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether the mapped table can be configured as temporal.
+ ///
+ /// The builder for the entity type being configured.
+ /// A value indicating whether the table is temporal.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the mapped table can be configured as temporal.
+ public static bool CanSetIsTemporal(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ bool temporal = true,
+ bool fromDataAnnotation = false)
+ {
+ Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
+
+ return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.IsTemporal, temporal, fromDataAnnotation);
+ }
+
+ ///
+ /// Configures a history table name for the entity mapped to a temporal table.
+ ///
+ /// The builder for the entity being configured.
+ /// The name of the history table.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionEntityTypeBuilder? WithHistoryTableName(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string name,
+ bool fromDataAnnotation = false)
+ {
+ if (entityTypeBuilder.CanSetHistoryTableName(name, fromDataAnnotation))
+ {
+ entityTypeBuilder.Metadata.SetTemporalHistoryTableName(name, fromDataAnnotation);
+
+ return entityTypeBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether the given history table name can be set for the entity.
+ ///
+ /// The builder for the entity type being configured.
+ /// The name of the history table.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the mapped table can have history table name.
+ public static bool CanSetHistoryTableName(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string name,
+ bool fromDataAnnotation = false)
+ {
+ Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
+ Check.NotNull(name, nameof(name));
+
+ return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName, name, fromDataAnnotation);
+ }
+
+ ///
+ /// Configures a history table schema for the entity mapped to a temporal table.
+ ///
+ /// The builder for the entity being configured.
+ /// The schema of the history table.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionEntityTypeBuilder? WithHistoryTableSchema(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string? schema,
+ bool fromDataAnnotation = false)
+ {
+ if (entityTypeBuilder.CanSetHistoryTableSchema(schema, fromDataAnnotation))
+ {
+ entityTypeBuilder.Metadata.SetTemporalHistoryTableSchema(schema, fromDataAnnotation);
+
+ return entityTypeBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether the mapped table can have history table schema.
+ ///
+ /// The builder for the entity type being configured.
+ /// The schema of the history table.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the mapped table can have history table schema.
+ public static bool CanSetHistoryTableSchema(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string? schema,
+ bool fromDataAnnotation = false)
+ {
+ Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
+
+ return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema, schema, fromDataAnnotation);
+ }
+
+ ///
+ /// Configures a period start property for the entity mapped to a temporal table.
+ ///
+ /// The builder for the entity being configured.
+ /// The name of the period start property.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionEntityTypeBuilder? HasPeriodStart(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string? propertyName,
+ bool fromDataAnnotation = false)
+ {
+ if (entityTypeBuilder.CanSetPeriodStart(propertyName, fromDataAnnotation))
+ {
+ entityTypeBuilder.Metadata.SetTemporalPeriodStartPropertyName(propertyName, fromDataAnnotation);
+
+ return entityTypeBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether the mapped table can have period start property.
+ ///
+ /// The builder for the entity type being configured.
+ /// The name of the period start property.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the mapped table can have period start property.
+ public static bool CanSetPeriodStart(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string? propertyName,
+ bool fromDataAnnotation = false)
+ {
+ Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
+
+ return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.TemporalPeriodStartPropertyName, propertyName, fromDataAnnotation);
+ }
+
+ ///
+ /// Configures a period end property for the entity mapped to a temporal table.
+ ///
+ /// The builder for the entity being configured.
+ /// The name of the period end property.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionEntityTypeBuilder? HasPeriodEnd(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string? propertyName,
+ bool fromDataAnnotation = false)
+ {
+ if (entityTypeBuilder.CanSetPeriodEnd(propertyName, fromDataAnnotation))
+ {
+ entityTypeBuilder.Metadata.SetTemporalPeriodEndPropertyName(propertyName, fromDataAnnotation);
+
+ return entityTypeBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether the mapped table can have period end property.
+ ///
+ /// The builder for the entity type being configured.
+ /// The name of the period end property.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the mapped table can have period end property.
+ public static bool CanSetPeriodEnd(
+ this IConventionEntityTypeBuilder entityTypeBuilder,
+ string? propertyName,
+ bool fromDataAnnotation = false)
+ {
+ Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
+
+ return entityTypeBuilder.CanSetAnnotation(SqlServerAnnotationNames.TemporalPeriodEndPropertyName, propertyName, fromDataAnnotation);
+ }
}
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs
index 386289a3fce..8cb963567a2 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs
@@ -12,6 +12,8 @@ namespace Microsoft.EntityFrameworkCore
///
public static class SqlServerEntityTypeExtensions
{
+ private const string DefaultHistoryTableNameSuffix = "History";
+
///
/// Returns a value indicating whether the entity type is mapped to a memory-optimized table.
///
@@ -52,5 +54,227 @@ public static void SetIsMemoryOptimized(this IMutableEntityType entityType, bool
/// The configuration source for the memory-optimized setting.
public static ConfigurationSource? GetIsMemoryOptimizedConfigurationSource(this IConventionEntityType entityType)
=> entityType.FindAnnotation(SqlServerAnnotationNames.MemoryOptimized)?.GetConfigurationSource();
+
+ ///
+ /// Returns a value indicating whether the entity type is mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// if the entity type is mapped to a temporal table.
+ public static bool IsTemporal(this IReadOnlyEntityType entityType)
+ => entityType[SqlServerAnnotationNames.IsTemporal] as bool? ?? false;
+
+ ///
+ /// Sets a value indicating whether the entity type is mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ public static void SetIsTemporal(this IMutableEntityType entityType, bool temporal)
+ => entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.IsTemporal, temporal);
+
+ ///
+ /// Sets a value indicating whether the entity type is mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static bool? SetIsTemporal(
+ this IConventionEntityType entityType,
+ bool? temporal,
+ bool fromDataAnnotation = false)
+ {
+ entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.IsTemporal, temporal, fromDataAnnotation);
+
+ return temporal;
+ }
+
+ ///
+ /// Gets the configuration source for the temporal table setting.
+ ///
+ /// The entity type.
+ /// The configuration source for the temporal table setting.
+ public static ConfigurationSource? GetIsTemporalConfigurationSource(this IConventionEntityType entityType)
+ => entityType.FindAnnotation(SqlServerAnnotationNames.IsTemporal)?.GetConfigurationSource();
+
+ ///
+ /// Returns a value representing the name of the period start property of the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// Name of the period start property.
+ public static string? GetTemporalPeriodStartPropertyName(this IReadOnlyEntityType entityType)
+ => entityType[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] as string;
+
+ ///
+ /// Sets a value representing the name of the period start property of the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ public static void SetTemporalPeriodStartPropertyName(this IMutableEntityType entityType, string? periodStartPropertyName)
+ => entityType.SetAnnotation(SqlServerAnnotationNames.TemporalPeriodStartPropertyName, periodStartPropertyName);
+
+ ///
+ /// Sets a value representing the name of the period start property of the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static string? SetTemporalPeriodStartPropertyName(
+ this IConventionEntityType entityType,
+ string? periodStartPropertyName,
+ bool fromDataAnnotation = false)
+ {
+ entityType.SetAnnotation(
+ SqlServerAnnotationNames.TemporalPeriodStartPropertyName,
+ periodStartPropertyName,
+ fromDataAnnotation);
+
+ return periodStartPropertyName;
+ }
+
+ ///
+ /// Gets the configuration source for the temporal table period start property name setting.
+ ///
+ /// The entity type.
+ /// The configuration source for the temporal table period start property name setting.
+ public static ConfigurationSource? GetTemporalPeriodStartPropertyNameConfigurationSource(this IConventionEntityType entityType)
+ => entityType.FindAnnotation(SqlServerAnnotationNames.TemporalPeriodStartPropertyName)?.GetConfigurationSource();
+
+ ///
+ /// Returns a value representing the name of the period end property of the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// Name of the period start property.
+ public static string? GetTemporalPeriodEndPropertyName(this IReadOnlyEntityType entityType)
+ => entityType[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] as string;
+
+ ///
+ /// Sets a value representing the name of the period end property of the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ public static void SetTemporalPeriodEndPropertyName(this IMutableEntityType entityType, string? periodEndPropertyName)
+ => entityType.SetAnnotation(SqlServerAnnotationNames.TemporalPeriodEndPropertyName, periodEndPropertyName);
+
+ ///
+ /// Sets a value representing the name of the period end property of the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static string? SetTemporalPeriodEndPropertyName(
+ this IConventionEntityType entityType,
+ string? periodEndPropertyName,
+ bool fromDataAnnotation = false)
+ {
+ entityType.SetAnnotation(
+ SqlServerAnnotationNames.TemporalPeriodEndPropertyName,
+ periodEndPropertyName,
+ fromDataAnnotation);
+
+ return periodEndPropertyName;
+ }
+
+ ///
+ /// Gets the configuration source for the temporal table period end property name setting.
+ ///
+ /// The entity type.
+ /// The configuration source for the temporal table period end property name setting.
+ public static ConfigurationSource? GetTemporalPeriodEndPropertyNameConfigurationSource(this IConventionEntityType entityType)
+ => entityType.FindAnnotation(SqlServerAnnotationNames.TemporalPeriodEndPropertyName)?.GetConfigurationSource();
+
+ ///
+ /// Returns a value representing the name of the history table associated with the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// Name of the history table.
+ public static string? GetTemporalHistoryTableName(this IReadOnlyEntityType entityType)
+ => entityType[SqlServerAnnotationNames.TemporalHistoryTableName] is string historyTableName
+ ? historyTableName
+ : entityType[SqlServerAnnotationNames.IsTemporal] as bool? == true
+ ? entityType.ShortName() + DefaultHistoryTableNameSuffix
+ : null;
+
+ ///
+ /// Sets a value representing the name of the history table associated with the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ public static void SetTemporalHistoryTableName(this IMutableEntityType entityType, string? historyTableName)
+ => entityType.SetAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName, historyTableName);
+
+ ///
+ /// Sets a value representing the name of the history table associated with the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static string? SetTemporalHistoryTableName(
+ this IConventionEntityType entityType,
+ string? historyTableName,
+ bool fromDataAnnotation = false)
+ {
+ entityType.SetAnnotation(
+ SqlServerAnnotationNames.TemporalHistoryTableName,
+ historyTableName,
+ fromDataAnnotation);
+
+ return historyTableName;
+ }
+
+ ///
+ /// Gets the configuration source for the temporal history table name setting.
+ ///
+ /// The entity type.
+ /// The configuration source for the temporal history table name setting.
+ public static ConfigurationSource? GetTemporalHistoryTableNameConfigurationSource(this IConventionEntityType entityType)
+ => entityType.FindAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName)?.GetConfigurationSource();
+
+ ///
+ /// Returns a value representing the schema of the history table associated with the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// Name of the history table.
+ public static string? GetTemporalHistoryTableSchema(this IReadOnlyEntityType entityType)
+ => entityType[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string
+ ?? entityType[RelationalAnnotationNames.Schema] as string;
+
+ ///
+ /// Sets a value representing the schema of the history table associated with the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ public static void SetTemporalHistoryTableSchema(this IMutableEntityType entityType, string? historyTableSchema)
+ => entityType.SetAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema, historyTableSchema);
+
+ ///
+ /// Sets a value representing the schema of the history table associated with the entity mapped to a temporal table.
+ ///
+ /// The entity type.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The configured value.
+ public static string? SetTemporalHistoryTableSchema(
+ this IConventionEntityType entityType,
+ string? historyTableSchema,
+ bool fromDataAnnotation = false)
+ {
+ entityType.SetAnnotation(
+ SqlServerAnnotationNames.TemporalHistoryTableSchema,
+ historyTableSchema,
+ fromDataAnnotation);
+
+ return historyTableSchema;
+ }
+
+ ///
+ /// Gets the configuration source for the temporal history table schema setting.
+ ///
+ /// The entity type.
+ /// The configuration source for the temporal history table schema setting.
+ public static ConfigurationSource? GetTemporalHistoryTableSchemaConfigurationSource(this IConventionEntityType entityType)
+ => entityType.FindAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema)?.GetConfigurationSource();
}
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerTableBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerTableBuilderExtensions.cs
new file mode 100644
index 00000000000..f9acd0a4c4f
--- /dev/null
+++ b/src/EFCore.SqlServer/Extensions/SqlServerTableBuilderExtensions.cs
@@ -0,0 +1,81 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+// ReSharper disable once CheckNamespace
+namespace Microsoft.EntityFrameworkCore
+{
+ ///
+ /// SQL Server specific extension methods for .
+ ///
+ public static class SqlServerTableBuilderExtensions
+ {
+ ///
+ /// Configures the table as temporal.
+ ///
+ /// The builder for the table being configured.
+ /// A value indicating whether the table is temporal.
+ /// An object that can be used to configure the temporal table.
+ public static TemporalTableBuilder IsTemporal(
+ this TableBuilder tableBuilder,
+ bool temporal = true)
+ {
+ tableBuilder.EntityTypeBuilder.Metadata.SetIsTemporal(temporal);
+
+ return new TemporalTableBuilder(tableBuilder.EntityTypeBuilder);
+ }
+
+ ///
+ /// Configures the table as temporal.
+ ///
+ /// The builder for the table being configured.
+ /// An action that performs configuration of the temporal table.
+ /// The same builder instance so that multiple calls can be chained.
+ public static TableBuilder IsTemporal(
+ this TableBuilder tableBuilder,
+ Action buildAction)
+ {
+ tableBuilder.EntityTypeBuilder.Metadata.SetIsTemporal(true);
+ buildAction(new TemporalTableBuilder(tableBuilder.EntityTypeBuilder));
+
+ return tableBuilder;
+ }
+
+ ///
+ /// Configures the table as temporal.
+ ///
+ /// The entity type being configured.
+ /// The builder for the table being configured.
+ /// A value indicating whether the table is temporal.
+ /// An object that can be used to configure the temporal table.
+ public static TemporalTableBuilder IsTemporal(
+ this TableBuilder tableBuilder,
+ bool temporal = true)
+ where TEntity : class
+ {
+ tableBuilder.EntityTypeBuilder.Metadata.SetIsTemporal(temporal);
+
+ return new TemporalTableBuilder(tableBuilder.EntityTypeBuilder);
+ }
+
+ ///
+ /// Configures the table as temporal.
+ ///
+ /// The entity type being configured.
+ /// The builder for the table being configured.
+ /// An action that performs configuration of the temporal table.
+ /// The same builder instance so that multiple calls can be chained.
+ public static TableBuilder IsTemporal(
+ this TableBuilder tableBuilder,
+ Action> buildAction)
+ where TEntity: class
+ {
+ tableBuilder.EntityTypeBuilder.Metadata.SetIsTemporal(true);
+ buildAction(new TemporalTableBuilder(tableBuilder.EntityTypeBuilder));
+
+ return tableBuilder;
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs
index 393c4f9fb6c..780de7f6e6f 100644
--- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs
+++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs
@@ -59,6 +59,7 @@ public override void Validate(IModel model, IDiagnosticsLogger
@@ -209,6 +210,112 @@ public override void Validate(IModel model, IDiagnosticsLogger
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected virtual void ValidateTemporalTables(
+ IModel model,
+ IDiagnosticsLogger logger)
+ {
+ var temporalEntityTypes = model.GetEntityTypes().Where(t => t.IsTemporal()).ToList();
+ foreach (var temporalEntityType in temporalEntityTypes)
+ {
+ if (temporalEntityType.BaseType != null)
+ {
+ throw new InvalidOperationException(SqlServerStrings.TemporalOnlyOnRoot(temporalEntityType.DisplayName()));
+ }
+
+ ValidateTemporalPeriodProperty(temporalEntityType, periodStart: true);
+ ValidateTemporalPeriodProperty(temporalEntityType, periodStart: false);
+
+ var derivedTableMappings = temporalEntityType.GetDerivedTypes().Select(t => t.GetTableName()).Distinct().ToList();
+ if (derivedTableMappings.Count > 0
+ && (derivedTableMappings.Count != 1 || derivedTableMappings.First() != temporalEntityType.GetTableName()))
+ {
+ throw new InvalidOperationException(SqlServerStrings.TemporalOnlySupportedForTPH(temporalEntityType.DisplayName()));
+ }
+ }
+ }
+
+ private void ValidateTemporalPeriodProperty(IEntityType temporalEntityType, bool periodStart)
+ {
+ var annotationPropertyName = periodStart
+ ? temporalEntityType.GetTemporalPeriodStartPropertyName()
+ : temporalEntityType.GetTemporalPeriodEndPropertyName();
+
+ if (annotationPropertyName == null)
+ {
+ throw new InvalidOperationException(
+ SqlServerStrings.TemporalMustDefinePeriodProperties(
+ temporalEntityType.DisplayName()));
+ }
+
+ var periodProperty = temporalEntityType.FindProperty(annotationPropertyName);
+ if (periodProperty == null)
+ {
+ throw new InvalidOperationException(
+ SqlServerStrings.TemporalExpectedPeriodPropertyNotFound(
+ temporalEntityType.DisplayName(), annotationPropertyName));
+ }
+
+ if (!periodProperty.IsShadowProperty() && !temporalEntityType.IsPropertyBag)
+ {
+ throw new InvalidOperationException(
+ SqlServerStrings.TemporalPeriodPropertyMustBeInShadowState(
+ temporalEntityType.DisplayName(), periodProperty.Name));
+ }
+
+ if (periodProperty.IsNullable
+ || periodProperty.ClrType != typeof(DateTime))
+ {
+ throw new InvalidOperationException(
+ SqlServerStrings.TemporalPeriodPropertyMustBeNonNullableDateTime(
+ temporalEntityType.DisplayName(), periodProperty.Name, nameof(DateTime)));
+ }
+
+ var expectedPeriodColumnName = "datetime2";
+ if (periodProperty.GetColumnType() != expectedPeriodColumnName)
+ {
+ throw new InvalidOperationException(
+ SqlServerStrings.TemporalPeriodPropertyMustBeMappedToDatetime2(
+ temporalEntityType.DisplayName(), periodProperty.Name, expectedPeriodColumnName));
+ }
+
+ if (periodProperty.TryGetDefaultValue(out var _))
+ {
+ throw new InvalidOperationException(
+ SqlServerStrings.TemporalPeriodPropertyCantHaveDefaultValue(
+ temporalEntityType.DisplayName(), periodProperty.Name));
+ }
+
+ if (temporalEntityType.GetTableName() is string tableName)
+ {
+ var storeObjectIdentifier = StoreObjectIdentifier.Table(tableName, temporalEntityType.GetSchema());
+ var periodColumnName = periodProperty.GetColumnName(storeObjectIdentifier);
+
+ var propertiesMappedToPeriodColumn = temporalEntityType.GetProperties().Where(p => p.Name != periodProperty.Name && p.GetColumnName(storeObjectIdentifier) == periodColumnName).ToList();
+ foreach (var propertyMappedToPeriodColumn in propertiesMappedToPeriodColumn)
+ {
+ if (propertyMappedToPeriodColumn.ValueGenerated != ValueGenerated.OnAddOrUpdate)
+ {
+ throw new InvalidOperationException(SqlServerStrings.TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate(
+ temporalEntityType.DisplayName(), propertyMappedToPeriodColumn.Name, nameof(ValueGenerated.OnAddOrUpdate)));
+ }
+
+ if (propertyMappedToPeriodColumn.TryGetDefaultValue(out var _))
+ {
+ throw new InvalidOperationException(SqlServerStrings.TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue(
+ temporalEntityType.DisplayName(), propertyMappedToPeriodColumn.Name));
+ }
+ }
+ }
+
+ // TODO: check that period property is excluded from query (once the annotation is added)
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -223,7 +330,6 @@ public override void Validate(IModel model, IDiagnosticsLogger t.IsTemporal())
+ && mappedTypes.Select(t => t.GetRootType()).Distinct().Count() > 1)
+ {
+ throw new InvalidOperationException(SqlServerStrings.TemporalNotSupportedForTableSplitting(tableName));
+ }
+
base.ValidateSharedTableCompatibility(mappedTypes, tableName, schema, logger);
}
diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs
new file mode 100644
index 00000000000..c3d5e892829
--- /dev/null
+++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs
@@ -0,0 +1,74 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.ComponentModel;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Builders
+{
+ ///
+ ///
+ /// Instances of this class are returned from methods when using the API
+ /// and it is not designed to be directly constructed in your application code.
+ ///
+ ///
+ public class TemporalPeriodPropertyBuilder
+ {
+ private readonly EntityTypeBuilder _entityTypeBuilder;
+ private readonly string _periodPropertyName;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [EntityFrameworkInternal]
+ public TemporalPeriodPropertyBuilder(EntityTypeBuilder entityTypeBuilder, string periodPropertyName)
+ {
+ _entityTypeBuilder = entityTypeBuilder;
+ _periodPropertyName = periodPropertyName;
+ }
+
+ ///
+ /// Configures the column name the period property maps to.
+ ///
+ /// The name of the column.
+ /// The same builder instance so that multiple calls can be chained.
+ public virtual TemporalPeriodPropertyBuilder HasColumnName(string name)
+ {
+ _entityTypeBuilder.Metadata.GetProperty(_periodPropertyName).SetColumnName(name);
+
+ return this;
+ }
+
+ #region Hidden System.Object members
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override string? ToString()
+ => base.ToString();
+
+ ///
+ /// Determines whether the specified object is equal to the current object.
+ ///
+ /// The object to compare with the current object.
+ /// if the specified object is equal to the current object; otherwise, .
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override bool Equals(object? obj)
+ => base.Equals(obj);
+
+ ///
+ /// Serves as the default hash function.
+ ///
+ /// A hash code for the current object.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override int GetHashCode()
+ => base.GetHashCode();
+
+ #endregion
+ }
+}
diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs
new file mode 100644
index 00000000000..c00b99edf1d
--- /dev/null
+++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs
@@ -0,0 +1,110 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.ComponentModel;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Builders
+{
+ ///
+ ///
+ /// Instances of this class are returned from methods when using the API
+ /// and it is not designed to be directly constructed in your application code.
+ ///
+ ///
+ public class TemporalTableBuilder
+ {
+ private readonly EntityTypeBuilder _entityTypeBuilder;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [EntityFrameworkInternal]
+ public TemporalTableBuilder(EntityTypeBuilder entityTypeBuilder)
+ {
+ _entityTypeBuilder = entityTypeBuilder;
+ }
+
+ ///
+ /// Configures a history table for the entity mapped to a temporal table.
+ ///
+ /// The name of the history table.
+ /// The same builder instance so that multiple calls can be chained.
+ public virtual TemporalTableBuilder WithHistoryTable(string name)
+ {
+ _entityTypeBuilder.Metadata.SetTemporalHistoryTableName(name);
+
+ return this;
+ }
+
+ ///
+ /// Configures a history table for the entity mapped to a temporal table.
+ ///
+ /// The name of the history table.
+ /// The schema of the history table.
+ /// The same builder instance so that multiple calls can be chained.
+ public virtual TemporalTableBuilder WithHistoryTable(string name, string? schema)
+ {
+ _entityTypeBuilder.Metadata.SetTemporalHistoryTableName(name);
+ _entityTypeBuilder.Metadata.SetTemporalHistoryTableSchema(schema);
+
+ return this;
+ }
+
+ ///
+ /// Returns an object that can be used to configure a period start property of the entity type mapped to a temporal table.
+ ///
+ /// The name of the period start property.
+ /// An object that can be used to configure the period start property.
+ public virtual TemporalPeriodPropertyBuilder HasPeriodStart(string propertyName)
+ {
+ _entityTypeBuilder.Metadata.SetTemporalPeriodStartPropertyName(propertyName);
+
+ return new TemporalPeriodPropertyBuilder(_entityTypeBuilder, propertyName);
+ }
+
+ ///
+ /// Returns an object that can be used to configure a period end property of the entity type mapped to a temporal table.
+ ///
+ /// The name of the period end property.
+ /// An object that can be used to configure the period end property.
+ public virtual TemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName)
+ {
+ _entityTypeBuilder.Metadata.SetTemporalPeriodEndPropertyName(propertyName);
+
+ return new TemporalPeriodPropertyBuilder(_entityTypeBuilder, propertyName);
+ }
+
+ #region Hidden System.Object members
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override string? ToString()
+ => base.ToString();
+
+ ///
+ /// Determines whether the specified object is equal to the current object.
+ ///
+ /// The object to compare with the current object.
+ /// if the specified object is equal to the current object; otherwise, .
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override bool Equals(object? obj)
+ => base.Equals(obj);
+
+ ///
+ /// Serves as the default hash function.
+ ///
+ /// A hash code for the current object.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override int GetHashCode()
+ => base.GetHashCode();
+
+ #endregion
+ }
+}
diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs
new file mode 100644
index 00000000000..b05a67d7806
--- /dev/null
+++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs
@@ -0,0 +1,39 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Builders
+{
+ ///
+ ///
+ /// Instances of this class are returned from methods when using the API
+ /// and it is not designed to be directly constructed in your application code.
+ ///
+ ///
+ /// The entity type being configured.
+ public class TemporalTableBuilder : TemporalTableBuilder
+ where TEntity : class
+ {
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [EntityFrameworkInternal]
+ public TemporalTableBuilder(EntityTypeBuilder entityTypeBuilder)
+ : base(entityTypeBuilder)
+ {
+ }
+
+ ///
+ /// Configures a history table for the entity mapped to a temporal table.
+ ///
+ /// The name of the history table.
+ /// The schema of the history table.
+ /// The same builder instance so that multiple calls can be chained.
+ public new virtual TemporalTableBuilder WithHistoryTable(string name, string? schema = null)
+ => (TemporalTableBuilder)base.WithHistoryTable(name, schema);
+ }
+}
diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs
index 3dc8e37e187..c7729b91c62 100644
--- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs
+++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs
@@ -64,6 +64,11 @@ public override ConventionSet CreateConventionSet()
ReplaceConvention(
conventionSet.EntityTypeAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention);
+ ConventionSet.AddBefore(
+ conventionSet.EntityTypeAnnotationChangedConventions,
+ new SqlServerTemporalConvention(),
+ typeof(SqlServerValueGenerationConvention));
+
ReplaceConvention(conventionSet.EntityTypePrimaryKeyChangedConventions, valueGenerationConvention);
conventionSet.KeyAddedConventions.Add(sqlServerInMemoryTablesConvention);
diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs
new file mode 100644
index 00000000000..bd3650a7780
--- /dev/null
+++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs
@@ -0,0 +1,85 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Metadata.Conventions
+{
+ ///
+ /// A convention that manipulates temporal settings for an entity mapped to a temporal table.
+ ///
+ public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConvention
+ {
+ private const string PeriodStartDefaultName = "PeriodStart";
+ private const string PeriodEndDefaultName = "PeriodEnd";
+
+ ///
+ public virtual void ProcessEntityTypeAnnotationChanged(
+ IConventionEntityTypeBuilder entityTypeBuilder,
+ string name,
+ IConventionAnnotation? annotation,
+ IConventionAnnotation? oldAnnotation,
+ IConventionContext context)
+ {
+ if (name == SqlServerAnnotationNames.IsTemporal)
+ {
+ if (annotation?.Value as bool? == true)
+ {
+ if (entityTypeBuilder.Metadata.GetTemporalPeriodStartPropertyName() == null)
+ {
+ entityTypeBuilder.HasPeriodStart(PeriodStartDefaultName);
+ }
+
+ if (entityTypeBuilder.Metadata.GetTemporalPeriodEndPropertyName() == null)
+ {
+ entityTypeBuilder.HasPeriodEnd(PeriodEndDefaultName);
+ }
+ }
+ else
+ {
+ entityTypeBuilder.HasPeriodStart(null);
+ entityTypeBuilder.HasPeriodEnd(null);
+ }
+ }
+
+ if (name == SqlServerAnnotationNames.TemporalPeriodStartPropertyName
+ || name == SqlServerAnnotationNames.TemporalPeriodEndPropertyName)
+ {
+ if (oldAnnotation?.Value is string oldPeriodPropertyName)
+ {
+ var oldPeriodProperty = entityTypeBuilder.Metadata.GetProperty(oldPeriodPropertyName);
+ entityTypeBuilder.RemoveUnusedImplicitProperties(new[] { oldPeriodProperty });
+
+ if (oldPeriodProperty.GetTypeConfigurationSource() == ConfigurationSource.Explicit)
+ {
+ if ((name == SqlServerAnnotationNames.TemporalPeriodStartPropertyName
+ && oldPeriodProperty.GetDefaultValue() is DateTime start
+ && start == DateTime.MinValue)
+ || (name == SqlServerAnnotationNames.TemporalPeriodEndPropertyName
+ && oldPeriodProperty.GetDefaultValue() is DateTime end
+ && end == DateTime.MaxValue))
+ {
+ oldPeriodProperty.Builder.HasDefaultValue(null);
+ }
+ }
+ }
+
+ if (annotation?.Value is string periodPropertyName)
+ {
+ var periodPropertyBuilder = entityTypeBuilder.Property(
+ typeof(DateTime),
+ periodPropertyName);
+
+ if (periodPropertyBuilder != null)
+ {
+ // set column name explicitly so that we don't try to uniquefy it to some other column
+ // in case another property is defined that maps to the same column
+ periodPropertyBuilder.HasColumnName(periodPropertyName);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.cs
index 6d892ade23e..4b944986043 100644
--- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.cs
+++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerValueGenerationConvention.cs
@@ -53,6 +53,46 @@ public class SqlServerValueGenerationConvention : RelationalValueGenerationConve
base.ProcessPropertyAnnotationChanged(propertyBuilder, name, annotation, oldAnnotation, context);
}
+ ///
+ /// Called after an annotation is changed on an entity.
+ ///
+ /// The builder for the entity type.
+ /// The annotation name.
+ /// The new annotation.
+ /// The old annotation.
+ /// Additional information associated with convention execution.
+ public override void ProcessEntityTypeAnnotationChanged(
+ IConventionEntityTypeBuilder entityTypeBuilder,
+ string name,
+ IConventionAnnotation? annotation,
+ IConventionAnnotation? oldAnnotation,
+ IConventionContext context)
+ {
+ if ((name == SqlServerAnnotationNames.TemporalPeriodStartPropertyName
+ || name == SqlServerAnnotationNames.TemporalPeriodEndPropertyName)
+ && annotation?.Value is string propertyName)
+ {
+ var periodProperty = entityTypeBuilder.Metadata.FindProperty(propertyName);
+ if (periodProperty != null)
+ {
+ periodProperty.Builder.ValueGenerated(GetValueGenerated(periodProperty));
+ }
+
+ // cleanup the previous period property - its possible that it won't be deleted
+ // (e.g. when removing period with default name, while the property with that same name has been explicitly defined)
+ if (oldAnnotation?.Value is string oldPropertyName)
+ {
+ var oldPeriodProperty = entityTypeBuilder.Metadata.FindProperty(oldPropertyName);
+ if (oldPeriodProperty != null)
+ {
+ oldPeriodProperty.Builder.ValueGenerated(GetValueGenerated(oldPeriodProperty));
+ }
+ }
+ }
+
+ base.ProcessEntityTypeAnnotationChanged(entityTypeBuilder, name, annotation, oldAnnotation, context);
+ }
+
///
/// Returns the store value generation strategy to set for the given property.
///
@@ -88,9 +128,20 @@ public class SqlServerValueGenerationConvention : RelationalValueGenerationConve
IReadOnlyProperty property,
in StoreObjectIdentifier storeObject,
ITypeMappingSource typeMappingSource)
- => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject)
+ => GetTemporalValueGenerated(property, storeObject)
+ ?? RelationalValueGenerationConvention.GetValueGenerated(property, storeObject)
?? (property.GetValueGenerationStrategy(storeObject, typeMappingSource) != SqlServerValueGenerationStrategy.None
? ValueGenerated.OnAdd
: (ValueGenerated?)null);
+
+ private ValueGenerated? GetTemporalValueGenerated(IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
+ {
+ var entityType = property.DeclaringEntityType;
+ return entityType.IsTemporal()
+ && (entityType.GetTemporalPeriodStartPropertyName() == property.Name
+ || entityType.GetTemporalPeriodEndPropertyName() == property.Name)
+ ? ValueGenerated.OnAddOrUpdate
+ : null;
+ }
}
}
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs
index 5665ee5f9fd..11e71994946 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs
@@ -139,6 +139,62 @@ public static class SqlServerAnnotationNames
///
public const string Sparse = Prefix + "Sparse";
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string IsTemporal = Prefix + "IsTemporal";
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string TemporalHistoryTableName = Prefix + "TemporalHistoryTableName";
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string TemporalHistoryTableSchema = Prefix + "TemporalHistoryTableSchema";
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string TemporalPeriodStartPropertyName = Prefix + "TemporalPeriodStartPropertyName";
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string TemporalPeriodStartColumnName = Prefix + "TemporalPeriodStartColumnName";
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string TemporalPeriodEndPropertyName = Prefix + "TemporalPeriodEndPropertyName";
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public const string TemporalPeriodEndColumnName = Prefix + "TemporalPeriodEndColumnName";
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
index f911acafd75..4a39e8ee221 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
@@ -102,11 +102,35 @@ public override IEnumerable For(ITable table, bool designTime)
yield break;
}
+ var entityType = table.EntityTypeMappings.First().EntityType;
+
// Model validation ensures that these facets are the same on all mapped entity types
- if (table.EntityTypeMappings.First().EntityType.IsMemoryOptimized())
+ if (entityType.IsMemoryOptimized())
{
yield return new Annotation(SqlServerAnnotationNames.MemoryOptimized, true);
}
+
+ if (entityType.IsTemporal() && designTime)
+ {
+ yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
+ yield return new Annotation(SqlServerAnnotationNames.TemporalHistoryTableName, entityType.GetTemporalHistoryTableName());
+ yield return new Annotation(SqlServerAnnotationNames.TemporalHistoryTableSchema, entityType.GetTemporalHistoryTableSchema());
+
+ var storeObjectIdentifier = StoreObjectIdentifier.Table(table.Name, table.Schema);
+ var periodStartPropertyName = entityType.GetTemporalPeriodStartPropertyName();
+ if (periodStartPropertyName != null)
+ {
+ var periodStartProperty = entityType.GetProperty(periodStartPropertyName);
+ yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName, periodStartProperty.GetColumnName(storeObjectIdentifier));
+ }
+
+ var periodEndPropertyName = entityType.GetTemporalPeriodEndPropertyName();
+ if (periodEndPropertyName != null)
+ {
+ var periodEndProperty = entityType.GetProperty(entityType.GetTemporalPeriodEndPropertyName()!);
+ yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndProperty.GetColumnName(storeObjectIdentifier));
+ }
+ }
}
///
@@ -214,6 +238,28 @@ public override IEnumerable For(IColumn column, bool designTime)
{
yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse);
}
+
+ var entityType = column.Table.EntityTypeMappings.First().EntityType;
+ if (entityType.IsTemporal() && designTime)
+ {
+ var periodStartPropertyName = entityType.GetTemporalPeriodStartPropertyName();
+ var periodEndPropertyName = entityType.GetTemporalPeriodEndPropertyName();
+
+ var periodStartProperty = entityType.GetProperty(periodStartPropertyName!);
+ var periodEndProperty = entityType.GetProperty(periodEndPropertyName!);
+
+ var storeObjectIdentifier = StoreObjectIdentifier.Table(table.Name, table.Schema);
+ var periodStartColumnName = periodStartProperty.GetColumnName(storeObjectIdentifier);
+ var periodEndColumnName = periodEndProperty.GetColumnName(storeObjectIdentifier);
+
+ if (column.Name == periodStartColumnName
+ || column.Name == periodEndColumnName)
+ {
+ yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
+ yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName, periodStartColumnName);
+ yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName);
+ }
+ }
}
}
}
diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
index 3e6a0f1941f..ce571dd8c8e 100644
--- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
+++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
@@ -44,5 +44,78 @@ public override IEnumerable ForRemove(IRelationalModel model)
///
public override IEnumerable ForRemove(ITable table)
=> table.GetAnnotations();
+
+ ///
+ public override IEnumerable ForRemove(IUniqueConstraint constraint)
+ {
+ if (constraint.Table[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalPeriodStartColumnName,
+ constraint.Table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalPeriodEndColumnName,
+ constraint.Table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableName,
+ constraint.Table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableSchema,
+ constraint.Table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ }
+ }
+
+ ///
+ public override IEnumerable ForRemove(IColumn column)
+ {
+ if (column.Table[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableName,
+ column.Table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableSchema,
+ column.Table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+
+ if (column[SqlServerAnnotationNames.TemporalPeriodStartColumnName] is string periodStartColumnName)
+ {
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalPeriodStartColumnName,
+ periodStartColumnName);
+ }
+
+ if (column[SqlServerAnnotationNames.TemporalPeriodEndColumnName] is string periodEndColumnName)
+ {
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalPeriodEndColumnName,
+ periodEndColumnName);
+ }
+ }
+ }
+
+ ///
+ public override IEnumerable ForRename(ITable table)
+ {
+ if (table[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableName,
+ table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ yield return new Annotation(
+ SqlServerAnnotationNames.TemporalHistoryTableSchema,
+ table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ }
+ }
}
}
diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
index 95f6cf7f618..7ea9207ed4f 100644
--- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -9,6 +10,7 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
+using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
@@ -65,7 +67,7 @@ public class SqlServerMigrationsSqlGenerator : MigrationsSqlGenerator
_operations = operations;
try
{
- return base.Generate(operations, model, options);
+ return base.Generate(RewriteOperations(operations, model), model, options);
}
finally
{
@@ -544,7 +546,67 @@ protected override void Generate(RenameSequenceOperation operation, IModel? mode
throw new ArgumentException(SqlServerStrings.CannotProduceUnterminatedSQLWithComments(nameof(CreateTableOperation)));
}
- base.Generate(operation, model, builder, terminate: false);
+ if (operation[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? model?.GetDefaultSchema();
+
+ if (historyTableSchema != operation.Schema && historyTableSchema != null)
+ {
+ Generate(new EnsureSchemaOperation { Name = historyTableSchema }, model, builder);
+ }
+
+ var needsExec = historyTableSchema == null;
+ var subBuilder = needsExec
+ ? new MigrationCommandListBuilder(Dependencies)
+ : builder;
+
+ subBuilder
+ .Append("CREATE TABLE ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema))
+ .AppendLine(" (");
+
+ using (subBuilder.Indent())
+ {
+ CreateTableColumns(operation, model, subBuilder);
+ CreateTableConstraints(operation, model, subBuilder);
+ subBuilder.AppendLine(",");
+ var startColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string;
+ var endColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string;
+ var start = Dependencies.SqlGenerationHelper.DelimitIdentifier(startColumnName!);
+ var end = Dependencies.SqlGenerationHelper.DelimitIdentifier(endColumnName!);
+ subBuilder.AppendLine($"PERIOD FOR SYSTEM_TIME({start}, {end})");
+ }
+
+ if (needsExec)
+ {
+ subBuilder
+ .EndCommand();
+
+ var execBody = subBuilder.GetCommandList().Single().CommandText.Replace("'", "''");
+
+ builder
+ .AppendLine("DECLARE @historyTableSchema sysname = SCHEMA_NAME()")
+ .Append("EXEC(N'")
+ .Append(execBody);
+ }
+
+ var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string;
+ var historyTable = default(string);
+ if (needsExec)
+ {
+ historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!);
+ builder.Append($") WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].{ historyTable}))')");
+ }
+ else
+ {
+ historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!, historyTableSchema);
+ builder.Append($") WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable}))");
+ }
+ }
+ else
+ {
+ base.Generate(operation, model, builder, terminate: false);
+ }
var memoryOptimized = IsMemoryOptimized(operation);
if (memoryOptimized)
@@ -644,6 +706,22 @@ protected override void Generate(RenameSequenceOperation operation, IModel? mode
.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator)
.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Name));
}
+
+ if (operation[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ var schema = operation.Schema ?? model?[RelationalAnnotationNames.DefaultSchema] as string;
+ var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? schema;
+ if (operation[SqlServerAnnotationNames.TemporalHistoryTableName] is string historyTableName)
+ {
+ var dropHistoryTableOperation = new DropTableOperation
+ {
+ Name = historyTableName,
+ Schema = historyTableSchema,
+ };
+
+ Generate(dropHistoryTableOperation, model, builder, terminate);
+ }
+ }
}
///
@@ -737,7 +815,6 @@ protected override void Generate(RenameSequenceOperation operation, IModel? mode
bool terminate = true)
{
base.Generate(operation, model, builder, terminate: false);
-
if (terminate)
{
builder
@@ -1214,6 +1291,29 @@ protected override void Generate(AlterTableOperation operation, IModel? model, M
.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator)
.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table));
}
+
+ if (operation[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string;
+ var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? model?.GetDefaultSchema();
+ var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string;
+ var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string;
+
+ // when dropping column, we only need to drop the column from history table as well if that column is not part of the period
+ // for columns that are part of the period - if we are removing them from the temporal table, it means
+ // that we are converting back to a regular table, and the history table will be removed anyway
+ // so we don't need to keep it in sync
+ if (operation.Name != periodStartColumnName
+ && operation.Name != periodEndColumnName)
+ {
+ Generate(new DropColumnOperation
+ {
+ Name = operation.Name,
+ Table = historyTableName!,
+ Schema = historyTableSchema
+ }, model, builder, terminate);
+ }
+ }
}
///
@@ -1471,6 +1571,24 @@ protected override void Generate(UpdateDataOperation operation, IModel? model, M
builder.Append(" SPARSE");
}
+ var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string;
+ var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string;
+
+ if (name == periodStartColumnName
+ || name == periodEndColumnName)
+ {
+ builder.Append(" GENERATED ALWAYS AS ROW ");
+
+ if (name == periodStartColumnName)
+ {
+ builder.Append("START");
+ }
+ else
+ {
+ builder.Append("END");
+ }
+ }
+
builder.Append(operation.IsNullable ? " NULL" : " NOT NULL");
DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder);
@@ -2091,5 +2209,377 @@ private static bool HasDifferences(IEnumerable source, IEnumerable<
return count != targetAnnotations.Count;
}
+
+ private IReadOnlyList RewriteOperations(
+ IReadOnlyList migrationOperations,
+ IModel? model)
+ {
+ var operations = new List();
+
+ var versioningMap = new Dictionary<(string?, string?), (string, string?)>();
+ var periodMap = new Dictionary<(string?, string?), (string, string)>();
+
+ foreach (var operation in migrationOperations)
+ {
+ var isTemporal = operation[SqlServerAnnotationNames.IsTemporal] as bool? == true;
+ if (isTemporal)
+ {
+ string? table = null;
+ string? schema = null;
+
+ if (operation is ITableMigrationOperation tableMigrationOperation)
+ {
+ table = tableMigrationOperation.Table;
+ schema = tableMigrationOperation.Schema;
+ }
+
+ schema ??= model?.GetDefaultSchema();
+ var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string;
+ var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? model?.GetDefaultSchema();
+ var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string;
+ var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string;
+
+ switch (operation)
+ {
+ case DropTableOperation dropTableOperation:
+ DisableVersioning(table!, schema, historyTableName!, historyTableSchema);
+ operations.Add(operation);
+
+ versioningMap.Remove((table, schema));
+ periodMap.Remove((table, schema));
+ break;
+
+ case RenameTableOperation renameTableOperation:
+ DisableVersioning(table!, schema, historyTableName!, historyTableSchema);
+ operations.Add(operation);
+
+ // since table was renamed, remove old entry and add new entry
+ // marked as versioning disabled, so we enable it in the end for the new table
+ versioningMap.Remove((table, schema));
+ versioningMap[(renameTableOperation.NewName, renameTableOperation.NewSchema)] = (historyTableName!, historyTableSchema);
+
+ // same thing for disabled system period - remove one associated with old table and add one for the new table
+ if (periodMap.TryGetValue((table, schema), out var result))
+ {
+ periodMap.Remove((table, schema));
+ periodMap[(renameTableOperation.NewName, renameTableOperation.NewSchema)] = result;
+ }
+
+ break;
+
+ case AlterTableOperation alterTableOperation:
+ var oldIsTemporal = alterTableOperation.OldTable[SqlServerAnnotationNames.IsTemporal] as bool? == true;
+ if (!oldIsTemporal)
+ {
+ periodMap[(alterTableOperation.Name, alterTableOperation.Schema)] = (periodStartColumnName!, periodEndColumnName!);
+ versioningMap[(alterTableOperation.Name, alterTableOperation.Schema)] = (historyTableName!, historyTableSchema);
+ }
+ else
+ {
+ var oldHistoryTableName = alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableName] as string;
+ var oldHistoryTableSchema = alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string
+ ?? alterTableOperation.OldTable.Schema
+ ?? model?[RelationalAnnotationNames.DefaultSchema] as string;
+
+ if (oldHistoryTableName != historyTableName
+ || oldHistoryTableSchema != historyTableSchema)
+ {
+ if (historyTableSchema != null)
+ {
+ operations.Add(new EnsureSchemaOperation { Name = historyTableSchema });
+ }
+
+ operations.Add(new RenameTableOperation
+ {
+ Name = oldHistoryTableName!,
+ Schema = oldHistoryTableSchema,
+ NewName = historyTableName,
+ NewSchema = historyTableSchema
+ });
+
+ if (versioningMap.ContainsKey((alterTableOperation.Name, alterTableOperation.Schema)))
+ {
+ versioningMap[(alterTableOperation.Name, alterTableOperation.Schema)] = (historyTableName!, historyTableSchema);
+ }
+ }
+ }
+
+ operations.Add(operation);
+ break;
+
+ case AlterColumnOperation alterColumnOperation:
+ // if only difference is in temporal annotations being removed or history table changed etc - we can ignore this operation
+ if (!CanSkipAlterColumnOperation(alterColumnOperation.OldColumn, alterColumnOperation))
+ {
+ // when modifying a period column, we need to perform the operations as a normal column first, and only later enable period
+ // removing the period information now, so that when we generate SQL that modifies the column we won't be making them auto generated as period
+ // (making column auto generated is not allowed in ALTER COLUMN statement)
+ // in later operation we enable the period and the period columns get set to auto generated automatically
+ if (alterColumnOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true
+ && alterColumnOperation.OldColumn[SqlServerAnnotationNames.IsTemporal] is null)
+ {
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
+ alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
+
+ // TODO: test what happens if default value just changes (from temporal to temporal)
+ }
+
+ operations.Add(operation);
+ }
+ break;
+
+ case DropPrimaryKeyOperation:
+ case AddPrimaryKeyOperation:
+ DisableVersioning(table!, schema, historyTableName!, historyTableSchema);
+ operations.Add(operation);
+ break;
+
+ case DropColumnOperation dropColumnOperation:
+ DisableVersioning(table!, schema, historyTableName!, historyTableSchema);
+ if (dropColumnOperation.Name == periodStartColumnName
+ || dropColumnOperation.Name == periodEndColumnName)
+ {
+ // period columns can be null here - it doesn't really matter since we are never enabling the period back
+ // if we remove the period columns, it means we will be dropping the table also or at least convert it back to regular
+ // which will clear the entry in the periodMap for this table
+ DisablePeriod(table!, schema, periodStartColumnName!, periodEndColumnName!);
+ }
+
+ operations.Add(operation);
+
+ break;
+
+ case AddColumnOperation addColumnOperation:
+ // when adding a period column, we need to add it as a normal column first, and only later enable period
+ // removing the period information now, so that when we generate SQL that adds the column we won't be making them auto generated as period
+ // it won't work, unless period is enabled
+ // but we can't enable period without adding the columns first - chicken and egg
+ if (addColumnOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
+ addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
+ addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
+
+ // model differ adds default value, but for period end we need to replace it with the correct one - DateTime.MaxValue
+ if (addColumnOperation.Name == periodEndColumnName)
+ {
+ addColumnOperation.DefaultValue = DateTime.MaxValue;
+ }
+ }
+
+ operations.Add(addColumnOperation);
+ break;
+
+ default:
+ // CreateTableOperation
+ // RenameColumnOperation
+ operations.Add(operation);
+ break;
+ }
+ }
+ else
+ {
+ if (operation is AlterTableOperation alterTableOperation
+ && alterTableOperation.OldTable[SqlServerAnnotationNames.IsTemporal] as bool? == true)
+ {
+ var historyTableName = alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableName] as string;
+ var historyTableSchema = alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string
+ ?? alterTableOperation.OldTable.Schema
+ ?? model?[RelationalAnnotationNames.DefaultSchema] as string;
+
+ var periodStartColumnName = alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string;
+ var periodEndColumnName = alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string;
+
+ DisableVersioning(alterTableOperation.Name, alterTableOperation.Schema, historyTableName!, historyTableSchema);
+ DisablePeriod(alterTableOperation.Name, alterTableOperation.Schema, periodStartColumnName!, periodEndColumnName!);
+
+ if (historyTableName != null)
+ {
+ operations.Add(
+ new DropTableOperation
+ {
+ Name = historyTableName,
+ Schema = alterTableOperation.OldTable.Schema
+ });
+ }
+
+ operations.Add(operation);
+
+ // when we disable versioning and period earlier, we marked it to be re-enabled
+ // since table is no longer temporal we don't need to do that anymore
+ versioningMap.Remove((alterTableOperation.Name, alterTableOperation.Schema));
+ periodMap.Remove((alterTableOperation.Name, alterTableOperation.Schema));
+ }
+ else if (operation is AlterColumnOperation alterColumnOperation)
+ {
+ // if only difference is in temporal annotations being removed or history table changed etc - we can ignore this operation
+ if (alterColumnOperation.OldColumn?[SqlServerAnnotationNames.IsTemporal] as bool? != true
+ || !CanSkipAlterColumnOperation(alterColumnOperation.OldColumn, alterColumnOperation))
+ {
+ operations.Add(operation);
+ }
+ }
+ else
+ {
+ operations.Add(operation);
+ }
+ }
+ }
+
+ foreach (var periodMapEntry in periodMap)
+ {
+ EnablePeriod(periodMapEntry.Key.Item1!, periodMapEntry.Key.Item2, periodMapEntry.Value.Item1, periodMapEntry.Value.Item2);
+ }
+
+ foreach (var versioningMapEntry in versioningMap)
+ {
+ EnableVersioning(versioningMapEntry.Key.Item1!, versioningMapEntry.Key.Item2, versioningMapEntry.Value.Item1, versioningMapEntry.Value.Item2);
+ }
+
+ return operations;
+
+ void DisableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema)
+ {
+ if (!versioningMap.TryGetValue((table, schema), out var result))
+ {
+ versioningMap[(table, schema)] = (historyTableName, historyTableSchema);
+
+ operations.Add(
+ new SqlOperation
+ {
+ Sql = new StringBuilder()
+ .Append("ALTER TABLE ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema))
+ .AppendLine(" SET (SYSTEM_VERSIONING = OFF)")
+ .ToString()
+ });
+ }
+ }
+
+ void EnableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema)
+ {
+ var stringBuilder = new StringBuilder();
+
+ if (historyTableSchema == null)
+ {
+ // need to run command using EXEC to inject default schema
+ stringBuilder.AppendLine("DECLARE @historyTableSchema sysname = SCHEMA_NAME()");
+ stringBuilder.Append("EXEC(N'");
+ }
+
+ var historyTable = default(string);
+ if (historyTableSchema != null)
+ {
+ historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName, historyTableSchema);
+ }
+ else
+ {
+ historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName);
+ }
+
+ stringBuilder
+ .Append("ALTER TABLE ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema));
+
+ if (historyTableSchema != null)
+ {
+ stringBuilder.AppendLine($" SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable}))");
+ }
+ else
+ {
+ stringBuilder.AppendLine($" SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].{historyTable}))')");
+ }
+
+ operations.Add(
+ new SqlOperation
+ {
+ Sql = stringBuilder.ToString()
+ });
+ }
+
+ void DisablePeriod(string table, string? schema, string periodStartColumnName, string periodEndColumnName)
+ {
+ if (!periodMap.TryGetValue((table, schema), out var result))
+ {
+ periodMap[(table, schema)] = (periodStartColumnName, periodEndColumnName);
+
+ operations.Add(
+ new SqlOperation
+ {
+ Sql = new StringBuilder()
+ .Append("ALTER TABLE ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema))
+ .AppendLine(" DROP PERIOD FOR SYSTEM_TIME")
+ .ToString()
+ });
+ }
+ }
+
+ void EnablePeriod(string table, string? schema, string periodStartColumnName, string periodEndColumnName)
+ {
+ operations.Add(
+ new SqlOperation
+ {
+ Sql = new StringBuilder()
+ .Append("ALTER TABLE ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema))
+ .Append(" ADD PERIOD FOR SYSTEM_TIME (")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName))
+ .Append(", ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName))
+ .Append(")")
+ .ToString()
+ });
+ }
+
+ static bool CanSkipAlterColumnOperation(ColumnOperation first, ColumnOperation second)
+ => ColumnPropertiesAreTheSame(first, second)
+ && ColumnOperationsOnlyDifferByTemporalTableAnnotation(first, second)
+ && ColumnOperationsOnlyDifferByTemporalTableAnnotation(second, first);
+
+ static bool ColumnPropertiesAreTheSame(ColumnOperation first, ColumnOperation second)
+ => first.ClrType == second.ClrType
+ && first.Collation == second.Collation
+ && first.ColumnType == second.ColumnType
+ && first.Comment == second.Comment
+ && first.ComputedColumnSql == second.ComputedColumnSql
+ && Equals(first.DefaultValue, second.DefaultValue)
+ && first.DefaultValueSql == second.DefaultValueSql
+ && first.IsDestructiveChange == second.IsDestructiveChange
+ && first.IsFixedLength == second.IsFixedLength
+ && first.IsNullable == second.IsNullable
+ && first.IsReadOnly == second.IsReadOnly
+ && first.IsRowVersion == second.IsRowVersion
+ && first.IsStored == second.IsStored
+ && first.IsUnicode == second.IsUnicode
+ && first.MaxLength == second.MaxLength
+ && first.Precision == second.Precision
+ && first.Scale == second.Scale;
+
+ static bool ColumnOperationsOnlyDifferByTemporalTableAnnotation(ColumnOperation first, ColumnOperation second)
+ {
+ var unmatched = first.GetAnnotations().ToList();
+ foreach (var annotation in second.GetAnnotations())
+ {
+ var index = unmatched.FindIndex(
+ a => a.Name == annotation.Name && StructuralComparisons.StructuralEqualityComparer.Equals(a.Value, annotation.Value));
+ if (index == -1)
+ {
+ continue;
+ }
+
+ unmatched.RemoveAt(index);
+ }
+
+ return unmatched.All(a => a.Name == SqlServerAnnotationNames.IsTemporal
+ || a.Name == SqlServerAnnotationNames.TemporalHistoryTableName
+ || a.Name == SqlServerAnnotationNames.TemporalHistoryTableSchema
+ || a.Name == SqlServerAnnotationNames.TemporalPeriodStartPropertyName
+ || a.Name == SqlServerAnnotationNames.TemporalPeriodEndPropertyName
+ || a.Name == SqlServerAnnotationNames.TemporalPeriodStartColumnName
+ || a.Name == SqlServerAnnotationNames.TemporalPeriodEndColumnName);
+ }
+ }
}
}
diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs
index 2f78d646cda..b37ba30cfed 100644
--- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs
+++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs
@@ -225,6 +225,94 @@ public static string SequenceBadType(object? property, object? entityType, objec
public static string TransientExceptionDetected
=> GetString("TransientExceptionDetected");
+ ///
+ /// Only root entity type should be marked as temporal. Entity type: '{entityType}'.
+ ///
+ public static string TemporalOnlyOnRoot(object? entityType)
+ => string.Format(
+ GetString("TemporalOnlyOnRoot", nameof(entityType)),
+ entityType);
+
+ ///
+ /// Temporal tables are only supported for entities using Table-Per-Hierarchy inheritance mapping. Entity type: '{entityType}'.
+ ///
+ public static string TemporalOnlySupportedForTPH(object? entityType)
+ => string.Format(
+ GetString("TemporalOnlySupportedForTPH", nameof(entityType)),
+ entityType);
+
+ ///
+ /// Temporal tables are not supported for table splitting scenario. Table: '{table}'.
+ ///
+ public static string TemporalNotSupportedForTableSplitting(object? table)
+ => string.Format(
+ GetString("TemporalNotSupportedForTableSplitting", nameof(table)),
+ table);
+
+ ///
+ /// Entity type '{entityType}' mapped to temporal table must have a period start and a period end property.
+ ///
+ public static string TemporalMustDefinePeriodProperties(object? entityType)
+ => string.Format(
+ GetString("TemporalMustDefinePeriodProperties", nameof(entityType)),
+ entityType);
+
+ ///
+ /// Entity type '{entityType}' mapped to temporal table does not contain the expected period property: '{propertyName}'.
+ ///
+ public static string TemporalExpectedPeriodPropertyNotFound(object? entityType, object? propertyName)
+ => string.Format(
+ GetString("TemporalExpectedPeriodPropertyNotFound", nameof(entityType), nameof(propertyName)),
+ entityType, propertyName);
+
+ ///
+ /// Period property '{entityType}.{propertyName}' must be a shadow property.
+ ///
+ public static string TemporalPeriodPropertyMustBeInShadowState(object? entityType, object? propertyName)
+ => string.Format(
+ GetString("TemporalPeriodPropertyMustBeInShadowState", nameof(entityType), nameof(propertyName)),
+ entityType, propertyName);
+
+ ///
+ /// Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'.
+ ///
+ public static string TemporalPeriodPropertyMustBeNonNullableDateTime(object? entityType, object? propertyName, object? dateTimeType)
+ => string.Format(
+ GetString("TemporalPeriodPropertyMustBeNonNullableDateTime", nameof(entityType), nameof(propertyName), nameof(dateTimeType)),
+ entityType, propertyName, dateTimeType);
+
+ ///
+ /// Period property '{entityType}.{propertyName}' must be mapped to a column of type '{columnType}'.
+ ///
+ public static string TemporalPeriodPropertyMustBeMappedToDatetime2(object? entityType, object? propertyName, object? columnType)
+ => string.Format(
+ GetString("TemporalPeriodPropertyMustBeMappedToDatetime2", nameof(entityType), nameof(propertyName), nameof(columnType)),
+ entityType, propertyName, columnType);
+
+ ///
+ /// Period property '{entityType}.{propertyName}' can't have a default value specified.
+ ///
+ public static string TemporalPeriodPropertyCantHaveDefaultValue(object? entityType, object? propertyName)
+ => string.Format(
+ GetString("TemporalPeriodPropertyCantHaveDefaultValue", nameof(entityType), nameof(propertyName)),
+ entityType, propertyName);
+
+ ///
+ /// Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'.
+ ///
+ public static string TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate(object? entityType, object? propertyName, object? valueGeneratedValue)
+ => string.Format(
+ GetString("TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate", nameof(entityType), nameof(propertyName), nameof(valueGeneratedValue)),
+ entityType, propertyName, valueGeneratedValue);
+
+ ///
+ /// Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified.
+ ///
+ public static string TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue(object? entityType, object? propertyName)
+ => string.Format(
+ GetString("TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue", nameof(entityType), nameof(propertyName)),
+ entityType, propertyName);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name)!;
diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx
index 4dfc644ac23..0d791f4e64e 100644
--- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx
+++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx
@@ -274,4 +274,49 @@
An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call.
+
+
+ Only root entity type should be marked as temporal. Entity type: '{entityType}'.
+
+
+
+ Temporal tables are only supported for entities using Table-Per-Hierarchy inheritance mapping. Entity type: '{entityType}'.
+
+
+
+ Temporal tables are not supported for table splitting scenario. Table: '{table}'.
+
+
+
+ Entity type '{entityType}' mapped to temporal table must have a period start and a period end property.
+
+
+
+ Entity type '{entityType}' mapped to temporal table does not contain the expected period property: '{propertyName}'.
+
+
+
+ Period property '{entityType}.{propertyName}' must be a shadow property.
+
+
+
+ Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'.
+
+
+
+ Period property '{entityType}.{propertyName}' must be mapped to a column of type '{columnType}'.
+
+
+
+ Period property '{entityType}.{propertyName}' can't have a default value specified.
+
+
+
+ Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'.
+
+
+
+ Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified.
+
+
\ No newline at end of file
diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
index 48e83186e45..3a5298219b6 100644
--- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
+++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs
@@ -7,7 +7,6 @@
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
-using Microsoft.EntityFrameworkCore.SqlServer.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal
diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
index 3a464834155..39b6d177f95 100644
--- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
+++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
@@ -492,6 +492,16 @@ private static string EscapeLiteral(string s)
[t].[is_memory_optimized]";
}
+ if (supportsTemporalTable)
+ {
+ commandText += @",
+ [t].[temporal_type],
+ (SELECT [t2].[name] FROM [sys].[tables] AS t2 WHERE [t2].[object_id] = [t].[history_table_id]) AS [history_table_name],
+ (SELECT SCHEMA_NAME([t2].[schema_id]) FROM [sys].[tables] AS t2 WHERE [t2].[object_id] = [t].[history_table_id]) AS [history_table_schema],
+ (SELECT [c].[name] FROM [sys].[columns] as [c] WHERE [c].[object_id] = [t].[object_id] AND [c].[generated_always_type] = 1) as [period_start_column],
+ (SELECT [c].[name] FROM [sys].[columns] as [c] WHERE [c].[object_id] = [t].[object_id] AND [c].[generated_always_type] = 2) as [period_end_column]";
+ }
+
commandText += @"
FROM [sys].[tables] AS [t]
LEFT JOIN [sys].[extended_properties] AS [e] ON [e].[major_id] = [t].[object_id] AND [e].[minor_id] = 0 AND [e].[class] = 1 AND [e].[name] = 'MS_Description'";
@@ -540,6 +550,16 @@ private static string EscapeLiteral(string s)
CAST(0 AS bit) AS [is_memory_optimized]";
}
+ if (supportsTemporalTable)
+ {
+ viewCommandText += @",
+ 1 AS [temporal_type],
+ NULL AS [history_table_name],
+ NULL AS [history_table_schema],
+ NULL AS [period_start_column],
+ NULL AS [period_end_column]";
+ }
+
viewCommandText += @"
FROM [sys].[views] AS [v]
LEFT JOIN [sys].[extended_properties] AS [e] ON [e].[major_id] = [v].[object_id] AND [e].[minor_id] = 0 AND [e].[class] = 1 AND [e].[name] = 'MS_Description'";
@@ -587,6 +607,26 @@ private static string EscapeLiteral(string s)
}
}
+ if (supportsTemporalTable)
+ {
+ if (reader.GetValueOrDefault("temporal_type") == 2)
+ {
+ table[SqlServerAnnotationNames.IsTemporal] = true;
+
+ var historyTableName = reader.GetValueOrDefault("history_table_name");
+ table[SqlServerAnnotationNames.TemporalHistoryTableName] = historyTableName;
+
+ var historyTableSchema = reader.GetValueOrDefault("history_table_schema");
+ table[SqlServerAnnotationNames.TemporalHistoryTableSchema] = historyTableSchema;
+
+ var periodStartColumnName = reader.GetValueOrDefault("period_start_column");
+ table[SqlServerAnnotationNames.TemporalPeriodStartColumnName] = periodStartColumnName;
+
+ var periodEndColumnName = reader.GetValueOrDefault("period_end_column");
+ table[SqlServerAnnotationNames.TemporalPeriodEndColumnName] = periodEndColumnName;
+ }
+ }
+
tables.Add(table);
}
}
@@ -629,8 +669,15 @@ private static string EscapeLiteral(string s)
[cc].[is_persisted] AS [computed_is_persisted],
CAST([e].[value] AS nvarchar(MAX)) AS [comment],
[c].[collation_name],
- [c].[is_sparse]
-FROM
+ [c].[is_sparse]";
+
+ if (SupportsTemporalTable())
+ {
+ commandText += @",
+ [c].[generated_always_type]";
+ }
+
+ commandText += @"FROM
(
SELECT[v].[name], [v].[object_id], [v].[schema_id]
FROM [sys].[views] v WHERE ";
@@ -692,6 +739,7 @@ UNION ALL
var comment = dataRecord.GetValueOrDefault("comment");
var collation = dataRecord.GetValueOrDefault("collation_name");
var isSparse = dataRecord.GetValueOrDefault("is_sparse");
+ var generatedAlwaysType = SupportsTemporalTable() ? dataRecord.GetValueOrDefault("generated_always_type") : 0;
_logger.ColumnFound(
DisplayName(tableSchema, tableName),
diff --git a/src/EFCore/Design/NestedClosureCodeFragment.cs b/src/EFCore/Design/NestedClosureCodeFragment.cs
index 25c99dd2912..a8a66993d17 100644
--- a/src/EFCore/Design/NestedClosureCodeFragment.cs
+++ b/src/EFCore/Design/NestedClosureCodeFragment.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Utilities;
namespace Microsoft.EntityFrameworkCore.Design
@@ -21,7 +22,21 @@ public NestedClosureCodeFragment(string parameter, MethodCallCodeFragment method
Check.NotNull(methodCall, nameof(methodCall));
Parameter = parameter;
- MethodCall = methodCall;
+ MethodCalls = new List { methodCall };
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The nested closure parameter's name.
+ /// The list of method calls used as the body of the nested closure.
+ public NestedClosureCodeFragment(string parameter, IReadOnlyList methodCalls)
+ {
+ Check.NotEmpty(parameter, nameof(parameter));
+ Check.NotEmpty(methodCalls, nameof(methodCalls));
+
+ Parameter = parameter;
+ MethodCalls = methodCalls;
}
///
@@ -31,9 +46,9 @@ public NestedClosureCodeFragment(string parameter, MethodCallCodeFragment method
public virtual string Parameter { get; }
///
- /// Gets the method call used as the body of the nested closure.
+ /// Gets the method calls used as the body of the nested closure.
///
/// The method call.
- public virtual MethodCallCodeFragment MethodCall { get; }
+ public virtual IReadOnlyList MethodCalls { get; }
}
}
diff --git a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs
index 8c9ca6beb07..8dec328db35 100644
--- a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs
+++ b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs
@@ -579,6 +579,7 @@ private Expression ProcessEntityShaper(EntityShaperExpression entityShaperExpres
var valueBufferExpression = Expression.Call(
materializationContextVariable, MaterializationContext.GetValueBufferMethod);
var shadowProperties = concreteEntityType.GetProperties().Where(p => p.IsShadowProperty());
+
blockExpressions.Add(
Expression.Assign(
shadowValuesVariable,
diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
index 6eac43f0675..2a99c04d80c 100644
--- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs
@@ -1793,6 +1793,65 @@ public virtual void Discriminator_of_enum_to_string()
});
}
+ [ConditionalFact]
+ public virtual void Temporal_table_information_is_stored_in_snapshot()
+ {
+ Test(
+ builder => builder.Entity().ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start").HasColumnName("PeriodStart");
+ ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd");
+ })),
+ AddBoilerPlate(
+ GetHeading()
+ + @"
+ modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty"", b =>
+ {
+ b.Property(""Id"")
+ .ValueGeneratedOnAdd()
+ .HasColumnType(""int"")
+ .HasAnnotation(""SqlServer:ValueGenerationStrategy"", SqlServerValueGenerationStrategy.IdentityColumn);
+
+ b.Property(""End"")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType(""datetime2"")
+ .HasColumnName(""PeriodEnd"");
+
+ b.Property(""Name"")
+ .HasColumnType(""nvarchar(max)"");
+
+ b.Property(""Start"")
+ .ValueGeneratedOnAddOrUpdate()
+ .HasColumnType(""datetime2"")
+ .HasColumnName(""PeriodStart"");
+
+ b.HasKey(""Id"");
+
+ b.ToTable(""EntityWithStringProperty"");
+
+ b
+ .ToTable(tb => tb.IsTemporal(ttb =>
+{
+ ttb.WithHistoryTable(""HistoryTable"");
+ ttb.HasPeriodStart(""Start"").HasColumnName(""PeriodStart"");
+ ttb.HasPeriodEnd(""End"").HasColumnName(""PeriodEnd"");
+}
+));
+ });", usingSystem: true),
+ o =>
+ {
+ var temporalEntity = o.FindEntityType("Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty");
+ var annotations = temporalEntity.GetAnnotations().ToList();
+
+ Assert.Equal(6, annotations.Count);
+ Assert.Contains(annotations, a => a.Name == SqlServerAnnotationNames.IsTemporal && a.Value as bool? == true);
+ Assert.Contains(annotations, a => a.Name == SqlServerAnnotationNames.TemporalHistoryTableName && a.Value as string == "HistoryTable");
+ Assert.Contains(annotations, a => a.Name == SqlServerAnnotationNames.TemporalPeriodStartPropertyName && a.Value as string == "Start");
+ Assert.Contains(annotations, a => a.Name == SqlServerAnnotationNames.TemporalPeriodEndPropertyName && a.Value as string == "End");
+ });
+ }
+
#endregion
#region Owned types
diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
index d783ed3cab1..632b8741840 100644
--- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
+++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs
@@ -1855,7 +1855,7 @@ public void Passes_for_unnamed_index_with_all_properties_not_mapped_to_any_table
{
var modelBuilder = CreateConventionalModelBuilder();
- modelBuilder.Entity().ToTable(null);
+ modelBuilder.Entity().ToTable((string)null);
modelBuilder.Entity().HasIndex(nameof(Animal.Id), nameof(Animal.Name));
var definition = RelationalResources
@@ -1874,7 +1874,7 @@ public void Passes_for_named_index_with_all_properties_not_mapped_to_any_table()
{
var modelBuilder = CreateConventionalModelBuilder();
- modelBuilder.Entity().ToTable(null);
+ modelBuilder.Entity().ToTable((string)null);
modelBuilder.Entity()
.HasIndex(
new[] { nameof(Animal.Id), nameof(Animal.Name) },
@@ -1897,7 +1897,7 @@ public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_unm
{
var modelBuilder = CreateConventionalModelBuilder();
- modelBuilder.Entity().ToTable(null);
+ modelBuilder.Entity().ToTable((string)null);
modelBuilder.Entity().ToTable("Cats");
modelBuilder.Entity().HasIndex(nameof(Animal.Name), nameof(Cat.Identity));
@@ -1918,7 +1918,7 @@ public void Detects_mix_of_index_property_mapped_and_not_mapped_to_any_table_map
{
var modelBuilder = CreateConventionalModelBuilder();
- modelBuilder.Entity().ToTable(null);
+ modelBuilder.Entity().ToTable((string)null);
modelBuilder.Entity().ToTable("Cats");
modelBuilder.Entity()
.HasIndex(
diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs
index 5759b9c3070..e497018cfa6 100644
--- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs
+++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
// ReSharper disable InconsistentNaming
@@ -14,7 +15,7 @@ public abstract class TestTableBuilder
public abstract TestTableBuilder ExcludeFromMigrations(bool excluded = true);
}
- public class GenericTestTableBuilder : TestTableBuilder
+ public class GenericTestTableBuilder : TestTableBuilder, IInfrastructure>
where TEntity : class
{
public GenericTestTableBuilder(TableBuilder tableBuilder)
@@ -24,6 +25,8 @@ public GenericTestTableBuilder(TableBuilder tableBuilder)
protected TableBuilder TableBuilder { get; }
+ public TableBuilder Instance => TableBuilder;
+
protected virtual TestTableBuilder Wrap(TableBuilder tableBuilder)
=> new GenericTestTableBuilder(tableBuilder);
@@ -31,7 +34,7 @@ public override TestTableBuilder ExcludeFromMigrations(bool excluded =
=> Wrap(TableBuilder.ExcludeFromMigrations(excluded));
}
- public class NonGenericTestTableBuilder : TestTableBuilder
+ public class NonGenericTestTableBuilder : TestTableBuilder, IInfrastructure
where TEntity : class
{
public NonGenericTestTableBuilder(TableBuilder tableBuilder)
@@ -41,6 +44,8 @@ public NonGenericTestTableBuilder(TableBuilder tableBuilder)
protected TableBuilder TableBuilder { get; }
+ public TableBuilder Instance => TableBuilder;
+
protected virtual TestTableBuilder Wrap(TableBuilder tableBuilder)
=> new NonGenericTestTableBuilder(tableBuilder);
diff --git a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs
index 9fff29b0c7e..baa23004753 100644
--- a/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs
+++ b/test/EFCore.Relational.Tests/RelationalApiConsistencyTest.cs
@@ -194,7 +194,10 @@ public override
{
typeof(IMutableSequence).GetMethod("set_ClrType"),
typeof(RelationalEntityTypeBuilderExtensions).GetMethod(
- nameof(RelationalEntityTypeBuilderExtensions.ExcludeTableFromMigrations))
+ nameof(RelationalEntityTypeBuilderExtensions.ExcludeTableFromMigrations)),
+ typeof(RelationalEntityTypeBuilderExtensions).GetMethod(
+ nameof(RelationalEntityTypeBuilderExtensions.ToTable),
+ new Type[] { typeof(EntityTypeBuilder), typeof(Action) })
};
public override HashSet AsyncMethodExceptions { get; } = new()
diff --git a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs
index 05aceeafe9c..aaea7c48144 100644
--- a/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs
+++ b/test/EFCore.Specification.Tests/ApiConsistencyTestBase.cs
@@ -481,7 +481,10 @@ private string ValidateConventionBuilderMethods(IReadOnlyList method
? method.Name[3..]
: method.Name.StartsWith("To", StringComparison.Ordinal)
? method.Name[2..]
- : method.Name);
+ : method.Name.StartsWith("With", StringComparison.Ordinal)
+ ? method.Name[4..]
+ : method.Name);
+
if (!methodLookup.TryGetValue(expectedName, out var canSetMethod))
{
return $"{declaringType.Name} expected to have a {expectedName} method";
diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
index cfde56958fe..26495b51fb0 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs
@@ -6,7 +6,6 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata.Internal;
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
@@ -1853,6 +1852,1804 @@ public override async Task UpdateDataOperation_multiple_columns()
SELECT @@ROWCOUNT;");
}
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_default_column_mappings_and_default_history_table()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("SystemTimeEnd", c.Name),
+ c => Assert.Equal("SystemTimeStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'CREATE TABLE [Customer] (
+ [Id] int NOT NULL,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
+ CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))');");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_custom_column_mappings_and_default_history_table()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart").HasColumnName("Start");
+ ttb.HasPeriodEnd("SystemTimeEnd").HasColumnName("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'CREATE TABLE [Customer] (
+ [Id] int NOT NULL,
+ [Name] nvarchar(max) NULL,
+ [End] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
+ [Start] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
+ CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([Start], [End])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))');");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_default_column_mappings_and_custom_history_table()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("SystemTimeEnd", c.Name),
+ c => Assert.Equal("SystemTimeStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'CREATE TABLE [Customer] (
+ [Id] int NOT NULL,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
+ CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[HistoryTable]))');");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_explicitly_defined_schema()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("mySchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("SystemTimeEnd", c.Name),
+ c => Assert.Equal("SystemTimeStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];');",
+ //
+ @"CREATE TABLE [mySchema].[Customers] (
+ [Id] int NOT NULL,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[CustomerHistory]));");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_no_explicit_table_schema_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myDefaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("myDefaultSchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("SystemTimeEnd", c.Name),
+ c => Assert.Equal("SystemTimeStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');",
+ //
+ @"CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomerHistory]));");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_explicit_table_schema_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myDefaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("mySchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("SystemTimeEnd", c.Name),
+ c => Assert.Equal("SystemTimeStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];');",
+ //
+ @"CREATE TABLE [mySchema].[Customers] (
+ [Id] int NOT NULL,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[CustomerHistory]));");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_schema_for_table_and_explicit_history_table_schema_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("historySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("SystemTimeEnd", c.Name),
+ c => Assert.Equal("SystemTimeStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];');",
+ //
+ @"CREATE TABLE [Customers] (
+ [Id] int NOT NULL,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]));");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Drop_temporal_table_default_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("Start").HasColumnName("PeriodStart");
+ ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd");
+ }));
+ }),
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"DROP TABLE [Customer];",
+ //
+ @"DROP TABLE [CustomerHistory];");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Drop_temporal_table_custom_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start").HasColumnName("PeriodStart");
+ ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd");
+ }));
+ }),
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"DROP TABLE [Customer];",
+ //
+ @"DROP TABLE [HistoryTable];");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Drop_temporal_table_custom_history_table_and_history_table_schema()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("Start").HasColumnName("PeriodStart");
+ ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd");
+ }));
+ }),
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"DROP TABLE [Customer];",
+ //
+ @"DROP TABLE [historySchema].[HistoryTable];");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("RenamedCustomers");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("RenamedCustomers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];",
+ //
+ @"EXEC sp_rename N'[Customers]', N'RenamedCustomers';",
+ //
+ @"ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);",
+ //
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table_with_custom_history_table_schema()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("RenamedCustomers");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("RenamedCustomers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];",
+ //
+ @"EXEC sp_rename N'[Customers]', N'RenamedCustomers';",
+ //
+ @"ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);",
+ //
+ @"ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]))");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("RenamedHistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("RenamedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"EXEC sp_rename N'[HistoryTable]', N'RenamedHistoryTable';");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Change_history_table_schema()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable", "modifiedHistorySchema");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("modifiedHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"IF SCHEMA_ID(N'modifiedHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [modifiedHistorySchema];');",
+ //
+ @"ALTER SCHEMA [modifiedHistorySchema] TRANSFER [historySchema].[HistoryTable];");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table_history_table_and_their_schemas()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+ }),
+
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers", "schema", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+
+
+ e.ToTable("Customers");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("RenamedCustomers", "newSchema", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("RenamedHistoryTable", "newHistorySchema");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("RenamedCustomers", table.Name);
+ Assert.Equal("newSchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("RenamedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("newHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];",
+ //
+ @"IF SCHEMA_ID(N'newSchema') IS NULL EXEC(N'CREATE SCHEMA [newSchema];');",
+ //
+ @"EXEC sp_rename N'[Customers]', N'RenamedCustomers';
+ALTER SCHEMA [newSchema] TRANSFER [RenamedCustomers];",
+ //
+ @"IF SCHEMA_ID(N'newHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [newHistorySchema];');",
+ //
+ @"EXEC sp_rename N'[historySchema].[HistoryTable]', N'RenamedHistoryTable';
+ALTER SCHEMA [newHistorySchema] TRANSFER [historySchema].[RenamedHistoryTable];",
+ //
+ @"ALTER TABLE [newSchema].[RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);",
+ //
+ @"ALTER TABLE [newSchema].[RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [newHistorySchema].[RenamedHistoryTable]))");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Remove_columns_from_temporal_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Name");
+ e.Property("Number");
+ }),
+ builder =>
+ {
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"DECLARE @var0 sysname;
+SELECT @var0 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name');
+IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];');
+ALTER TABLE [Customers] DROP COLUMN [Name];",
+ //
+ @"DECLARE @var1 sysname;
+SELECT @var1 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[HistoryTable]') AND [c].[name] = N'Name');
+IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];');
+ALTER TABLE [HistoryTable] DROP COLUMN [Name];",
+ //
+ @"DECLARE @var2 sysname;
+SELECT @var2 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Number');
+IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];');
+ALTER TABLE [Customers] DROP COLUMN [Number];",
+ //
+ @"DECLARE @var3 sysname;
+SELECT @var3 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[HistoryTable]') AND [c].[name] = N'Number');
+IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];');
+ALTER TABLE [HistoryTable] DROP COLUMN [Number];",
+ //
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Add_columns_to_temporal_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable("Customers", tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Name");
+ e.Property("Number");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Start", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Number", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customers] ADD [Name] nvarchar(max) NULL;",
+ //
+ @"ALTER TABLE [Customers] ADD [Number] int NOT NULL DEFAULT 0;");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_temporal_table_with_default_column_mappings_and_custom_history_table_to_normal_table_keep_period_columns()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("PeriodStart");
+ ttb.HasPeriodEnd("PeriodEnd");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("PeriodStart");
+ e.Property("PeriodEnd");
+ e.HasKey("Id");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Null(table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("PeriodEnd", c.Name),
+ c => Assert.Equal("PeriodStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME",
+ //
+ @"DROP TABLE [HistoryTable];");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_temporal_table_with_default_column_mappings_and_default_history_table_to_normal_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("PeriodStart");
+ ttb.HasPeriodEnd("PeriodEnd");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.HasKey("Id");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Null(table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME",
+ //
+ @"DECLARE @var0 sysname;
+SELECT @var0 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodEnd');
+IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var0 + '];');
+ALTER TABLE [Customer] DROP COLUMN [PeriodEnd];",
+ //
+ @"DECLARE @var1 sysname;
+SELECT @var1 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodStart');
+IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];');
+ALTER TABLE [Customer] DROP COLUMN [PeriodStart];",
+ //
+ @"DROP TABLE [CustomerHistory];");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_temporal_table_with_default_column_mappings_and_custom_history_table_to_normal_table_remove_period_columns()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("PeriodStart");
+ ttb.HasPeriodEnd("PeriodEnd");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.HasKey("Id");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Null(table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)",
+ //
+ @"ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME",
+ //
+ @"DECLARE @var0 sysname;
+SELECT @var0 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodEnd');
+IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var0 + '];');
+ALTER TABLE [Customer] DROP COLUMN [PeriodEnd];",
+ //
+ @"DECLARE @var1 sysname;
+SELECT @var1 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodStart');
+IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];');
+ALTER TABLE [Customer] DROP COLUMN [PeriodStart];",
+ //
+ @"DROP TABLE [HistoryTable];");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_normal_table_to_temporal_table_with_minimal_configuration()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+ e.ToTable(tb => tb.IsTemporal());
+
+ e.Metadata[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] = "PeriodStart";
+ e.Metadata[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] = "PeriodEnd";
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("PeriodEnd", c.Name),
+ c => Assert.Equal("PeriodStart", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999';",
+ //
+ @"ALTER TABLE [Customer] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000';",
+ //
+ @"ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd])",
+ //
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))')");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_normal_table_with_period_columns_to_temporal_table_default_column_mappings_and_default_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start");
+ e.Property("End");
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End])",
+ //
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))')");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_normal_table_with_period_columns_to_temporal_table_default_column_mappings_and_specified_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start");
+ e.Property("End");
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End])",
+ //
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_normal_table_to_temporal_table_default_column_mappings_and_default_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.NotNull(table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999';",
+ //
+ @"ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000';",
+ //
+ @"ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End])",
+ //
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))')");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Convert_normal_table_without_period_columns_to_temporal_table_default_column_mappings_and_specified_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartColumnName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndColumnName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name),
+ c => Assert.Equal("End", c.Name),
+ c => Assert.Equal("Start", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ @"ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999';",
+ //
+ @"ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000';",
+ //
+ @"ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End])",
+ //
+ @"DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_period_properties_of_temporal_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(tb => tb.IsTemporal(ttb =>
+ {
+ ttb.WithHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("ModifiedStart").ValueGeneratedOnAddOrUpdate();
+ e.Property