Skip to content

Commit 9bdedb0

Browse files
authored
Allow configuring SQL Server temporal table period columns as not hidden (#38225)
Adds TemporalPeriodPropertyBuilder.IsHidden(bool) to opt SQL Server temporal period columns out of the HIDDEN flag, exposed as a general SQL Server property facet (mirroring IsSparse). The default remains HIDDEN for backward compatibility. Fixes #36608
1 parent 16bde33 commit 9bdedb0

13 files changed

Lines changed: 559 additions & 39 deletions

src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ private static readonly MethodInfo TemporalPropertyHasColumnNameMethodInfo
148148
= typeof(TemporalPeriodPropertyBuilder).GetRuntimeMethod(
149149
nameof(TemporalPeriodPropertyBuilder.HasColumnName), [typeof(string)])!;
150150

151+
private static readonly MethodInfo TemporalPropertyIsHiddenMethodInfo
152+
= typeof(TemporalPeriodPropertyBuilder).GetRuntimeMethod(
153+
nameof(TemporalPeriodPropertyBuilder.IsHidden), [typeof(bool)])!;
154+
151155
private static readonly MethodInfo ModelHasFullTextCatalogMethodInfo
152156
= typeof(SqlServerModelBuilderExtensions).GetRuntimeMethod(
153157
nameof(SqlServerModelBuilderExtensions.HasFullTextCatalog), [typeof(ModelBuilder), typeof(string)])!;
@@ -304,6 +308,11 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
304308
: new MethodCallCodeFragment(methodInfo, false));
305309
}
306310

311+
// The HIDDEN flag on temporal period columns is emitted via the temporal table builder
312+
// (ttb.HasPeriodStart(...).IsHidden(false)), so remove it here to avoid also emitting it as a
313+
// raw property-level annotation in the snapshot.
314+
annotations.Remove(SqlServerAnnotationNames.IsHidden);
315+
307316
return fragments;
308317
}
309318

@@ -371,19 +380,17 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
371380
: new MethodCallCodeFragment(TemporalTableUseHistoryTableMethodInfo2, historyTableName));
372381
}
373382

374-
// ttb => ttb.HasPeriodStart("Start").HasColumnName("ColumnStart")
383+
// ttb => ttb.HasPeriodStart("Start").HasColumnName("ColumnStart").IsHidden(false)
384+
// IsHidden(false) is only chained when the user explicitly configured the column visible —
385+
// the default is HIDDEN, so omitting matches the legacy snapshot output.
375386
temporalTableBuilderCalls.Add(
376-
periodStartColumnName != null
377-
? new MethodCallCodeFragment(TemporalTableHasPeriodStartMethodInfo, periodStartPropertyName)
378-
.Chain(new MethodCallCodeFragment(TemporalPropertyHasColumnNameMethodInfo, periodStartColumnName))
379-
: new MethodCallCodeFragment(TemporalTableHasPeriodStartMethodInfo, periodStartPropertyName));
387+
BuildPeriodPropertyCall(
388+
TemporalTableHasPeriodStartMethodInfo, periodStartPropertyName, periodStartColumnName, periodStartProperty));
380389

381-
// ttb => ttb.HasPeriodEnd("End").HasColumnName("ColumnEnd")
390+
// ttb => ttb.HasPeriodEnd("End").HasColumnName("ColumnEnd").IsHidden(false)
382391
temporalTableBuilderCalls.Add(
383-
periodEndColumnName != null
384-
? new MethodCallCodeFragment(TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName)
385-
.Chain(new MethodCallCodeFragment(TemporalPropertyHasColumnNameMethodInfo, periodEndColumnName))
386-
: new MethodCallCodeFragment(TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName));
392+
BuildPeriodPropertyCall(
393+
TemporalTableHasPeriodEndMethodInfo, periodEndPropertyName, periodEndColumnName, periodEndProperty));
387394

388395
// ToTable(tb => tb.IsTemporal(ttb => { ... }))
389396
var toTemporalTableCall = new MethodCallCodeFragment(
@@ -406,6 +413,27 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
406413
}
407414

408415
return fragments;
416+
417+
static MethodCallCodeFragment BuildPeriodPropertyCall(
418+
MethodInfo hasPeriodMethod,
419+
string? periodPropertyName,
420+
string? periodColumnName,
421+
IReadOnlyProperty? periodProperty)
422+
{
423+
var call = new MethodCallCodeFragment(hasPeriodMethod, periodPropertyName);
424+
425+
if (periodColumnName != null)
426+
{
427+
call = call.Chain(new MethodCallCodeFragment(TemporalPropertyHasColumnNameMethodInfo, periodColumnName));
428+
}
429+
430+
if (periodProperty?.IsHidden() == false)
431+
{
432+
call = call.Chain(new MethodCallCodeFragment(TemporalPropertyIsHiddenMethodInfo, false));
433+
}
434+
435+
return call;
436+
}
409437
}
410438

