From 7f62f0cbfeab86ed154cd74e1ea9aa48a5e93ece Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Tue, 5 May 2026 23:01:45 +0500 Subject: [PATCH 1/5] Allow configuring SQL Server temporal table period columns as not hidden Adds TemporalTableBuilder.PeriodColumnsHidden(bool hidden = true) to opt out of the HIDDEN flag on period start/end columns. Default remains HIDDEN to preserve backward compatibility. Fixes #36608 --- .../SqlServerAnnotationCodeGenerator.cs | 13 ++++ ...verCSharpRuntimeAnnotationCodeGenerator.cs | 3 + .../EFCore.SqlServer.baseline.json | 24 ++++++++ .../SqlServerEntityTypeExtensions.cs | 47 +++++++++++++++ .../OwnedNavigationTemporalTableBuilder.cs | 23 +++++++ .../OwnedNavigationTemporalTableBuilder``.cs | 19 ++++++ .../Metadata/Builders/TemporalTableBuilder.cs | 23 +++++++ .../Builders/TemporalTableBuilder`.cs | 19 ++++++ .../Internal/SqlServerAnnotationNames.cs | 8 +++ .../Internal/SqlServerAnnotationProvider.cs | 15 +++++ .../SqlServerMigrationsSqlGenerator.cs | 24 +++++--- .../MigrationsSqlServerTest.TemporalTables.cs | 51 ++++++++++++++++ .../SqlServerModelBuilderTestBase.cs | 60 +++++++++++++++++++ 13 files changed, 322 insertions(+), 7 deletions(-) diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 6a7713f503d..483e29d6432 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -148,6 +148,10 @@ private static readonly MethodInfo TemporalPropertyHasColumnNameMethodInfo = typeof(TemporalPeriodPropertyBuilder).GetRuntimeMethod( nameof(TemporalPeriodPropertyBuilder.HasColumnName), [typeof(string)])!; + private static readonly MethodInfo TemporalTablePeriodColumnsHiddenMethodInfo + = typeof(TemporalTableBuilder).GetRuntimeMethod( + nameof(TemporalTableBuilder.PeriodColumnsHidden), [typeof(bool)])!; + private static readonly MethodInfo ModelHasFullTextCatalogMethodInfo = typeof(SqlServerModelBuilderExtensions).GetRuntimeMethod( nameof(SqlServerModelBuilderExtensions.HasFullTextCatalog), [typeof(ModelBuilder), typeof(string)])!; @@ -385,6 +389,14 @@ public override IReadOnlyList GenerateFluentApiCalls( .Chain(new MethodCallCodeFragment(TemporalPropertyHasColumnNameMethodInfo, periodEndColumnName)) : new MethodCallCodeFragment(TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName)); + // ttb => ttb.PeriodColumnsHidden(false) + // Only emit when explicitly set to false; the default (true) matches legacy behavior. + if (annotations.TryGetValue(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, out var periodColumnsHiddenAnnotation) + && periodColumnsHiddenAnnotation.Value as bool? == false) + { + temporalTableBuilderCalls.Add(new MethodCallCodeFragment(TemporalTablePeriodColumnsHiddenMethodInfo, false)); + } + // ToTable(tb => tb.IsTemporal(ttb => { ... })) var toTemporalTableCall = new MethodCallCodeFragment( EntityTypeToTableMethodInfo, @@ -403,6 +415,7 @@ public override IReadOnlyList GenerateFluentApiCalls( annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartPropertyName); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndPropertyName); + annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); } return fragments; diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs index d6244c596a6..792eea42837 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs @@ -88,6 +88,7 @@ public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGenerat annotations.Remove(SqlServerAnnotationNames.Sparse); annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); } base.Generate(column, parameters); @@ -171,6 +172,7 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndPropertyName); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartPropertyName); + annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); } base.Generate(entityType, parameters); @@ -187,6 +189,7 @@ public override void Generate(ITable table, CSharpRuntimeAnnotationCodeGenerator annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndColumnName); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartColumnName); + annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); } base.Generate(table, parameters); diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json index 8de92252d64..006746c92c6 100644 --- a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json +++ b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json @@ -238,6 +238,9 @@ { "Member": "override string? ToString();" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" + }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder UseHistoryTable(string name);" }, @@ -255,6 +258,9 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart(System.Linq.Expressions.Expression> propertyExpression);" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" + }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder UseHistoryTable(string name);" }, @@ -1122,6 +1128,9 @@ { "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsTemporalConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType);" }, + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsTemporalPeriodColumnsHiddenConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType);" + }, { "Member": "static string? GetPeriodEndPropertyName(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType entityType);" }, @@ -1155,6 +1164,9 @@ { "Member": "static bool IsTemporal(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType entityType);" }, + { + "Member": "static bool IsTemporalPeriodColumnsHidden(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType entityType);" + }, { "Member": "static void SetHistoryTableName(this Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entityType, string? historyTableName);" }, @@ -1179,6 +1191,12 @@ { "Member": "static bool? SetIsTemporal(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType, bool? temporal, bool fromDataAnnotation = false);" }, + { + "Member": "static void SetIsTemporalPeriodColumnsHidden(this Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entityType, bool? hidden);" + }, + { + "Member": "static bool? SetIsTemporalPeriodColumnsHidden(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType, bool? hidden, bool fromDataAnnotation = false);" + }, { "Member": "static void SetPeriodEndPropertyName(this Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entityType, string? periodEndPropertyName);" }, @@ -3049,6 +3067,9 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder HasPeriodStart(string propertyName);" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" + }, { "Member": "override string? ToString();" }, @@ -3069,6 +3090,9 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder HasPeriodStart(System.Linq.Expressions.Expression> propertyExpression);" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" + }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder UseHistoryTable(string name);" }, diff --git a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs index c9417eb9e04..d2f7d0c116c 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs @@ -297,6 +297,53 @@ public static void SetHistoryTableSchema(this IMutableEntityType entityType, str return (string.IsNullOrEmpty(schema) ? "" : schema + ".") + historyTableName; } + /// + /// Returns a value indicating whether the period columns of the entity type mapped to a temporal table are + /// defined with the HIDDEN flag, which excludes them from SELECT * results. + /// + /// + /// The default value is , matching the behavior of EF Core releases prior to this option + /// being introduced. Set to to make the period columns visible. + /// + /// The entity type. + /// if the period columns are hidden; otherwise . + public static bool IsTemporalPeriodColumnsHidden(this IReadOnlyEntityType entityType) + => entityType[SqlServerAnnotationNames.TemporalPeriodColumnsHidden] as bool? ?? true; + + /// + /// Sets a value indicating whether the period columns of the entity type mapped to a temporal table are + /// defined with the HIDDEN flag. + /// + /// The entity type. + /// The value to set; to remove the explicit configuration. + public static void SetIsTemporalPeriodColumnsHidden(this IMutableEntityType entityType, bool? hidden) + => entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, hidden); + + /// + /// Sets a value indicating whether the period columns of the entity type mapped to a temporal table are + /// defined with the HIDDEN flag. + /// + /// The entity type. + /// The value to set; to remove the explicit configuration. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static bool? SetIsTemporalPeriodColumnsHidden( + this IConventionEntityType entityType, + bool? hidden, + bool fromDataAnnotation = false) + => (bool?)entityType.SetOrRemoveAnnotation( + SqlServerAnnotationNames.TemporalPeriodColumnsHidden, + hidden, + fromDataAnnotation)?.Value; + + /// + /// Gets the configuration source for the period-columns-hidden setting. + /// + /// The entity type. + /// The configuration source for the period-columns-hidden setting. + public static ConfigurationSource? GetIsTemporalPeriodColumnsHiddenConfigurationSource(this IConventionEntityType entityType) + => entityType.FindAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden)?.GetConfigurationSource(); + #endregion Temporal table #region SQL OUTPUT clause diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs index 56bd7ed1570..debe091e295 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs @@ -91,6 +91,29 @@ public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd(string #pragma warning restore EF1001 // Internal EF Core API usage. } + /// + /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, + /// which excludes them from SELECT * results. + /// + /// + /// + /// The default value is , matching the behavior of EF Core releases prior to this option + /// being introduced. Set to to make the period columns visible in SELECT *. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information. + /// + /// + /// A value indicating whether the period columns should be hidden. + /// The same builder instance so that multiple calls can be chained. + public virtual OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + { + _referenceOwnershipBuilder.OwnedEntityType.SetIsTemporalPeriodColumnsHidden(hidden); + + return this; + } + private IMutableProperty ConfigurePeriodProperty(string propertyName) { // TODO: Configure the property explicitly, but remove it if it's no longer used, issue #15898 diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs index e2f33345c3e..4d636ad6952 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs @@ -81,4 +81,23 @@ public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression) => HasPeriodEnd(Check.NotNull(propertyExpression).GetMemberAccess().Name); + + /// + /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, + /// which excludes them from SELECT * results. + /// + /// + /// + /// The default value is , matching the behavior of EF Core releases prior to this option + /// being introduced. Set to to make the period columns visible in SELECT *. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information. + /// + /// + /// A value indicating whether the period columns should be hidden. + /// The same builder instance so that multiple calls can be chained. + public new virtual OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + => (OwnedNavigationTemporalTableBuilder)base.PeriodColumnsHidden(hidden); } diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs index bf465982f67..8cb84a881ba 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs @@ -91,6 +91,29 @@ public virtual TemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName) #pragma warning restore EF1001 // Internal EF Core API usage. } + /// + /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, + /// which excludes them from SELECT * results. + /// + /// + /// + /// The default value is , matching the behavior of EF Core releases prior to this option + /// being introduced. Set to to make the period columns visible in SELECT *. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information and examples. + /// + /// + /// A value indicating whether the period columns should be hidden. + /// The same builder instance so that multiple calls can be chained. + public virtual TemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + { + _entityTypeBuilder.Metadata.SetIsTemporalPeriodColumnsHidden(hidden); + + return this; + } + private IMutableProperty ConfigurePeriodProperty(string propertyName) { // TODO: Configure the property explicitly, but remove it if it's no longer used, issue #15898 diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs index 99fc3d67506..f9b38a0129f 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs @@ -77,4 +77,23 @@ public virtual TemporalPeriodPropertyBuilder HasPeriodStart(ExpressionAn object that can be used to configure the period end property. public virtual TemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression) => HasPeriodEnd(Check.NotNull(propertyExpression).GetMemberAccess().Name); + + /// + /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, + /// which excludes them from SELECT * results. + /// + /// + /// + /// The default value is , matching the behavior of EF Core releases prior to this option + /// being introduced. Set to to make the period columns visible in SELECT *. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information and examples. + /// + /// + /// A value indicating whether the period columns should be hidden. + /// The same builder instance so that multiple calls can be chained. + public new virtual TemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + => (TemporalTableBuilder)base.PeriodColumnsHidden(hidden); } diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs index fb932681f6f..065a4c90a74 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs @@ -299,6 +299,14 @@ public static class SqlServerAnnotationNames /// public const string TemporalIsPeriodEndColumn = Prefix + "TemporalIsPeriodEndColumn"; + /// + /// 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 TemporalPeriodColumnsHidden = Prefix + "TemporalPeriodColumnsHidden"; + /// /// 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 2e8784e2afd..c96d27b18ac 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -130,6 +130,11 @@ public override IEnumerable For(ITable table, bool designTime) yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName); } + + if (!entityType.IsTemporalPeriodColumnsHidden()) + { + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, false); + } } } @@ -351,10 +356,20 @@ public override IEnumerable For(IColumn column, bool designTime) if (column.Name == periodStartColumnName) { yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true); + + if (!entityType.IsTemporalPeriodColumnsHidden()) + { + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, false); + } } else if (column.Name == periodEndColumnName) { yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true); + + if (!entityType.IsTemporalPeriodColumnsHidden()) + { + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, false); + } } } } diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 0d550996239..538b5e30aab 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1882,7 +1882,14 @@ protected override void ColumnDefinition( { builder.Append(" GENERATED ALWAYS AS ROW "); builder.Append(isPeriodStartColumn ? "START" : "END"); - builder.Append(" HIDDEN"); + + // Defaults to true to preserve backward compatibility - the period columns have always been hidden. + // Set to false via TemporalTableBuilder.PeriodColumnsHidden(false) to make them visible. + var hidden = operation[SqlServerAnnotationNames.TemporalPeriodColumnsHidden] as bool? ?? true; + if (hidden) + { + builder.Append(" HIDDEN"); + } } builder.Append(operation.IsNullable ? " NULL" : " NOT NULL"); @@ -3199,6 +3206,7 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema { addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); // model differ adds default value, but for period end we need to replace it with the correct one - // DateTime.MaxValue @@ -3360,8 +3368,10 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema // generating ALTER COLUMN operations and could just muddy the waters alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); if (temporalInformation.IsTemporalTable) { @@ -3432,12 +3442,12 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema } else { - // identity columns are not allowed inside HistoryTables - if (historyTables.Contains((tableName, schema))) - { - RemoveIdentityAnnotations(alterColumnOperation); - RemoveIdentityAnnotations(alterColumnOperation.OldColumn); - } + // identity columns are not allowed inside HistoryTables + if (historyTables.Contains((tableName, schema))) + { + RemoveIdentityAnnotations(alterColumnOperation); + RemoveIdentityAnnotations(alterColumnOperation.OldColumn); + } operations.Add(alterColumnOperation); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index b6016a8dde3..2f1d3546165 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -60,6 +60,57 @@ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) """); } + [ConditionalFact] + public virtual async Task Create_temporal_table_with_period_columns_not_hidden() + { + 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"); + ttb.PeriodColumnsHidden(false); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + 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)); + }); + + AssertSql( + """ +DECLARE @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'CREATE TABLE [Customer] ( + [Id] int NOT NULL IDENTITY, + [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() { diff --git a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs index 185d3bf3292..89d24e4f1f8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -1367,6 +1367,51 @@ public virtual void Owner_can_be_mapped_to_a_view() Assert.Null(owned.GetSchema()); } + [ConditionalFact] + public virtual void Temporal_table_period_columns_hidden_by_default() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal()); + modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Customer))!; + Assert.True(entity.IsTemporal()); + Assert.True(entity.IsTemporalPeriodColumnsHidden()); + } + + [ConditionalFact] + public virtual void Temporal_table_period_columns_can_be_made_visible() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => ttb.PeriodColumnsHidden(false))); + modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Customer))!; + Assert.True(entity.IsTemporal()); + Assert.False(entity.IsTemporalPeriodColumnsHidden()); + } + + [ConditionalFact] + public virtual void Temporal_table_period_columns_hidden_default_argument_is_true() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => + { + ttb.PeriodColumnsHidden(false); + ttb.PeriodColumnsHidden(); + })); + modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Customer))!; + Assert.True(entity.IsTemporalPeriodColumnsHidden()); + } + [ConditionalFact] public virtual void Temporal_table_default_settings() { @@ -2273,6 +2318,7 @@ public abstract class TestTemporalTableBuilder public abstract TestTemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName); public abstract TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression); public abstract TestTemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression); + public abstract TestTemporalTableBuilder PeriodColumnsHidden(bool hidden = true); } public class GenericTestTemporalTableBuilder(TemporalTableBuilder temporalTableBuilder) @@ -2302,6 +2348,9 @@ public override TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression) => new(TemporalTableBuilder.HasPeriodEnd(propertyExpression)); + + public override TestTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public class NonGenericTestTemporalTableBuilder(TemporalTableBuilder temporalTableBuilder) @@ -2330,6 +2379,9 @@ public override TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression) => HasPeriodEnd(propertyExpression.GetMemberAccess().Name); + + public override TestTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public abstract class TestOwnedNavigationTemporalTableBuilder @@ -2347,6 +2399,8 @@ public abstract TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public abstract TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression); + + public abstract TestOwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true); } public class GenericTestOwnedNavigationTemporalTableBuilder( @@ -2382,6 +2436,9 @@ public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression) => new(TemporalTableBuilder.HasPeriodEnd(propertyExpression)); + + public override TestOwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public class NonGenericTestOwnedNavigationTemporalTableBuilder( @@ -2416,6 +2473,9 @@ public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression) => HasPeriodEnd(propertyExpression.GetMemberAccess().Name); + + public override TestOwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) + => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public class TestTemporalPeriodPropertyBuilder(TemporalPeriodPropertyBuilder temporalPeriodPropertyBuilder) From 76554e03f085180000b2a2e58246cf934b08312e Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Wed, 6 May 2026 10:08:43 +0500 Subject: [PATCH 2/5] Fix Create_temporal_table_with_period_columns_not_hidden test Period columns are shadow properties at the model level regardless of the HIDDEN SQL flag, so the IColumn collection only exposes Id and Name. The HIDDEN flag affects emitted SQL only, not table.Columns. --- .../Migrations/MigrationsSqlServerTest.TemporalTables.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index 2f1d3546165..c8eaee14d86 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -92,9 +92,7 @@ await Test( 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)); + c => Assert.Equal("Name", c.Name)); }); AssertSql( From 6956bbd594da6d312de1ee1ae656606684be5271 Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Wed, 6 May 2026 16:24:25 +0500 Subject: [PATCH 3/5] Pivot to property-level IsHidden API per review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses feedback on PR #38225: - roji #1: per-column HIDDEN configuration → IsHidden(bool) on TemporalPeriodPropertyBuilder + OwnedNavigationTemporalPeriodPropertyBuilder lets users hide the start and end columns independently. - roji #2: HIDDEN as a property facet rather than a temporal-specific entity setting → new SqlServerAnnotationNames.IsHidden plus SqlServerPropertyExtensions IsHidden / SetIsHidden / GetIsHiddenConfigurationSource (mirroring IsSparse). PeriodColumnsHidden(bool) is preserved on TemporalTableBuilder as a convenience that calls SetIsHidden on both period properties. - Copilot bug: convert-to-temporal path was always emitting ALTER COLUMN ... ADD HIDDEN. Two new table-level annotations TemporalPeriodStartHidden / TemporalPeriodEndHidden are emitted by SqlServerAnnotationProvider.For(ITable) when a period property is configured visible. BuildTemporalInformationFromMigrationOperation reads them into TemporalOperationInformation, and EnablePeriod skips the ADD HIDDEN ALTER COLUMN operations when the column is configured visible. - New functional test Convert_normal_table_to_temporal_with_visible_period_columns asserts the ADD HIDDEN operations are omitted. - Snapshot generator chains .IsHidden(false) onto the period property fluent call when the column is configured visible. - Removed entity-level TemporalPeriodColumnsHidden annotation and the IsTemporalPeriodColumnsHidden / SetIsTemporalPeriodColumnsHidden / GetIsTemporalPeriodColumnsHiddenConfigurationSource entity extensions. - Updated baseline.json accordingly. --- .../SqlServerAnnotationCodeGenerator.cs | 54 ++++++----- ...verCSharpRuntimeAnnotationCodeGenerator.cs | 7 +- .../EFCore.SqlServer.baseline.json | 30 +++--- .../SqlServerEntityTypeExtensions.cs | 47 ---------- .../Extensions/SqlServerPropertyExtensions.cs | 48 ++++++++++ ...NavigationTemporalPeriodPropertyBuilder.cs | 23 +++++ .../OwnedNavigationTemporalTableBuilder.cs | 13 ++- .../Builders/TemporalPeriodPropertyBuilder.cs | 23 +++++ .../Metadata/Builders/TemporalTableBuilder.cs | 13 ++- .../Internal/SqlServerAnnotationNames.cs | 18 +++- .../Internal/SqlServerAnnotationProvider.cs | 27 ++++-- .../SqlServerMigrationsSqlGenerator.cs | 92 ++++++++++++------- .../MigrationsSqlServerTest.TemporalTables.cs | 51 ++++++++++ .../SqlServerModelBuilderTestBase.cs | 23 +++-- 14 files changed, 336 insertions(+), 133 deletions(-) diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 483e29d6432..5f45c4ac87b 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -148,9 +148,9 @@ private static readonly MethodInfo TemporalPropertyHasColumnNameMethodInfo = typeof(TemporalPeriodPropertyBuilder).GetRuntimeMethod( nameof(TemporalPeriodPropertyBuilder.HasColumnName), [typeof(string)])!; - private static readonly MethodInfo TemporalTablePeriodColumnsHiddenMethodInfo - = typeof(TemporalTableBuilder).GetRuntimeMethod( - nameof(TemporalTableBuilder.PeriodColumnsHidden), [typeof(bool)])!; + private static readonly MethodInfo TemporalPropertyIsHiddenMethodInfo + = typeof(TemporalPeriodPropertyBuilder).GetRuntimeMethod( + nameof(TemporalPeriodPropertyBuilder.IsHidden), [typeof(bool)])!; private static readonly MethodInfo ModelHasFullTextCatalogMethodInfo = typeof(SqlServerModelBuilderExtensions).GetRuntimeMethod( @@ -375,27 +375,17 @@ public override IReadOnlyList GenerateFluentApiCalls( : new MethodCallCodeFragment(TemporalTableUseHistoryTableMethodInfo2, historyTableName)); } - // ttb => ttb.HasPeriodStart("Start").HasColumnName("ColumnStart") + // ttb => ttb.HasPeriodStart("Start").HasColumnName("ColumnStart").IsHidden(false) + // IsHidden(false) is only chained when the user explicitly configured the column visible — + // the default is HIDDEN, so omitting matches the legacy snapshot output. temporalTableBuilderCalls.Add( - periodStartColumnName != null - ? new MethodCallCodeFragment(TemporalTableHasPeriodStartMethodInfo, periodStartPropertyName) - .Chain(new MethodCallCodeFragment(TemporalPropertyHasColumnNameMethodInfo, periodStartColumnName)) - : new MethodCallCodeFragment(TemporalTableHasPeriodStartMethodInfo, periodStartPropertyName)); + BuildPeriodPropertyCall( + TemporalTableHasPeriodStartMethodInfo, periodStartPropertyName, periodStartColumnName, periodStartProperty)); - // ttb => ttb.HasPeriodEnd("End").HasColumnName("ColumnEnd") + // ttb => ttb.HasPeriodEnd("End").HasColumnName("ColumnEnd").IsHidden(false) temporalTableBuilderCalls.Add( - periodEndColumnName != null - ? new MethodCallCodeFragment(TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName) - .Chain(new MethodCallCodeFragment(TemporalPropertyHasColumnNameMethodInfo, periodEndColumnName)) - : new MethodCallCodeFragment(TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName)); - - // ttb => ttb.PeriodColumnsHidden(false) - // Only emit when explicitly set to false; the default (true) matches legacy behavior. - if (annotations.TryGetValue(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, out var periodColumnsHiddenAnnotation) - && periodColumnsHiddenAnnotation.Value as bool? == false) - { - temporalTableBuilderCalls.Add(new MethodCallCodeFragment(TemporalTablePeriodColumnsHiddenMethodInfo, false)); - } + BuildPeriodPropertyCall( + TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName, periodEndColumnName, periodEndProperty)); // ToTable(tb => tb.IsTemporal(ttb => { ... })) var toTemporalTableCall = new MethodCallCodeFragment( @@ -415,10 +405,30 @@ public override IReadOnlyList GenerateFluentApiCalls( annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartPropertyName); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndPropertyName); - annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); } return fragments; + + static MethodCallCodeFragment BuildPeriodPropertyCall( + MethodInfo hasPeriodMethod, + string? periodPropertyName, + string? periodColumnName, + IReadOnlyProperty? periodProperty) + { + var call = new MethodCallCodeFragment(hasPeriodMethod, periodPropertyName); + + if (periodColumnName != null) + { + call = call.Chain(new MethodCallCodeFragment(TemporalPropertyHasColumnNameMethodInfo, periodColumnName)); + } + + if (periodProperty?.IsHidden() == false) + { + call = call.Chain(new MethodCallCodeFragment(TemporalPropertyIsHiddenMethodInfo, false)); + } + + return call; + } } /// diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs index 792eea42837..94855e4e239 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs @@ -68,6 +68,7 @@ public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGen annotations.Remove(SqlServerAnnotationNames.IdentityIncrement); annotations.Remove(SqlServerAnnotationNames.IdentitySeed); annotations.Remove(SqlServerAnnotationNames.Sparse); + annotations.Remove(SqlServerAnnotationNames.IsHidden); if (!annotations.ContainsKey(SqlServerAnnotationNames.ValueGenerationStrategy)) { @@ -88,7 +89,7 @@ public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGenerat annotations.Remove(SqlServerAnnotationNames.Sparse); annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); - annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); + annotations.Remove(SqlServerAnnotationNames.IsHidden); } base.Generate(column, parameters); @@ -172,7 +173,6 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndPropertyName); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartPropertyName); - annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); } base.Generate(entityType, parameters); @@ -189,7 +189,8 @@ public override void Generate(ITable table, CSharpRuntimeAnnotationCodeGenerator annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndColumnName); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartColumnName); - annotations.Remove(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); + annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartHidden); + annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndHidden); } base.Generate(table, parameters); diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json index 006746c92c6..1af3d1c81a8 100644 --- a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json +++ b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json @@ -215,6 +215,9 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder HasPrecision(int precision);" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder IsHidden(bool hidden = true);" + }, { "Member": "override string? ToString();" } @@ -1128,9 +1131,6 @@ { "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsTemporalConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType);" }, - { - "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsTemporalPeriodColumnsHiddenConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType);" - }, { "Member": "static string? GetPeriodEndPropertyName(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType entityType);" }, @@ -1164,9 +1164,6 @@ { "Member": "static bool IsTemporal(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType entityType);" }, - { - "Member": "static bool IsTemporalPeriodColumnsHidden(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType entityType);" - }, { "Member": "static void SetHistoryTableName(this Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entityType, string? historyTableName);" }, @@ -1191,12 +1188,6 @@ { "Member": "static bool? SetIsTemporal(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType, bool? temporal, bool fromDataAnnotation = false);" }, - { - "Member": "static void SetIsTemporalPeriodColumnsHidden(this Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entityType, bool? hidden);" - }, - { - "Member": "static bool? SetIsTemporalPeriodColumnsHidden(this Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType entityType, bool? hidden, bool fromDataAnnotation = false);" - }, { "Member": "static void SetPeriodEndPropertyName(this Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType entityType, string? periodEndPropertyName);" }, @@ -2533,6 +2524,9 @@ { "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIdentitySeedConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionRelationalPropertyOverrides overrides);" }, + { + "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsHiddenConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property);" + }, { "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsSparseConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property);" }, @@ -2575,6 +2569,9 @@ { "Member": "static bool IsCompatibleWithValueGeneration(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" }, + { + "Member": "static bool? IsHidden(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" + }, { "Member": "static bool? IsSparse(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" }, @@ -2629,6 +2626,12 @@ { "Member": "static long? SetIdentitySeed(this Microsoft.EntityFrameworkCore.Metadata.IConventionRelationalPropertyOverrides overrides, long? seed, bool fromDataAnnotation = false);" }, + { + "Member": "static void SetIsHidden(this Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property, bool? hidden);" + }, + { + "Member": "static bool? SetIsHidden(this Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property, bool? hidden, bool fromDataAnnotation = false);" + }, { "Member": "static void SetIsSparse(this Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property, bool? sparse);" }, @@ -3047,6 +3050,9 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder HasPrecision(int precision);" }, + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder IsHidden(bool hidden = true);" + }, { "Member": "override string? ToString();" } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs index d2f7d0c116c..c9417eb9e04 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs @@ -297,53 +297,6 @@ public static void SetHistoryTableSchema(this IMutableEntityType entityType, str return (string.IsNullOrEmpty(schema) ? "" : schema + ".") + historyTableName; } - /// - /// Returns a value indicating whether the period columns of the entity type mapped to a temporal table are - /// defined with the HIDDEN flag, which excludes them from SELECT * results. - /// - /// - /// The default value is , matching the behavior of EF Core releases prior to this option - /// being introduced. Set to to make the period columns visible. - /// - /// The entity type. - /// if the period columns are hidden; otherwise . - public static bool IsTemporalPeriodColumnsHidden(this IReadOnlyEntityType entityType) - => entityType[SqlServerAnnotationNames.TemporalPeriodColumnsHidden] as bool? ?? true; - - /// - /// Sets a value indicating whether the period columns of the entity type mapped to a temporal table are - /// defined with the HIDDEN flag. - /// - /// The entity type. - /// The value to set; to remove the explicit configuration. - public static void SetIsTemporalPeriodColumnsHidden(this IMutableEntityType entityType, bool? hidden) - => entityType.SetOrRemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, hidden); - - /// - /// Sets a value indicating whether the period columns of the entity type mapped to a temporal table are - /// defined with the HIDDEN flag. - /// - /// The entity type. - /// The value to set; to remove the explicit configuration. - /// Indicates whether the configuration was specified using a data annotation. - /// The configured value. - public static bool? SetIsTemporalPeriodColumnsHidden( - this IConventionEntityType entityType, - bool? hidden, - bool fromDataAnnotation = false) - => (bool?)entityType.SetOrRemoveAnnotation( - SqlServerAnnotationNames.TemporalPeriodColumnsHidden, - hidden, - fromDataAnnotation)?.Value; - - /// - /// Gets the configuration source for the period-columns-hidden setting. - /// - /// The entity type. - /// The configuration source for the period-columns-hidden setting. - public static ConfigurationSource? GetIsTemporalPeriodColumnsHiddenConfigurationSource(this IConventionEntityType entityType) - => entityType.FindAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden)?.GetConfigurationSource(); - #endregion Temporal table #region SQL OUTPUT clause diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs index 1d8964d40e8..df98919384a 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs @@ -1050,4 +1050,52 @@ public static void SetIsSparse(this IMutableProperty property, bool? sparse) /// The for whether the property's column is sparse. public static ConfigurationSource? GetIsSparseConfigurationSource(this IConventionProperty property) => property.FindAnnotation(SqlServerAnnotationNames.Sparse)?.GetConfigurationSource(); + + /// + /// Returns a value indicating whether the property's column is defined with the SQL Server HIDDEN flag, + /// which excludes the column from SELECT * results. + /// + /// + /// This applies to columns defined with GENERATED ALWAYS AS, including SQL Server temporal table + /// period columns. The default for temporal period columns is ; for other columns + /// this annotation has no effect unless the column is generated. + /// + /// The property. + /// if the property's column is hidden. + public static bool? IsHidden(this IReadOnlyProperty property) + => (property is RuntimeProperty) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (bool?)property[SqlServerAnnotationNames.IsHidden]; + + /// + /// Sets a value indicating whether the property's column is defined with the SQL Server HIDDEN flag. + /// + /// The property. + /// The value to set; to remove the explicit configuration. + public static void SetIsHidden(this IMutableProperty property, bool? hidden) + => property.SetOrRemoveAnnotation(SqlServerAnnotationNames.IsHidden, hidden); + + /// + /// Sets a value indicating whether the property's column is defined with the SQL Server HIDDEN flag. + /// + /// The property. + /// The value to set; to remove the explicit configuration. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static bool? SetIsHidden( + this IConventionProperty property, + bool? hidden, + bool fromDataAnnotation = false) + => (bool?)property.SetOrRemoveAnnotation( + SqlServerAnnotationNames.IsHidden, + hidden, + fromDataAnnotation)?.Value; + + /// + /// Returns the for whether the property's column is hidden. + /// + /// The property. + /// The for whether the property's column is hidden. + public static ConfigurationSource? GetIsHiddenConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(SqlServerAnnotationNames.IsHidden)?.GetConfigurationSource(); } diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalPeriodPropertyBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalPeriodPropertyBuilder.cs index 893c6cb1a5d..e27d6971836 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalPeriodPropertyBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalPeriodPropertyBuilder.cs @@ -56,6 +56,29 @@ public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPrecision(int pre return this; } + /// + /// Configures whether the period column is defined with the SQL Server HIDDEN flag, + /// which excludes it from SELECT * results. + /// + /// + /// + /// The default is for period columns, matching the behavior of EF Core releases + /// prior to this option being introduced. Set to to make the column visible. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information. + /// + /// + /// A value indicating whether the column should be hidden. + /// The same builder instance so that multiple calls can be chained. + public virtual OwnedNavigationTemporalPeriodPropertyBuilder IsHidden(bool hidden = true) + { + ((IMutableProperty)_propertyBuilder.Metadata).SetIsHidden(hidden); + + return this; + } + #region Hidden System.Object members /// diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs index debe091e295..f6dc55d9410 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs @@ -109,7 +109,18 @@ public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd(string /// The same builder instance so that multiple calls can be chained. public virtual OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) { - _referenceOwnershipBuilder.OwnedEntityType.SetIsTemporalPeriodColumnsHidden(hidden); + var entityType = _referenceOwnershipBuilder.OwnedEntityType; + if (entityType.GetPeriodStartPropertyName() is { } startName + && entityType.FindProperty(startName) is { } startProperty) + { + startProperty.SetIsHidden(hidden); + } + + if (entityType.GetPeriodEndPropertyName() is { } endName + && entityType.FindProperty(endName) is { } endProperty) + { + endProperty.SetIsHidden(hidden); + } return this; } diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs index d258accabf3..6050b165f13 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs @@ -57,6 +57,29 @@ public virtual TemporalPeriodPropertyBuilder HasPrecision(int precision) return this; } + /// + /// Configures whether the period column is defined with the SQL Server HIDDEN flag, + /// which excludes it from SELECT * results. + /// + /// + /// + /// The default is for period columns, matching the behavior of EF Core releases + /// prior to this option being introduced. Set to to make the column visible. + /// + /// + /// See Using SQL Server temporal tables with EF Core + /// for more information. + /// + /// + /// A value indicating whether the column should be hidden. + /// The same builder instance so that multiple calls can be chained. + public virtual TemporalPeriodPropertyBuilder IsHidden(bool hidden = true) + { + ((IMutableProperty)_propertyBuilder.Metadata).SetIsHidden(hidden); + + return this; + } + PropertyBuilder IInfrastructure.Instance => _propertyBuilder; diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs index 8cb84a881ba..9e931dac3aa 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs @@ -109,7 +109,18 @@ public virtual TemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName) /// The same builder instance so that multiple calls can be chained. public virtual TemporalTableBuilder PeriodColumnsHidden(bool hidden = true) { - _entityTypeBuilder.Metadata.SetIsTemporalPeriodColumnsHidden(hidden); + var entityType = _entityTypeBuilder.Metadata; + if (entityType.GetPeriodStartPropertyName() is { } startName + && entityType.FindProperty(startName) is { } startProperty) + { + startProperty.SetIsHidden(hidden); + } + + if (entityType.GetPeriodEndPropertyName() is { } endName + && entityType.FindProperty(endName) is { } endProperty) + { + endProperty.SetIsHidden(hidden); + } return this; } diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs index 065a4c90a74..2f973ff209d 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs @@ -305,7 +305,23 @@ public static class SqlServerAnnotationNames /// 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 TemporalPeriodColumnsHidden = Prefix + "TemporalPeriodColumnsHidden"; + public const string IsHidden = Prefix + "IsHidden"; + + /// + /// 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 TemporalPeriodStartHidden = Prefix + "TemporalPeriodStartHidden"; + + /// + /// 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 TemporalPeriodEndHidden = Prefix + "TemporalPeriodEndHidden"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index c96d27b18ac..d28250ec9d1 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -110,9 +110,10 @@ public override IEnumerable For(ITable table, bool designTime) // see #26007 var storeObjectIdentifier = StoreObjectIdentifier.Table(table.Name, table.Schema); var periodStartPropertyName = entityType.GetPeriodStartPropertyName(); + IReadOnlyProperty? periodStartProperty = null; if (periodStartPropertyName != null) { - var periodStartProperty = entityType.FindProperty(periodStartPropertyName); + periodStartProperty = entityType.FindProperty(periodStartPropertyName); var periodStartColumnName = periodStartProperty != null ? periodStartProperty.GetColumnName(storeObjectIdentifier) : periodStartPropertyName; @@ -121,9 +122,10 @@ public override IEnumerable For(ITable table, bool designTime) } var periodEndPropertyName = entityType.GetPeriodEndPropertyName(); + IReadOnlyProperty? periodEndProperty = null; if (periodEndPropertyName != null) { - var periodEndProperty = entityType.FindProperty(periodEndPropertyName); + periodEndProperty = entityType.FindProperty(periodEndPropertyName); var periodEndColumnName = periodEndProperty != null ? periodEndProperty.GetColumnName(storeObjectIdentifier) : periodEndPropertyName; @@ -131,9 +133,17 @@ public override IEnumerable For(ITable table, bool designTime) yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName); } - if (!entityType.IsTemporalPeriodColumnsHidden()) + // Emit the per-period-column hidden flags on the table operation so the migrations generator + // can read them in BuildTemporalInformationFromMigrationOperation. Only emit when the user + // explicitly configured the column visible (default is HIDDEN, omitted to keep table ops clean). + if (periodStartProperty?.IsHidden() == false) { - yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, false); + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodStartHidden, false); + } + + if (periodEndProperty?.IsHidden() == false) + { + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndHidden, false); } } } @@ -357,18 +367,19 @@ public override IEnumerable For(IColumn column, bool designTime) { yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true); - if (!entityType.IsTemporalPeriodColumnsHidden()) + // Period columns default to HIDDEN; only emit the annotation when explicitly visible. + if (periodStartProperty?.IsHidden() == false) { - yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, false); + yield return new Annotation(SqlServerAnnotationNames.IsHidden, false); } } else if (column.Name == periodEndColumnName) { yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true); - if (!entityType.IsTemporalPeriodColumnsHidden()) + if (periodEndProperty?.IsHidden() == false) { - yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden, false); + yield return new Annotation(SqlServerAnnotationNames.IsHidden, false); } } } diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 538b5e30aab..891a6ef6f06 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1884,8 +1884,9 @@ protected override void ColumnDefinition( builder.Append(isPeriodStartColumn ? "START" : "END"); // Defaults to true to preserve backward compatibility - the period columns have always been hidden. - // Set to false via TemporalTableBuilder.PeriodColumnsHidden(false) to make them visible. - var hidden = operation[SqlServerAnnotationNames.TemporalPeriodColumnsHidden] as bool? ?? true; + // Set to false via TemporalPeriodPropertyBuilder.IsHidden(false) (or + // TemporalTableBuilder.PeriodColumnsHidden(false) which sets both period columns). + var hidden = operation[SqlServerAnnotationNames.IsHidden] as bool? ?? true; if (hidden) { builder.Append(" HIDDEN"); @@ -3206,7 +3207,7 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema { addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); - addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); // model differ adds default value, but for period end we need to replace it with the correct one - // DateTime.MaxValue @@ -3368,10 +3369,10 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema // generating ALTER COLUMN operations and could just muddy the waters alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); - alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); - alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodColumnsHidden); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); if (temporalInformation.IsTemporalTable) { @@ -3483,6 +3484,8 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema temporalInformation.Key.Schema, temporalInformation.Value.PeriodStartColumnName!, temporalInformation.Value.PeriodEndColumnName!, + temporalInformation.Value.PeriodStartHidden, + temporalInformation.Value.PeriodEndHidden, temporalInformation.Value.SuppressTransaction); } @@ -3508,13 +3511,19 @@ static TemporalOperationInformation BuildTemporalInformationFromMigrationOperati var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + // Period columns default to HIDDEN; the annotation is only present when explicitly configured visible. + var periodStartHidden = operation[SqlServerAnnotationNames.TemporalPeriodStartHidden] as bool? ?? true; + var periodEndHidden = operation[SqlServerAnnotationNames.TemporalPeriodEndHidden] as bool? ?? true; + return new TemporalOperationInformation { IsTemporalTable = isTemporalTable, HistoryTableName = historyTableName, HistoryTableSchema = historyTableSchema, PeriodStartColumnName = periodStartColumnName, - PeriodEndColumnName = periodEndColumnName + PeriodEndColumnName = periodEndColumnName, + PeriodStartHidden = periodStartHidden, + PeriodEndHidden = periodEndHidden }; } @@ -3610,7 +3619,14 @@ void DisablePeriod( }); } - void EnablePeriod(string table, string? schema, string periodStartColumnName, string periodEndColumnName, bool suppressTransaction) + void EnablePeriod( + string table, + string? schema, + string periodStartColumnName, + string periodEndColumnName, + bool periodStartHidden, + bool periodEndHidden, + bool suppressTransaction) { var addPeriodSql = new StringBuilder() .Append("ALTER TABLE ") @@ -3634,31 +3650,39 @@ void EnablePeriod(string table, string? schema, string periodStartColumnName, st operations.Add( new SqlOperation { Sql = addPeriodSql, SuppressTransaction = suppressTransaction }); - operations.Add( - new SqlOperation - { - Sql = new StringBuilder() - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) - .Append(" ALTER COLUMN ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName)) - .Append(" ADD HIDDEN") - .ToString(), - SuppressTransaction = suppressTransaction - }); + // Period columns are HIDDEN by default. Skip the `ADD HIDDEN` ALTER when the column was + // configured visible via TemporalPeriodPropertyBuilder.IsHidden(false). + if (periodStartHidden) + { + operations.Add( + new SqlOperation + { + Sql = new StringBuilder() + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) + .Append(" ALTER COLUMN ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName)) + .Append(" ADD HIDDEN") + .ToString(), + SuppressTransaction = suppressTransaction + }); + } - operations.Add( - new SqlOperation - { - Sql = new StringBuilder() - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) - .Append(" ALTER COLUMN ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName)) - .Append(" ADD HIDDEN") - .ToString(), - SuppressTransaction = suppressTransaction - }); + if (periodEndHidden) + { + operations.Add( + new SqlOperation + { + Sql = new StringBuilder() + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) + .Append(" ALTER COLUMN ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName)) + .Append(" ADD HIDDEN") + .ToString(), + SuppressTransaction = suppressTransaction + }); + } } void DecompressTable(string tableName, string? schema, bool suppressTransaction) @@ -3740,5 +3764,11 @@ private sealed class TemporalOperationInformation public bool ShouldEnableVersioning { get; set; } public bool ShouldEnablePeriod { get; set; } public bool SuppressTransaction { get; set; } + + // Period columns default to HIDDEN. When converting an existing table to temporal, these flags + // capture the user-configured visibility from the period column annotations so EnablePeriod can + // conditionally emit `ALTER COLUMN ... ADD HIDDEN`. + public bool PeriodStartHidden { get; set; } = true; + public bool PeriodEndHidden { get; set; } = true; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index c8eaee14d86..98a3a98b8fd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -109,6 +109,57 @@ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) """); } + [ConditionalFact] + public virtual async Task Convert_normal_table_to_temporal_with_visible_period_columns() + { + 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(ttb => ttb.PeriodColumnsHidden(false))); + + e.Metadata[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] = "PeriodStart"; + e.Metadata[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] = "PeriodEnd"; + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + }); + + // The convert-to-temporal path should NOT emit `ALTER COLUMN ... ADD HIDDEN` operations + // when the user has configured the period columns visible. + 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 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + '.[CustomerHistory]))') +"""); + } + [ConditionalFact] public virtual async Task Create_temporal_table_custom_column_mappings_and_default_history_table() { diff --git a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs index 89d24e4f1f8..53097f82aaf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -1378,11 +1378,12 @@ public virtual void Temporal_table_period_columns_hidden_by_default() var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.True(entity.IsTemporalPeriodColumnsHidden()); + Assert.Null(entity.GetProperty(entity.GetPeriodStartPropertyName()!).IsHidden()); + Assert.Null(entity.GetProperty(entity.GetPeriodEndPropertyName()!).IsHidden()); } [ConditionalFact] - public virtual void Temporal_table_period_columns_can_be_made_visible() + public virtual void Temporal_table_period_columns_can_be_made_visible_via_pair_API() { var modelBuilder = CreateModelBuilder(); var model = modelBuilder.Model; @@ -1392,24 +1393,26 @@ public virtual void Temporal_table_period_columns_can_be_made_visible() var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.False(entity.IsTemporalPeriodColumnsHidden()); + Assert.False(entity.GetProperty(entity.GetPeriodStartPropertyName()!).IsHidden()); + Assert.False(entity.GetProperty(entity.GetPeriodEndPropertyName()!).IsHidden()); } [ConditionalFact] - public virtual void Temporal_table_period_columns_hidden_default_argument_is_true() + public virtual void Temporal_table_period_column_can_be_made_visible_per_column() { var modelBuilder = CreateModelBuilder(); var model = modelBuilder.Model; modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => { - ttb.PeriodColumnsHidden(false); - ttb.PeriodColumnsHidden(); + ttb.HasPeriodStart("PeriodStart").IsHidden(false); + ttb.HasPeriodEnd("PeriodEnd"); })); modelBuilder.FinalizeModel(); var entity = model.FindEntityType(typeof(Customer))!; - Assert.True(entity.IsTemporalPeriodColumnsHidden()); + Assert.False(entity.GetProperty("PeriodStart").IsHidden()); + Assert.Null(entity.GetProperty("PeriodEnd").IsHidden()); } [ConditionalFact] @@ -2484,6 +2487,9 @@ public class TestTemporalPeriodPropertyBuilder(TemporalPeriodPropertyBuilder tem public TestTemporalPeriodPropertyBuilder HasColumnName(string name) => new(TemporalPeriodPropertyBuilder.HasColumnName(name)); + + public TestTemporalPeriodPropertyBuilder IsHidden(bool hidden = true) + => new(TemporalPeriodPropertyBuilder.IsHidden(hidden)); } public class TestOwnedNavigationTemporalPeriodPropertyBuilder( @@ -2493,6 +2499,9 @@ public class TestOwnedNavigationTemporalPeriodPropertyBuilder( public TestOwnedNavigationTemporalPeriodPropertyBuilder HasColumnName(string name) => new(TemporalPeriodPropertyBuilder.HasColumnName(name)); + + public TestOwnedNavigationTemporalPeriodPropertyBuilder IsHidden(bool hidden = true) + => new(TemporalPeriodPropertyBuilder.IsHidden(hidden)); } #pragma warning disable EF9105 // Vector indexes are experimental From 3b773497b3ba35a7a06223dd659f664288697ce9 Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Wed, 6 May 2026 18:23:45 +0500 Subject: [PATCH 4/5] Remove redundant TemporalTableBuilder.PeriodColumnsHidden API The pair-level convenience PeriodColumnsHidden(bool) on TemporalTableBuilder had a silent-no-op bug: when called before HasPeriodStart/HasPeriodEnd, the period property names were not yet set, so the implementation could not find the properties to call SetIsHidden on. The property-level IsHidden(bool) on TemporalPeriodPropertyBuilder is order-safe because it chains directly off HasPeriodStart/HasPeriodEnd which return the period property builder for the property that was just configured. It also matches roji's preference for HIDDEN as a property-level facet rather than a temporal-table-specific entity setting. - Removed PeriodColumnsHidden from TemporalTableBuilder + generic + OwnedNavigationTemporalTableBuilder + generic - Removed corresponding abstract / override methods from test wrappers - Updated functional tests to chain IsHidden(false) per period property: ttb.HasPeriodStart("Start").IsHidden(false); ttb.HasPeriodEnd("End").IsHidden(false); - Updated baseline.json (4 entries removed) This also fixes the CI failure on Convert_normal_table_to_temporal_with_visible_period_columns on SqlServer 2019/2022/2025: the test was using PeriodColumnsHidden(false) inside ToTable(...IsTemporal(...)) before period property names were set, making the call a no-op and leaving ALTER COLUMN ... ADD HIDDEN in the emitted SQL. --- .../EFCore.SqlServer.baseline.json | 12 ------- .../OwnedNavigationTemporalTableBuilder.cs | 34 ------------------- .../OwnedNavigationTemporalTableBuilder``.cs | 19 ----------- .../Metadata/Builders/TemporalTableBuilder.cs | 34 ------------------- .../Builders/TemporalTableBuilder`.cs | 19 ----------- .../SqlServerMigrationsSqlGenerator.cs | 3 +- .../MigrationsSqlServerTest.TemporalTables.cs | 14 ++++---- .../SqlServerModelBuilderTestBase.cs | 27 +++++---------- 8 files changed, 16 insertions(+), 146 deletions(-) diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json index 1af3d1c81a8..02666144859 100644 --- a/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json +++ b/src/EFCore.SqlServer/EFCore.SqlServer.baseline.json @@ -241,9 +241,6 @@ { "Member": "override string? ToString();" }, - { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" - }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder UseHistoryTable(string name);" }, @@ -261,9 +258,6 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart(System.Linq.Expressions.Expression> propertyExpression);" }, - { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" - }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalTableBuilder UseHistoryTable(string name);" }, @@ -3073,9 +3067,6 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder HasPeriodStart(string propertyName);" }, - { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" - }, { "Member": "override string? ToString();" }, @@ -3096,9 +3087,6 @@ { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder HasPeriodStart(System.Linq.Expressions.Expression> propertyExpression);" }, - { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder PeriodColumnsHidden(bool hidden = true);" - }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalTableBuilder UseHistoryTable(string name);" }, diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs index f6dc55d9410..56bd7ed1570 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder.cs @@ -91,40 +91,6 @@ public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd(string #pragma warning restore EF1001 // Internal EF Core API usage. } - /// - /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, - /// which excludes them from SELECT * results. - /// - /// - /// - /// The default value is , matching the behavior of EF Core releases prior to this option - /// being introduced. Set to to make the period columns visible in SELECT *. - /// - /// - /// See Using SQL Server temporal tables with EF Core - /// for more information. - /// - /// - /// A value indicating whether the period columns should be hidden. - /// The same builder instance so that multiple calls can be chained. - public virtual OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - { - var entityType = _referenceOwnershipBuilder.OwnedEntityType; - if (entityType.GetPeriodStartPropertyName() is { } startName - && entityType.FindProperty(startName) is { } startProperty) - { - startProperty.SetIsHidden(hidden); - } - - if (entityType.GetPeriodEndPropertyName() is { } endName - && entityType.FindProperty(endName) is { } endProperty) - { - endProperty.SetIsHidden(hidden); - } - - return this; - } - private IMutableProperty ConfigurePeriodProperty(string propertyName) { // TODO: Configure the property explicitly, but remove it if it's no longer used, issue #15898 diff --git a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs index 4d636ad6952..e2f33345c3e 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalTableBuilder``.cs @@ -81,23 +81,4 @@ public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression) => HasPeriodEnd(Check.NotNull(propertyExpression).GetMemberAccess().Name); - - /// - /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, - /// which excludes them from SELECT * results. - /// - /// - /// - /// The default value is , matching the behavior of EF Core releases prior to this option - /// being introduced. Set to to make the period columns visible in SELECT *. - /// - /// - /// See Using SQL Server temporal tables with EF Core - /// for more information. - /// - /// - /// A value indicating whether the period columns should be hidden. - /// The same builder instance so that multiple calls can be chained. - public new virtual OwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - => (OwnedNavigationTemporalTableBuilder)base.PeriodColumnsHidden(hidden); } diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs index 9e931dac3aa..bf465982f67 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder.cs @@ -91,40 +91,6 @@ public virtual TemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName) #pragma warning restore EF1001 // Internal EF Core API usage. } - /// - /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, - /// which excludes them from SELECT * results. - /// - /// - /// - /// The default value is , matching the behavior of EF Core releases prior to this option - /// being introduced. Set to to make the period columns visible in SELECT *. - /// - /// - /// See Using SQL Server temporal tables with EF Core - /// for more information and examples. - /// - /// - /// A value indicating whether the period columns should be hidden. - /// The same builder instance so that multiple calls can be chained. - public virtual TemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - { - var entityType = _entityTypeBuilder.Metadata; - if (entityType.GetPeriodStartPropertyName() is { } startName - && entityType.FindProperty(startName) is { } startProperty) - { - startProperty.SetIsHidden(hidden); - } - - if (entityType.GetPeriodEndPropertyName() is { } endName - && entityType.FindProperty(endName) is { } endProperty) - { - endProperty.SetIsHidden(hidden); - } - - return this; - } - private IMutableProperty ConfigurePeriodProperty(string propertyName) { // TODO: Configure the property explicitly, but remove it if it's no longer used, issue #15898 diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs index f9b38a0129f..99fc3d67506 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalTableBuilder`.cs @@ -77,23 +77,4 @@ public virtual TemporalPeriodPropertyBuilder HasPeriodStart(ExpressionAn object that can be used to configure the period end property. public virtual TemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression) => HasPeriodEnd(Check.NotNull(propertyExpression).GetMemberAccess().Name); - - /// - /// Configures whether the period columns of the temporal table are defined with the HIDDEN flag, - /// which excludes them from SELECT * results. - /// - /// - /// - /// The default value is , matching the behavior of EF Core releases prior to this option - /// being introduced. Set to to make the period columns visible in SELECT *. - /// - /// - /// See Using SQL Server temporal tables with EF Core - /// for more information and examples. - /// - /// - /// A value indicating whether the period columns should be hidden. - /// The same builder instance so that multiple calls can be chained. - public new virtual TemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - => (TemporalTableBuilder)base.PeriodColumnsHidden(hidden); } diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 891a6ef6f06..a99a741f599 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1884,8 +1884,7 @@ protected override void ColumnDefinition( builder.Append(isPeriodStartColumn ? "START" : "END"); // Defaults to true to preserve backward compatibility - the period columns have always been hidden. - // Set to false via TemporalPeriodPropertyBuilder.IsHidden(false) (or - // TemporalTableBuilder.PeriodColumnsHidden(false) which sets both period columns). + // Set to false via TemporalPeriodPropertyBuilder.IsHidden(false). var hidden = operation[SqlServerAnnotationNames.IsHidden] as bool? ?? true; if (hidden) { diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index 98a3a98b8fd..f8f8d63c5bf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -76,9 +76,8 @@ await Test( e.ToTable(tb => tb.IsTemporal(ttb => { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.PeriodColumnsHidden(false); + ttb.HasPeriodStart("SystemTimeStart").IsHidden(false); + ttb.HasPeriodEnd("SystemTimeEnd").IsHidden(false); })); }), model => @@ -128,10 +127,11 @@ await Test( e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); e.HasKey("Id"); - e.ToTable(tb => tb.IsTemporal(ttb => ttb.PeriodColumnsHidden(false))); - - e.Metadata[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] = "PeriodStart"; - e.Metadata[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] = "PeriodEnd"; + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("PeriodStart").IsHidden(false); + ttb.HasPeriodEnd("PeriodEnd").IsHidden(false); + })); }), model => { diff --git a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs index 53097f82aaf..6e7105ed678 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -1383,18 +1383,22 @@ public virtual void Temporal_table_period_columns_hidden_by_default() } [ConditionalFact] - public virtual void Temporal_table_period_columns_can_be_made_visible_via_pair_API() + public virtual void Temporal_table_period_columns_can_be_made_visible_per_column() { var modelBuilder = CreateModelBuilder(); var model = modelBuilder.Model; - modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => ttb.PeriodColumnsHidden(false))); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("PeriodStart").IsHidden(false); + ttb.HasPeriodEnd("PeriodEnd").IsHidden(false); + })); modelBuilder.FinalizeModel(); var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.False(entity.GetProperty(entity.GetPeriodStartPropertyName()!).IsHidden()); - Assert.False(entity.GetProperty(entity.GetPeriodEndPropertyName()!).IsHidden()); + Assert.False(entity.GetProperty("PeriodStart").IsHidden()); + Assert.False(entity.GetProperty("PeriodEnd").IsHidden()); } [ConditionalFact] @@ -2321,7 +2325,6 @@ public abstract class TestTemporalTableBuilder public abstract TestTemporalPeriodPropertyBuilder HasPeriodEnd(string propertyName); public abstract TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression); public abstract TestTemporalPeriodPropertyBuilder HasPeriodEnd(Expression> propertyExpression); - public abstract TestTemporalTableBuilder PeriodColumnsHidden(bool hidden = true); } public class GenericTestTemporalTableBuilder(TemporalTableBuilder temporalTableBuilder) @@ -2351,9 +2354,6 @@ public override TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression) => new(TemporalTableBuilder.HasPeriodEnd(propertyExpression)); - - public override TestTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public class NonGenericTestTemporalTableBuilder(TemporalTableBuilder temporalTableBuilder) @@ -2382,9 +2382,6 @@ public override TestTemporalPeriodPropertyBuilder HasPeriodStart(Expression> propertyExpression) => HasPeriodEnd(propertyExpression.GetMemberAccess().Name); - - public override TestTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public abstract class TestOwnedNavigationTemporalTableBuilder @@ -2402,8 +2399,6 @@ public abstract TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public abstract TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression); - - public abstract TestOwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true); } public class GenericTestOwnedNavigationTemporalTableBuilder( @@ -2439,9 +2434,6 @@ public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression) => new(TemporalTableBuilder.HasPeriodEnd(propertyExpression)); - - public override TestOwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public class NonGenericTestOwnedNavigationTemporalTableBuilder( @@ -2476,9 +2468,6 @@ public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodStart( public override TestOwnedNavigationTemporalPeriodPropertyBuilder HasPeriodEnd( Expression> propertyExpression) => HasPeriodEnd(propertyExpression.GetMemberAccess().Name); - - public override TestOwnedNavigationTemporalTableBuilder PeriodColumnsHidden(bool hidden = true) - => Wrap(TemporalTableBuilder.PeriodColumnsHidden(hidden)); } public class TestTemporalPeriodPropertyBuilder(TemporalPeriodPropertyBuilder temporalPeriodPropertyBuilder) From f3b3da21bde30dff2ea4092f3634beb7245abf32 Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Wed, 6 May 2026 22:15:49 +0500 Subject: [PATCH 5/5] Fix convert-to-temporal ignoring IsHidden(false) on period columns In RewriteOperations, the AlterTableOperation case builds the temporal information from alterTableOperation.OldTable to capture the initial (non-temporal) state. But that source has no TemporalPeriodStartHidden / TemporalPeriodEndHidden annotations, so PeriodStartHidden/PeriodEndHidden defaulted to true and EnablePeriod always emitted ALTER COLUMN ... ADD HIDDEN, ignoring the user's IsHidden(false) configuration. Override just those two flags from alterTableOperation itself, which carries the target-state annotations emitted by SqlServerAnnotationProvider. All other initial-state reads from OldTable remain intact. --- .../Migrations/SqlServerMigrationsSqlGenerator.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index a99a741f599..7b095d10a5c 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -2895,6 +2895,19 @@ private IReadOnlyList RewriteOperations( { // we create the temporal info based on the OLD table here - we want the initial state var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, alterTableOperation.OldTable); + + // The period-column hidden flags reflect the user's intent for the NEW state of the table, + // not the old state, so override them from the AlterTable operation itself when present. + if (alterTableOperation[SqlServerAnnotationNames.TemporalPeriodStartHidden] is bool startHidden) + { + temporalTableInformation.PeriodStartHidden = startHidden; + } + + if (alterTableOperation[SqlServerAnnotationNames.TemporalPeriodEndHidden] is bool endHidden) + { + temporalTableInformation.PeriodEndHidden = endHidden; + } + temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; }