diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 6a7713f503d..5f45c4ac87b 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 TemporalPropertyIsHiddenMethodInfo + = typeof(TemporalPeriodPropertyBuilder).GetRuntimeMethod( + nameof(TemporalPeriodPropertyBuilder.IsHidden), [typeof(bool)])!; + private static readonly MethodInfo ModelHasFullTextCatalogMethodInfo = typeof(SqlServerModelBuilderExtensions).GetRuntimeMethod( nameof(SqlServerModelBuilderExtensions.HasFullTextCatalog), [typeof(ModelBuilder), typeof(string)])!; @@ -371,19 +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)); + BuildPeriodPropertyCall( + TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName, periodEndColumnName, periodEndProperty)); // ToTable(tb => tb.IsTemporal(ttb => { ... })) var toTemporalTableCall = new MethodCallCodeFragment( @@ -406,6 +408,27 @@ public override IReadOnlyList GenerateFluentApiCalls( } 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 d6244c596a6..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,6 +89,7 @@ public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGenerat annotations.Remove(SqlServerAnnotationNames.Sparse); annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + annotations.Remove(SqlServerAnnotationNames.IsHidden); } base.Generate(column, parameters); @@ -187,6 +189,8 @@ public override void Generate(ITable table, CSharpRuntimeAnnotationCodeGenerator annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndColumnName); annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartColumnName); + 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 8de92252d64..02666144859 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();" } @@ -2515,6 +2518,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);" }, @@ -2557,6 +2563,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);" }, @@ -2611,6 +2620,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);" }, @@ -3029,6 +3044,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/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/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/Internal/SqlServerAnnotationNames.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs index fb932681f6f..2f973ff209d 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationNames.cs @@ -299,6 +299,30 @@ 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 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 /// 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..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,15 +122,29 @@ 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; yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName); } + + // 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.TemporalPeriodStartHidden, false); + } + + if (periodEndProperty?.IsHidden() == false) + { + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndHidden, false); + } } } @@ -351,10 +366,21 @@ public override IEnumerable For(IColumn column, bool designTime) if (column.Name == periodStartColumnName) { yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true); + + // Period columns default to HIDDEN; only emit the annotation when explicitly visible. + if (periodStartProperty?.IsHidden() == false) + { + yield return new Annotation(SqlServerAnnotationNames.IsHidden, false); + } } else if (column.Name == periodEndColumnName) { yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true); + + if (periodEndProperty?.IsHidden() == 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 0d550996239..7b095d10a5c 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 TemporalPeriodPropertyBuilder.IsHidden(false). + var hidden = operation[SqlServerAnnotationNames.IsHidden] as bool? ?? true; + if (hidden) + { + builder.Append(" HIDDEN"); + } } builder.Append(operation.IsNullable ? " NULL" : " NOT NULL"); @@ -2888,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; } @@ -3199,6 +3219,7 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema { addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); // model differ adds default value, but for period end we need to replace it with the correct one - // DateTime.MaxValue @@ -3360,8 +3381,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.IsHidden); alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); if (temporalInformation.IsTemporalTable) { @@ -3432,12 +3455,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); } @@ -3473,6 +3496,8 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema temporalInformation.Key.Schema, temporalInformation.Value.PeriodStartColumnName!, temporalInformation.Value.PeriodEndColumnName!, + temporalInformation.Value.PeriodStartHidden, + temporalInformation.Value.PeriodEndHidden, temporalInformation.Value.SuppressTransaction); } @@ -3498,13 +3523,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 }; } @@ -3600,7 +3631,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 ") @@ -3624,31 +3662,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) @@ -3730,5 +3776,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 b6016a8dde3..f8f8d63c5bf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -60,6 +60,106 @@ 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").IsHidden(false); + ttb.HasPeriodEnd("SystemTimeEnd").IsHidden(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)); + }); + + 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 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.HasPeriodStart("PeriodStart").IsHidden(false); + ttb.HasPeriodEnd("PeriodEnd").IsHidden(false); + })); + }), + 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 185d3bf3292..6e7105ed678 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -1367,6 +1367,58 @@ 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.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_per_column() + { + var modelBuilder = CreateModelBuilder(); + var model = modelBuilder.Model; + + 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("PeriodStart").IsHidden()); + Assert.False(entity.GetProperty("PeriodEnd").IsHidden()); + } + + [ConditionalFact] + 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.HasPeriodStart("PeriodStart").IsHidden(false); + ttb.HasPeriodEnd("PeriodEnd"); + })); + modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Customer))!; + Assert.False(entity.GetProperty("PeriodStart").IsHidden()); + Assert.Null(entity.GetProperty("PeriodEnd").IsHidden()); + } + [ConditionalFact] public virtual void Temporal_table_default_settings() { @@ -2424,6 +2476,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( @@ -2433,6 +2488,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