411439
/// <summary>

src/EFCore.SqlServer/Design/Internal/SqlServerCSharpRuntimeAnnotationCodeGenerator.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public override void Generate(IProperty property, CSharpRuntimeAnnotationCodeGen
6868
annotations.Remove(SqlServerAnnotationNames.IdentityIncrement);
6969
annotations.Remove(SqlServerAnnotationNames.IdentitySeed);
7070
annotations.Remove(SqlServerAnnotationNames.Sparse);
71+
annotations.Remove(SqlServerAnnotationNames.IsHidden);
7172

7273
if (!annotations.ContainsKey(SqlServerAnnotationNames.ValueGenerationStrategy))
7374
{
@@ -88,6 +89,7 @@ public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGenerat
8889
annotations.Remove(SqlServerAnnotationNames.Sparse);
8990
annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodStartColumn);
9091
annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodEndColumn);
92+
annotations.Remove(SqlServerAnnotationNames.IsHidden);
9193
}
9294

9395
base.Generate(column, parameters);
@@ -187,6 +189,8 @@ public override void Generate(ITable table, CSharpRuntimeAnnotationCodeGenerator
187189
annotations.Remove(SqlServerAnnotationNames.TemporalHistoryTableSchema);
188190
annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
189191
annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
192+
annotations.Remove(SqlServerAnnotationNames.TemporalPeriodStartHidden);
193+
annotations.Remove(SqlServerAnnotationNames.TemporalPeriodEndHidden);
190194
}
191195

192196
base.Generate(table, parameters);

src/EFCore.SqlServer/EFCore.SqlServer.baseline.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@
215215
{
216216
"Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder HasPrecision(int precision);"
217217
},
218+
{
219+
"Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.OwnedNavigationTemporalPeriodPropertyBuilder IsHidden(bool hidden = true);"
220+
},
218221
{
219222
"Member": "override string? ToString();"
220223
}
@@ -2339,6 +2342,9 @@
23392342
{
23402343
"Member": "static bool CanSetIdentityColumnSeed(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, long? seed, in Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier storeObject, bool fromDataAnnotation = false);"
23412344
},
2345+
{
2346+
"Member": "static bool CanSetIsHidden(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, bool? hidden, bool fromDataAnnotation = false);"
2347+
},
23422348
{
23432349
"Member": "static bool CanSetIsSparse(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder property, bool? sparse, bool fromDataAnnotation = false);"
23442350
},
@@ -2393,6 +2399,9 @@
23932399
{
23942400
"Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder? HasValueGenerationStrategy(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, Microsoft.EntityFrameworkCore.Metadata.SqlServerValueGenerationStrategy? valueGenerationStrategy, in Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier storeObject, bool fromDataAnnotation = false);"
23952401
},
2402+
{
2403+
"Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder? IsHidden(this Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionPropertyBuilder propertyBuilder, bool? hidden, bool fromDataAnnotation = false);"
2404+
},
23962405
{
23972406
"Member": "static Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder IsSparse(this Microsoft.EntityFrameworkCore.Metadata.Builders.PropertyBuilder propertyBuilder, bool sparse = true);"
23982407
},
@@ -2515,6 +2524,9 @@
25152524
{
25162525
"Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIdentitySeedConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionRelationalPropertyOverrides overrides);"
25172526
},
2527+
{
2528+
"Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsHiddenConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property);"
2529+
},
25182530
{
25192531
"Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetIsSparseConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property);"
25202532
},
@@ -2557,6 +2569,9 @@
25572569
{
25582570
"Member": "static bool IsCompatibleWithValueGeneration(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);"
25592571
},
2572+
{
2573+
"Member": "static bool IsHidden(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);"
2574+
},
25602575
{
25612576
"Member": "static bool? IsSparse(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);"
25622577
},
@@ -2611,6 +2626,12 @@
26112626
{
26122627
"Member": "static long? SetIdentitySeed(this Microsoft.EntityFrameworkCore.Metadata.IConventionRelationalPropertyOverrides overrides, long? seed, bool fromDataAnnotation = false);"
26132628
},
2629+
{
2630+
"Member": "static void SetIsHidden(this Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property, bool? hidden);"
2631+
},
2632+
{
2633+
"Member": "static bool? SetIsHidden(this Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property, bool? hidden, bool fromDataAnnotation = false);"
2634+
},
26142635
{
26152636
"Member": "static void SetIsSparse(this Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property, bool? sparse);"
26162637
},
@@ -3029,6 +3050,9 @@
30293050
{
30303051
"Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder HasPrecision(int precision);"
30313052
},
3053+
{
3054+
"Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Builders.TemporalPeriodPropertyBuilder IsHidden(bool hidden = true);"
3055+
},
30323056
{
30333057
"Member": "override string? ToString();"
30343058
}

