Skip to content

Commit 9aba6fb

Browse files
committed
Fix to #11502 - Allow to specify constraint name for default values
Adding model builder API to allow specifying explicit constraint name when adding default value (sql) to a column. Also added switch on the model level (opt-in) to switch from system-generated constraints to generating them automatically by name and storing in the model. This is opt-in because we don't want to cause unnecessary migrations. Added de-duplication logic in the finalize model step, in case our generated names happen to clash with ones specified explicitly by user. Also split temporal tables migration tests for SqlServer into a separate file - there were way to many tests in MigrationsSqlServerTest Fixes #11502
1 parent 667c569 commit 9aba6fb

18 files changed

+10362
-8751
lines changed

src/EFCore.Design/Design/Internal/CSharpHelper.cs

+6
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,12 @@ public virtual string UnknownLiteral(object? value)
10631063
return "null";
10641064
}
10651065

1066+
if (value is DBNull)
1067+
{
1068+
// ???
1069+
return "null";
1070+
}
1071+
10661072
var literalType = value.GetType();
10671073

10681074
if (LiteralFuncs.TryGetValue(literalType.UnwrapNullableType(), out var literalFunc))

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

+45
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,53 @@ public override IReadOnlyList<MethodCallCodeFragment> GenerateFluentApiCalls(
201201
IProperty property,
202202
IDictionary<string, IAnnotation> annotations)
203203
{
204+
var defaultConstraintNameAnnotation = default(IAnnotation);
205+
var defaultValueAnnotation = default(IAnnotation);
206+
var defaultValueSqlAnnotation = default(IAnnotation);
207+
208+
// named default constraint must be handled on the provider level, so removing the annotations before calling base
209+
if (annotations.TryGetValue(SqlServerAnnotationNames.DefaultConstraintName, out defaultConstraintNameAnnotation))
210+
{
211+
if (defaultConstraintNameAnnotation.Value as string != string.Empty)
212+
{
213+
if (annotations.TryGetValue(RelationalAnnotationNames.DefaultValue, out defaultValueAnnotation))
214+
{
215+
annotations.Remove(RelationalAnnotationNames.DefaultValue);
216+
}
217+
else
218+
{
219+
var defaultValueSqlAnnotationExists = annotations.TryGetValue(RelationalAnnotationNames.DefaultValueSql, out defaultValueSqlAnnotation);
220+
Check.DebugAssert(defaultValueSqlAnnotationExists, "If default constaint name was set, one of DefaultValue or DefaultValueSql must also be set.");
221+
annotations.Remove(RelationalAnnotationNames.DefaultValueSql);
222+
}
223+
}
224+
225+
annotations.Remove(SqlServerAnnotationNames.DefaultConstraintName);
226+
}
227+
204228
var fragments = new List<MethodCallCodeFragment>(base.GenerateFluentApiCalls(property, annotations));
205229

230+
if (defaultConstraintNameAnnotation != null && defaultConstraintNameAnnotation.Value as string != string.Empty)
231+
{
232+
if (defaultValueAnnotation != null)
233+
{
234+
fragments.Add(
235+
new MethodCallCodeFragment(
236+
nameof(SqlServerPropertyBuilderExtensions.HasDefaultValue),
237+
defaultValueAnnotation.Value,
238+
defaultConstraintNameAnnotation.Value));
239+
}
240+
else
241+
{
242+
Check.NotNull(defaultValueSqlAnnotation, "Both DefaultValue and DefaultValueSql annotations are null.");
243+
fragments.Add(
244+
new MethodCallCodeFragment(
245+
nameof(SqlServerPropertyBuilderExtensions.HasDefaultValueSql),
246+
defaultValueSqlAnnotation.Value,
247+
defaultConstraintNameAnnotation.Value));
248+
}
249+
}
250+
206251
var isPrimitiveCollection = property.IsPrimitiveCollection;
207252

208253
if (GenerateValueGenerationStrategy(annotations, property.DeclaringType.Model, onModel: false, complexType: property.DeclaringType is IComplexType) is MethodCallCodeFragment

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

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public override void Generate(IColumn column, CSharpRuntimeAnnotationCodeGenerat
8686
annotations.Remove(SqlServerAnnotationNames.Sparse);
8787
annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodStartColumn);
8888
annotations.Remove(SqlServerAnnotationNames.TemporalIsPeriodEndColumn);
89+
annotations.Remove(SqlServerAnnotationNames.DefaultConstraintName);
8990
}
9091

9192
base.Generate(column, parameters);

src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs

+52
Original file line numberDiff line numberDiff line change
@@ -651,4 +651,56 @@ public static bool CanSetPerformanceLevelSql(
651651
string? performanceLevel,
652652
bool fromDataAnnotation = false)
653653
=> modelBuilder.CanSetAnnotation(SqlServerAnnotationNames.PerformanceLevelSql, performanceLevel, fromDataAnnotation);
654+
655+
/// <summary>
656+
/// TODO
657+
/// </summary>
658+
public static ModelBuilder UseNamedDefaultConstraints(this ModelBuilder modelBuilder, bool value = true)
659+
{
660+
Check.NotNull(value, nameof(value));
661+
662+
modelBuilder.Model.UseNamedDefaultConstraints(value);
663+
664+
return modelBuilder;
665+
}
666+
667+
/// <summary>
668+
/// TODO
669+
/// </summary>
670+
public static IConventionModelBuilder? UseNamedDefaultConstraints(
671+
this IConventionModelBuilder modelBuilder,
672+
bool value,
673+
bool fromDataAnnotation = false)
674+
{
675+
if (modelBuilder.CanUseNamedDefaultConstraints(value, fromDataAnnotation))
676+
{
677+
modelBuilder.Metadata.UseNamedDefaultConstraints(value, fromDataAnnotation);
678+
return modelBuilder;
679+
}
680+
681+
return null;
682+
}
683+
684+
/// <summary>
685+
/// TODO
686+
/// </summary>
687+
public static bool CanUseNamedDefaultConstraints(
688+
this IConventionModelBuilder modelBuilder,
689+
bool value,
690+
bool fromDataAnnotation = false)
691+
=> modelBuilder.CanSetAnnotation(SqlServerAnnotationNames.UseNamedDefaultConstraints, value, fromDataAnnotation);
692+
693+
694+
695+
696+
697+
698+
699+
700+
701+
702+
703+
704+
705+
654706
}

