diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs
index b49811c5e21..e772d17ff9f 100644
--- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs
+++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs
@@ -751,7 +751,15 @@ private void Create(
/// The column to which the annotations are applied.
/// Additional parameters used during code generation.
public virtual void Generate(IColumn column, CSharpRuntimeAnnotationCodeGeneratorParameters parameters)
- => GenerateSimpleAnnotations(parameters);
+ {
+ if (!parameters.IsRuntime)
+ {
+ var annotations = parameters.Annotations;
+ annotations.Remove(RelationalAnnotationNames.DefaultConstraintName);
+ }
+
+ GenerateSimpleAnnotations(parameters);
+ }
private void Create(
IViewColumn column,
diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs
index d36f9b026d7..b23372623b8 100644
--- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs
@@ -530,4 +530,48 @@ public static void SetCollation(this IMutableModel model, string? value)
=> model.FindAnnotation(RelationalAnnotationNames.Collation)?.GetConfigurationSource();
#endregion Collation
+
+ #region UseNamedDefaultConstraints
+
+ ///
+ /// Returns the value indicating whether named default constraints should be used.
+ ///
+ /// The model to get the value for.
+ public static bool AreNamedDefaultConstraintsUsed(this IReadOnlyModel model)
+ => (model is RuntimeModel)
+ ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
+ : (bool?)model[RelationalAnnotationNames.UseNamedDefaultConstraints] ?? false;
+
+ ///
+ /// Sets the value indicating whether named default constraints should be used.
+ ///
+ /// The model to get the value for.
+ /// The value to set.
+ public static void UseNamedDefaultConstraints(this IMutableModel model, bool value)
+ => model.SetOrRemoveAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints, value);
+
+ ///
+ /// Sets the value indicating whether named default constraints should be used.
+ ///
+ /// The model to get the value for.
+ /// The value to set.
+ /// Indicates whether the configuration was specified using a data annotation.
+ public static bool? UseNamedDefaultConstraints(
+ this IConventionModel model,
+ bool? value,
+ bool fromDataAnnotation = false)
+ => (bool?)model.SetOrRemoveAnnotation(
+ RelationalAnnotationNames.UseNamedDefaultConstraints,
+ value,
+ fromDataAnnotation)?.Value;
+
+ ///
+ /// Returns the configuration source for the named default constraints setting.
+ ///
+ /// The model to find configuration source for.
+ /// The configuration source for the named default constraints setting.
+ public static ConfigurationSource? UseNamedDefaultConstraintsConfigurationSource(this IConventionModel model)
+ => model.FindAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints)?.GetConfigurationSource();
+
+ #endregion
}
diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
index cd18f05e611..6c703b725bd 100644
--- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
+++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs
@@ -2086,4 +2086,82 @@ public static void SetJsonPropertyName(this IMutableProperty property, string? n
/// The for the JSON property name for a given entity property.
public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionProperty property)
=> property.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource();
+
+ ///
+ /// Gets the default constraint name.
+ ///
+ /// The property.
+ public static string? GetDefaultConstraintName(this IReadOnlyProperty property)
+ => property is RuntimeProperty
+ ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
+ : (string?)property[RelationalAnnotationNames.DefaultConstraintName]
+ ?? (ShouldHaveDefaultConstraintName(property)
+ && StoreObjectIdentifier.Create(property.DeclaringType, StoreObjectType.Table) is StoreObjectIdentifier table
+ ? property.GenerateDefaultConstraintName(table)
+ : null);
+
+ ///
+ /// Gets the default constraint name.
+ ///
+ /// The property.
+ /// The store object identifier to generate the name for.
+ public static string? GetDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
+ => property is RuntimeProperty
+ ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData)
+ : (string?)property[RelationalAnnotationNames.DefaultConstraintName]
+ ?? (ShouldHaveDefaultConstraintName(property)
+ ? property.GenerateDefaultConstraintName(storeObject)
+ : null);
+
+ private static bool ShouldHaveDefaultConstraintName(IReadOnlyProperty property)
+ => property.DeclaringType.Model.AreNamedDefaultConstraintsUsed()
+ && (property[RelationalAnnotationNames.DefaultValue] is not null
+ || property[RelationalAnnotationNames.DefaultValueSql] is not null);
+
+ ///
+ /// Generates the default constraint name based on the table and column name.
+ ///
+ /// The property.
+ /// The store object identifier to generate the name for.
+ public static string GenerateDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject)
+ {
+ var candidate = $"DF_{storeObject.Name}_{property.GetColumnName(storeObject)}";
+
+ return Uniquifier.Truncate(candidate, property.DeclaringType.Model.GetMaxIdentifierLength());
+ }
+
+ ///
+ /// Sets the default constraint name.
+ ///
+ /// The property.
+ /// The name to be used.
+ public static void SetDefaultConstraintName(this IMutableProperty property, string? defaultConstraintName)
+ => property.SetAnnotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName);
+
+ ///
+ /// Sets the default constraint name.
+ ///
+ /// The property.
+ /// The name to be used.
+ /// Indicates whether the configuration was specified using a data annotation.
+ public static string? SetDefaultConstraintName(
+ this IConventionProperty property,
+ string? defaultConstraintName,
+ bool fromDataAnnotation = false)
+ {
+ property.SetAnnotation(
+ RelationalAnnotationNames.DefaultConstraintName,
+ defaultConstraintName,
+ fromDataAnnotation);
+
+ return defaultConstraintName;
+ }
+
+ ///
+ /// Returns the configuration source for the default constraint name.
+ ///
+ /// The property.
+ /// The configuration source for the default constraint name.
+ public static ConfigurationSource? GetDefaultConstraintNameConfigurationSource(this IConventionProperty property)
+ => property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName)?.GetConfigurationSource();
}
diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
index be7355e1a53..85663dd18d4 100644
--- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
+++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
@@ -51,6 +51,7 @@ public virtual void ProcessModelFinalizing(
var foreignKeys = new Dictionary();
var indexes = new Dictionary();
var checkConstraints = new Dictionary<(string, string?), (IConventionCheckConstraint, StoreObjectIdentifier)>();
+ var defaultConstraints = new Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)>();
var triggers = new Dictionary();
foreach (var ((tableName, schema), conventionEntityTypes) in tables)
{
@@ -76,6 +77,11 @@ public virtual void ProcessModelFinalizing(
checkConstraints.Clear();
}
+ if (!DefaultConstraintsUniqueAcrossTables)
+ {
+ defaultConstraints.Clear();
+ }
+
if (!TriggersUniqueAcrossTables)
{
triggers.Clear();
@@ -89,6 +95,7 @@ public virtual void ProcessModelFinalizing(
UniquifyForeignKeyNames(entityType, foreignKeys, storeObject, maxLength);
UniquifyIndexNames(entityType, indexes, storeObject, maxLength);
UniquifyCheckConstraintNames(entityType, checkConstraints, storeObject, maxLength);
+ UniquifyDefaultConstraintNames(entityType, defaultConstraints, storeObject, maxLength);
UniquifyTriggerNames(entityType, triggers, storeObject, maxLength);
}
}
@@ -124,14 +131,19 @@ protected virtual bool CheckConstraintsUniqueAcrossTables
protected virtual bool TriggersUniqueAcrossTables
=> true;
+ ///
+ /// Gets a value indicating whether default constraint names should be unique across tables.
+ ///
+ protected virtual bool DefaultConstraintsUniqueAcrossTables
+ => false;
+
private static void TryUniquifyTableNames(
IConventionModel model,
Dictionary<(string Name, string? Schema), List> tables,
int maxLength)
{
Dictionary<(string Name, string? Schema), Dictionary<(string Name, string? Schema), List>>?
- clashingTables
- = null;
+ clashingTables = null;
foreach (var entityType in model.GetEntityTypes())
{
var tableName = entityType.GetTableName();
@@ -646,6 +658,107 @@ protected virtual bool AreCompatible(
return null;
}
+ private void UniquifyDefaultConstraintNames(
+ IConventionEntityType entityType,
+ Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)> defaultConstraints,
+ in StoreObjectIdentifier storeObject,
+ int maxLength)
+ {
+ foreach (var property in entityType.GetProperties())
+ {
+ var constraintName = property.GetDefaultConstraintName(storeObject);
+ if (constraintName == null)
+ {
+ continue;
+ }
+
+ var columnName = property.GetColumnName(storeObject);
+ if (columnName == null)
+ {
+ continue;
+ }
+
+ if (!defaultConstraints.TryGetValue((constraintName, storeObject.Schema), out var otherPropertyPair))
+ {
+ defaultConstraints[(constraintName, storeObject.Schema)] = (property, storeObject);
+ continue;
+ }
+
+ var (otherProperty, otherStoreObject) = otherPropertyPair;
+ if (storeObject == otherStoreObject
+ && columnName == otherProperty.GetColumnName(storeObject)
+ && AreCompatibleDefaultConstraints(property, otherProperty, storeObject))
+ {
+ continue;
+ }
+
+ var newConstraintName = TryUniquifyDefaultConstraint(property, constraintName, storeObject.Schema, defaultConstraints, storeObject, maxLength);
+ if (newConstraintName != null)
+ {
+ defaultConstraints[(newConstraintName, storeObject.Schema)] = (property, storeObject);
+ continue;
+ }
+
+ var newOtherConstraintName = TryUniquifyDefaultConstraint(otherProperty, constraintName, storeObject.Schema, defaultConstraints, otherStoreObject, maxLength);
+ if (newOtherConstraintName != null)
+ {
+ defaultConstraints[(constraintName, storeObject.Schema)] = (property, storeObject);
+ defaultConstraints[(newOtherConstraintName, otherStoreObject.Schema)] = otherPropertyPair;
+ }
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether two default constraints with the same name are compatible.
+ ///
+ /// A property with a default constraint.
+ /// Another property with a default constraint.
+ /// The identifier of the store object.
+ /// if compatible
+ protected virtual bool AreCompatibleDefaultConstraints(
+ IReadOnlyProperty property,
+ IReadOnlyProperty duplicateProperty,
+ in StoreObjectIdentifier storeObject)
+ => property.GetDefaultValue(storeObject) == duplicateProperty.GetDefaultValue(storeObject)
+ && property.GetDefaultValueSql(storeObject) == duplicateProperty.GetDefaultValueSql(storeObject);
+
+ private static string? TryUniquifyDefaultConstraint(
+ IConventionProperty property,
+ string constraintName,
+ string? schema,
+ Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)> defaultConstraints,
+ in StoreObjectIdentifier storeObject,
+ int maxLength)
+ {
+ var mappedTables = property.GetMappedStoreObjects(StoreObjectType.Table);
+ if (mappedTables.Count() > 1)
+ {
+ // For TPC and some entity splitting scenarios we end up with multiple tables having to define the constraint.
+ // Since constraint name has to be unique, we can't keep the same name for all
+ // Disabling this scenario until we have better way to configure the constraint name
+ // see issue #27970
+ if (property.GetDefaultConstraintNameConfigurationSource() == null)
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(constraintName));
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc(constraintName));
+ }
+ }
+
+ if (property.Builder.CanSetAnnotation(RelationalAnnotationNames.DefaultConstraintName, null))
+ {
+ constraintName = Uniquifier.Uniquify(constraintName, defaultConstraints, n => (n, schema), maxLength);
+ property.Builder.HasAnnotation(RelationalAnnotationNames.DefaultConstraintName, constraintName);
+ return constraintName;
+ }
+
+ return null;
+ }
+
private void UniquifyTriggerNames(
IConventionEntityType entityType,
Dictionary triggers,
diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
index efbf0a4e205..97084eee01a 100644
--- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
+++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
@@ -52,6 +52,16 @@ public static class RelationalAnnotationNames
///
public const string DefaultValue = Prefix + "DefaultValue";
+ ///
+ /// The name for default constraint annotations.
+ ///
+ public const string DefaultConstraintName = Prefix + "DefaultConstraintName";
+
+ ///
+ /// The name for using named default constraints annotations.
+ ///
+ public const string UseNamedDefaultConstraints = Prefix + "UseNamedDefaultConstraints";
+
///
/// The name for table name annotations.
///
@@ -360,6 +370,8 @@ public static class RelationalAnnotationNames
ComputedColumnSql,
IsStored,
DefaultValue,
+ DefaultConstraintName,
+ UseNamedDefaultConstraints,
TableName,
Schema,
ViewName,
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index 0eee71da13f..0aa88b18eb3 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -827,6 +827,14 @@ public static string ExecuteUpdateSubqueryNotSupportedOverComplexTypes(object? c
GetString("ExecuteUpdateSubqueryNotSupportedOverComplexTypes", nameof(complexType)),
complexType);
+ ///
+ /// Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'.
+ ///
+ public static string ExplicitDefaultConstraintNamesNotSupportedForTpc(object? explicitDefaultConstraintName)
+ => string.Format(
+ GetString("ExplicitDefaultConstraintNamesNotSupportedForTpc", nameof(explicitDefaultConstraintName)),
+ explicitDefaultConstraintName);
+
///
/// The required column '{column}' was not present in the results of a 'FromSql' operation.
///
@@ -857,6 +865,14 @@ public static string HasDataNotSupportedForEntitiesMappedToJson(object? entity)
GetString("HasDataNotSupportedForEntitiesMappedToJson", nameof(entity)),
entity);
+ ///
+ /// Named default constraints can't be used with TPC or entity splitting if they result in non-unique constraint name. Constraint name: '{constraintNameCandidate}'.
+ ///
+ public static string ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(object? constraintNameCandidate)
+ => string.Format(
+ GetString("ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash", nameof(constraintNameCandidate)),
+ constraintNameCandidate);
+
///
/// Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the comment '{comment}' does not match the comment '{otherComment}'.
///
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index c5604c77a73..54f1313fe43 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -427,6 +427,9 @@
ExecuteUpdate is being used over a LINQ operator which isn't natively supported by the database; this cannot be translated because complex type '{complexType}' is projected out. Rewrite your query to project out the containing entity type instead.
+
+ Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'.
+
The required column '{column}' was not present in the results of a 'FromSql' operation.
@@ -439,6 +442,9 @@
Can't use HasData for entity type '{entity}'. HasData is not supported for entities mapped to JSON.
+
+ Named default constraints can't be used with TPC or entity splitting if they result in non-unique constraint name. Constraint name: '{constraintNameCandidate}'.
+
Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the comment '{comment}' does not match the comment '{otherComment}'.
diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
index 01e6918c559..eb29b0c3b03 100644
--- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
+++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs
@@ -201,8 +201,53 @@ public override IReadOnlyList GenerateFluentApiCalls(
IProperty property,
IDictionary annotations)
{
+ var defaultConstraintNameAnnotation = default(IAnnotation);
+ var defaultValueAnnotation = default(IAnnotation);
+ var defaultValueSqlAnnotation = default(IAnnotation);
+
+ // named default constraint must be handled on the provider level - model builder methods live on provider rather than relational
+ // so removing the annotations before calling base
+ if (annotations.TryGetValue(RelationalAnnotationNames.DefaultConstraintName, out defaultConstraintNameAnnotation))
+ {
+ if (defaultConstraintNameAnnotation.Value as string != string.Empty)
+ {
+ if (annotations.TryGetValue(RelationalAnnotationNames.DefaultValue, out defaultValueAnnotation))
+ {
+ annotations.Remove(RelationalAnnotationNames.DefaultValue);
+ }
+ else
+ {
+ var defaultValueSqlAnnotationExists = annotations.TryGetValue(RelationalAnnotationNames.DefaultValueSql, out defaultValueSqlAnnotation);
+ annotations.Remove(RelationalAnnotationNames.DefaultValueSql);
+ }
+ }
+
+ annotations.Remove(RelationalAnnotationNames.DefaultConstraintName);
+ }
+
var fragments = new List(base.GenerateFluentApiCalls(property, annotations));
+ if (defaultConstraintNameAnnotation != null && defaultConstraintNameAnnotation.Value as string != string.Empty)
+ {
+ if (defaultValueAnnotation != null)
+ {
+ fragments.Add(
+ new MethodCallCodeFragment(
+ nameof(SqlServerPropertyBuilderExtensions.HasDefaultValue),
+ defaultValueAnnotation.Value,
+ defaultConstraintNameAnnotation.Value));
+ }
+ else
+ {
+ Check.DebugAssert(defaultValueSqlAnnotation != null, $"Default constraint name was set for {property.Name}, but DefaultValue and DefaultValueSql are both null.");
+ fragments.Add(
+ new MethodCallCodeFragment(
+ nameof(SqlServerPropertyBuilderExtensions.HasDefaultValueSql),
+ defaultValueSqlAnnotation.Value,
+ defaultConstraintNameAnnotation.Value));
+ }
+ }
+
var isPrimitiveCollection = property.IsPrimitiveCollection;
if (GenerateValueGenerationStrategy(annotations, property.DeclaringType.Model, onModel: false, complexType: property.DeclaringType is IComplexType) is MethodCallCodeFragment
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs
index 07675cb2d59..03efbf87538 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs
@@ -53,4 +53,86 @@ public static ComplexTypePrimitiveCollectionBuilder IsSparse (ComplexTypePrimitiveCollectionBuilder)IsSparse(
(ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, sparse);
+
+ ///
+ /// Configures the default value for the column that the property maps
+ /// to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue(
+ this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder,
+ object? value,
+ string defaultConstraintName)
+ {
+ primitiveCollectionBuilder.Metadata.SetDefaultValue(value);
+ primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return primitiveCollectionBuilder;
+ }
+
+ ///
+ /// Configures the default value for the column that the property maps
+ /// to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue(
+ this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder,
+ object? value,
+ string defaultConstraintName)
+ => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValue(
+ (ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, value, defaultConstraintName);
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql(
+ this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder,
+ string? sql,
+ string defaultConstraintName)
+ {
+ Check.NullButNotEmpty(sql, nameof(sql));
+
+ primitiveCollectionBuilder.Metadata.SetDefaultValueSql(sql);
+ primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return primitiveCollectionBuilder;
+ }
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql(
+ this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder,
+ string? sql,
+ string defaultConstraintName)
+ => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValueSql(
+ (ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, sql, defaultConstraintName);
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs
index 2893f858f05..6aaa54826e4 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs
@@ -256,4 +256,84 @@ public static ComplexTypePropertyBuilder IsSparse(
this ComplexTypePropertyBuilder propertyBuilder,
bool sparse = true)
=> (ComplexTypePropertyBuilder)IsSparse((ComplexTypePropertyBuilder)propertyBuilder, sparse);
+
+ ///
+ /// Configures the default value for the column that the property maps
+ /// to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePropertyBuilder HasDefaultValue(
+ this ComplexTypePropertyBuilder propertyBuilder,
+ object? value,
+ string defaultConstraintName)
+ {
+ propertyBuilder.Metadata.SetDefaultValue(value);
+ propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return propertyBuilder;
+ }
+
+ ///
+ /// Configures the default value for the column that the property maps
+ /// to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePropertyBuilder HasDefaultValue(
+ this ComplexTypePropertyBuilder propertyBuilder,
+ object? value,
+ string defaultConstraintName)
+ => (ComplexTypePropertyBuilder)HasDefaultValue((ComplexTypePropertyBuilder)propertyBuilder, value, defaultConstraintName);
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePropertyBuilder HasDefaultValueSql(
+ this ComplexTypePropertyBuilder propertyBuilder,
+ string? sql,
+ string defaultConstraintName)
+ {
+ Check.NullButNotEmpty(sql, nameof(sql));
+
+ propertyBuilder.Metadata.SetDefaultValueSql(sql);
+ propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return propertyBuilder;
+ }
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ComplexTypePropertyBuilder HasDefaultValueSql(
+ this ComplexTypePropertyBuilder propertyBuilder,
+ string? sql,
+ string defaultConstraintName)
+ => (ComplexTypePropertyBuilder)HasDefaultValueSql((ComplexTypePropertyBuilder)propertyBuilder, sql, defaultConstraintName);
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs
index f615ffad5c3..4ab71074d5f 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs
@@ -651,4 +651,71 @@ public static bool CanSetPerformanceLevelSql(
string? performanceLevel,
bool fromDataAnnotation = false)
=> modelBuilder.CanSetAnnotation(SqlServerAnnotationNames.PerformanceLevelSql, performanceLevel, fromDataAnnotation);
+
+ ///
+ /// Configures the model to use named default constraints.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The model builder.
+ /// The value to use.
+ /// The same builder instance so that multiple calls can be chained.
+ public static ModelBuilder UseNamedDefaultConstraints(this ModelBuilder modelBuilder, bool value = true)
+ {
+ Check.NotNull(value, nameof(value));
+
+ modelBuilder.Model.UseNamedDefaultConstraints(value);
+
+ return modelBuilder;
+ }
+
+ ///
+ /// Configures the model to use named default constraints.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The model builder.
+ /// The value to use.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionModelBuilder? UseNamedDefaultConstraints(
+ this IConventionModelBuilder modelBuilder,
+ bool value,
+ bool fromDataAnnotation = false)
+ {
+ if (modelBuilder.CanUseNamedDefaultConstraints(value, fromDataAnnotation))
+ {
+ modelBuilder.Metadata.UseNamedDefaultConstraints(value, fromDataAnnotation);
+ return modelBuilder;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Returns a value indicating whether named default constraints should be used in the model.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The model builder.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The value to use.
+ /// if the given value can be set as the configuration for named default constraints setting.
+ public static bool CanUseNamedDefaultConstraints(
+ this IConventionModelBuilder modelBuilder,
+ bool value,
+ bool fromDataAnnotation = false)
+ => modelBuilder.CanSetAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints, value, fromDataAnnotation);
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs
index 87fa60c5132..cfeeadf2309 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs
@@ -50,4 +50,84 @@ public static PrimitiveCollectionBuilder IsSparse(
this PrimitiveCollectionBuilder primitiveCollectionBuilder,
bool sparse = true)
=> (PrimitiveCollectionBuilder)IsSparse((PrimitiveCollectionBuilder)primitiveCollectionBuilder, sparse);
+
+ ///
+ /// Configures the default value for the column that the property maps
+ /// to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PrimitiveCollectionBuilder HasDefaultValue(
+ this PrimitiveCollectionBuilder primitiveCollectionBuilder,
+ object? value,
+ string defaultConstraintName)
+ {
+ primitiveCollectionBuilder.Metadata.SetDefaultValue(value);
+ primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return primitiveCollectionBuilder;
+ }
+
+ ///
+ /// Configures the default value for the column that the property maps
+ /// to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PrimitiveCollectionBuilder HasDefaultValue(
+ this PrimitiveCollectionBuilder primitiveCollectionBuilder,
+ object? value,
+ string defaultConstraintName)
+ => (PrimitiveCollectionBuilder)HasDefaultValue((PrimitiveCollectionBuilder)primitiveCollectionBuilder, value, defaultConstraintName);
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PrimitiveCollectionBuilder HasDefaultValueSql(
+ this PrimitiveCollectionBuilder primitiveCollectionBuilder,
+ string? sql,
+ string defaultConstraintName)
+ {
+ Check.NullButNotEmpty(sql, nameof(sql));
+
+ primitiveCollectionBuilder.Metadata.SetDefaultValueSql(sql);
+ primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return primitiveCollectionBuilder;
+ }
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting a relational database.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PrimitiveCollectionBuilder HasDefaultValueSql(
+ this PrimitiveCollectionBuilder primitiveCollectionBuilder,
+ string? sql,
+ string defaultConstraintName)
+ => (PrimitiveCollectionBuilder)HasDefaultValueSql((PrimitiveCollectionBuilder)primitiveCollectionBuilder, sql, defaultConstraintName);
}
diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs
index 2f92f859692..478966c319a 100644
--- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs
+++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs
@@ -812,4 +812,207 @@ public static bool CanSetIsSparse(
bool? sparse,
bool fromDataAnnotation = false)
=> property.CanSetAnnotation(SqlServerAnnotationNames.Sparse, sparse, fromDataAnnotation);
+
+ ///
+ /// Configures the default value for the column that the property maps to when targeting SQL Server.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PropertyBuilder HasDefaultValue(
+ this PropertyBuilder propertyBuilder,
+ object? value,
+ string defaultConstraintName)
+ {
+ Check.NotEmpty(defaultConstraintName, nameof(defaultConstraintName));
+
+ propertyBuilder.Metadata.SetDefaultValue(value);
+ propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return propertyBuilder;
+ }
+
+ ///
+ /// Configures the default value for the column that the property maps to when targeting SQL Server.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PropertyBuilder HasDefaultValue(
+ this PropertyBuilder propertyBuilder,
+ object? value,
+ string defaultConstraintName)
+ => (PropertyBuilder)HasDefaultValue((PropertyBuilder)propertyBuilder, value, defaultConstraintName);
+
+ ///
+ /// Configures the default value for the column that the property maps to when targeting SQL Server.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// Indicates whether the configuration was specified using a data annotation.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionPropertyBuilder? HasDefaultValue(
+ this IConventionPropertyBuilder propertyBuilder,
+ object? value,
+ string defaultConstraintName,
+ bool fromDataAnnotation = false)
+ {
+ if (!propertyBuilder.CanSetDefaultValue(value, defaultConstraintName, fromDataAnnotation))
+ {
+ return null;
+ }
+
+ propertyBuilder.Metadata.SetDefaultValue(value, fromDataAnnotation);
+ propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName, fromDataAnnotation);
+ return propertyBuilder;
+ }
+
+ ///
+ /// Returns a value indicating whether the given value can be set as default for the column.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The default value of the column.
+ /// The default constraint name.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// if the given value can be set as default for the column.
+ public static bool CanSetDefaultValue(
+ this IConventionPropertyBuilder propertyBuilder,
+ object? value,
+ string defaultConstraintName,
+ bool fromDataAnnotation = false)
+ => propertyBuilder.CanSetAnnotation(
+ RelationalAnnotationNames.DefaultValue,
+ value,
+ fromDataAnnotation)
+ && propertyBuilder.CanSetAnnotation(
+ RelationalAnnotationNames.DefaultConstraintName,
+ defaultConstraintName,
+ fromDataAnnotation);
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting SQL Server.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PropertyBuilder HasDefaultValueSql(
+ this PropertyBuilder propertyBuilder,
+ string? sql,
+ string defaultConstraintName)
+ {
+ Check.NotEmpty(defaultConstraintName, nameof(defaultConstraintName));
+
+ propertyBuilder.Metadata.SetDefaultValueSql(sql);
+ propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName);
+
+ return propertyBuilder;
+ }
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting SQL Server.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The type of the property being configured.
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// The default constraint name.
+ /// The same builder instance so that multiple calls can be chained.
+ public static PropertyBuilder HasDefaultValueSql(
+ this PropertyBuilder propertyBuilder,
+ string? sql,
+ string defaultConstraintName)
+ => (PropertyBuilder)HasDefaultValueSql((PropertyBuilder)propertyBuilder, sql, defaultConstraintName);
+
+ ///
+ /// Configures the default value expression for the column that the property maps to when targeting SQL Server.
+ ///
+ ///
+ /// See Modeling entity types and relationships, and
+ /// Accessing SQL Server and Azure SQL databases with EF Core
+ /// for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The default constraint name.
+ ///
+ /// The same builder instance if the configuration was applied,
+ /// otherwise.
+ ///
+ public static IConventionPropertyBuilder? HasDefaultValueSql(
+ this IConventionPropertyBuilder propertyBuilder,
+ string? sql,
+ string defaultConstraintName,
+ bool fromDataAnnotation = false)
+ {
+ if (!propertyBuilder.CanSetDefaultValueSql(sql, defaultConstraintName, fromDataAnnotation))
+ {
+ return null;
+ }
+
+ propertyBuilder.Metadata.SetDefaultValueSql(sql, fromDataAnnotation);
+ propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName, fromDataAnnotation);
+ return propertyBuilder;
+ }
+
+ ///
+ /// Returns a value indicating whether the given default value expression can be set for the column.
+ ///
+ ///
+ /// See Database default values for more information and examples.
+ ///
+ /// The builder for the property being configured.
+ /// The SQL expression for the default value of the column.
+ /// Indicates whether the configuration was specified using a data annotation.
+ /// The default constraint name.
+ /// if the given default value expression can be set for the column.
+ public static bool CanSetDefaultValueSql(
+ this IConventionPropertyBuilder propertyBuilder,
+ string? sql,
+ string defaultConstraintName,
+ bool fromDataAnnotation = false)
+ => propertyBuilder.CanSetAnnotation(
+ RelationalAnnotationNames.DefaultValueSql,
+ sql,
+ fromDataAnnotation)
+ && propertyBuilder.CanSetAnnotation(
+ RelationalAnnotationNames.DefaultConstraintName,
+ defaultConstraintName,
+ fromDataAnnotation);
}
diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs
index d40b142c7e2..59b753466aa 100644
--- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs
+++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs
@@ -32,6 +32,10 @@ public SqlServerSharedTableConvention(
protected override bool IndexesUniqueAcrossTables
=> false;
+ ///
+ protected override bool DefaultConstraintsUniqueAcrossTables
+ => true;
+
///
protected override bool AreCompatible(IReadOnlyKey key, IReadOnlyKey duplicateKey, in StoreObjectIdentifier storeObject)
=> base.AreCompatible(key, duplicateKey, storeObject)
diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
index a6fecde4a4f..24fbda9033a 100644
--- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
+++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs
@@ -250,11 +250,36 @@ public override IEnumerable For(IColumn column, bool designTime)
}
// JSON columns have no property mappings so all annotations that rely on property mappings should be skipped for them
- if (column is not JsonColumn
- && column.PropertyMappings.FirstOrDefault()?.Property.IsSparse() is bool isSparse)
+ if (column is not JsonColumn)
{
- // Model validation ensures that these facets are the same on all mapped properties
- yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse);
+ if (column.PropertyMappings.FirstOrDefault()?.Property.IsSparse() is bool isSparse)
+ {
+ // Model validation ensures that these facets are the same on all mapped properties
+ yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse);
+ }
+
+ var mappedProperty = column.PropertyMappings.FirstOrDefault()?.Property;
+ if (mappedProperty != null)
+ {
+ if (mappedProperty.GetDefaultConstraintName(table) is string defaultConstraintName)
+ {
+ // named constraint stored as annotation are either explicitly configured by user
+ // or generated by EF because of naming duplicates (SqlServerDefaultValueConvention)
+ yield return new Annotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName);
+ }
+ else if (mappedProperty.DeclaringType.Model.AreNamedDefaultConstraintsUsed() == true
+ && (mappedProperty.FindAnnotation(RelationalAnnotationNames.DefaultValue) != null
+ || mappedProperty.FindAnnotation(RelationalAnnotationNames.DefaultValueSql) != null))
+ {
+ // named default constraints opt-in + default value (sql) was specified
+ // generate the default constraint name (based on table and column name)
+ // it's not stored as annotation, meaning it won't be clashing with other names
+ // (we already checked that in the finalize model convention step)
+ yield return new Annotation(
+ RelationalAnnotationNames.DefaultConstraintName,
+ mappedProperty.GenerateDefaultConstraintName(table));
+ }
+ }
}
var entityType = (IEntityType)column.Table.EntityTypeMappings.First().TypeBase;
diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
index c971efad482..8aae2c65c39 100644
--- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
+++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs
@@ -47,6 +47,11 @@ public override IEnumerable ForRemove(IColumn column)
yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true);
}
}
+
+ if (column[RelationalAnnotationNames.DefaultConstraintName] is string defaultConstraintName)
+ {
+ yield return new Annotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName);
+ }
}
///
diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
index 10ad36fcdb4..4bf6e2ae7aa 100644
--- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
+++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs
@@ -363,7 +363,9 @@ protected override void Generate(
|| !Equals(operation.DefaultValue, oldDefaultValue)
|| operation.DefaultValueSql != oldDefaultValueSql)
{
- DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, builder);
+ var oldDefaultConstraintName = operation.OldColumn[RelationalAnnotationNames.DefaultConstraintName] as string;
+
+ DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, oldDefaultConstraintName, builder);
(oldDefaultValue, oldDefaultValueSql) = (null, null);
}
@@ -459,11 +461,13 @@ protected override void Generate(
if (!Equals(operation.DefaultValue, oldDefaultValue) || operation.DefaultValueSql != oldDefaultValueSql)
{
+ var defaultConstraintName = operation[RelationalAnnotationNames.DefaultConstraintName] as string;
+
builder
.Append("ALTER TABLE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema))
.Append(" ADD");
- DefaultValue(operation.DefaultValue, operation.DefaultValueSql, operation.ColumnType, builder);
+ DefaultValue(operation.DefaultValue, operation.DefaultValueSql, operation.ColumnType, defaultConstraintName, builder);
builder
.Append(" FOR ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name))
@@ -1352,7 +1356,9 @@ protected override void Generate(
MigrationCommandListBuilder builder,
bool terminate = true)
{
- DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, builder);
+ var defaultConstraintName = operation[RelationalAnnotationNames.DefaultConstraintName] as string;
+
+ DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, defaultConstraintName, builder);
base.Generate(operation, model, builder, terminate: false);
if (terminate)
@@ -1545,6 +1551,32 @@ protected override void Generate(DeleteDataOperation operation, IModel? model, M
protected override void Generate(UpdateDataOperation operation, IModel? model, MigrationCommandListBuilder builder)
=> GenerateExecWhenIdempotent(builder, b => base.Generate(operation, model, b));
+ ///
+ /// Generates a SQL fragment for the named default constraint of a column.
+ ///
+ /// The default value for the column.
+ /// The SQL expression to use for the column's default constraint.
+ /// Store/database type of the column.
+ /// The command builder to use to add the SQL fragment.
+ /// The constraint name to use to add the SQL fragment.
+ protected virtual void DefaultValue(
+ object? defaultValue,
+ string? defaultValueSql,
+ string? columnType,
+ string? constraintName,
+ MigrationCommandListBuilder builder)
+ {
+ if (constraintName != null && (defaultValue != null || defaultValueSql != null))
+ {
+ builder
+ .Append(" CONSTRAINT [")
+ .Append(constraintName)
+ .Append("]");
+ }
+
+ base.DefaultValue(defaultValue, defaultValueSql, columnType, builder);
+ }
+
///
protected override void SequenceOptions(
string? schema,
@@ -1637,11 +1669,13 @@ protected override void ColumnDefinition(
builder.Append(operation.IsNullable ? " NULL" : " NOT NULL");
+ var defaultConstraintName = operation[RelationalAnnotationNames.DefaultConstraintName] as string;
+
if (!string.Equals(columnType, "rowversion", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(columnType, "timestamp", StringComparison.OrdinalIgnoreCase))
{
// rowversion/timestamp columns cannot have default values, but also don't need them when adding a new column.
- DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder);
+ DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, defaultConstraintName, builder);
}
var identity = operation[SqlServerAnnotationNames.Identity] as string;
@@ -1921,13 +1955,28 @@ protected override void ForeignKeyAction(ReferentialAction referentialAction, Mi
/// The schema that contains the table.
/// The table that contains the column.
/// The column.
+ /// The name of the default constraint.
/// The command builder to use to add the SQL fragment.
protected virtual void DropDefaultConstraint(
string? schema,
string tableName,
string columnName,
+ string? defaultConstraintName,
MigrationCommandListBuilder builder)
{
+ if (defaultConstraintName != null)
+ {
+ builder
+ .Append("ALTER TABLE ")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema))
+ .Append(" DROP CONSTRAINT [")
+ .Append(defaultConstraintName)
+ .Append("]")
+ .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
+
+ return;
+ }
+
var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string));
var variable = Uniquify("@var");
diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
index dc3572b09a7..4b6fde64fdf 100644
--- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
+++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs
@@ -733,6 +733,8 @@ private void GetColumns(
[c].[is_nullable],
[c].[is_identity],
[dc].[definition] AS [default_sql],
+ [dc].[name] AS [default_constraint_name],
+ [dc].[is_system_named] AS [default_constraint_is_system_named],
[cc].[definition] AS [computed_sql],
[cc].[is_persisted] AS [computed_is_persisted],
CAST([e].[value] AS nvarchar(MAX)) AS [comment],
@@ -802,6 +804,8 @@ FROM [sys].[views] v
var nullable = dataRecord.GetValueOrDefault("is_nullable");
var isIdentity = dataRecord.GetValueOrDefault("is_identity");
var defaultValueSql = dataRecord.GetValueOrDefault("default_sql");
+ var defaultConstraintName = dataRecord.GetValueOrDefault("default_constraint_name");
+ var defaultConstraintIsSystemNamed = dataRecord.GetValueOrDefault("default_constraint_is_system_named");
var computedValue = dataRecord.GetValueOrDefault("computed_sql");
var computedIsPersisted = dataRecord.GetValueOrDefault("computed_is_persisted");
var comment = dataRecord.GetValueOrDefault("comment");
@@ -875,6 +879,11 @@ FROM [sys].[views] v
column[SqlServerAnnotationNames.Sparse] = true;
}
+ if (defaultConstraintName != null && !defaultConstraintIsSystemNamed)
+ {
+ column[RelationalAnnotationNames.DefaultConstraintName] = defaultConstraintName;
+ }
+
table.Columns.Add(column);
}
}
diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs
index 501d53b2301..497d05d20c4 100644
--- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs
+++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs
@@ -72,6 +72,7 @@ public void Test_new_annotations_handled_for_entity_types()
RelationalAnnotationNames.DefaultValueSql,
RelationalAnnotationNames.ComputedColumnSql,
RelationalAnnotationNames.DefaultValue,
+ RelationalAnnotationNames.DefaultConstraintName,
RelationalAnnotationNames.Name,
#pragma warning disable CS0618 // Type or member is obsolete
RelationalAnnotationNames.SequencePrefix,
@@ -99,7 +100,8 @@ public void Test_new_annotations_handled_for_entity_types()
#pragma warning disable CS0618
RelationalAnnotationNames.ContainerColumnTypeMapping,
#pragma warning restore CS0618
- RelationalAnnotationNames.StoreType
+ RelationalAnnotationNames.StoreType,
+ RelationalAnnotationNames.UseNamedDefaultConstraints
};
// Add a line here if the code generator is supposed to handle this annotation
@@ -260,6 +262,7 @@ public void Test_new_annotations_handled_for_properties()
#pragma warning restore CS0618
RelationalAnnotationNames.JsonPropertyName,
RelationalAnnotationNames.StoreType,
+ RelationalAnnotationNames.UseNamedDefaultConstraints
};
var columnMapping = $@"{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasColumnType)}(""default_int_mapping"")";
@@ -304,6 +307,10 @@ public void Test_new_annotations_handled_for_properties()
RelationalAnnotationNames.DefaultValue,
("1", $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasDefaultValue)}(""1"")")
},
+ {
+ RelationalAnnotationNames.DefaultConstraintName,
+ ("some name", $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasDefaultValue)}(""1"", ""some name"")")
+ },
{
RelationalAnnotationNames.IsFixedLength,
(true, $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.IsFixedLength)}()")
@@ -389,12 +396,23 @@ private static void MissingAnnotationCheck(
if (!invalidAnnotations.Contains(annotationName))
{
var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder();
+
var metadataItem = createMetadataItem(modelBuilder);
metadataItem.SetAnnotation(
annotationName, validAnnotations.ContainsKey(annotationName)
? validAnnotations[annotationName].Value
: null);
+ // code generator for default value with named constraint contains validation
+ // to check that constraint name must be accompanied by either DefaultValue
+ // or DefaultValueSql - so we need to add it here also
+ if (annotationName == RelationalAnnotationNames.DefaultConstraintName)
+ {
+ metadataItem.SetAnnotation(
+ RelationalAnnotationNames.DefaultValue,
+ validAnnotations[RelationalAnnotationNames.DefaultValue].Value);
+ }
+
modelBuilder.FinalizeModel(designTime: true, skipValidation: true);
var sb = new IndentedStringBuilder();
diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs
new file mode 100644
index 00000000000..e3b1b580a15
--- /dev/null
+++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs
@@ -0,0 +1,1048 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Migrations;
+
+public partial class MigrationsSqlServerTest : MigrationsTestBase
+{
+ #region basic operations with explicit name
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_add_column_with_explicit_name()
+ {
+ await Test(
+ builder => builder.Entity("Entity").Property("Id"),
+ builder => { },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [Number] int NOT NULL CONSTRAINT [MyConstraint] DEFAULT 7;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_drop_column_with_explicit_name()
+ {
+ await Test(
+ builder => builder.Entity("Entity").Property("Id"),
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ builder => { },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var column = Assert.Single(table.Columns);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraintSql];
+ALTER TABLE [Entity] DROP COLUMN [Guid];
+""",
+ //
+ """
+ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraint];
+ALTER TABLE [Entity] DROP COLUMN [Number];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_create_table_with_column_with_explicit_name()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]);
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+CREATE TABLE [Entity] (
+ [Id] nvarchar(450) NOT NULL,
+ [Guid] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT (NEWID()),
+ [Number] int NOT NULL CONSTRAINT [MyConstraint] DEFAULT 7,
+ CONSTRAINT [PK_Entity] PRIMARY KEY ([Id])
+);
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_drop_table_with_column_with_explicit_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+"""
+DROP TABLE [Entity];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_rename_constraint()
+ {
+ await Test(
+ builder => builder.Entity("Entity").Property("Id"),
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "RenamedConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "RenamedConstraintSql");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("RenamedConstraint", number[RelationalAnnotationNames.DefaultConstraintName]);
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("RenamedConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraint];
+ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [RenamedConstraint] DEFAULT 7 FOR [Number];
+""",
+ //
+ """
+ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraintSql];
+ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [RenamedConstraintSql] DEFAULT (NEWID()) FOR [Guid];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_add_explicit_constraint_name()
+ {
+ await Test(
+ builder => builder.Entity("Entity").Property("Id"),
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]);
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+DECLARE @var sysname;
+SELECT @var = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Number');
+IF @var IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var + '];');
+ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [MyConstraint] DEFAULT 7 FOR [Number];
+""",
+ //
+ """
+DECLARE @var1 sysname;
+SELECT @var1 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Guid');
+IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var1 + '];');
+ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [MyConstraintSql] DEFAULT (NEWID()) FOR [Guid];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_remove_explicit_constraint_name()
+ {
+ await Test(
+ builder => builder.Entity("Entity").Property("Id"),
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Null(number[RelationalAnnotationNames.DefaultConstraintName]);
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Null(guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraint];
+ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL;
+ALTER TABLE [Entity] ADD DEFAULT 7 FOR [Number];
+""",
+ //
+ """
+ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraintSql];
+ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL;
+ALTER TABLE [Entity] ADD DEFAULT (NEWID()) FOR [Guid];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_add_column_with_implicit_name_on_nested_owned()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").OwnsOne("OwnedType", "MyOwned", b =>
+ {
+ b.OwnsOne("NestedType", "MyNested", bb =>
+ {
+ bb.Property("Foo");
+ });
+ });
+ },
+ builder => { },
+ builder =>
+ {
+ builder.Entity("Entity").OwnsOne("OwnedType", "MyOwned", b =>
+ {
+ b.OwnsOne("NestedType", "MyNested", bb =>
+ {
+ bb.Property("Number").HasDefaultValue(7);
+ bb.Property("Guid").HasDefaultValueSql("NEWID()");
+ });
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "MyOwned_MyNested_Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_Entity_MyOwned_MyNested_Number", number[RelationalAnnotationNames.DefaultConstraintName]);
+ var guid = Assert.Single(table.Columns, c => c.Name == "MyOwned_MyNested_Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_Entity_MyOwned_MyNested_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] ADD [MyOwned_MyNested_Guid] uniqueidentifier NULL CONSTRAINT [DF_Entity_MyOwned_MyNested_Guid] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [MyOwned_MyNested_Number] int NULL CONSTRAINT [DF_Entity_MyOwned_MyNested_Number] DEFAULT 7;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_add_column_with_explicit_name_and_null_value()
+ {
+ await Test(
+ builder => builder.Entity("Entity").Property("Id"),
+ builder => { },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(null);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql(null);
+ builder.Entity("Entity").Property("NumberNamed").HasDefaultValue(null, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("GuidNamed").HasDefaultValueSql(null, defaultConstraintName: "MyConstraintSql");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "NumberNamed");
+ Assert.Null(number.DefaultValue);
+ Assert.Null(number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "GuidNamed");
+ Assert.Equal("('00000000-0000-0000-0000-000000000000')", guid.DefaultValueSql);
+ Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] ADD [Guid] uniqueidentifier NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000';
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [GuidNamed] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT '00000000-0000-0000-0000-000000000000';
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [Number] int NULL;
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [NumberNamed] int NULL;
+""");
+ }
+
+ #endregion
+
+ #region basic operations with implicit name
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_add_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("Entity").Property("Id");
+ },
+ builder => { },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_Guid] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [Number] int NOT NULL CONSTRAINT [DF_Entity_Number] DEFAULT 7;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_drop_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("Entity").Property("Id");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ builder => { },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var column = Assert.Single(table.Columns);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Guid];
+ALTER TABLE [Entity] DROP COLUMN [Guid];
+""",
+ //
+ """
+ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Number];
+ALTER TABLE [Entity] DROP COLUMN [Number];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_create_table_with_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder => builder.UseNamedDefaultConstraints(),
+ builder => { },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+CREATE TABLE [Entity] (
+ [Id] nvarchar(450) NOT NULL,
+ [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_Guid] DEFAULT (NEWID()),
+ [Number] int NOT NULL CONSTRAINT [DF_Entity_Number] DEFAULT 7,
+ CONSTRAINT [PK_Entity] PRIMARY KEY ([Id])
+);
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_drop_table_with_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder => builder.UseNamedDefaultConstraints(),
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+"""
+DROP TABLE [Entity];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_rename_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("Entity").Property("Id");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasColumnName("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasColumnName("Guid").HasDefaultValueSql("NEWID()");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Number").HasColumnName("ModifiedNumber").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasColumnName("ModifiedGuid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "ModifiedNumber");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_Entity_ModifiedNumber", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "ModifiedGuid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_Entity_ModifiedGuid", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+EXEC sp_rename N'[Entity].[Number]', N'ModifiedNumber', 'COLUMN';
+""",
+ //
+ """
+EXEC sp_rename N'[Entity].[Guid]', N'ModifiedGuid', 'COLUMN';
+""",
+ //
+ """
+ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Number];
+ALTER TABLE [Entity] ALTER COLUMN [ModifiedNumber] int NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_ModifiedNumber] DEFAULT 7 FOR [ModifiedNumber];
+""",
+ //
+ """
+ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Guid];
+ALTER TABLE [Entity] ALTER COLUMN [ModifiedGuid] uniqueidentifier NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_ModifiedGuid] DEFAULT (NEWID()) FOR [ModifiedGuid];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_rename_table_with_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("Entity").Property("Id");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").ToTable("Entities").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").ToTable("Entities").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ builder =>
+ {
+ builder.Entity("Entity").ToTable("RenamedEntities").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").ToTable("RenamedEntities").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_RenamedEntities_Number", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_RenamedEntities_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entities] DROP CONSTRAINT [PK_Entities];
+""",
+ //
+ """
+EXEC sp_rename N'[Entities]', N'RenamedEntities', 'OBJECT';
+""",
+ //
+ """
+ALTER TABLE [RenamedEntities] DROP CONSTRAINT [DF_Entities_Number];
+ALTER TABLE [RenamedEntities] ALTER COLUMN [Number] int NOT NULL;
+ALTER TABLE [RenamedEntities] ADD CONSTRAINT [DF_RenamedEntities_Number] DEFAULT 7 FOR [Number];
+""",
+ //
+ """
+ALTER TABLE [RenamedEntities] DROP CONSTRAINT [DF_Entities_Guid];
+ALTER TABLE [RenamedEntities] ALTER COLUMN [Guid] uniqueidentifier NOT NULL;
+ALTER TABLE [RenamedEntities] ADD CONSTRAINT [DF_RenamedEntities_Guid] DEFAULT (NEWID()) FOR [Guid];
+""",
+ //
+ """
+ALTER TABLE [RenamedEntities] ADD CONSTRAINT [PK_RenamedEntities] PRIMARY KEY ([Id]);
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_add_opt_in_with_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ builder => { },
+ builder => builder.UseNamedDefaultConstraints(),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+DECLARE @var sysname;
+SELECT @var = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Number');
+IF @var IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var + '];');
+ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_Number] DEFAULT 7 FOR [Number];
+""",
+ //
+ """
+DECLARE @var1 sysname;
+SELECT @var1 = [d].[name]
+FROM [sys].[default_constraints] [d]
+INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
+WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Guid');
+IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var1 + '];');
+ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL;
+ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_Guid] DEFAULT (NEWID()) FOR [Guid];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_remove_opt_in_with_column_with_implicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ builder => builder.UseNamedDefaultConstraints(),
+ builder => { },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Null(number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Null(guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Number];
+ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL;
+ALTER TABLE [Entity] ADD DEFAULT 7 FOR [Number];
+""",
+ //
+ """
+ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Guid];
+ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL;
+ALTER TABLE [Entity] ADD DEFAULT (NEWID()) FOR [Guid];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_add_opt_in_with_column_with_explicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ builder => { },
+ builder => builder.UseNamedDefaultConstraints(),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ // opt-in doesn't make a difference when constraint name is explicitly defined
+ AssertSql();
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_remove_opt_in_with_column_with_explicit_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ builder => builder.UseNamedDefaultConstraints(),
+ builder => { },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ // opt-in doesn't make a difference when constraint name is explicitly defined
+ AssertSql();
+ }
+
+ #endregion
+
+ #region edge/advanced cases (e.g. table sharing, name clashes)
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_TPT_inheritance_explicit_default_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("RootEntity").UseTptMappingStrategy();
+ builder.Entity("RootEntity").ToTable("Roots");
+ builder.Entity("RootEntity").Property("Id");
+ builder.Entity("BranchEntity").HasBaseType("RootEntity");
+ builder.Entity("BranchEntity").ToTable("Branches");
+ builder.Entity("LeafEntity").HasBaseType("BranchEntity");
+ builder.Entity("LeafEntity").ToTable("Leaves");
+ },
+ builder => { },
+ builder =>
+ {
+ builder.Entity("BranchEntity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint");
+ builder.Entity("BranchEntity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql");
+ },
+ model =>
+ {
+ var roots = Assert.Single(model.Tables, x => x.Name == "Roots");
+ var branches = Assert.Single(model.Tables, x => x.Name == "Branches");
+ var leaves = Assert.Single(model.Tables, x => x.Name == "Leaves");
+
+ var branchGuid = Assert.Single(branches.Columns, x => x.Name == "Guid");
+ Assert.Equal("MyConstraintSql", branchGuid[RelationalAnnotationNames.DefaultConstraintName]);
+ var branchNumber = Assert.Single(branches.Columns, x => x.Name == "Number");
+ Assert.Equal("MyConstraint", branchNumber[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Branches] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Branches] ADD [Number] int NOT NULL CONSTRAINT [MyConstraint] DEFAULT 7;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_TPT_inheritance_implicit_default_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("RootEntity").UseTptMappingStrategy();
+ builder.Entity("RootEntity").ToTable("Roots");
+ builder.Entity("RootEntity").Property("Id");
+ builder.Entity("BranchEntity").HasBaseType("RootEntity");
+ builder.Entity("BranchEntity").ToTable("Branches");
+ builder.Entity("LeafEntity").HasBaseType("BranchEntity");
+ builder.Entity("LeafEntity").ToTable("Leaves");
+ },
+ builder => { },
+ builder =>
+ {
+ builder.Entity("BranchEntity").Property("Number").HasDefaultValue(7);
+ builder.Entity("BranchEntity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var roots = Assert.Single(model.Tables, x => x.Name == "Roots");
+ var branches = Assert.Single(model.Tables, x => x.Name == "Branches");
+ var leaves = Assert.Single(model.Tables, x => x.Name == "Leaves");
+
+ var branchGuid = Assert.Single(branches.Columns, x => x.Name == "Guid");
+ Assert.Equal("DF_Branches_Guid", branchGuid[RelationalAnnotationNames.DefaultConstraintName]);
+ var branchNumber = Assert.Single(branches.Columns, x => x.Name == "Number");
+ Assert.Equal("DF_Branches_Number", branchNumber[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Branches] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Branches_Guid] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Branches] ADD [Number] int NOT NULL CONSTRAINT [DF_Branches_Number] DEFAULT 7;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_with_opt_in_TPC_inheritance_implicit_default_constraint_name()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("RootEntity").UseTpcMappingStrategy();
+ builder.Entity("RootEntity").ToTable("Roots");
+ builder.Entity("RootEntity").Property("Id");
+ builder.Entity("BranchEntity").HasBaseType("RootEntity");
+ builder.Entity("BranchEntity").ToTable("Branches");
+ builder.Entity("LeafEntity").HasBaseType("BranchEntity");
+ builder.Entity("LeafEntity").ToTable("Leaves");
+ },
+ builder => { },
+ builder =>
+ {
+ builder.Entity("BranchEntity").Property("Number").HasDefaultValue(7);
+ builder.Entity("BranchEntity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var roots = Assert.Single(model.Tables, x => x.Name == "Roots");
+ var branches = Assert.Single(model.Tables, x => x.Name == "Branches");
+ var leaves = Assert.Single(model.Tables, x => x.Name == "Leaves");
+
+ var branchGuid = Assert.Single(branches.Columns, x => x.Name == "Guid");
+ Assert.Equal("DF_Branches_Guid", branchGuid[RelationalAnnotationNames.DefaultConstraintName]);
+ var branchNumber = Assert.Single(branches.Columns, x => x.Name == "Number");
+ Assert.Equal("DF_Branches_Number", branchNumber[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var leafGuid = Assert.Single(leaves.Columns, x => x.Name == "Guid");
+ Assert.Equal("DF_Leaves_Guid", leafGuid[RelationalAnnotationNames.DefaultConstraintName]);
+ var leafNumber = Assert.Single(leaves.Columns, x => x.Name == "Number");
+ Assert.Equal("DF_Leaves_Number", leafNumber[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Leaves] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Leaves_Guid] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Leaves] ADD [Number] int NOT NULL CONSTRAINT [DF_Leaves_Number] DEFAULT 7;
+""",
+ //
+ """
+ALTER TABLE [Branches] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Branches_Guid] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Branches] ADD [Number] int NOT NULL CONSTRAINT [DF_Branches_Number] DEFAULT 7;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_name_clash_between_explicit_and_implicit_default_constraint_gets_deduplicated()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("Entity").Property("Id");
+ builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "DF_Entity_Another");
+ builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "DF_Entity_YetAnother");
+ },
+ builder => { },
+ builder =>
+ {
+ builder.Entity("Entity").Property("Another").HasDefaultValue(7);
+ builder.Entity("Entity").Property("YetAnother").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal("DF_Entity_Another", number[RelationalAnnotationNames.DefaultConstraintName]);
+ Assert.Equal(7, number.DefaultValue);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("DF_Entity_YetAnother", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+
+ var another = Assert.Single(table.Columns, c => c.Name == "Another");
+ Assert.Equal("DF_Entity_Another1", another[RelationalAnnotationNames.DefaultConstraintName]);
+ Assert.Equal(7, another.DefaultValue);
+
+ var yetAnother = Assert.Single(table.Columns, c => c.Name == "YetAnother");
+ Assert.Equal("DF_Entity_YetAnother1", yetAnother[RelationalAnnotationNames.DefaultConstraintName]);
+ Assert.Equal("(newid())", yetAnother.DefaultValueSql);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] ADD [Another] int NOT NULL CONSTRAINT [DF_Entity_Another1] DEFAULT 7;
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [YetAnother] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_YetAnother1] DEFAULT (NEWID());
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_very_long_implicit_constraint_name_gets_trimmed_and_deduplicated()
+ {
+ await Test(
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity", b =>
+ {
+ b.Property("Id");
+ b.OwnsOne("Owned", "YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation", bb =>
+ {
+ bb.Property("Name");
+ });
+ });
+ },
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity", b =>
+ {
+ b.Property("Id");
+ b.OwnsOne("Owned", "YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation", bb =>
+ {
+ bb.Property("Name");
+ bb.Property("Prop").HasDefaultValue(7);
+ bb.Property("AnotherProp").HasDefaultValueSql("NEWID()");
+ bb.Property("YetAnotherProp").HasDefaultValue(27);
+ });
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var columns = table.Columns.Where(x => x.Name.EndsWith("Prop"));
+ Assert.Equal(3, columns.Count());
+ Assert.True(columns.All(x => x[RelationalAnnotationNames.DefaultConstraintName] != null));
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_AnotherProp] uniqueidentifier NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnn~] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_Prop] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnn~1] DEFAULT 7;
+""",
+ //
+ """
+ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_YetAnotherProp] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnn~2] DEFAULT 27;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_funky_table_name_with_implicit_constraint()
+ {
+ await Test(
+ builder => builder.Entity("My Entity").Property("Id"),
+ builder => { },
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("My Entity").Property("Number").HasDefaultValue(7);
+ builder.Entity("My Entity").Property("Guid").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Number");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_My Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Guid");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_My Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [My Entity] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_My Entity_Guid] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [My Entity] ADD [Number] int NOT NULL CONSTRAINT [DF_My Entity_Number] DEFAULT 7;
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Named_default_constraints_funky_column_name_with_implicit_constraint()
+ {
+ await Test(
+ builder => builder.Entity("Entity").Property("Id"),
+ builder => { },
+ builder =>
+ {
+ builder.UseNamedDefaultConstraints();
+ builder.Entity("Entity").Property("Num$be<>r").HasDefaultValue(7);
+ builder.Entity("Entity").Property("Gu!d").HasDefaultValueSql("NEWID()");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ var number = Assert.Single(table.Columns, c => c.Name == "Num$be<>r");
+ Assert.Equal(7, number.DefaultValue);
+ Assert.Equal("DF_Entity_Num$be<>r", number[RelationalAnnotationNames.DefaultConstraintName]);
+
+ var guid = Assert.Single(table.Columns, c => c.Name == "Gu!d");
+ Assert.Equal("(newid())", guid.DefaultValueSql);
+ Assert.Equal("DF_Entity_Gu!d", guid[RelationalAnnotationNames.DefaultConstraintName]);
+ });
+
+ AssertSql(
+"""
+ALTER TABLE [Entity] ADD [Gu!d] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_Gu!d] DEFAULT (NEWID());
+""",
+ //
+ """
+ALTER TABLE [Entity] ADD [Num$be<>r] int NOT NULL CONSTRAINT [DF_Entity_Num$be<>r] DEFAULT 7;
+""");
+ }
+
+ #endregion
+}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs
new file mode 100644
index 00000000000..a52a1c59c7b
--- /dev/null
+++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs
@@ -0,0 +1,8756 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.SqlServer.Internal;
+using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Migrations;
+
+public partial class MigrationsSqlServerTest : MigrationsTestBase
+{
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_default_column_mappings_and_default_history_table()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'CREATE TABLE [Customer] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))');
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_custom_column_mappings_and_default_history_table()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart").HasColumnName("Start");
+ ttb.HasPeriodEnd("SystemTimeEnd").HasColumnName("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'CREATE TABLE [Customer] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [End] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [Start] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([Start], [End])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))');
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_default_column_mappings_and_custom_history_table()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customer", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+DECLARE @historyTableSchema sysname = SCHEMA_NAME()
+EXEC(N'CREATE TABLE [Customer] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[HistoryTable]))');
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_explicitly_defined_schema()
+ {
+ await Test(
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", "mySchema", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("mySchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];');
+""",
+ //
+ """
+CREATE TABLE [mySchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[CustomersHistory]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_no_explicit_table_schema_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myDefaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("myDefaultSchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');
+""",
+ //
+ """
+CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomersHistory]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_explicit_table_schema_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myDefaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", "mySchema", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("mySchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];');
+""",
+ //
+ """
+CREATE TABLE [mySchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[CustomersHistory]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_model_schema()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myDefaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("myDefaultSchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("myDefaultSchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');
+""",
+ //
+ """
+CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomersHistory]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_model_schema_specified_after_entity_definition()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+
+ builder.Entity("Customer", e => e.ToTable("Customers", "mySchema1"));
+ builder.Entity("Customer", e => e.ToTable("Customers"));
+ builder.HasDefaultSchema("myDefaultSchema");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("myDefaultSchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("myDefaultSchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');
+""",
+ //
+ """
+CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomersHistory]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task
+ Create_temporal_table_with_default_model_schema_specified_after_entity_definition_and_history_table_schema_specified_explicitly()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("History", "myHistorySchema");
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+
+ builder.Entity("Customer", e => e.ToTable("Customers", "mySchema1"));
+ builder.Entity("Customer", e => e.ToTable("Customers"));
+ builder.HasDefaultSchema("myDefaultSchema");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("myDefaultSchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("History", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("myHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');
+""",
+ //
+ """
+IF SCHEMA_ID(N'myHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [myHistorySchema];');
+""",
+ //
+ """
+CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[History]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_model_schema_changed_after_entity_definition()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myFakeSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+
+ builder.HasDefaultSchema("myDefaultSchema");
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("myDefaultSchema", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("myDefaultSchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');
+""",
+ //
+ """
+CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomersHistory]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task
+ Create_temporal_table_with_default_schema_for_model_changed_and_explicit_history_table_schema_not_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myDefaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("myDefaultSchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ 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));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');
+""",
+ //
+ """
+CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[HistoryTable]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_explicit_history_table_schema_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.HasDefaultSchema("myDefaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("historySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');
+""",
+ //
+ """
+IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];');
+""",
+ //
+ """
+CREATE TABLE [myDefaultSchema].[Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Create_temporal_table_with_default_schema_for_table_and_explicit_history_table_schema_provided()
+ {
+ await Test(
+ builder => { },
+ builder =>
+ {
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate();
+ e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("SystemTimeStart");
+ ttb.HasPeriodEnd("SystemTimeEnd");
+ }));
+ });
+ },
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("historySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+ Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];');
+""",
+ //
+ """
+CREATE TABLE [Customers] (
+ [Id] int NOT NULL IDENTITY,
+ [Name] nvarchar(max) NULL,
+ [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
+ [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
+ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]),
+ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd])
+) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]));
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Drop_temporal_table_default_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.HasPeriodStart("Start").HasColumnName("PeriodStart");
+ ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd");
+ }));
+ }),
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+ """
+ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+DROP TABLE [Customer];
+""",
+ //
+ """
+DROP TABLE [CustomerHistory];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Drop_temporal_table_custom_history_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start").HasColumnName("PeriodStart");
+ ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd");
+ }));
+ }),
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+ """
+ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+DROP TABLE [Customer];
+""",
+ //
+ """
+DROP TABLE [HistoryTable];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Drop_temporal_table_custom_history_table_and_history_table_schema()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("Start").HasColumnName("PeriodStart");
+ ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd");
+ }));
+ }),
+ builder => { },
+ model =>
+ {
+ Assert.Empty(model.Tables);
+ });
+
+ AssertSql(
+ """
+ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+DROP TABLE [Customer];
+""",
+ //
+ """
+DROP TABLE [historySchema].[HistoryTable];
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("RenamedCustomers");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("RenamedCustomers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];
+""",
+ //
+ """
+EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT';
+""",
+ //
+ """
+ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);
+""",
+ //
+ """
+DECLARE @historyTableSchema1 sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))')
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table_rename_and_modify_column_in_same_migration()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+ e.Property("Discount");
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("DoB");
+ e.ToTable("Customers");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Discount").HasComment("for VIP only");
+ e.Property("DateOfBirth");
+ e.ToTable("RenamedCustomers");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("RenamedCustomers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Discount", c.Name),
+ c => Assert.Equal("DateOfBirth", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];
+""",
+ //
+ """
+EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT';
+""",
+ //
+ """
+EXEC sp_rename N'[RenamedCustomers].[DoB]', N'DateOfBirth', 'COLUMN';
+""",
+ //
+ """
+EXEC sp_rename N'[HistoryTable].[DoB]', N'DateOfBirth', 'COLUMN';
+""",
+ //
+ """
+DECLARE @defaultSchema2 AS sysname;
+SET @defaultSchema2 = SCHEMA_NAME();
+DECLARE @description2 AS sql_variant;
+SET @description2 = N'for VIP only';
+EXEC sp_addextendedproperty 'MS_Description', @description2, 'SCHEMA', @defaultSchema2, 'TABLE', N'RenamedCustomers', 'COLUMN', N'Discount';
+""",
+ //
+ """
+DECLARE @defaultSchema3 AS sysname;
+SET @defaultSchema3 = SCHEMA_NAME();
+DECLARE @description3 AS sql_variant;
+SET @description3 = N'for VIP only';
+EXEC sp_addextendedproperty 'MS_Description', @description3, 'SCHEMA', @defaultSchema3, 'TABLE', N'HistoryTable', 'COLUMN', N'Discount';
+""",
+ //
+ """
+ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);
+""",
+ //
+ """
+DECLARE @historyTableSchema1 sysname = SCHEMA_NAME()
+EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))')
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table_with_custom_history_table_schema()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable", "historySchema");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("RenamedCustomers");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("RenamedCustomers", table.Name);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];
+""",
+ //
+ """
+EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT';
+""",
+ //
+ """
+ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);
+""",
+ //
+ """
+ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]))
+""");
+ }
+
+ public virtual async Task Rename_temporal_table_schema_when_history_table_doesnt_have_its_schema_specified()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", "mySchema", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers", "mySchema2");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("mySchema2", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');
+""",
+ //
+ """
+ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[Customers];
+""",
+ //
+ """
+ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[HistoryTable];
+""",
+ //
+ """
+ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[HistoryTable]))
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table_schema_when_history_table_has_its_schema_specified()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+
+ e.ToTable(
+ "Customers", "mySchema", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable", "myHistorySchema");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => { },
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable("Customers", "mySchema2");
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("mySchema2", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+ Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("myHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');
+""",
+ //
+ """
+ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[Customers];
+""",
+ //
+ """
+ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable]))
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task Rename_temporal_table_schema_and_history_table_name_when_history_table_doesnt_have_its_schema_specified()
+ {
+ await Test(
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id").ValueGeneratedOnAdd();
+ e.Property("Name");
+ e.Property("Start").ValueGeneratedOnAddOrUpdate();
+ e.Property("End").ValueGeneratedOnAddOrUpdate();
+ e.HasKey("Id");
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable(
+ "Customers", "mySchema", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ builder => builder.Entity(
+ "Customer", e =>
+ {
+ e.ToTable(
+ "Customers", "mySchema2", tb => tb.IsTemporal(
+ ttb =>
+ {
+ ttb.UseHistoryTable("HistoryTable2");
+ ttb.HasPeriodStart("Start");
+ ttb.HasPeriodEnd("End");
+ }));
+ }),
+ model =>
+ {
+ var table = Assert.Single(model.Tables);
+ Assert.Equal("Customers", table.Name);
+ Assert.Equal("mySchema2", table.Schema);
+ Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]);
+ Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
+ Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);
+ Assert.Equal("HistoryTable2", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
+ Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]);
+
+ Assert.Collection(
+ table.Columns,
+ c => Assert.Equal("Id", c.Name),
+ c => Assert.Equal("Name", c.Name));
+ Assert.Same(
+ table.Columns.Single(c => c.Name == "Id"),
+ Assert.Single(table.PrimaryKey!.Columns));
+ });
+
+ AssertSql(
+ """
+IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');
+""",
+ //
+ """
+ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)
+""",
+ //
+ """
+ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[Customers];
+""",
+ //
+ """
+EXEC sp_rename N'[mySchema].[HistoryTable]', N'HistoryTable2', 'OBJECT';
+ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[HistoryTable2];
+""",
+ //
+ """
+ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[HistoryTable2]))
+""");
+ }
+
+ [ConditionalFact]
+ public virtual async Task
+ Rename_temporal_table_schema_and_history_table_name_when_history_table_doesnt_have_its_schema_specified_convention_with_default_global_schema22()
+ {
+ await Test(
+ builder =>
+ {
+ builder.HasDefaultSchema("defaultSchema");
+ builder.Entity(
+ "Customer", e =>
+ {
+ e.Property("Id");
+ e.Property