src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,52 @@ public static bool CanSetIsSparse(
813813
bool fromDataAnnotation = false)
814814
=> property.CanSetAnnotation(SqlServerAnnotationNames.Sparse, sparse, fromDataAnnotation);
815815

816+
/// <summary>
817+
/// Configures whether the property's column is defined with the SQL Server <c>HIDDEN</c> flag, which excludes the
818+
/// column from <c>SELECT *</c> results. This applies to generated columns such as temporal table period columns.
819+
/// </summary>
820+
/// <remarks>
821+
/// See <see href="https://aka.ms/efcore-docs-temporal">Using SQL Server temporal tables with EF Core</see>
822+
/// for more information and examples.
823+
/// </remarks>
824+
/// <param name="propertyBuilder">The builder for the property being configured.</param>
825+
/// <param name="hidden">A value indicating whether the property's column is hidden.</param>
826+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
827+
/// <returns>The same builder instance if the configuration was applied, <see langword="null" /> otherwise.</returns>
828+
public static IConventionPropertyBuilder? IsHidden(
829+
this IConventionPropertyBuilder propertyBuilder,
830+
bool? hidden,
831+
bool fromDataAnnotation = false)
832+
{
833+
if (propertyBuilder.CanSetIsHidden(hidden, fromDataAnnotation))
834+
{
835+
propertyBuilder.Metadata.SetIsHidden(hidden, fromDataAnnotation);
836+
837+
return propertyBuilder;
838+
}
839+
840+
return null;
841+
}
842+
843+
/// <summary>
844+
/// Returns a value indicating whether the property's column can be configured with the SQL Server <c>HIDDEN</c> flag.
845+
/// </summary>
846+
/// <remarks>
847+
/// See <see href="https://aka.ms/efcore-docs-temporal">Using SQL Server temporal tables with EF Core</see>
848+
/// for more information and examples.
849+
/// </remarks>
850+
/// <param name="propertyBuilder">The builder for the property being configured.</param>
851+
/// <param name="hidden">A value indicating whether the property's column is hidden.</param>
852+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
853+
/// <returns>
854+
/// <see langword="true" /> if the property's column can be configured with the <c>HIDDEN</c> flag.
855+
/// </returns>
856+
public static bool CanSetIsHidden(
857+
this IConventionPropertyBuilder propertyBuilder,
858+
bool? hidden,
859+
bool fromDataAnnotation = false)
860+
=> propertyBuilder.CanSetAnnotation(SqlServerAnnotationNames.IsHidden, hidden, fromDataAnnotation);
861+
816862
/// <summary>
817863
/// Configures the default value for the column that the property maps to when targeting SQL Server.
818864
/// </summary>