src/EFCore.SqlServer/Extensions/SqlServerModelExtensions.cs

+32
Original file line numberDiff line numberDiff line change
@@ -472,4 +472,36 @@ public static void SetPerformanceLevelSql(this IMutableModel model, string? valu
472472
/// <returns>The <see cref="ConfigurationSource" /> for the performance level of the database.</returns>
473473
public static ConfigurationSource? GetPerformanceLevelSqlConfigurationSource(this IConventionModel model)
474474
=> model.FindAnnotation(SqlServerAnnotationNames.PerformanceLevelSql)?.GetConfigurationSource();
475+
476+
/// <summary>
477+
/// TODO
478+
/// </summary>
479+
public static bool? AreNamedDefaultConstraintsUsed(this IReadOnlyModel model)
480+
=> (model is RuntimeModel)
481+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
482+
: (bool?)model[SqlServerAnnotationNames.UseNamedDefaultConstraints];
483+
484+
/// <summary>
485+
/// TODO
486+
/// </summary>
487+
public static void UseNamedDefaultConstraints(this IMutableModel model, bool value)
488+
=> model.SetOrRemoveAnnotation(SqlServerAnnotationNames.UseNamedDefaultConstraints, value);
489+
490+
/// <summary>
491+
/// TODO
492+
/// </summary>
493+
public static bool? UseNamedDefaultConstraints(
494+
this IConventionModel model,
495+
bool value,
496+
bool fromDataAnnotation = false)
497+
=> (bool?)model.SetOrRemoveAnnotation(
498+
SqlServerAnnotationNames.UseNamedDefaultConstraints,
499+
value,
500+
fromDataAnnotation)?.Value;
501+
502+
/// <summary>
503+
/// TODO
504+
/// </summary>
505+
public static ConfigurationSource? UseNamedDefaultConstraintsConfigurationSource(this IConventionModel model)
506+
=> model.FindAnnotation(SqlServerAnnotationNames.UseNamedDefaultConstraints)?.GetConfigurationSource();
475507
}

src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs

+116
Original file line numberDiff line numberDiff line change
@@ -812,4 +812,120 @@ public static bool CanSetIsSparse(
812812
bool? sparse,
813813
bool fromDataAnnotation = false)
814814
=> property.CanSetAnnotation(SqlServerAnnotationNames.Sparse, sparse, fromDataAnnotation);
815+
816+
/// <summary>
817+
/// TODO
818+
/// </summary>
819+
public static PropertyBuilder HasDefaultValue(
820+
this PropertyBuilder propertyBuilder,
821+
object? value,
822+
string defaultConstraintName)
823+
{
824+
propertyBuilder.Metadata.SetDefaultValue(value);
825+
propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
826+
827+
return propertyBuilder;
828+
}
829+
830+
/// <summary>
831+
/// TODO
832+
/// </summary>
833+
public static PropertyBuilder<TProperty> HasDefaultValue<TProperty>(
834+
this PropertyBuilder<TProperty> propertyBuilder,
835+
object? value,
836+
string defaultConstraintName)
837+
=> (PropertyBuilder<TProperty>)HasDefaultValue((PropertyBuilder)propertyBuilder, value, defaultConstraintName);
838+
839+
/// <summary>
840+
/// TODO
841+
/// </summary>
842+
public static IConventionPropertyBuilder? HasDefaultValue(
843+
this IConventionPropertyBuilder propertyBuilder,
844+
object? value,
845+
string defaultConstraintName,
846+
bool fromDataAnnotation = false)
847+
{
848+
if (!propertyBuilder.CanSetDefaultValue(value, defaultConstraintName, fromDataAnnotation))
849+
{
850+
return null;
851+
}
852+
853+
propertyBuilder.Metadata.SetDefaultValue(value, fromDataAnnotation);
854+
return propertyBuilder;
855+
}
856+
857+
/// <summary>
858+
/// TODO
859+
/// </summary>
860+
public static bool CanSetDefaultValue(
861+
this IConventionPropertyBuilder propertyBuilder,
862+
object? value,
863+
string defaultConstraintName,
864+
bool fromDataAnnotation = false)
865+
=> propertyBuilder.CanSetAnnotation(
866+
RelationalAnnotationNames.DefaultValue,
867+
value,
868+
fromDataAnnotation)
869+
&& propertyBuilder.CanSetAnnotation(
870+
SqlServerAnnotationNames.DefaultConstraintName,
871+
defaultConstraintName,
872+
fromDataAnnotation);
873+
874+
/// <summary>
875+
/// TODO
876+
/// </summary>
877+
public static PropertyBuilder HasDefaultValueSql(
878+
this PropertyBuilder propertyBuilder,
879+
string? sql,
880+
string defaultConstraintName)
881+
{
882+
propertyBuilder.Metadata.SetDefaultValueSql(sql);
883+
propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
884+
885+
return propertyBuilder;
886+
}
887+
888+
/// <summary>
889+
/// TODO
890+
/// </summary>
891+
public static PropertyBuilder<TProperty> HasDefaultValueSql<TProperty>(
892+
this PropertyBuilder<TProperty> propertyBuilder,
893+
string? sql,
894+
string defaultConstraintName)
895+
=> (PropertyBuilder<TProperty>)HasDefaultValueSql((PropertyBuilder)propertyBuilder, sql, defaultConstraintName);
896+
897+
/// <summary>
898+
/// TODO
899+
/// </summary>
900+
public static IConventionPropertyBuilder? HasDefaultValueSql(
901+
this IConventionPropertyBuilder propertyBuilder,
902+
string? sql,
903+
string defaultConstraintName,
904+
bool fromDataAnnotation = false)
905+
{
906+
if (!propertyBuilder.CanSetDefaultValueSql(sql, defaultConstraintName, fromDataAnnotation))
907+
{
908+
return null;
909+
}
910+
911+
propertyBuilder.Metadata.SetDefaultValueSql(sql, fromDataAnnotation);
912+
return propertyBuilder;
913+
}
914+
915+
/// <summary>
916+
/// TODO
917+
/// </summary>
918+
public static bool CanSetDefaultValueSql(
919+
this IConventionPropertyBuilder propertyBuilder,
920+
string? sql,
921+
string defaultConstraintName,
922+
bool fromDataAnnotation = false)
923+
=> propertyBuilder.CanSetAnnotation(
924+
RelationalAnnotationNames.DefaultValueSql,
925+
sql,
926+
fromDataAnnotation)
927+
&& propertyBuilder.CanSetAnnotation(
928+
SqlServerAnnotationNames.DefaultConstraintName,
929+
defaultConstraintName,
930+
fromDataAnnotation);
815931
}