src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,4 +1050,55 @@ public static void SetIsSparse(this IMutableProperty property, bool? sparse)
10501050
/// <returns>The <see cref="ConfigurationSource" /> for whether the property's column is sparse.</returns>
10511051
public static ConfigurationSource? GetIsSparseConfigurationSource(this IConventionProperty property)
10521052
=> property.FindAnnotation(SqlServerAnnotationNames.Sparse)?.GetConfigurationSource();
1053+
1054+
/// <summary>
1055+
/// Returns a value indicating whether the property's column is defined with the SQL Server <c>HIDDEN</c> flag,
1056+
/// which excludes the column from <c>SELECT *</c> results.
1057+
/// </summary>
1058+
/// <remarks>
1059+
/// This applies to columns defined with <c>GENERATED ALWAYS AS</c>, including SQL Server temporal table
1060+
/// period columns. The default for temporal period columns is <see langword="true" />; for other columns
1061+
/// this annotation has no effect unless the column is generated.
1062+
/// </remarks>
1063+
/// <param name="property">The property.</param>
1064+
/// <returns>
1065+
/// <see langword="true" /> if the property's column is hidden. Defaults to <see langword="true" /> when not
1066+
/// explicitly configured, since temporal period columns are hidden by default.
1067+
/// </returns>
1068+
public static bool IsHidden(this IReadOnlyProperty property)
1069+
=> (property is RuntimeProperty)
1070+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
1071+
: (bool?)property[SqlServerAnnotationNames.IsHidden] ?? true;
1072+
1073+
/// <summary>
1074+
/// Sets a value indicating whether the property's column is defined with the SQL Server <c>HIDDEN</c> flag.
1075+
/// </summary>
1076+
/// <param name="property">The property.</param>
1077+
/// <param name="hidden">The value to set; <see langword="null" /> to remove the explicit configuration.</param>
1078+
public static void SetIsHidden(this IMutableProperty property, bool? hidden)
1079+
=> property.SetOrRemoveAnnotation(SqlServerAnnotationNames.IsHidden, hidden);
1080+
1081+
/// <summary>
1082+
/// Sets a value indicating whether the property's column is defined with the SQL Server <c>HIDDEN</c> flag.
1083+
/// </summary>
1084+
/// <param name="property">The property.</param>
1085+
/// <param name="hidden">The value to set; <see langword="null" /> to remove the explicit configuration.</param>
1086+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
1087+
/// <returns>The configured value.</returns>
1088+
public static bool? SetIsHidden(
1089+
this IConventionProperty property,
1090+
bool? hidden,
1091+
bool fromDataAnnotation = false)
1092+
=> (bool?)property.SetOrRemoveAnnotation(
1093+
SqlServerAnnotationNames.IsHidden,
1094+
hidden,
1095+
fromDataAnnotation)?.Value;
1096+
1097+
/// <summary>
1098+
/// Returns the <see cref="ConfigurationSource" /> for whether the property's column is hidden.
1099+
/// </summary>
1100+
/// <param name="property">The property.</param>
1101+
/// <returns>The <see cref="ConfigurationSource" /> for whether the property's column is hidden.</returns>
1102+
public static ConfigurationSource? GetIsHiddenConfigurationSource(this IConventionProperty property)
1103+
=> property.FindAnnotation(SqlServerAnnotationNames.IsHidden)?.GetConfigurationSource();
10531104
}

src/EFCore.SqlServer/Metadata/Builders/OwnedNavigationTemporalPeriodPropertyBuilder.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,29 @@ public virtual OwnedNavigationTemporalPeriodPropertyBuilder HasPrecision(int pre
5656
return this;
5757
}
5858

59+
/// <summary>
60+
/// Configures whether the period column is defined with the SQL Server <c>HIDDEN</c> flag,
61+
/// which excludes it from <c>SELECT *</c> results.
62+
/// </summary>
63+
/// <remarks>
64+
/// <para>
65+
/// The default is <see langword="true" /> for period columns, matching the behavior of EF Core releases
66+
/// prior to this option being introduced. Set to <see langword="false" /> to make the column visible.
67+
/// </para>
68+
/// <para>
69+
/// See <see href="https://aka.ms/efcore-docs-temporal">Using SQL Server temporal tables with EF Core</see>
70+
/// for more information.
71+
/// </para>
72+
/// </remarks>
73+
/// <param name="hidden">A value indicating whether the column should be hidden.</param>
74+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
75+
public virtual OwnedNavigationTemporalPeriodPropertyBuilder IsHidden(bool hidden = true)
76+
{
77+
((IMutableProperty)_propertyBuilder.Metadata).SetIsHidden(hidden);
78+
79+
return this;
80+
}
81+
5982
#region Hidden System.Object members
6083

6184
/// <summary>

src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ public virtual TemporalPeriodPropertyBuilder HasPrecision(int precision)
5757
return this;
5858
}
5959

60+
/// <summary>
61+
/// Configures whether the period column is defined with the SQL Server <c>HIDDEN</c> flag,
62+
/// which excludes it from <c>SELECT *</c> results.
63+
/// </summary>
64+
/// <remarks>
65+
/// <para>
66+
/// The default is <see langword="true" /> for period columns, matching the behavior of EF Core releases
67+
/// prior to this option being introduced. Set to <see langword="false" /> to make the column visible.
68+
/// </para>
69+
/// <para>
70+
/// See <see href="https://aka.ms/efcore-docs-temporal">Using SQL Server temporal tables with EF Core</see>
71+
/// for more information.
72+
/// </para>
73+
/// </remarks>
74+
/// <param name="hidden">A value indicating whether the column should be hidden.</param>
75+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
76+
public virtual TemporalPeriodPropertyBuilder IsHidden(bool hidden = true)
77+
{
78+
((IMutableProperty)_propertyBuilder.Metadata).SetIsHidden(hidden);
79+
80+
return this;
81+
}
82+
6083
PropertyBuilder IInfrastructure<PropertyBuilder>.Instance
6184
=> _propertyBuilder;
6285

0 commit comments

Comments
 (0)