src/EFCore.SqlServer/Extensions/SqlServerPropertyExtensions.cs

+46
Original file line numberDiff line numberDiff line change
@@ -1054,4 +1054,50 @@ public static void SetIsSparse(this IMutableProperty property, bool? sparse)
10541054
/// <returns>The <see cref="ConfigurationSource" /> for whether the property's column is sparse.</returns>
10551055
public static ConfigurationSource? GetIsSparseConfigurationSource(this IConventionProperty property)
10561056
=> property.FindAnnotation(SqlServerAnnotationNames.Sparse)?.GetConfigurationSource();
1057+
1058+
/// <summary>
1059+
/// TODO
1060+
/// </summary>
1061+
public static string? DefaultConstraintName(this IReadOnlyProperty property)
1062+
=> (property is RuntimeProperty)
1063+
? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
1064+
: (string?)property[SqlServerAnnotationNames.DefaultConstraintName];
1065+
1066+
/// <summary>
1067+
/// TODO
1068+
/// </summary>
1069+
public static string DefaultDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
1070+
{
1071+
var candidate = $"DF_{storeObject.Name}_{property.GetColumnName(storeObject)}";
1072+
1073+
return candidate.Length > 120 ? candidate[..120] : candidate;
1074+
}
1075+
1076+
/// <summary>
1077+
/// TODO
1078+
/// </summary>
1079+
public static void SetDefaultConstraintName(this IMutableProperty property, string? defaultConstraintName)
1080+
=> property.SetAnnotation(SqlServerAnnotationNames.DefaultConstraintName, defaultConstraintName);
1081+
1082+
/// <summary>
1083+
/// TODO
1084+
/// </summary>
1085+
public static string? SetDefaultConstraintName(
1086+
this IConventionProperty property,
1087+
string? defaultConstraintName,
1088+
bool fromDataAnnotation = false)
1089+
{
1090+
property.SetAnnotation(
1091+
SqlServerAnnotationNames.DefaultConstraintName,
1092+
defaultConstraintName,
1093+
fromDataAnnotation);
1094+
1095+
return defaultConstraintName;
1096+
}
1097+
1098+
/// <summary>
1099+
/// TODO
1100+
/// </summary>
1101+
public static ConfigurationSource? GetDefaultConstraintNameConfigurationSource(this IConventionProperty property)
1102+
=> property.FindAnnotation(SqlServerAnnotationNames.DefaultConstraintName)?.GetConfigurationSource();
10571103
}

src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
// ReSharper disable once CheckNamespace
55

6+
using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Conventions;
7+
68
namespace Microsoft.EntityFrameworkCore.Metadata.Conventions;
79

810
/// <summary>
@@ -72,6 +74,12 @@ public override ConventionSet CreateConventionSet()
7274
conventionSet.SkipNavigationForeignKeyChangedConventions.Add(sqlServerTemporalConvention);
7375
conventionSet.ModelFinalizingConventions.Add(sqlServerTemporalConvention);
7476

77+
var sqlServerDefaultValueConvention = new SqlServerDefaultValueConvention(Dependencies, RelationalDependencies);
78+
ConventionSet.AddAfter(
79+
conventionSet.ModelFinalizingConventions,
80+
sqlServerDefaultValueConvention,
81+
typeof(SqlServerSharedTableConvention));
82+
7583
return conventionSet;
7684
}
7785

0 commit comments

Comments
 (0)