diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 1166fec64c5..3554478c958 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -1095,10 +1095,19 @@ protected virtual void GenerateCheckConstraint( stringBuilder .Append(builderName) .Append(".HasCheckConstraint(") - .Append(Code.Literal(checkConstraint.Name)) + .Append(Code.Literal(checkConstraint.ModelName)) .Append(", ") - .Append(Code.Literal(checkConstraint.Sql)) - .AppendLine(");"); + .Append(Code.Literal(checkConstraint.Sql)); + + if (checkConstraint.Name != (checkConstraint.GetDefaultName() ?? checkConstraint.ModelName)) + { + stringBuilder + .Append(", c => c.HasName(") + .Append(Code.Literal(checkConstraint.Name)) + .Append(")"); + } + + stringBuilder.AppendLine(");"); } /// diff --git a/src/EFCore.Relational/Design/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index dfaba72a23c..1b67fba9dbc 100644 --- a/src/EFCore.Relational/Design/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -326,22 +326,7 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod } else { - if (annotations.TryGetAndRemove(RelationalAnnotationNames.CheckConstraints, - out SortedDictionary constraints)) - { - parameters.Namespaces.Add(typeof(SortedDictionary).Namespace!); - var constraintsVariable = Dependencies.CSharpHelper.Identifier("constraints", parameters.ScopeVariables, capitalize: false); - parameters.MainBuilder - .Append("var ").Append(constraintsVariable).AppendLine(" = new SortedDictionary();"); - - foreach (var constraintPair in constraints) - { - Create(constraintPair.Value, constraintsVariable, parameters); - } - - GenerateSimpleAnnotation(RelationalAnnotationNames.CheckConstraints, constraintsVariable, parameters); - } - + annotations.Remove(RelationalAnnotationNames.CheckConstraints); annotations.Remove(RelationalAnnotationNames.Comment); annotations.Remove(RelationalAnnotationNames.IsTableExcludedFromMigrations); @@ -357,29 +342,6 @@ public override void Generate(IEntityType entityType, CSharpRuntimeAnnotationCod base.Generate(entityType, parameters); } - private void Create(ICheckConstraint constraint, string constraintsVariable, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - { - var code = Dependencies.CSharpHelper; - var constraintVariable = code.Identifier(constraint.Name, parameters.ScopeVariables, capitalize: false); - var mainBuilder = parameters.MainBuilder; - mainBuilder - .Append("var ").Append(constraintVariable).AppendLine(" = new RuntimeCheckConstraint(").IncrementIndent() - .Append(code.Literal(constraint.Name)).AppendLine(",") - .Append(parameters.TargetName).AppendLine(",") - .Append(code.Literal(constraint.Sql)).AppendLine(");").DecrementIndent() - .AppendLine(); - - CreateAnnotations( - constraint, - Generate, - parameters with { TargetName = constraintVariable }); - - mainBuilder - .Append(constraintsVariable).Append("[").Append(code.Literal(constraint.Name)).Append("] = ") - .Append(constraintVariable).AppendLine(";") - .AppendLine(); - } - /// /// Generates code to create the given annotations. /// diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs index a6cd84696ad..3e724ca0651 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -972,27 +974,36 @@ public static EntityTypeBuilder HasCheckConstraint( string? sql) { Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(sql, nameof(sql)); - var entityType = entityTypeBuilder.Metadata; + InternalCheckConstraintBuilder.HasCheckConstraint( + (IConventionEntityType)entityTypeBuilder.Metadata, + name, + sql, + ConfigurationSource.Explicit); - var constraint = entityType.FindCheckConstraint(name); - if (constraint != null) - { - if (constraint.Sql == sql) - { - ((CheckConstraint)constraint).UpdateConfigurationSource(ConfigurationSource.Explicit); - return entityTypeBuilder; - } + return entityTypeBuilder; + } - entityType.RemoveCheckConstraint(name); - } + /// + /// Configures a database check constraint when targeting a relational database. + /// + /// The entity type builder. + /// The name of the check constraint. + /// The logical constraint sql used in the check constraint. + /// An action that performs configuration of the check constraint. + /// A builder to further configure the entity type. + public static EntityTypeBuilder HasCheckConstraint( + this EntityTypeBuilder entityTypeBuilder, + string name, + string sql, + Action buildAction) + { + Check.NotEmpty(sql, nameof(sql)); + Check.NotNull(buildAction, nameof(buildAction)); - if (sql != null) - { - entityType.AddCheckConstraint(name, sql); - } + entityTypeBuilder.HasCheckConstraint(name, sql); + + buildAction(new CheckConstraintBuilder(entityTypeBuilder.Metadata.FindCheckConstraint(name)!)); return entityTypeBuilder; } @@ -1012,6 +1023,23 @@ public static EntityTypeBuilder HasCheckConstraint( where TEntity : class => (EntityTypeBuilder)HasCheckConstraint((EntityTypeBuilder)entityTypeBuilder, name, sql); + /// + /// Configures a database check constraint when targeting a relational database. + /// + /// The entity type being configured. + /// The entity type builder. + /// The name of the check constraint. + /// The logical constraint sql used in the check constraint. + /// An action that performs configuration of the check constraint. + /// A builder to further configure the entity type. + public static EntityTypeBuilder HasCheckConstraint( + this EntityTypeBuilder entityTypeBuilder, + string name, + string sql, + Action buildAction) + where TEntity : class + => (EntityTypeBuilder)HasCheckConstraint((EntityTypeBuilder)entityTypeBuilder, name, sql, buildAction); + /// /// Configures a database check constraint when targeting a relational database. /// @@ -1023,44 +1051,17 @@ public static EntityTypeBuilder HasCheckConstraint( /// The same builder instance if the check constraint was configured, /// otherwise. /// - public static IConventionEntityTypeBuilder? HasCheckConstraint( + public static IConventionCheckConstraintBuilder? HasCheckConstraint( this IConventionEntityTypeBuilder entityTypeBuilder, string name, string? sql, bool fromDataAnnotation = false) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(sql, nameof(sql)); - - var entityType = entityTypeBuilder.Metadata; - - var constraint = entityType.FindCheckConstraint(name); - if (constraint != null) - { - if (constraint.Sql == sql) - { - ((CheckConstraint)constraint).UpdateConfigurationSource( - fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); - return entityTypeBuilder; - } - - if (!(fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(constraint.GetConfigurationSource())) - { - return null; - } - - entityType.RemoveCheckConstraint(name); - } - - if (sql != null) - { - entityType.AddCheckConstraint(name, sql, fromDataAnnotation); - } - - return entityTypeBuilder; - } + => InternalCheckConstraintBuilder.HasCheckConstraint( + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)).Metadata, + name, + sql, + fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) + ?.Builder; /// /// Returns a value indicating whether the check constraint can be configured. @@ -1070,23 +1071,32 @@ public static EntityTypeBuilder HasCheckConstraint( /// The logical constraint sql used in the check constraint. /// Indicates whether the configuration was specified using a data annotation. /// if the configuration can be applied. + [Obsolete("Use CanHaveCheckConstraint")] public static bool CanSetCheckConstraint( this IConventionEntityTypeBuilder entityTypeBuilder, string name, string? sql, bool fromDataAnnotation = false) - { - Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(sql, nameof(sql)); - - var constraint = entityTypeBuilder.Metadata.FindCheckConstraint(name); + => entityTypeBuilder.CanHaveCheckConstraint(name, sql, fromDataAnnotation); - return constraint == null - || constraint.Sql == sql - || (fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention) - .Overrides(constraint.GetConfigurationSource()); - } + /// + /// Returns a value indicating whether the check constraint can be configured. + /// + /// The builder for the entity type being configured. + /// The name of the check constraint. + /// The logical constraint sql used in the check constraint. + /// Indicates whether the configuration was specified using a data annotation. + /// if the configuration can be applied. + public static bool CanHaveCheckConstraint( + this IConventionEntityTypeBuilder entityTypeBuilder, + string name, + string? sql, + bool fromDataAnnotation = false) + => InternalCheckConstraintBuilder.CanHaveCheckConstraint( + Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder)).Metadata, + name, + sql, + fromDataAnnotation? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// /// Configures a comment to be applied to the table diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index d44cf895115..bd0603e1f55 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -600,7 +600,7 @@ public static IEnumerable GetFunctionMappings(this IEntityType public static IMutableCheckConstraint? FindCheckConstraint( this IMutableEntityType entityType, string name) - => (IMutableCheckConstraint?)((IEntityType)entityType).FindCheckConstraint(name); + => (IMutableCheckConstraint?)((IReadOnlyEntityType)entityType).FindCheckConstraint(name); /// /// Finds an with the given name. @@ -614,7 +614,7 @@ public static IEnumerable GetFunctionMappings(this IEntityType public static IConventionCheckConstraint? FindCheckConstraint( this IConventionEntityType entityType, string name) - => (IConventionCheckConstraint?)((IEntityType)entityType).FindCheckConstraint(name); + => (IConventionCheckConstraint?)((IReadOnlyEntityType)entityType).FindCheckConstraint(name); /// /// Finds an with the given name. @@ -677,7 +677,7 @@ public static IConventionCheckConstraint AddCheckConstraint( /// /// The entity type to remove the check constraint from. /// The check constraint name to be removed. - /// The removed . + /// The removed check constraint. public static IMutableCheckConstraint? RemoveCheckConstraint( this IMutableEntityType entityType, string name) @@ -688,39 +688,96 @@ public static IConventionCheckConstraint AddCheckConstraint( /// /// The entity type to remove the check constraint from. /// The check constraint name. - /// The removed . + /// The removed check constraint. public static IConventionCheckConstraint? RemoveCheckConstraint( this IConventionEntityType entityType, string name) - => CheckConstraint.RemoveCheckConstraint((IMutableEntityType)entityType, Check.NotEmpty(name, nameof(name))); + => (IConventionCheckConstraint?)CheckConstraint.RemoveCheckConstraint( + (IMutableEntityType)entityType, Check.NotEmpty(name, nameof(name))); /// - /// Returns all contained in the entity type. + /// Returns all check constraints contained in the entity type. /// /// The entity type to get the check constraints for. public static IEnumerable GetCheckConstraints(this IReadOnlyEntityType entityType) => CheckConstraint.GetCheckConstraints(entityType); /// - /// Returns all contained in the entity type. + /// Returns all check constraints contained in the entity type. /// /// The entity type to get the check constraints for. public static IEnumerable GetCheckConstraints(this IMutableEntityType entityType) => CheckConstraint.GetCheckConstraints(entityType).Cast(); /// - /// Returns all contained in the entity type. + /// Returns all check constraints contained in the entity type. /// /// The entity type to get the check constraints for. public static IEnumerable GetCheckConstraints(this IConventionEntityType entityType) => CheckConstraint.GetCheckConstraints(entityType).Cast(); /// - /// Returns all contained in the entity type. + /// Returns all check constraints contained in the entity type. /// /// The entity type to get the check constraints for. public static IEnumerable GetCheckConstraints(this IEntityType entityType) - => CheckConstraint.GetCheckConstraints(entityType); + => CheckConstraint.GetCheckConstraints(entityType).Cast(); + + /// + /// + /// Returns all check constraints declared on the entity type. + /// + /// + /// This method does not return check constraints declared on base types. + /// It is useful when iterating over all entity types to avoid processing the same check constraint more than once. + /// Use to also return check constraints declared on base types. + /// + /// + /// The entity type to get the check constraints for. + public static IEnumerable GetDeclaredCheckConstraints(this IReadOnlyEntityType entityType) + => CheckConstraint.GetDeclaredCheckConstraints(entityType); + + /// + /// + /// Returns all check constraints declared on the entity type. + /// + /// + /// This method does not return check constraints declared on base types. + /// It is useful when iterating over all entity types to avoid processing the same check constraint more than once. + /// Use to also return check constraints declared on base types. + /// + /// + /// The entity type to get the check constraints for. + public static IEnumerable GetDeclaredCheckConstraints(this IMutableEntityType entityType) + => CheckConstraint.GetDeclaredCheckConstraints(entityType).Cast(); + + /// + /// + /// Returns all check constraints declared on the entity type. + /// + /// + /// This method does not return check constraints declared on base types. + /// It is useful when iterating over all entity types to avoid processing the same check constraint more than once. + /// Use to also return check constraints declared on base types. + /// + /// + /// The entity type to get the check constraints for. + public static IEnumerable GetDeclaredCheckConstraints(this IConventionEntityType entityType) + => CheckConstraint.GetDeclaredCheckConstraints(entityType).Cast(); + + /// + /// + /// Returns all check constraints declared on the entity type. + /// + /// + /// This method does not return check constraints declared on base types. + /// It is useful when iterating over all entity types to avoid processing the same check constraint more than once. + /// Use to also return check constraints declared on base types. + /// + /// + /// The entity type to get the check constraints for. + public static IEnumerable GetDeclaredCheckConstraints(this IEntityType entityType) + => CheckConstraint.GetDeclaredCheckConstraints(entityType).Cast(); /// /// Returns the comment for the table this entity is mapped to. diff --git a/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs b/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs index 5b62022ceaf..0c4ccf8631a 100644 --- a/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalOwnedNavigationBuilderExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -27,27 +28,54 @@ public static OwnedNavigationBuilder HasCheckConstraint( string? sql) { Check.NotNull(ownedNavigationBuilder, nameof(ownedNavigationBuilder)); - Check.NotEmpty(name, nameof(name)); - Check.NullButNotEmpty(sql, nameof(sql)); - var entityType = ownedNavigationBuilder.OwnedEntityType; + InternalCheckConstraintBuilder.HasCheckConstraint( + (IConventionEntityType)ownedNavigationBuilder.OwnedEntityType, + name, + sql, + ConfigurationSource.Explicit); - var constraint = entityType.FindCheckConstraint(name); - if (constraint != null) - { - if (constraint.Sql == sql) - { - ((CheckConstraint)constraint).UpdateConfigurationSource(ConfigurationSource.Explicit); - return ownedNavigationBuilder; - } + return ownedNavigationBuilder; + } + + /// + /// Configures a database check constraint when targeting a relational database. + /// + /// The entity type owning the relationship. + /// The dependent entity type of the relationship. + /// The navigation builder for the owned type. + /// The name of the check constraint. + /// The logical constraint sql used in the check constraint. + /// A builder to further configure the navigation. + public static OwnedNavigationBuilder HasCheckConstraint( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string? sql) + where TEntity : class + where TDependentEntity : class + => (OwnedNavigationBuilder) + HasCheckConstraint((OwnedNavigationBuilder)ownedNavigationBuilder, name, sql); - entityType.RemoveCheckConstraint(name); - } + /// + /// Configures a database check constraint when targeting a relational database. + /// + /// The navigation builder for the owned type. + /// The name of the check constraint. + /// The logical constraint sql used in the check constraint. + /// An action that performs configuration of the check constraint. + /// A builder to further configure the navigation. + public static OwnedNavigationBuilder HasCheckConstraint( + this OwnedNavigationBuilder ownedNavigationBuilder, + string name, + string sql, + Action buildAction) + { + Check.NotEmpty(sql, nameof(sql)); + Check.NotNull(buildAction, nameof(buildAction)); + + ownedNavigationBuilder.HasCheckConstraint(name, sql); - if (sql != null) - { - entityType.AddCheckConstraint(name, sql); - } + buildAction(new CheckConstraintBuilder(ownedNavigationBuilder.OwnedEntityType.FindCheckConstraint(name)!)); return ownedNavigationBuilder; } @@ -60,14 +88,16 @@ public static OwnedNavigationBuilder HasCheckConstraint( /// The navigation builder for the owned type. /// The name of the check constraint. /// The logical constraint sql used in the check constraint. + /// An action that performs configuration of the check constraint. /// A builder to further configure the navigation. public static OwnedNavigationBuilder HasCheckConstraint( this OwnedNavigationBuilder ownedNavigationBuilder, string name, - string? sql) + string sql, + Action buildAction) where TEntity : class where TDependentEntity : class => (OwnedNavigationBuilder) - HasCheckConstraint((OwnedNavigationBuilder)ownedNavigationBuilder, name, sql); + HasCheckConstraint((OwnedNavigationBuilder)ownedNavigationBuilder, name, sql, buildAction); } } diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 189b06c492e..c8b7ff2084a 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -301,6 +301,7 @@ protected virtual void ValidateSharedTableCompatibility( ValidateSharedKeysCompatibility(mappedTypes, table, logger); ValidateSharedForeignKeysCompatibility(mappedTypes, table, logger); ValidateSharedIndexesCompatibility(mappedTypes, table, logger); + ValidateSharedCheckConstraintCompatibility(mappedTypes, table, logger); // Validate optional dependents if (mappedTypes.Count == 1) @@ -1164,10 +1165,54 @@ protected virtual void ValidateCompatible( string keyName, in StoreObjectIdentifier storeObject, IDiagnosticsLogger logger) + => key.AreCompatible(duplicateKey, storeObject, shouldThrow: true); + + /// + /// Validates the compatibility of check constraint in a given shared table. + /// + /// The mapped entity types. + /// The identifier of the store object. + /// The logger to use. + protected virtual void ValidateSharedCheckConstraintCompatibility( + IReadOnlyList mappedTypes, + in StoreObjectIdentifier storeObject, + IDiagnosticsLogger logger) { - key.AreCompatible(duplicateKey, storeObject, shouldThrow: true); + var checkConstraintMappings = new Dictionary(); + foreach (var checkConstraint in mappedTypes.SelectMany(et => et.GetDeclaredCheckConstraints())) + { + var checkConstraintName = checkConstraint.GetName(storeObject); + if (checkConstraintName == null) + { + continue; + } + + if (!checkConstraintMappings.TryGetValue(checkConstraintName, out var duplicateCheckConstraint)) + { + checkConstraintMappings[checkConstraintName] = checkConstraint; + continue; + } + + ValidateCompatible(checkConstraint, duplicateCheckConstraint, checkConstraintName, storeObject, logger); + } } + /// + /// Validates the compatibility of two check constraints with the same name. + /// + /// An check constraints. + /// Another check constraints. + /// The name of the check constraint. + /// The identifier of the store object. + /// The logger to use. + protected virtual void ValidateCompatible( + ICheckConstraint checkConstraint, + ICheckConstraint duplicateCheckConstraint, + string indexName, + in StoreObjectIdentifier storeObject, + IDiagnosticsLogger logger) + => CheckConstraint.AreCompatible(checkConstraint, duplicateCheckConstraint, storeObject, shouldThrow: true); + /// /// Validates the mapping/configuration of inheritance in the model. /// diff --git a/src/EFCore.Relational/Metadata/Builders/CheckConstraintBuilder.cs b/src/EFCore.Relational/Metadata/Builders/CheckConstraintBuilder.cs new file mode 100644 index 00000000000..0f6a680251b --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/CheckConstraintBuilder.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// Provides a simple API for configuring a check constraint. + /// + public class CheckConstraintBuilder : IInfrastructure + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public CheckConstraintBuilder(IMutableCheckConstraint checkConstraint) + { + Check.NotNull(checkConstraint, nameof(checkConstraint)); + + Builder = ((CheckConstraint)checkConstraint).Builder; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + protected virtual InternalCheckConstraintBuilder Builder { [DebuggerStepThrough] get; } + + /// + IConventionCheckConstraintBuilder IInfrastructure.Instance + { + [DebuggerStepThrough] get => Builder; + } + + /// + /// The check constraint being configured. + /// + public virtual IMutableCheckConstraint Metadata + => Builder.Metadata; + + /// + /// Sets the database name of the check constraint. + /// + /// The database name of the check constraint. + /// The same builder instance so that multiple configuration calls can be chained. + public virtual CheckConstraintBuilder HasName(string name) + { + Builder.HasName(name, ConfigurationSource.Explicit); + + return this; + } + + #region Hidden System.Object members + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() + => base.ToString(); + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, . + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectEqualsIsObjectEquals + public override bool Equals(object? obj) + => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + // ReSharper disable once BaseObjectGetHashCodeCallInGetHashCode + public override int GetHashCode() + => base.GetHashCode(); + + #endregion + } +} diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs index 38cf2bbb8c5..170a5f7589b 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionBuilderBase.cs @@ -4,7 +4,6 @@ using System.ComponentModel; using System.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Builders.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs index 1600a7ce60c..423fc49074c 100644 --- a/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/DbFunctionParameterBuilder.cs @@ -4,7 +4,6 @@ using System.ComponentModel; using System.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Builders.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore.Relational/Metadata/Builders/IConventionCheckConstraintBuilder.cs b/src/EFCore.Relational/Metadata/Builders/IConventionCheckConstraintBuilder.cs new file mode 100644 index 00000000000..a159a24bd82 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Builders/IConventionCheckConstraintBuilder.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders +{ + /// + /// Provides a simple API for configuring a check constraint. + /// + public interface IConventionCheckConstraintBuilder : IConventionAnnotatableBuilder + { + /// + /// The check constraint being configured. + /// + new IConventionCheckConstraint Metadata { get; } + + /// + /// Sets the database name of the check constraint. + /// + /// The database name of the check constraint. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + IConventionCheckConstraintBuilder? HasName(string? name, bool fromDataAnnotation = false); + + /// + /// Returns a value indicating whether the given name can be set for the check constraint. + /// + /// The database name of the check constraint. + /// Indicates whether the configuration was specified using a data annotation. + /// if the database name can be set for the check constraint. + bool CanSetName(string? name, bool fromDataAnnotation = false); + } +} diff --git a/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs b/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs index 9297621380a..74ea347e80b 100644 --- a/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs +++ b/src/EFCore.Relational/Metadata/Builders/SequenceBuilder.cs @@ -4,7 +4,6 @@ using System.ComponentModel; using System.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Builders.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs b/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs new file mode 100644 index 00000000000..8a718f23ec3 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/CheckConstraintConvention.cs @@ -0,0 +1,142 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions +{ + /// + /// A convention that ensures that the check constraints on the derived types are compatible with + /// the check constraints on the base type. + /// + public class CheckConstraintConvention : IEntityTypeBaseTypeChangedConvention + { + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public CheckConstraintConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Parameter object containing service dependencies. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Parameter object containing relational service dependencies. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + /// Called after the base type of an entity type changes. + /// + /// The builder for the entity type. + /// The new base entity type. + /// The old base entity type. + /// Additional information associated with convention execution. + public virtual void ProcessEntityTypeBaseTypeChanged( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionEntityType? newBaseType, + IConventionEntityType? oldBaseType, + IConventionContext context) + { + var entityType = entityTypeBuilder.Metadata; + if (newBaseType != null) + { + var configurationSource = entityType.GetBaseTypeConfigurationSource(); + var baseCheckConstraints = newBaseType.GetCheckConstraints().ToDictionary(c => c.ModelName); + List? checkConstraintsToBeDetached = null; + List? checkConstraintsToBeRemoved = null; + foreach (var checkConstraint in entityType.GetDerivedTypesInclusive().SelectMany(et => et.GetDeclaredCheckConstraints())) + { + if (baseCheckConstraints.TryGetValue(checkConstraint.ModelName, out var baseCheckConstraint) + && baseCheckConstraint.GetConfigurationSource().Overrides(checkConstraint.GetConfigurationSource()) + && !AreCompatible(checkConstraint, baseCheckConstraint)) + { + if (baseCheckConstraint.GetConfigurationSource() == ConfigurationSource.Explicit + && configurationSource == ConfigurationSource.Explicit + && checkConstraint.GetConfigurationSource() == ConfigurationSource.Explicit) + { + throw new InvalidOperationException(RelationalStrings.DuplicateCheckConstraint( + checkConstraint.ModelName, + checkConstraint.EntityType.DisplayName(), + baseCheckConstraint.EntityType.DisplayName())); + } + + if (checkConstraintsToBeRemoved == null) + { + checkConstraintsToBeRemoved = new List(); + } + + checkConstraintsToBeRemoved.Add(checkConstraint); + continue; + } + + if (baseCheckConstraint != null) + { + if (checkConstraintsToBeDetached == null) + { + checkConstraintsToBeDetached = new List(); + } + + checkConstraintsToBeDetached.Add(checkConstraint); + } + } + + if (checkConstraintsToBeRemoved != null) + { + foreach (var checkConstraintToBeRemoved in checkConstraintsToBeRemoved) + { + checkConstraintToBeRemoved.EntityType.RemoveCheckConstraint(checkConstraintToBeRemoved.ModelName); + } + } + + if (checkConstraintsToBeDetached != null) + { + foreach (var checkConstraintToBeDetached in checkConstraintsToBeDetached) + { + var baseCheckConstraint = baseCheckConstraints[checkConstraintToBeDetached.ModelName]; + CheckConstraint.Attach(checkConstraintToBeDetached, baseCheckConstraint); + + checkConstraintToBeDetached.EntityType.RemoveCheckConstraint(checkConstraintToBeDetached.ModelName); + } + } + } + } + + private bool AreCompatible(IConventionCheckConstraint checkConstraint, IConventionCheckConstraint baseCheckConstraint) + { + var baseTable = StoreObjectIdentifier.Create(baseCheckConstraint.EntityType, StoreObjectType.Table); + if (baseTable == null) + { + return true; + } + + if (checkConstraint.GetName(baseTable.Value) != baseCheckConstraint.GetName(baseTable.Value) + && checkConstraint.GetNameConfigurationSource() is ConfigurationSource nameConfigurationSource + && !nameConfigurationSource.OverridesStrictly(baseCheckConstraint.GetNameConfigurationSource())) + { + return false; + } + + return CheckConstraint.AreCompatible( + checkConstraint, + baseCheckConstraint, + baseTable.Value, + shouldThrow: false); + } + } +} diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 279648abaae..3782b66037e 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -74,6 +74,7 @@ public override ConventionSet CreateConventionSet() new RelationalValueGenerationConvention(Dependencies, RelationalDependencies); ReplaceConvention(conventionSet.EntityTypeBaseTypeChangedConventions, valueGenerationConvention); conventionSet.EntityTypeBaseTypeChangedConventions.Add(tableNameFromDbSetConvention); + conventionSet.EntityTypeBaseTypeChangedConventions.Add(new CheckConstraintConvention(Dependencies, RelationalDependencies)); conventionSet.EntityTypeAnnotationChangedConventions.Add((RelationalValueGenerationConvention)valueGenerationConvention); diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs index bd2a118645e..9df97d4313b 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalRuntimeModelConvention.cs @@ -119,21 +119,7 @@ protected override void ProcessEntityTypeAnnotations( } else { - if (annotations.TryGetValue(RelationalAnnotationNames.CheckConstraints, out var constraints)) - { - var runtimeCheckConstraints = new SortedDictionary(); - foreach (var constraintPair in (SortedDictionary?)constraints!) - { - var runtimeCheckConstraint = Create(constraintPair.Value, runtimeEntityType); - runtimeCheckConstraints[constraintPair.Key] = runtimeCheckConstraint; - - CreateAnnotations(constraintPair.Value, runtimeCheckConstraint, static (convention, annotations, source, target, runtime) => - convention.ProcessCheckConstraintAnnotations(annotations, source, target, runtime)); - } - - annotations[RelationalAnnotationNames.CheckConstraints] = runtimeCheckConstraints; - } - + annotations.Remove(RelationalAnnotationNames.CheckConstraints); annotations.Remove(RelationalAnnotationNames.Comment); annotations.Remove(RelationalAnnotationNames.IsTableExcludedFromMigrations); @@ -244,27 +230,6 @@ protected virtual void ProcessSequenceAnnotations( { } - private RuntimeCheckConstraint Create(ICheckConstraint checkConstraint, RuntimeEntityType runtimeEntityType) - => new RuntimeCheckConstraint( - checkConstraint.Name, - runtimeEntityType, - checkConstraint.Sql); - - /// - /// Updates the check constraint annotations that will be set on the read-only object. - /// - /// The annotations to be processed. - /// The source check constraint. - /// The target check constraint that will contain the annotations. - /// Indicates whether the given annotations are runtime annotations. - protected virtual void ProcessCheckConstraintAnnotations( - Dictionary annotations, - ICheckConstraint checkConstraint, - RuntimeCheckConstraint runtimeCheckConstraint, - bool runtime) - { - } - /// /// Updates the property annotations that will be set on the read-only object. /// diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index a52d0a5011a..d872354bb14 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -44,10 +44,11 @@ public virtual void ProcessModelFinalizing( TryUniquifyTableNames(modelBuilder.Metadata, tables, maxLength); - var columns = new Dictionary(StringComparer.Ordinal); - var keys = new Dictionary(StringComparer.Ordinal); - var foreignKeys = new Dictionary(StringComparer.Ordinal); - var indexes = new Dictionary(StringComparer.Ordinal); + var columns = new Dictionary(); + var keys = new Dictionary(); + var foreignKeys = new Dictionary(); + var indexes = new Dictionary(); + var checkConstraints = new Dictionary<(string, string?), IConventionCheckConstraint>(); foreach (var table in tables) { columns.Clear(); @@ -61,6 +62,7 @@ public virtual void ProcessModelFinalizing( TryUniquifyKeyNames(entityType, keys, storeObject, maxLength); TryUniquifyForeignKeyNames(entityType, foreignKeys, storeObject, maxLength); TryUniquifyIndexNames(entityType, indexes, storeObject, maxLength); + TryUniquifyCheckConstraintNames(entityType, checkConstraints, storeObject, maxLength); } } } @@ -482,5 +484,71 @@ protected virtual bool AreCompatible( return null; } + + private void TryUniquifyCheckConstraintNames( + IConventionEntityType entityType, + Dictionary<(string, string?), IConventionCheckConstraint> checkConstraints, + in StoreObjectIdentifier storeObject, + int maxLength) + { + foreach (var checkConstraint in entityType.GetDeclaredCheckConstraints()) + { + var constraintName = checkConstraint.GetName(storeObject); + if (!checkConstraints.TryGetValue((constraintName, storeObject.Schema), out var otherCheckConstraint)) + { + checkConstraints[(constraintName, storeObject.Schema)] = checkConstraint; + continue; + } + + if (AreCompatible(checkConstraint, otherCheckConstraint, storeObject)) + { + continue; + } + + var newConstraintName = TryUniquify(checkConstraint, constraintName, storeObject.Schema, checkConstraints, maxLength); + if (newConstraintName != null) + { + checkConstraints[(newConstraintName, storeObject.Schema)] = checkConstraint; + continue; + } + + var newOtherConstraintName = TryUniquify(otherCheckConstraint, constraintName, storeObject.Schema, checkConstraints, maxLength); + if (newOtherConstraintName != null) + { + checkConstraints[(constraintName, storeObject.Schema)] = checkConstraint; + checkConstraints[(newOtherConstraintName, storeObject.Schema)] = otherCheckConstraint; + } + } + } + + /// + /// Gets a value indicating whether two check constraints with the same name are compatible. + /// + /// An check constraints. + /// Another check constraints. + /// The identifier of the store object. + /// if compatible + protected virtual bool AreCompatible( + IReadOnlyCheckConstraint checkConstraint, + IReadOnlyCheckConstraint duplicateCheckConstraint, + in StoreObjectIdentifier storeObject) + => CheckConstraint.AreCompatible(checkConstraint, duplicateCheckConstraint, storeObject, shouldThrow: false); + + private static string? TryUniquify( + IConventionCheckConstraint checkConstraint, + string checkConstraintName, + string? schema, + Dictionary<(string, string?), T> checkConstraints, + int maxLength) + { + if (checkConstraint.Builder.CanSetName(null)) + { + checkConstraintName = Uniquifier.Uniquify(checkConstraintName, checkConstraints, n => (n, schema), maxLength); + checkConstraint.Builder.HasName(checkConstraintName); + return checkConstraintName; + } + + return null; + } } } diff --git a/src/EFCore.Relational/Metadata/ICheckConstraint.cs b/src/EFCore.Relational/Metadata/ICheckConstraint.cs index f5c391aa799..3ace8dd7fc4 100644 --- a/src/EFCore.Relational/Metadata/ICheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/ICheckConstraint.cs @@ -7,7 +7,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata { /// - /// Represents a check constraint in the . + /// Represents a check constraint on the entity type. /// public interface ICheckConstraint : IReadOnlyCheckConstraint, IAnnotatable { @@ -37,7 +37,7 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt .Append(indentString) .Append("Check: "); - builder.Append(Name) + builder.Append(ModelName) .Append(" \"") .Append(Sql) .Append('"'); diff --git a/src/EFCore.Relational/Metadata/IConventionCheckConstraint.cs b/src/EFCore.Relational/Metadata/IConventionCheckConstraint.cs index fbae9a108ab..295e60c19ec 100644 --- a/src/EFCore.Relational/Metadata/IConventionCheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/IConventionCheckConstraint.cs @@ -1,13 +1,22 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + namespace Microsoft.EntityFrameworkCore.Metadata { /// - /// Represents a check constraint in the . + /// Represents a check constraint on the entity type. /// public interface IConventionCheckConstraint : IReadOnlyCheckConstraint, IConventionAnnotatable { + /// + /// Gets the builder that can be used to configure this check constraint. + /// + /// If the check constraint has been removed from the model. + new IConventionCheckConstraintBuilder Builder { get; } + /// /// Gets the entity type on which this check constraint is defined. /// @@ -18,5 +27,19 @@ public interface IConventionCheckConstraint : IReadOnlyCheckConstraint, IConvent /// /// The configuration source for this check constraint. ConfigurationSource GetConfigurationSource(); + + /// + /// Sets the name of the check constraint in the database. + /// + /// The name of the check constraint in the database. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + string? SetName(string? name, bool fromDataAnnotation = false); + + /// + /// Gets the configuration source for the database name. + /// + /// The configuration source for the database name. + ConfigurationSource? GetNameConfigurationSource(); } } diff --git a/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs b/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs index 9e7bbe3f659..b630702178d 100644 --- a/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/IMutableCheckConstraint.cs @@ -4,7 +4,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata { /// - /// Represents a check constraint in the . + /// Represents a check constraint on the entity type. /// public interface IMutableCheckConstraint : IReadOnlyCheckConstraint, IMutableAnnotatable { @@ -12,5 +12,10 @@ public interface IMutableCheckConstraint : IReadOnlyCheckConstraint, IMutableAnn /// Gets the entity type on which this check constraint is defined. /// new IMutableEntityType EntityType { get; } + + /// + /// Gets or sets the name of the check constraint in the database. + /// + new string Name { get; set; } } } diff --git a/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs b/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs index 269587dd487..533247aae48 100644 --- a/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/IReadOnlyCheckConstraint.cs @@ -6,15 +6,52 @@ namespace Microsoft.EntityFrameworkCore.Metadata { /// - /// Represents a check constraint in the . + /// Represents a check constraint on the entity type. /// public interface IReadOnlyCheckConstraint : IReadOnlyAnnotatable { /// - /// Gets the name of the check constraint in the database. + /// Gets the name of the check constraint in the model. + /// + string ModelName { get; } + + /// + /// Gets the database name of the check constraint. /// string Name { get; } + /// + /// Returns the default database name that would be used for this check constraint. + /// + /// The default name that would be used for this check constraint. + string? GetDefaultName() + { + var table = StoreObjectIdentifier.Create(EntityType, StoreObjectType.Table); + return !table.HasValue ? null : GetDefaultName(table.Value); + } + + /// + /// Gets the database name of the check constraint. + /// + /// The identifier of the store object. + /// The database name of the check constraint for the given store object. + string GetName(in StoreObjectIdentifier storeObject); + + /// + /// Returns the default database name that would be used for this check constraint. + /// + /// The identifier of the store object. + /// The default name that would be used for this check constraint. + string GetDefaultName(in StoreObjectIdentifier storeObject) + { + var prefix = $"CK_{storeObject.Name}_"; + return Uniquifier.Truncate( + ModelName.StartsWith(prefix, System.StringComparison.Ordinal) + ? ModelName + : prefix + ModelName, + EntityType.Model.GetMaxIdentifierLength()); + } + /// /// Gets the entity type on which this check constraint is defined. /// diff --git a/src/EFCore.Relational/Metadata/ITable.cs b/src/EFCore.Relational/Metadata/ITable.cs index 91115a416a6..d836f5eda89 100644 --- a/src/EFCore.Relational/Metadata/ITable.cs +++ b/src/EFCore.Relational/Metadata/ITable.cs @@ -54,7 +54,7 @@ public interface ITable : ITableBase /// Gets the check constraints for this table. /// IEnumerable CheckConstraints - => EntityTypeMappings.SelectMany(m => CheckConstraint.GetCheckConstraints(m.EntityType)) + => EntityTypeMappings.SelectMany(m => m.EntityType.GetDeclaredCheckConstraints()) .Distinct((x, y) => x!.Name == y!.Name); /// diff --git a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs index dbcff4117dd..38e1fb297d6 100644 --- a/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs +++ b/src/EFCore.Relational/Metadata/Internal/CheckConstraint.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Internal @@ -19,7 +20,11 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal /// public class CheckConstraint : ConventionAnnotatable, IMutableCheckConstraint, IConventionCheckConstraint, ICheckConstraint { + private string? _name; + private InternalCheckConstraintBuilder? _builder; + private ConfigurationSource _configurationSource; + private ConfigurationSource? _nameConfigurationSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -38,7 +43,7 @@ public CheckConstraint( Check.NotEmpty(sql, nameof(sql)); EntityType = entityType; - Name = name; + ModelName = name; Sql = sql; _configurationSource = configurationSource; @@ -49,14 +54,34 @@ public CheckConstraint( ((IMutableEntityType)EntityType).SetOrRemoveAnnotation(RelationalAnnotationNames.CheckConstraints, constraints); } - if (constraints.ContainsKey(Name)) + if (constraints.ContainsKey(name)) + { + throw new InvalidOperationException(RelationalStrings.DuplicateCheckConstraint( + name, EntityType.DisplayName(), EntityType.DisplayName())); + } + + var baseCheckConstraint = entityType.BaseType?.FindCheckConstraint(name); + if (baseCheckConstraint != null) { - throw new InvalidOperationException(RelationalStrings.DuplicateCheckConstraint(Name, EntityType.DisplayName())); + throw new InvalidOperationException(RelationalStrings.DuplicateCheckConstraint( + name, EntityType.DisplayName(), baseCheckConstraint.EntityType.DisplayName())); + } + + foreach (var derivedType in entityType.GetDerivedTypes()) + { + var derivedCheckConstraint = FindCheckConstraint(derivedType, name); + if (derivedCheckConstraint != null) + { + throw new InvalidOperationException(RelationalStrings.DuplicateCheckConstraint( + name, EntityType.DisplayName(), derivedCheckConstraint.EntityType.DisplayName())); + } } EnsureMutable(); constraints.Add(name, this); + + _builder = new InternalCheckConstraintBuilder(this, ((IConventionModel)entityType.Model).Builder); } /// @@ -65,9 +90,13 @@ public CheckConstraint( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static IEnumerable GetCheckConstraints(IReadOnlyEntityType entityType) + public static IEnumerable GetDeclaredCheckConstraints(IReadOnlyEntityType entityType) { Check.NotNull(entityType, nameof(entityType)); + if (entityType is RuntimeEntityType) + { + throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); + } return GetConstraintsDictionary(entityType)?.Values ?? Enumerable.Empty(); } @@ -78,12 +107,32 @@ public static IEnumerable GetCheckConstraints(IReadOnlyEntityT /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static ICheckConstraint? FindCheckConstraint( + public static IEnumerable GetCheckConstraints(IReadOnlyEntityType entityType) + => entityType.BaseType != null + ? GetCheckConstraints(entityType.BaseType).Concat(GetDeclaredCheckConstraints(entityType)) + : GetDeclaredCheckConstraints(entityType); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyCheckConstraint? FindCheckConstraint( IReadOnlyEntityType entityType, string name) + => entityType.BaseType?.FindCheckConstraint(name) + ?? FindDeclaredCheckConstraint(entityType, name); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyCheckConstraint? FindDeclaredCheckConstraint(IReadOnlyEntityType entityType, string name) { var dataDictionary = GetConstraintsDictionary(entityType); - return dataDictionary == null ? null : dataDictionary.TryGetValue(name, out var checkConstraint) @@ -97,7 +146,7 @@ public static IEnumerable GetCheckConstraints(IReadOnlyEntityT /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public static CheckConstraint? RemoveCheckConstraint( + public static IMutableCheckConstraint? RemoveCheckConstraint( IMutableEntityType entityType, string name) { @@ -108,6 +157,7 @@ public static IEnumerable GetCheckConstraints(IReadOnlyEntityT { var checkConstraint = (CheckConstraint)constraint; checkConstraint.EnsureMutable(); + checkConstraint.SetRemovedFromModel(); dataDictionary.Remove(name); return checkConstraint; @@ -116,6 +166,83 @@ public static IEnumerable GetCheckConstraints(IReadOnlyEntityT return null; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static void Attach(IConventionCheckConstraint detachedCheckConstraint, IConventionCheckConstraint existingCheckConstraint) + { + var nameConfigurationSource = detachedCheckConstraint.GetNameConfigurationSource(); + if (nameConfigurationSource != null) + { + ((InternalCheckConstraintBuilder)existingCheckConstraint.Builder).HasName( + detachedCheckConstraint.Name, nameConfigurationSource.Value); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static bool AreCompatible( + IReadOnlyCheckConstraint checkConstraint, + IReadOnlyCheckConstraint duplicateCheckConstraint, + in StoreObjectIdentifier storeObject, + bool shouldThrow) + { + if (checkConstraint.Sql != duplicateCheckConstraint.Sql) + { + if (shouldThrow) + { + throw new InvalidOperationException( + RelationalStrings.DuplicateCheckConstraintSqlMismatch( + checkConstraint.ModelName, + checkConstraint.EntityType.DisplayName(), + duplicateCheckConstraint.ModelName, + duplicateCheckConstraint.EntityType.DisplayName(), + checkConstraint.GetName(storeObject))); + } + + return false; + } + + return true; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalCheckConstraintBuilder Builder + { + [DebuggerStepThrough] + get => _builder ?? throw new InvalidOperationException(CoreStrings.ObjectRemovedFromModel); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsInModel + => _builder is not null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void SetRemovedFromModel() + => _builder = null; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -135,7 +262,51 @@ public static IEnumerable GetCheckConstraints(IReadOnlyEntityT /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual string Name { get; } + public virtual string ModelName { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string Name + { + get => _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName() ?? ModelName; + set => SetName(value, ConfigurationSource.Explicit); + } + + /// + public virtual string GetName(in StoreObjectIdentifier storeObject) + => _name ?? ((IReadOnlyCheckConstraint)this).GetDefaultName(storeObject) ?? ModelName; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual string? SetName(string? name, ConfigurationSource configurationSource) + { + Check.NullButNotEmpty(name, nameof(name)); + + EnsureMutable(); + + _name = name; + + _nameConfigurationSource = configurationSource.Max(_nameConfigurationSource); + + return name; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ConfigurationSource? GetNameConfigurationSource() + => _nameConfigurationSource; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -212,5 +383,27 @@ IEntityType ICheckConstraint.EntityType [DebuggerStepThrough] get => (IEntityType)EntityType; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + IConventionCheckConstraintBuilder IConventionCheckConstraint.Builder + { + [DebuggerStepThrough] + get => Builder; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [DebuggerStepThrough] + string? IConventionCheckConstraint.SetName(string? name, bool fromDataAnnotation) + => SetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); } } diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 482db686a45..ea478a9bdb0 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Metadata.Builders.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs index aa0cf60b435..e3d03600109 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunctionParameter.cs @@ -7,7 +7,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Metadata.Builders.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs new file mode 100644 index 00000000000..c87ade75408 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Internal/InternalCheckConstraintBuilder.cs @@ -0,0 +1,202 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Metadata.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class InternalCheckConstraintBuilder : + AnnotatableBuilder, + IConventionCheckConstraintBuilder + { + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public InternalCheckConstraintBuilder(CheckConstraint checkConstraint, IConventionModelBuilder modelBuilder) + : base(checkConstraint, modelBuilder) + { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IConventionCheckConstraintBuilder? HasName(string? name, ConfigurationSource configurationSource) + { + if (CanSetName(name, configurationSource)) + { + Metadata.SetName(name, configurationSource); + return this; + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool CanSetName(string? name, ConfigurationSource configurationSource) + => configurationSource.Overrides(Metadata.GetNameConfigurationSource()) + || Metadata.Name == name; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IConventionCheckConstraint? HasCheckConstraint( + IConventionEntityType entityType, + string name, + string? sql, + ConfigurationSource configurationSource) + { + Check.NotEmpty(name, nameof(name)); + Check.NullButNotEmpty(sql, nameof(sql)); + + List? checkConstraintsToBeDetached = null; + var constraint = entityType.FindCheckConstraint(name); + if (constraint != null) + { + if (constraint.Sql == sql) + { + ((CheckConstraint)constraint).UpdateConfigurationSource(configurationSource); + return constraint; + } + + if (!configurationSource.Overrides(constraint.GetConfigurationSource())) + { + return null; + } + + entityType.RemoveCheckConstraint(name); + constraint = null; + } + else + { + foreach (var derivedType in entityType.GetDerivedTypes()) + { + var derivedCheckConstraint = (IConventionCheckConstraint?)CheckConstraint.FindDeclaredCheckConstraint(derivedType, name); + if (derivedCheckConstraint == null) + { + continue; + } + + if (derivedCheckConstraint.Sql != sql + && !configurationSource.Overrides(derivedCheckConstraint.GetConfigurationSource())) + { + return null; + } + + if (checkConstraintsToBeDetached == null) + { + checkConstraintsToBeDetached = new List(); + } + + checkConstraintsToBeDetached.Add(derivedCheckConstraint); + } + } + + List? detachedCheckConstraints = null; + if (checkConstraintsToBeDetached != null) + { + detachedCheckConstraints = new List(); + foreach (var checkConstraintToBeDetached in checkConstraintsToBeDetached) + { + detachedCheckConstraints.Add( + checkConstraintToBeDetached.EntityType.RemoveCheckConstraint(checkConstraintToBeDetached.ModelName)!); + } + } + + if (sql != null) + { + constraint = new CheckConstraint((IMutableEntityType)entityType, name, sql, configurationSource); + + if (detachedCheckConstraints != null) + { + foreach (var detachedCheckConstraint in detachedCheckConstraints) + { + CheckConstraint.Attach(detachedCheckConstraint, constraint); + } + } + } + + return constraint; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static bool CanHaveCheckConstraint( + IConventionEntityType entityType, + string name, + string? sql, + ConfigurationSource configurationSource) + { + Check.NotEmpty(name, nameof(name)); + Check.NullButNotEmpty(sql, nameof(sql)); + + var constraint = entityType.FindCheckConstraint(name); + if (constraint != null) + { + return constraint.Sql == sql + || configurationSource.Overrides(constraint.GetConfigurationSource()); + } + + foreach (var derivedType in entityType.GetDerivedTypes()) + { + var derivedCheckConstraint = (IConventionCheckConstraint?)CheckConstraint.FindDeclaredCheckConstraint(derivedType, name); + if (derivedCheckConstraint == null) + { + continue; + } + + if (derivedCheckConstraint.Sql != sql + && !configurationSource.Overrides(derivedCheckConstraint.GetConfigurationSource())) + { + return false; + } + } + + return true; + } + + IConventionCheckConstraint IConventionCheckConstraintBuilder.Metadata + { + [DebuggerStepThrough] + get => Metadata; + } + + /// + [DebuggerStepThrough] + IConventionCheckConstraintBuilder? IConventionCheckConstraintBuilder.HasName(string? name, bool fromDataAnnotation) + => HasName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + + /// + [DebuggerStepThrough] + bool IConventionCheckConstraintBuilder.CanSetName(string? name, bool fromDataAnnotation) + => CanSetName(name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); + } +} diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs index cd9eecbd5f0..c69876f85ae 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs @@ -7,11 +7,12 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; -namespace Microsoft.EntityFrameworkCore.Metadata.Builders.Internal +namespace Microsoft.EntityFrameworkCore.Metadata.Internal { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs index 7307a696573..80f171f952d 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionParameterBuilder.cs @@ -3,10 +3,10 @@ using System.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage; -namespace Microsoft.EntityFrameworkCore.Metadata.Builders.Internal +namespace Microsoft.EntityFrameworkCore.Metadata.Internal { /// /// diff --git a/src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs index 68461572268..2cddee782ae 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalSequenceBuilder.cs @@ -5,9 +5,9 @@ using System.Diagnostics; using System.Linq; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Microsoft.EntityFrameworkCore.Metadata.Builders.Internal +namespace Microsoft.EntityFrameworkCore.Metadata.Internal { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 960d97c54c8..891a0d3d8da 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -182,9 +182,12 @@ public static IRelationalModel Create( constraint.AddAnnotations(relationalAnnotationProvider.For(constraint, designTime)); } - foreach (var checkConstraint in ((ITable)table).CheckConstraints) + if (designTime) { - ((AnnotatableBase)checkConstraint).AddAnnotations(relationalAnnotationProvider.For(checkConstraint, designTime)); + foreach (var checkConstraint in ((ITable)table).CheckConstraints) + { + ((AnnotatableBase)checkConstraint).AddAnnotations(relationalAnnotationProvider.For(checkConstraint, designTime)); + } } table.AddAnnotations(relationalAnnotationProvider.For(table, designTime)); diff --git a/src/EFCore.Relational/Metadata/Internal/Sequence.cs b/src/EFCore.Relational/Metadata/Internal/Sequence.cs index a9133fefabc..460a4be9658 100644 --- a/src/EFCore.Relational/Metadata/Internal/Sequence.cs +++ b/src/EFCore.Relational/Metadata/Internal/Sequence.cs @@ -10,7 +10,6 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Metadata.Builders.Internal; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Metadata.Internal diff --git a/src/EFCore.Relational/Metadata/RuntimeCheckConstraint.cs b/src/EFCore.Relational/Metadata/RuntimeCheckConstraint.cs deleted file mode 100644 index 29ef96876e4..00000000000 --- a/src/EFCore.Relational/Metadata/RuntimeCheckConstraint.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace Microsoft.EntityFrameworkCore.Metadata -{ - /// - /// Represents a check constraint in the . - /// - public class RuntimeCheckConstraint : AnnotatableBase, ICheckConstraint - { - /// - /// Initializes a new instance of the class. - /// - /// The constraint name. - /// The affected entity type. - /// The SQL string. - public RuntimeCheckConstraint( - string name, - RuntimeEntityType entityType, - string sql) - { - EntityType = entityType; - Name = name; - Sql = sql; - } - - /// - /// Gets the entity type on which this check constraint is defined. - /// - public virtual RuntimeEntityType EntityType { get; } - - /// - /// Gets the name of the check constraint in the database. - /// - public virtual string Name { get; } - - private string Sql { get; } - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - public override string ToString() - => ((ICheckConstraint)this).ToDebugString(MetadataDebugStringOptions.SingleLineDefault); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - [EntityFrameworkInternal] - public virtual DebugView DebugView - => new( - () => ((ICheckConstraint)this).ToDebugString(MetadataDebugStringOptions.ShortDefault), - () => ((ICheckConstraint)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); - - /// - IReadOnlyEntityType IReadOnlyCheckConstraint.EntityType - { - [DebuggerStepThrough] - get => EntityType; - } - - /// - IEntityType ICheckConstraint.EntityType - { - [DebuggerStepThrough] - get => EntityType; - } - - /// - string IReadOnlyCheckConstraint.Sql - { - [DebuggerStepThrough] - get => Sql; - } - } -} diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 7c27804de4c..eb95cd991a4 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -310,12 +310,20 @@ public static string DistinctOnCollectionNotSupported => GetString("DistinctOnCollectionNotSupported"); /// - /// The check constraint '{checkConstraint}' cannot be added to the entity type '{entityType}' because another check constraint with the same name already exists. + /// The check constraint '{checkConstraint}' cannot be added to the entity type '{entityType}' because another check constraint with the same name already exists on entity type '{conflictingEntityType}'. /// - public static string DuplicateCheckConstraint(object? checkConstraint, object? entityType) + public static string DuplicateCheckConstraint(object? checkConstraint, object? entityType, object? conflictingEntityType) => string.Format( - GetString("DuplicateCheckConstraint", nameof(checkConstraint), nameof(entityType)), - checkConstraint, entityType); + GetString("DuplicateCheckConstraint", nameof(checkConstraint), nameof(entityType), nameof(conflictingEntityType)), + checkConstraint, entityType, conflictingEntityType); + + /// + /// The check constraints '{checkConstraint1}' on '{entityType1}' and '{checkConstraint2}' on '{entityType2}' are both mapped to '{checkConstraintName}', but with different defining SQL. + /// + public static string DuplicateCheckConstraintSqlMismatch(object? checkConstraint1, object? entityType1, object? checkConstraint2, object? entityType2, object? checkConstraintName) + => string.Format( + GetString("DuplicateCheckConstraintSqlMismatch", nameof(checkConstraint1), nameof(entityType1), nameof(checkConstraint2), nameof(entityType2), nameof(checkConstraintName)), + checkConstraint1, entityType1, checkConstraint2, entityType2, checkConstraintName); /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use different collations ('{collation1}' and '{collation2}'). diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index ac1def7f471..d465a7c314f 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -232,7 +232,10 @@ Using 'Distinct' operation on a projection containing a collection is not supported. - The check constraint '{checkConstraint}' cannot be added to the entity type '{entityType}' because another check constraint with the same name already exists. + The check constraint '{checkConstraint}' cannot be added to the entity type '{entityType}' because another check constraint with the same name already exists on entity type '{conflictingEntityType}'. + + + The check constraints '{checkConstraint1}' on '{entityType1}' and '{checkConstraint2}' on '{entityType2}' are both mapped to '{checkConstraintName}', but with different defining SQL. '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use different collations ('{collation1}' and '{collation2}'). diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index c0c667a6180..7b5b72695b4 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -631,7 +631,7 @@ public virtual void CheckConstraint_is_stored_in_snapshot_as_fluent_api() builder => { builder.Entity() - .HasCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + .HasCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id", ck => ck.HasName("CK_Customer_AlternateId")); builder.Ignore(); }, AddBoilerPlate( @@ -651,11 +651,12 @@ public virtual void CheckConstraint_is_stored_in_snapshot_as_fluent_api() b.ToTable(""EntityWithTwoProperties""); - b.HasCheckConstraint(""CK_Customer_AlternateId"", ""AlternateId > Id""); + b.HasCheckConstraint(""CK_Customer_AlternateId"", ""AlternateId > Id"", c => c.HasName(""CK_Customer_AlternateId"")); });"), o => { - Assert.Equal(2, o.GetAnnotations().Count()); + var constraint = o.GetEntityTypes().Single().GetCheckConstraints().Single(); + Assert.Equal("CK_Customer_AlternateId", constraint.Name); }); } @@ -666,7 +667,7 @@ public virtual void CheckConstraint_is_only_stored_in_snapshot_once_for_TPH() builder => { builder.Entity() - .HasCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + .HasCheckConstraint("CK_BaseEntity_AlternateId", "AlternateId > Id"); builder.Entity(); }, AddBoilerPlate( @@ -699,11 +700,12 @@ public virtual void CheckConstraint_is_only_stored_in_snapshot_once_for_TPH() b.HasDiscriminator().HasValue(""DerivedEntity""); - b.HasCheckConstraint(""CK_Customer_AlternateId"", ""AlternateId > Id""); + b.HasCheckConstraint(""CK_BaseEntity_AlternateId"", ""AlternateId > Id""); });"), o => { - Assert.Equal(2, o.GetAnnotations().Count()); + var constraint = o.FindEntityType(typeof(DerivedEntity)).GetDeclaredCheckConstraints().Single(); + Assert.Equal("CK_BaseEntity_AlternateId", constraint.Name); }); } diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs index fa1c072dab4..8a42fc3a8d9 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpRuntimeModelCodeGeneratorTest.cs @@ -2600,7 +2600,6 @@ partial void Initialize() c => AssertFileContents("DataEntityType.cs", @"// using System; -using System.Collections.Generic; using System.Reflection; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Scaffolding.Internal; @@ -2643,22 +2642,6 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) { - var constraints = new SortedDictionary(); - var anotherConstraint = new RuntimeCheckConstraint( - ""anotherConstraint"", - runtimeEntityType, - ""Id <> -1""); - - constraints[""anotherConstraint""] = anotherConstraint; - - var idConstraint = new RuntimeCheckConstraint( - ""idConstraint"", - runtimeEntityType, - ""Id <> 0""); - - constraints[""idConstraint""] = idConstraint; - - runtimeEntityType.AddAnnotation(""Relational:CheckConstraints"", constraints); runtimeEntityType.AddAnnotation(""Relational:FunctionName"", null); runtimeEntityType.AddAnnotation(""Relational:Schema"", null); runtimeEntityType.AddAnnotation(""Relational:SqlQuery"", null); @@ -2676,21 +2659,11 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) c)), model => { - Assert.Single(model.GetEntityTypes()); - var dataEntity = model.FindEntityType(typeof(Data)); + var dataEntity = model.GetEntityTypes().Single(); - Assert.Equal(2, dataEntity.GetCheckConstraints().Count()); - var anotherConstraint = dataEntity.GetCheckConstraints().First(); - Assert.Same(dataEntity, anotherConstraint.EntityType); - Assert.Equal("anotherConstraint", anotherConstraint.Name); - Assert.Equal("Id <> -1", anotherConstraint.Sql); - Assert.NotNull(anotherConstraint.ToString()); - - var idConstraint = dataEntity.GetCheckConstraints().Last(); - Assert.Same(dataEntity, ((IReadOnlyCheckConstraint)idConstraint).EntityType); - Assert.Equal("idConstraint", idConstraint.Name); - Assert.Equal("Id <> 0", idConstraint.Sql); - Assert.NotNull(idConstraint.ToString()); + Assert.Equal( + CoreStrings.RuntimeModelMissingData, + Assert.Throws(() => dataEntity.GetCheckConstraints()).Message); }); } diff --git a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs index c7420cfeb02..1ee02252375 100644 --- a/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Migrations/MigrationsTestBase.cs @@ -89,7 +89,7 @@ await Test( e.HasKey("CustomId"); e.HasAlternateKey("SSN"); - e.HasCheckConstraint("CK_EmployerId", $"{DelimitIdentifier("EmployerId")} > 0"); + e.HasCheckConstraint("EmployerId", $"{DelimitIdentifier("EmployerId")} > 0"); e.HasOne("Employers").WithMany("People").HasForeignKey("EmployerId"); e.HasComment("Table comment"); @@ -639,7 +639,7 @@ public virtual Task Add_column_with_check_constraint() "People", e => { e.Property("DriverLicense"); - e.HasCheckConstraint("CK_Foo", $"{DelimitIdentifier("DriverLicense")} > 0"); + e.HasCheckConstraint("Foo", $"{DelimitIdentifier("DriverLicense")} > 0"); }), model => { @@ -1250,7 +1250,7 @@ public virtual Task Add_check_constraint_with_name() e.Property("DriverLicense"); }), builder => { }, - builder => builder.Entity("People").HasCheckConstraint("CK_Foo", $"{DelimitIdentifier("DriverLicense")} > 0"), + builder => builder.Entity("People").HasCheckConstraint("Foo", $"{DelimitIdentifier("DriverLicense")} > 0"), model => { // TODO: no scaffolding support for check constraints, https://github.com/aspnet/EntityFrameworkCore/issues/15408 @@ -1265,8 +1265,8 @@ public virtual Task Alter_check_constraint() e.Property("Id"); e.Property("DriverLicense"); }), - builder => builder.Entity("People").HasCheckConstraint("CK_Foo", $"{DelimitIdentifier("DriverLicense")} > 0"), - builder => builder.Entity("People").HasCheckConstraint("CK_Foo", $"{DelimitIdentifier("DriverLicense")} > 1"), + builder => builder.Entity("People").HasCheckConstraint("Foo", $"{DelimitIdentifier("DriverLicense")} > 0"), + builder => builder.Entity("People").HasCheckConstraint("Foo", $"{DelimitIdentifier("DriverLicense")} > 1"), model => { // TODO: no scaffolding support for check constraints, https://github.com/aspnet/EntityFrameworkCore/issues/15408 @@ -1281,7 +1281,7 @@ public virtual Task Drop_check_constraint() e.Property("Id"); e.Property("DriverLicense"); }), - builder => builder.Entity("People").HasCheckConstraint("CK_Foo", $"{DelimitIdentifier("DriverLicense")} > 0"), + builder => builder.Entity("People").HasCheckConstraint("Foo", $"{DelimitIdentifier("DriverLicense")} > 0"), builder => { }, model => { diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 632b8741840..9894d169f4b 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -341,7 +341,7 @@ public virtual void Detects_incompatible_primary_keys_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().HasKey(a => a.Id).HasName("Key"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().ToTable("Table"); @@ -357,7 +357,7 @@ public virtual void Detects_incompatible_comments_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table").HasComment("My comment"); modelBuilder.Entity().ToTable("Table").HasComment("my comment"); @@ -372,7 +372,7 @@ public virtual void Passes_on_null_comments() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasPrincipalKey(a => a.Id).HasForeignKey(b => b.Id).IsRequired(); modelBuilder.Entity().ToTable("Table").HasComment("My comment"); modelBuilder.Entity().ToTable("Table"); @@ -384,7 +384,7 @@ public virtual void Detects_incompatible_primary_key_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.Id).ValueGeneratedNever().HasColumnName("Key"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().Property(a => a.Id).ValueGeneratedNever().HasColumnName(nameof(B.Id)); @@ -400,7 +400,7 @@ public virtual void Passes_on_not_configured_shared_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)); modelBuilder.Entity().Property(a => a.P1).IsRequired(); modelBuilder.Entity().ToTable("Table"); @@ -440,7 +440,7 @@ public virtual void Detects_incompatible_shared_columns_with_shared_table() { var modelBuilder = CreateConventionalModelBuilder(); - modelBuilder.Entity().HasOne().WithOne().HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)).HasColumnType("someInt"); modelBuilder.Entity().ToTable("Table"); modelBuilder.Entity().Property(b => b.P0).HasColumnName(nameof(A.P0)).HasColumnType("default_int_mapping"); @@ -452,6 +452,57 @@ public virtual void Detects_incompatible_shared_columns_with_shared_table() modelBuilder); } + [ConditionalFact] + public virtual void Detects_incompatible_shared_check_constraints_with_shared_table() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasCheckConstraint("SomeCK", "Id > 0", c => c.HasName("CK_Table_SomeCK")); + modelBuilder.Entity().ToTable("Table"); + modelBuilder.Entity().HasCheckConstraint("SomeOtherCK", "Id > 10", c => c.HasName("CK_Table_SomeCK")); + modelBuilder.Entity().ToTable("Table"); + + VerifyError( + RelationalStrings.DuplicateCheckConstraintSqlMismatch( + "SomeOtherCK", nameof(B), "SomeCK", nameof(A), "CK_Table_SomeCK"), + modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_for_incompatible_uniquified_check_constraints_with_shared_table() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasCheckConstraint("SomeCK", "Id > 0"); + modelBuilder.Entity().ToTable("Table"); + modelBuilder.Entity().HasCheckConstraint("SomeCK", "Id > 10"); + modelBuilder.Entity().ToTable("Table"); + + var model = Validate(modelBuilder); + + Assert.Equal("CK_Table_SomeCK1", model.FindEntityType(typeof(A)).GetCheckConstraints().Single().Name); + Assert.Equal("CK_Table_SomeCK", model.FindEntityType(typeof(B)).GetCheckConstraints().Single().Name); + } + + [ConditionalFact] + public virtual void Passes_for_compatible_shared_check_constraints_with_shared_table() + { + var modelBuilder = CreateConventionalModelBuilder(); + + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id).IsRequired(); + modelBuilder.Entity().HasCheckConstraint("SomeCK", "Id > 0"); + modelBuilder.Entity().ToTable("Table"); + modelBuilder.Entity().HasCheckConstraint("SomeCK", "Id > 0"); + modelBuilder.Entity().ToTable("Table"); + + var model = Validate(modelBuilder); + + Assert.Equal("CK_Table_SomeCK", model.FindEntityType(typeof(A)).GetCheckConstraints().Single().Name); + Assert.Equal("CK_Table_SomeCK", model.FindEntityType(typeof(B)).GetCheckConstraints().Single().Name); + } + [ConditionalFact] public virtual void Detects_multiple_shared_table_roots() { diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs index 1b98a2cbef5..b659e760638 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalBuilderExtensionsTest.cs @@ -1,11 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; @@ -39,23 +38,6 @@ public void Can_set_fixed_length() Assert.False(property.IsFixedLength()); } - [ConditionalFact] - public void Can_write_index_builder_extension_with_where_clauses() - { - var builder = CreateConventionModelBuilder(); - - var returnedBuilder = builder - .Entity() - .HasIndex(e => e.Id) - .HasFilter("[Id] % 2 = 0"); - - Assert.IsType>(returnedBuilder); - - var model = builder.Model; - var index = model.FindEntityType(typeof(Customer)).GetIndexes().Single(); - Assert.Equal("[Id] % 2 = 0", index.GetFilter()); - } - [ConditionalFact] public void Can_set_column_name() { @@ -251,18 +233,21 @@ public void Default_alternate_key_name_is_based_on_key_column_names() } [ConditionalFact] - public void Can_set_key_name() + public void Can_access_key() { - var modelBuilder = CreateConventionModelBuilder(); + var modelBuilder = CreateBuilder(); + var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); + var idProperty = entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention).Metadata; + var keyBuilder = entityTypeBuilder.HasKey(new[] { idProperty.Name }, ConfigurationSource.Convention); - modelBuilder - .Entity() - .HasKey(e => e.Id) - .HasName("KeyLimePie"); + Assert.NotNull(keyBuilder.HasName("Splew")); + Assert.Equal("Splew", keyBuilder.Metadata.GetName()); - var key = modelBuilder.Model.FindEntityType(typeof(Customer)).FindPrimaryKey(); + Assert.NotNull(keyBuilder.HasName("Splow", fromDataAnnotation: true)); + Assert.Equal("Splow", keyBuilder.Metadata.GetName()); - Assert.Equal("KeyLimePie", key.GetName()); + Assert.Null(keyBuilder.HasName("Splod")); + Assert.Equal("Splow", keyBuilder.Metadata.GetName()); } [ConditionalFact] @@ -394,6 +379,47 @@ public void Can_set_foreign_key_name_for_one_to_one_with_FK_specified() Assert.Equal("LemonSupreme", foreignKey.GetConstraintName()); } + [ConditionalFact] + public void Can_access_index() + { + var modelBuilder = CreateBuilder(); + var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); + entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention); + var indexBuilder = entityTypeBuilder.HasIndex(new[] { "Id" }, ConfigurationSource.Convention); + +#pragma warning disable CS0618 // Type or member is obsolete + Assert.NotNull(indexBuilder.HasName("Splew")); + Assert.Equal("Splew", indexBuilder.Metadata.GetName()); + + Assert.NotNull(indexBuilder.HasName("Splow", fromDataAnnotation: true)); + Assert.Equal("Splow", indexBuilder.Metadata.GetName()); + + Assert.Null(indexBuilder.HasName("Splod")); + Assert.Equal("Splow", indexBuilder.Metadata.GetName()); + + Assert.NotNull(indexBuilder.HasName(null, fromDataAnnotation: true)); + Assert.Equal("IX_Splot_Id", indexBuilder.Metadata.GetName()); + + Assert.NotNull(indexBuilder.HasName("Splod")); + Assert.Equal("Splod", indexBuilder.Metadata.GetName()); +#pragma warning restore CS0618 // Type or member is obsolete + + Assert.NotNull(indexBuilder.HasFilter("Splew")); + Assert.Equal("Splew", indexBuilder.Metadata.GetFilter()); + + Assert.NotNull(indexBuilder.HasFilter("Splow", fromDataAnnotation: true)); + Assert.Equal("Splow", indexBuilder.Metadata.GetFilter()); + + Assert.Null(indexBuilder.HasFilter("Splod")); + Assert.Equal("Splow", indexBuilder.Metadata.GetFilter()); + + Assert.NotNull(indexBuilder.HasFilter(null, fromDataAnnotation: true)); + Assert.Null(indexBuilder.Metadata.GetFilter()); + + Assert.Null(indexBuilder.HasFilter("Splod")); + Assert.Null(indexBuilder.Metadata.GetFilter()); + } + [ConditionalFact] public void Default_index_database_name_is_based_on_index_column_names() { @@ -431,67 +457,76 @@ public void Can_set_index_database_name() } [ConditionalFact] - public void Can_set_table_name() + public void Can_write_index_filter_with_where_clauses() { - var modelBuilder = CreateConventionModelBuilder(); + var builder = CreateConventionModelBuilder(); - modelBuilder + var returnedBuilder = builder .Entity() - .ToTable("Customizer"); + .HasIndex(e => e.Id) + .HasFilter("[Id] % 2 = 0"); - var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); + Assert.IsType>(returnedBuilder); - Assert.Equal("Customer", entityType.DisplayName()); - Assert.Equal("Customizer", entityType.GetTableName()); + var model = builder.Model; + var index = model.FindEntityType(typeof(Customer)).GetIndexes().Single(); + Assert.Equal("[Id] % 2 = 0", index.GetFilter()); } [ConditionalFact] - public void Can_set_table_name_non_generic() + public void Can_set_table_name() { - var modelBuilder = CreateConventionModelBuilder(); + var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - modelBuilder - .Entity(typeof(Customer)) - .ToTable("Customizer"); + Assert.NotNull(typeBuilder.ToTable("Splew")); + Assert.Equal("Splew", typeBuilder.Metadata.GetTableName()); - var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); + Assert.NotNull(typeBuilder.ToTable("Splow", fromDataAnnotation: true)); + Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); - Assert.Equal("Customer", entityType.DisplayName()); - Assert.Equal("Customizer", entityType.GetTableName()); + Assert.Null(typeBuilder.ToTable("Splod")); + Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); } [ConditionalFact] - public void Can_set_table_and_schema_name() + public void Can_set_table_name_and_schema() { - var modelBuilder = CreateConventionModelBuilder(); + var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - modelBuilder - .Entity() - .ToTable("Customizer", "db0"); + Assert.NotNull(typeBuilder.ToTable("Splew", "1")); + Assert.Equal("Splew", typeBuilder.Metadata.GetTableName()); + Assert.Equal("1", typeBuilder.Metadata.GetSchema()); - var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); + Assert.NotNull(typeBuilder.ToTable("Splow", "2", fromDataAnnotation: true)); + Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); + Assert.Equal("2", typeBuilder.Metadata.GetSchema()); - Assert.Equal("Customer", entityType.DisplayName()); - Assert.Equal("Customizer", entityType.GetTableName()); - Assert.Equal("db0", entityType.GetSchema()); + Assert.Null(typeBuilder.ToTable("Splod", "3")); + Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); + Assert.Equal("2", typeBuilder.Metadata.GetSchema()); } [ConditionalFact] - public void Can_set_table_and_schema_name_non_generic() + public void Can_override_existing_schema() { - var modelBuilder = CreateConventionModelBuilder(); + var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - modelBuilder - .Entity(typeof(Customer)) - .ToTable("Customizer", "db0"); + typeBuilder.Metadata.SetSchema("Explicit"); - var entityType = modelBuilder.Model.FindEntityType(typeof(Customer)); + Assert.Null(typeBuilder.ToTable("Splod", "2", fromDataAnnotation: true)); + Assert.Equal("Splot", typeBuilder.Metadata.GetTableName()); + Assert.Equal("Explicit", typeBuilder.Metadata.GetSchema()); - Assert.Equal("Customer", entityType.DisplayName()); - Assert.Equal("Customizer", entityType.GetTableName()); - Assert.Equal("db0", entityType.GetSchema()); + Assert.NotNull(typeBuilder.ToTable("Splod", "Explicit", fromDataAnnotation: true)); + Assert.Equal("Splod", typeBuilder.Metadata.GetTableName()); + Assert.Equal("Explicit", typeBuilder.Metadata.GetSchema()); + + Assert.NotNull(new EntityTypeBuilder(typeBuilder.Metadata).ToTable("Splew", "1")); + Assert.Equal("Splew", typeBuilder.Metadata.GetTableName()); + Assert.Equal("1", typeBuilder.Metadata.GetSchema()); } + [ConditionalFact] public void Can_create_check_constraint() { @@ -533,38 +568,106 @@ public void Can_create_check_constraint_with_duplicate_name_replaces_existing() } [ConditionalFact] - public void AddCheckConstraint_with_duplicate_names_throws_exception() + public void Can_access_check_constraint() { - var entityTypeBuilder = CreateConventionModelBuilder().Entity(); - var entityType = entityTypeBuilder.Metadata; + var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); + IReadOnlyEntityType entityType = typeBuilder.Metadata; + + Assert.NotNull(typeBuilder.HasCheckConstraint("Splew", "s > p")); + Assert.Equal("Splew", entityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s > p", entityType.GetCheckConstraints().Single().Sql); - entityType.AddCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + Assert.NotNull(typeBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true)); + Assert.Equal("Splew", entityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", entityType.GetCheckConstraints().Single().Sql); - Assert.Equal( - RelationalStrings.DuplicateCheckConstraint("CK_Customer_AlternateId", entityType.DisplayName()), - Assert.Throws( - () => - entityType.AddCheckConstraint("CK_Customer_AlternateId", "AlternateId < Id")).Message); + Assert.Null(typeBuilder.HasCheckConstraint("Splew", "s > p")); + Assert.Equal("Splew", entityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", entityType.GetCheckConstraints().Single().Sql); } [ConditionalFact] - public void RemoveCheckConstraint_returns_constraint_when_constraint_exists() + public void Base_check_constraint_overrides_derived_one() { - var entityTypeBuilder = CreateConventionModelBuilder().Entity(); - var entityType = entityTypeBuilder.Metadata; + var modelBuilder = CreateBuilder(); + + var derivedBuilder = modelBuilder.Entity(typeof(Splow), ConfigurationSource.Convention); + IReadOnlyEntityType derivedEntityType = derivedBuilder.Metadata; + + Assert.NotNull(derivedBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) + .HasName("CK_Splow", fromDataAnnotation: true)); + Assert.Equal("Splew", derivedEntityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", derivedEntityType.GetCheckConstraints().Single().Sql); + Assert.Equal("CK_Splow", derivedEntityType.GetCheckConstraints().Single().Name); + + Assert.True(derivedBuilder.CanHaveCheckConstraint("Splew", "s < p")); + Assert.True(derivedBuilder.CanHaveCheckConstraint("Splew", "s > p", fromDataAnnotation: true)); + Assert.False(derivedBuilder.CanHaveCheckConstraint("Splew", "s > p")); + Assert.True(derivedBuilder.CanHaveCheckConstraint("Splot", "s > p")); + + var baseBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.DataAnnotation); + IReadOnlyEntityType baseEntityType = baseBuilder.Metadata; + + Assert.True(baseBuilder.CanHaveCheckConstraint("Splew", "s < p")); + Assert.True(baseBuilder.CanHaveCheckConstraint("Splew", "s > p", fromDataAnnotation: true)); + Assert.False(baseBuilder.CanHaveCheckConstraint("Splew", "s > p")); + Assert.True(baseBuilder.CanHaveCheckConstraint("Splot", "s > p")); - var constraint = entityType.AddCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + Assert.Null(baseBuilder.HasCheckConstraint("Splew", "s > p")); + Assert.Empty(baseEntityType.GetCheckConstraints()); + Assert.Equal("s < p", derivedEntityType.GetCheckConstraints().Single().Sql); - Assert.Same(constraint, entityType.RemoveCheckConstraint("CK_Customer_AlternateId")); + Assert.NotNull(baseBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) + .HasName("CK_Splot", fromDataAnnotation: true)); + Assert.Equal("Splew", baseEntityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", baseEntityType.GetCheckConstraints().Single().Sql); + Assert.Equal("CK_Splot", baseEntityType.GetCheckConstraints().Single().Name); + + derivedBuilder.HasBaseType((EntityType)baseEntityType, ConfigurationSource.Convention); + + Assert.Null(baseBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) + .HasName("CK_Splew")); + Assert.Equal("Splew", baseEntityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", baseEntityType.GetCheckConstraints().Single().Sql); + Assert.Equal("CK_Splot", baseEntityType.GetCheckConstraints().Single().Name); + Assert.Empty(derivedEntityType.GetDeclaredCheckConstraints()); + Assert.Same(baseEntityType.GetCheckConstraints().Single(), derivedEntityType.GetCheckConstraints().Single()); } [ConditionalFact] - public void RemoveCheckConstraint_returns_null_when_constraint_is_missing() + public void Base_check_constraint_overrides_derived_one_after_base_is_set() { - var entityTypeBuilder = CreateConventionModelBuilder().Entity(); - var entityType = entityTypeBuilder.Metadata; + var modelBuilder = CreateBuilder(); + + var derivedBuilder = modelBuilder.Entity(typeof(Splow), ConfigurationSource.Convention); + Assert.NotNull(derivedBuilder.HasBaseType((string)null, ConfigurationSource.DataAnnotation)); + IReadOnlyEntityType derivedEntityType = derivedBuilder.Metadata; + + Assert.NotNull(derivedBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) + .HasName("CK_Splow", fromDataAnnotation: true)); + Assert.Equal("Splew", derivedEntityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", derivedEntityType.GetCheckConstraints().Single().Sql); + Assert.Equal("CK_Splow", derivedEntityType.GetCheckConstraints().Single().Name); + + var baseBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); + IReadOnlyEntityType baseEntityType = baseBuilder.Metadata; + Assert.Null(derivedEntityType.BaseType); + + Assert.NotNull(baseBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) + .HasName("CK_Splot", fromDataAnnotation: true)); + Assert.Equal("Splew", baseEntityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", baseEntityType.GetCheckConstraints().Single().Sql); + Assert.Equal("CK_Splot", baseEntityType.GetCheckConstraints().Single().Name); + + Assert.NotNull(derivedBuilder.HasBaseType((EntityType)baseEntityType, ConfigurationSource.DataAnnotation)); - Assert.Null(entityType.RemoveCheckConstraint("CK_Customer_AlternateId")); + Assert.Null(baseBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true) + .HasName("CK_Splew")); + Assert.Equal("Splew", baseEntityType.GetCheckConstraints().Single().ModelName); + Assert.Equal("s < p", baseEntityType.GetCheckConstraints().Single().Sql); + Assert.Equal("CK_Splot", baseEntityType.GetCheckConstraints().Single().Name); + Assert.Empty(derivedEntityType.GetDeclaredCheckConstraints()); + Assert.Same(baseEntityType.GetCheckConstraints().Single(), derivedEntityType.GetCheckConstraints().Single()); } [ConditionalFact] @@ -1020,6 +1123,22 @@ public void Can_create_schema_named_sequence_with_specific_facets_using_nested_c ValidateSchemaNamedSpecificSequence(sequence); } + [ConditionalFact] + public void Can_access_comment() + { + var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); + var entityType = typeBuilder.Metadata; + + Assert.NotNull(typeBuilder.HasComment("My Comment")); + Assert.Equal("My Comment", entityType.GetComment()); + + Assert.NotNull(typeBuilder.HasComment("My Comment 2", fromDataAnnotation: true)); + Assert.Equal("My Comment 2", entityType.GetComment()); + + Assert.Null(typeBuilder.HasComment("My Comment")); + Assert.Equal("My Comment 2", entityType.GetComment()); + } + [ConditionalFact] public void Can_create_dbFunction() { @@ -1156,6 +1275,48 @@ public void Relational_property_methods_have_non_generic_overloads() .HasDefaultValue("Neil"); } + [ConditionalFact] + public void Can_access_property() + { + var propertyBuilder = CreateBuilder() + .Entity(typeof(Splot), ConfigurationSource.Convention) + .Property(typeof(int), "Id", ConfigurationSource.Convention); + + Assert.NotNull(propertyBuilder.IsFixedLength(true)); + Assert.True(propertyBuilder.Metadata.IsFixedLength()); + Assert.NotNull(propertyBuilder.HasColumnName("Splew")); + Assert.Equal("Splew", propertyBuilder.Metadata.GetColumnBaseName()); + Assert.NotNull(propertyBuilder.HasColumnType("int")); + Assert.Equal("int", propertyBuilder.Metadata.GetColumnType()); + Assert.NotNull(propertyBuilder.HasDefaultValue(1)); + Assert.Equal(1, propertyBuilder.Metadata.GetDefaultValue()); + Assert.NotNull(propertyBuilder.HasDefaultValueSql("2")); + Assert.Equal("2", propertyBuilder.Metadata.GetDefaultValueSql()); + Assert.Equal(0, propertyBuilder.Metadata.GetDefaultValue()); + Assert.NotNull(propertyBuilder.HasComputedColumnSql("3")); + Assert.Equal("3", propertyBuilder.Metadata.GetComputedColumnSql()); + Assert.Null(propertyBuilder.Metadata.GetDefaultValueSql()); + + Assert.NotNull(propertyBuilder.IsFixedLength(false, fromDataAnnotation: true)); + Assert.Null(propertyBuilder.IsFixedLength(true)); + Assert.False(propertyBuilder.Metadata.IsFixedLength()); + Assert.NotNull(propertyBuilder.HasColumnName("Splow", fromDataAnnotation: true)); + Assert.Null(propertyBuilder.HasColumnName("Splod")); + Assert.Equal("Splow", propertyBuilder.Metadata.GetColumnBaseName()); + Assert.NotNull(propertyBuilder.HasColumnType("varchar", fromDataAnnotation: true)); + Assert.Null(propertyBuilder.HasColumnType("int")); + Assert.Equal("varchar", propertyBuilder.Metadata.GetColumnType()); + Assert.NotNull(propertyBuilder.HasDefaultValue(0, fromDataAnnotation: true)); + Assert.Null(propertyBuilder.HasDefaultValue(1)); + Assert.Equal(0, propertyBuilder.Metadata.GetDefaultValue()); + Assert.NotNull(propertyBuilder.HasDefaultValueSql("NULL", fromDataAnnotation: true)); + Assert.Null(propertyBuilder.HasDefaultValueSql("2")); + Assert.Equal("NULL", propertyBuilder.Metadata.GetDefaultValueSql()); + Assert.NotNull(propertyBuilder.HasComputedColumnSql("runthis()", fromDataAnnotation: true)); + Assert.Null(propertyBuilder.HasComputedColumnSql("3")); + Assert.Equal("runthis()", propertyBuilder.Metadata.GetComputedColumnSql()); + } + [ConditionalFact] public void Relational_relationship_methods_dont_break_out_of_the_generics() { @@ -1205,6 +1366,24 @@ public void Relational_relationship_methods_have_non_generic_overloads() .HasConstraintName("Simon"); } + [ConditionalFact] + public void Can_access_relationship() + { + var modelBuilder = CreateBuilder(); + var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); + entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention); + var relationshipBuilder = entityTypeBuilder.HasRelationship("Splot", new[] { "Id" }, ConfigurationSource.Convention); + + Assert.NotNull(relationshipBuilder.HasConstraintName("Splew")); + Assert.Equal("Splew", relationshipBuilder.Metadata.GetConstraintName()); + + Assert.NotNull(relationshipBuilder.HasConstraintName("Splow", fromDataAnnotation: true)); + Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); + + Assert.Null(relationshipBuilder.HasConstraintName("Splod")); + Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); + } + private void AssertIsGeneric(EntityTypeBuilder _) { } @@ -1224,6 +1403,9 @@ private void AssertIsGeneric(ReferenceReferenceBuilder _) protected virtual ModelBuilder CreateConventionModelBuilder() => RelationalTestHelpers.Instance.CreateConventionBuilder(); + private InternalModelBuilder CreateBuilder() + => (InternalModelBuilder)CreateConventionModelBuilder().GetInfrastructure(); + private static void ValidateSchemaNamedSpecificSequence(IReadOnlySequence sequence) { Assert.Equal("Snook", sequence.Name); @@ -1273,5 +1455,20 @@ private class OrderDetails public int OrderId { get; set; } public Order Order { get; set; } } + + private class Splot + { + public static readonly PropertyInfo SplowedProperty = typeof(Splot).GetProperty("Splowed"); + + public int? Splowed { get; set; } + } + + private class Splow : Splot + { + } + + private class Splod : Splow + { + } } } diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs deleted file mode 100644 index 5e22dfd01ea..00000000000 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataBuilderExtensionsTest.cs +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Linq; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Xunit; - -// ReSharper disable UnusedMember.Local -// ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore.Metadata -{ - public class RelationalMetadataBuilderExtensionsTest - { - private InternalModelBuilder CreateBuilder() - => new(new Model()); - - [ConditionalFact] - public void Can_access_model() - { - var builder = CreateBuilder(); - - ((IMutableModel)builder.Metadata).AddSequence("Mine").IncrementBy = 77; - - Assert.Equal(77, ((IMutableModel)builder.Metadata).FindSequence("Mine").IncrementBy); - } - - [ConditionalFact] - public void Can_set_table_name() - { - var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - - Assert.NotNull(typeBuilder.ToTable("Splew")); - Assert.Equal("Splew", typeBuilder.Metadata.GetTableName()); - - Assert.NotNull(typeBuilder.ToTable("Splow", fromDataAnnotation: true)); - Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); - - Assert.Null(typeBuilder.ToTable("Splod")); - Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); - } - - [ConditionalFact] - public void Can_set_table_name_and_schema() - { - var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - - Assert.NotNull(typeBuilder.ToTable("Splew", "1")); - Assert.Equal("Splew", typeBuilder.Metadata.GetTableName()); - Assert.Equal("1", typeBuilder.Metadata.GetSchema()); - - Assert.NotNull(typeBuilder.ToTable("Splow", "2", fromDataAnnotation: true)); - Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); - Assert.Equal("2", typeBuilder.Metadata.GetSchema()); - - Assert.Null(typeBuilder.ToTable("Splod", "3")); - Assert.Equal("Splow", typeBuilder.Metadata.GetTableName()); - Assert.Equal("2", typeBuilder.Metadata.GetSchema()); - } - - [ConditionalFact] - public void Can_override_existing_schema() - { - var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - - typeBuilder.Metadata.SetSchema("Explicit"); - - Assert.Null(typeBuilder.ToTable("Splod", "2", fromDataAnnotation: true)); - Assert.Equal("Splot", typeBuilder.Metadata.GetTableName()); - Assert.Equal("Explicit", typeBuilder.Metadata.GetSchema()); - - Assert.NotNull(typeBuilder.ToTable("Splod", "Explicit", fromDataAnnotation: true)); - Assert.Equal("Splod", typeBuilder.Metadata.GetTableName()); - Assert.Equal("Explicit", typeBuilder.Metadata.GetSchema()); - - Assert.NotNull(new EntityTypeBuilder(typeBuilder.Metadata).ToTable("Splew", "1")); - Assert.Equal("Splew", typeBuilder.Metadata.GetTableName()); - Assert.Equal("1", typeBuilder.Metadata.GetSchema()); - } - - [ConditionalFact] - public void Can_access_property() - { - var propertyBuilder = CreateBuilder() - .Entity(typeof(Splot), ConfigurationSource.Convention) - .Property(typeof(int), "Id", ConfigurationSource.Convention); - - Assert.NotNull(propertyBuilder.IsFixedLength(true)); - Assert.True(propertyBuilder.Metadata.IsFixedLength()); - Assert.NotNull(propertyBuilder.HasColumnName("Splew")); - Assert.Equal("Splew", propertyBuilder.Metadata.GetColumnBaseName()); - Assert.NotNull(propertyBuilder.HasColumnType("int")); - Assert.Equal("int", propertyBuilder.Metadata.GetColumnType()); - Assert.NotNull(propertyBuilder.HasDefaultValue(1)); - Assert.Equal(1, propertyBuilder.Metadata.GetDefaultValue()); - Assert.NotNull(propertyBuilder.HasDefaultValueSql("2")); - Assert.Equal("2", propertyBuilder.Metadata.GetDefaultValueSql()); - Assert.Equal(1, propertyBuilder.Metadata.GetDefaultValue()); - Assert.NotNull(propertyBuilder.HasComputedColumnSql("3")); - Assert.Equal("3", propertyBuilder.Metadata.GetComputedColumnSql()); - Assert.Equal("2", propertyBuilder.Metadata.GetDefaultValueSql()); - - Assert.NotNull(propertyBuilder.IsFixedLength(false, fromDataAnnotation: true)); - Assert.Null(propertyBuilder.IsFixedLength(true)); - Assert.False(propertyBuilder.Metadata.IsFixedLength()); - Assert.NotNull(propertyBuilder.HasColumnName("Splow", fromDataAnnotation: true)); - Assert.Null(propertyBuilder.HasColumnName("Splod")); - Assert.Equal("Splow", propertyBuilder.Metadata.GetColumnBaseName()); - Assert.NotNull(propertyBuilder.HasColumnType("varchar", fromDataAnnotation: true)); - Assert.Null(propertyBuilder.HasColumnType("int")); - Assert.Equal("varchar", propertyBuilder.Metadata.GetColumnType()); - Assert.NotNull(propertyBuilder.HasDefaultValue(0, fromDataAnnotation: true)); - Assert.Null(propertyBuilder.HasDefaultValue(1)); - Assert.Equal(0, propertyBuilder.Metadata.GetDefaultValue()); - Assert.NotNull(propertyBuilder.HasDefaultValueSql("NULL", fromDataAnnotation: true)); - Assert.Null(propertyBuilder.HasDefaultValueSql("2")); - Assert.Equal("NULL", propertyBuilder.Metadata.GetDefaultValueSql()); - Assert.NotNull(propertyBuilder.HasComputedColumnSql("runthis()", fromDataAnnotation: true)); - Assert.Null(propertyBuilder.HasComputedColumnSql("3")); - Assert.Equal("runthis()", propertyBuilder.Metadata.GetComputedColumnSql()); - } - - [ConditionalFact] - public void Can_access_key() - { - var modelBuilder = CreateBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); - var idProperty = entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention).Metadata; - var keyBuilder = entityTypeBuilder.HasKey(new[] { idProperty.Name }, ConfigurationSource.Convention); - - Assert.NotNull(keyBuilder.HasName("Splew")); - Assert.Equal("Splew", keyBuilder.Metadata.GetName()); - - Assert.NotNull(keyBuilder.HasName("Splow", fromDataAnnotation: true)); - Assert.Equal("Splow", keyBuilder.Metadata.GetName()); - - Assert.Null(keyBuilder.HasName("Splod")); - Assert.Equal("Splow", keyBuilder.Metadata.GetName()); - } - - [ConditionalFact] - public void Can_access_index() - { - var modelBuilder = CreateBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); - entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention); - var indexBuilder = entityTypeBuilder.HasIndex(new[] { "Id" }, ConfigurationSource.Convention); - -#pragma warning disable CS0618 // Type or member is obsolete - Assert.NotNull(indexBuilder.HasName("Splew")); - Assert.Equal("Splew", indexBuilder.Metadata.GetName()); - - Assert.NotNull(indexBuilder.HasName("Splow", fromDataAnnotation: true)); - Assert.Equal("Splow", indexBuilder.Metadata.GetName()); - - Assert.Null(indexBuilder.HasName("Splod")); - Assert.Equal("Splow", indexBuilder.Metadata.GetName()); - - Assert.NotNull(indexBuilder.HasName(null, fromDataAnnotation: true)); - Assert.Equal("IX_Splot_Id", indexBuilder.Metadata.GetName()); - - Assert.NotNull(indexBuilder.HasName("Splod")); - Assert.Equal("Splod", indexBuilder.Metadata.GetName()); -#pragma warning restore CS0618 // Type or member is obsolete - - Assert.NotNull(indexBuilder.HasFilter("Splew")); - Assert.Equal("Splew", indexBuilder.Metadata.GetFilter()); - - Assert.NotNull(indexBuilder.HasFilter("Splow", fromDataAnnotation: true)); - Assert.Equal("Splow", indexBuilder.Metadata.GetFilter()); - - Assert.Null(indexBuilder.HasFilter("Splod")); - Assert.Equal("Splow", indexBuilder.Metadata.GetFilter()); - - Assert.NotNull(indexBuilder.HasFilter(null, fromDataAnnotation: true)); - Assert.Null(indexBuilder.Metadata.GetFilter()); - - Assert.Null(indexBuilder.HasFilter("Splod")); - Assert.Null(indexBuilder.Metadata.GetFilter()); - } - - [ConditionalFact] - public void Can_access_relationship() - { - var modelBuilder = CreateBuilder(); - var entityTypeBuilder = modelBuilder.Entity(typeof(Splot), ConfigurationSource.Convention); - entityTypeBuilder.Property(typeof(int), "Id", ConfigurationSource.Convention); - var relationshipBuilder = entityTypeBuilder.HasRelationship("Splot", new[] { "Id" }, ConfigurationSource.Convention); - - Assert.NotNull(relationshipBuilder.HasConstraintName("Splew")); - Assert.Equal("Splew", relationshipBuilder.Metadata.GetConstraintName()); - - Assert.NotNull(relationshipBuilder.HasConstraintName("Splow", fromDataAnnotation: true)); - Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); - - Assert.Null(relationshipBuilder.HasConstraintName("Splod")); - Assert.Equal("Splow", relationshipBuilder.Metadata.GetConstraintName()); - } - - [ConditionalFact] - public void Can_access_check_constraint() - { - var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - IReadOnlyEntityType entityType = typeBuilder.Metadata; - - Assert.NotNull(typeBuilder.HasCheckConstraint("Splew", "s > p")); - Assert.Equal("Splew", entityType.GetCheckConstraints().Single().Name); - Assert.Equal("s > p", entityType.GetCheckConstraints().Single().Sql); - - Assert.NotNull(typeBuilder.HasCheckConstraint("Splew", "s < p", fromDataAnnotation: true)); - Assert.Equal("Splew", entityType.GetCheckConstraints().Single().Name); - Assert.Equal("s < p", entityType.GetCheckConstraints().Single().Sql); - - Assert.Null(typeBuilder.HasCheckConstraint("Splew", "s > p")); - Assert.Equal("Splew", entityType.GetCheckConstraints().Single().Name); - Assert.Equal("s < p", entityType.GetCheckConstraints().Single().Sql); - } - - [ConditionalFact] - public void Can_access_comment() - { - var typeBuilder = CreateBuilder().Entity(typeof(Splot), ConfigurationSource.Convention); - var entityType = typeBuilder.Metadata; - - Assert.NotNull(typeBuilder.HasComment("My Comment")); - Assert.Equal("My Comment", entityType.GetComment()); - - Assert.NotNull(typeBuilder.HasComment("My Comment 2", fromDataAnnotation: true)); - Assert.Equal("My Comment 2", entityType.GetComment()); - - Assert.Null(typeBuilder.HasComment("My Comment")); - Assert.Equal("My Comment 2", entityType.GetComment()); - } - - private class Splot - { - public static readonly PropertyInfo SplowedProperty = typeof(Splot).GetProperty("Splowed"); - - public int? Splowed { get; set; } - } - - private class Splow : Splot - { - } - - private class Splod : Splow - { - } - } -} diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs index 09f91467235..e66d2aac2ba 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalMetadataExtensionsTest.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; namespace Microsoft.EntityFrameworkCore.Metadata @@ -589,6 +590,43 @@ public void Can_get_multiple_sequences() Assert.Contains(sequences, s => s.Name == "Golomb"); } + [ConditionalFact] + public void AddCheckConstraint_with_duplicate_names_throws_exception() + { + var entityTypeBuilder = CreateConventionModelBuilder().Entity(); + var entityType = entityTypeBuilder.Metadata; + + entityType.AddCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + + Assert.Equal( + RelationalStrings.DuplicateCheckConstraint("CK_Customer_AlternateId", entityType.DisplayName(), entityType.DisplayName()), + Assert.Throws( + () => entityType.AddCheckConstraint("CK_Customer_AlternateId", "AlternateId < Id")).Message); + } + + [ConditionalFact] + public void RemoveCheckConstraint_returns_constraint_when_constraint_exists() + { + var entityTypeBuilder = CreateConventionModelBuilder().Entity(); + var entityType = entityTypeBuilder.Metadata; + + var constraint = entityType.AddCheckConstraint("CK_Customer_AlternateId", "AlternateId > Id"); + + Assert.Same(constraint, entityType.RemoveCheckConstraint("CK_Customer_AlternateId")); + } + + [ConditionalFact] + public void RemoveCheckConstraint_returns_null_when_constraint_is_missing() + { + var entityTypeBuilder = CreateConventionModelBuilder().Entity(); + var entityType = entityTypeBuilder.Metadata; + + Assert.Null(entityType.RemoveCheckConstraint("CK_Customer_AlternateId")); + } + + protected virtual ModelBuilder CreateConventionModelBuilder() + => RelationalTestHelpers.Instance.CreateConventionBuilder(); + private enum MyEnum : byte { Son, diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 604edd98d10..a4b2f5c8907 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -377,7 +377,7 @@ public void Create_table() Assert.NotNull(createTableOperation.PrimaryKey); Assert.Single(createTableOperation.UniqueConstraints); var checkConstraint = createTableOperation.CheckConstraints.Single(); - Assert.Equal("SomeCheckConstraint", checkConstraint.Name); + Assert.Equal("CK_Node_SomeCheckConstraint", checkConstraint.Name); Assert.Equal("[Id] > 10", checkConstraint.Sql); Assert.Single(createTableOperation.ForeignKeys); @@ -2857,7 +2857,7 @@ public void Drop_check_constraint() x.ToTable("Penguin", "dbo"); x.Property("Id"); x.Property("AlternateId"); - x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId > Id"); + x.HasCheckConstraint("CK_Penguin_AlternateId", "AlternateId > Id"); }), target => target.Entity( "Penguin", @@ -2874,7 +2874,7 @@ public void Drop_check_constraint() var operation = Assert.IsType(operations[0]); Assert.Equal("dbo", operation.Schema); Assert.Equal("Penguin", operation.Table); - Assert.Equal("CK_Flamingo_AlternateId", operation.Name); + Assert.Equal("CK_Penguin_AlternateId", operation.Name); }); } @@ -2889,7 +2889,7 @@ public void Rename_check_constraint() x.ToTable("Pelican", "dbo"); x.Property("Id"); x.Property("AlternateId"); - x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId > Id"); + x.HasCheckConstraint("CK_Pelican_AlternateId", "AlternateId > Id"); }), target => target.Entity( "Pelican", @@ -2898,7 +2898,7 @@ public void Rename_check_constraint() x.ToTable("Pelican", "dbo"); x.Property("Id"); x.Property("AlternateId"); - x.HasCheckConstraint("CK_Flamingo", "AlternateId > Id"); + x.HasCheckConstraint("CK_Pelican_AlternateId", "AlternateId > Id", c => c.HasName("CK_Flamingo")); }), operations => { @@ -2907,7 +2907,7 @@ public void Rename_check_constraint() var dropOperation = Assert.IsType(operations[0]); Assert.Equal("dbo", dropOperation.Schema); Assert.Equal("Pelican", dropOperation.Table); - Assert.Equal("CK_Flamingo_AlternateId", dropOperation.Name); + Assert.Equal("CK_Pelican_AlternateId", dropOperation.Name); var createOperation = Assert.IsType(operations[1]); Assert.Equal("dbo", createOperation.Schema); @@ -2928,7 +2928,7 @@ public void Alter_check_constraint_expression() x.ToTable("Rook", "dbo"); x.Property("Id"); x.Property("AlternateId"); - x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId > Id"); + x.HasCheckConstraint("CK_Rook_AlternateId", "AlternateId > Id"); }), target => target.Entity( "Rook", @@ -2937,7 +2937,7 @@ public void Alter_check_constraint_expression() x.ToTable("Rook", "dbo"); x.Property("Id"); x.Property("AlternateId"); - x.HasCheckConstraint("CK_Flamingo_AlternateId", "AlternateId < Id"); + x.HasCheckConstraint("CK_Rook_AlternateId", "AlternateId < Id"); }), operations => { @@ -2946,12 +2946,12 @@ public void Alter_check_constraint_expression() var dropOperation = Assert.IsType(operations[0]); Assert.Equal("dbo", dropOperation.Schema); Assert.Equal("Rook", dropOperation.Table); - Assert.Equal("CK_Flamingo_AlternateId", dropOperation.Name); + Assert.Equal("CK_Rook_AlternateId", dropOperation.Name); var createOperation = Assert.IsType(operations[1]); Assert.Equal("dbo", createOperation.Schema); Assert.Equal("Rook", createOperation.Table); - Assert.Equal("CK_Flamingo_AlternateId", createOperation.Name); + Assert.Equal("CK_Rook_AlternateId", createOperation.Name); Assert.Equal("AlternateId < Id", createOperation.Sql); }); } diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs index e497018cfa6..4df8ecafb38 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalModelBuilderGenericTestBase.cs @@ -4,6 +4,8 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; +#nullable enable + // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.ModelBuilding { @@ -52,5 +54,28 @@ protected virtual TestTableBuilder Wrap(TableBuilder tableBuilder) public override TestTableBuilder ExcludeFromMigrations(bool excluded = true) => Wrap(TableBuilder.ExcludeFromMigrations(excluded)); } + + public abstract class TestCheckConstraintBuilder + { + public abstract TestCheckConstraintBuilder HasName(string name); + } + + public class NonGenericTestCheckConstraintBuilder : TestCheckConstraintBuilder, IInfrastructure + { + public NonGenericTestCheckConstraintBuilder(CheckConstraintBuilder checkConstraintBuilder) + { + CheckConstraintBuilder = checkConstraintBuilder; + } + + protected CheckConstraintBuilder CheckConstraintBuilder { get; } + + public CheckConstraintBuilder Instance => CheckConstraintBuilder; + + protected virtual TestCheckConstraintBuilder Wrap(CheckConstraintBuilder checkConstraintBuilder) + => new NonGenericTestCheckConstraintBuilder(checkConstraintBuilder); + + public override TestCheckConstraintBuilder HasName(string name) + => Wrap(CheckConstraintBuilder.HasName(name)); + } } } diff --git a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs index fcfb50bd886..cc4e9689a19 100644 --- a/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs +++ b/test/EFCore.Relational.Tests/ModelBuilding/RelationalTestModelBuilderExtensions.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; +#nullable enable + namespace Microsoft.EntityFrameworkCore.ModelBuilding { public static class RelationalTestModelBuilderExtensions @@ -271,6 +273,90 @@ public static ModelBuilderTest.TestOwnedNavigationBuilder HasCheckConstraint( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + string? sql) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasCheckConstraint(name, sql); + break; + case IInfrastructure nongenericBuilder: + nongenericBuilder.Instance.HasCheckConstraint(name, sql); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestEntityTypeBuilder HasCheckConstraint( + this ModelBuilderTest.TestEntityTypeBuilder builder, + string name, + string sql, + Action buildAction) + where TEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasCheckConstraint(name, sql, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestCheckConstraintBuilder(b))); + break; + case IInfrastructure nongenericBuilder: + nongenericBuilder.Instance.HasCheckConstraint(name, sql, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestCheckConstraintBuilder(b))); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder HasCheckConstraint( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + string? sql) + where TEntity : class + where TRelatedEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasCheckConstraint(name, sql); + break; + case IInfrastructure nongenericBuilder: + nongenericBuilder.Instance.HasCheckConstraint(name, sql); + break; + } + + return builder; + } + + public static ModelBuilderTest.TestOwnedNavigationBuilder HasCheckConstraint( + this ModelBuilderTest.TestOwnedNavigationBuilder builder, + string name, + string sql, + Action buildAction) + where TEntity : class + where TRelatedEntity : class + { + switch (builder) + { + case IInfrastructure> genericBuilder: + genericBuilder.Instance.HasCheckConstraint(name, sql, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestCheckConstraintBuilder(b))); + break; + case IInfrastructure nongenericBuilder: + nongenericBuilder.Instance.HasCheckConstraint(name, sql, + b => buildAction(new RelationalModelBuilderTest.NonGenericTestCheckConstraintBuilder(b))); + break; + } + + return builder; + } + public static ModelBuilderTest.TestOwnershipBuilder HasConstraintName( this ModelBuilderTest.TestOwnershipBuilder builder, string name) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 26495b51fb0..d0cc5754aa7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -57,7 +57,7 @@ public override async Task Create_table_all_settings() [SSN] nvarchar(11) COLLATE German_PhoneBook_CI_AS NOT NULL, CONSTRAINT [PK_People] PRIMARY KEY ([CustomId]), CONSTRAINT [AK_People_SSN] UNIQUE ([SSN]), - CONSTRAINT [CK_EmployerId] CHECK ([EmployerId] > 0), + CONSTRAINT [CK_People_EmployerId] CHECK ([EmployerId] > 0), CONSTRAINT [FK_People_Employers_EmployerId] FOREIGN KEY ([EmployerId]) REFERENCES [Employers] ([Id]) ON DELETE NO ACTION ); DECLARE @description AS sql_variant; @@ -455,7 +455,7 @@ public override async Task Add_column_with_check_constraint() AssertSql( @"ALTER TABLE [People] ADD [DriverLicense] int NOT NULL DEFAULT 0;", // - @"ALTER TABLE [People] ADD CONSTRAINT [CK_Foo] CHECK ([DriverLicense] > 0);"); + @"ALTER TABLE [People] ADD CONSTRAINT [CK_People_Foo] CHECK ([DriverLicense] > 0);"); } [ConditionalFact] @@ -1679,7 +1679,7 @@ public override async Task Add_check_constraint_with_name() await base.Add_check_constraint_with_name(); AssertSql( - @"ALTER TABLE [People] ADD CONSTRAINT [CK_Foo] CHECK ([DriverLicense] > 0);"); + @"ALTER TABLE [People] ADD CONSTRAINT [CK_People_Foo] CHECK ([DriverLicense] > 0);"); } public override async Task Alter_check_constraint() @@ -1687,9 +1687,9 @@ public override async Task Alter_check_constraint() await base.Alter_check_constraint(); AssertSql( - @"ALTER TABLE [People] DROP CONSTRAINT [CK_Foo];", + @"ALTER TABLE [People] DROP CONSTRAINT [CK_People_Foo];", // - @"ALTER TABLE [People] ADD CONSTRAINT [CK_Foo] CHECK ([DriverLicense] > 1);"); + @"ALTER TABLE [People] ADD CONSTRAINT [CK_People_Foo] CHECK ([DriverLicense] > 1);"); } public override async Task Drop_check_constraint() @@ -1697,7 +1697,7 @@ public override async Task Drop_check_constraint() await base.Drop_check_constraint(); AssertSql( - @"ALTER TABLE [People] DROP CONSTRAINT [CK_Foo];"); + @"ALTER TABLE [People] DROP CONSTRAINT [CK_People_Foo];"); } public override async Task Create_sequence() diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs index 328a3eae132..619dac13446 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderGenericTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -326,6 +327,69 @@ public virtual void TPT_index_can_use_inherited_properties() Assert.All(bunType.GetIndexes(), i => Assert.Null(i.GetFilter())); } + [ConditionalFact] + public void Can_add_check_constraints() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity() + .HasBaseType(null) + .HasCheckConstraint("LargeId", "Id > 1000", c => c.HasName("CK_LargeId")); + modelBuilder.Entity() + .HasCheckConstraint("PositiveId", "Id > 0") + .HasCheckConstraint("LargeId", "Id > 1000"); + modelBuilder.Entity() + .HasBaseType(); + modelBuilder.Entity(); + + var model = modelBuilder.FinalizeModel(); + + var @base = model.FindEntityType(typeof(ChildBase)); + Assert.Equal(2, @base.GetCheckConstraints().Count()); + + var firstCheckConstraint = @base.FindCheckConstraint("PositiveId"); + Assert.Equal("PositiveId", firstCheckConstraint.ModelName); + Assert.Equal("Id > 0", firstCheckConstraint.Sql); + Assert.Equal("CK_ChildBase_PositiveId", firstCheckConstraint.Name); + + var secondCheckConstraint = @base.FindCheckConstraint("LargeId"); + Assert.Equal("LargeId", secondCheckConstraint.ModelName); + Assert.Equal("Id > 1000", secondCheckConstraint.Sql); + Assert.Equal("CK_LargeId", secondCheckConstraint.Name); + + var child = model.FindEntityType(typeof(Child)); + Assert.Equal(@base.GetCheckConstraints(), child.GetCheckConstraints()); + Assert.Empty(child.GetDeclaredCheckConstraints()); + } + + [ConditionalFact] + public void Adding_conflicting_check_constraint_to_derived_type_throws() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity() + .HasCheckConstraint("LargeId", "Id > 100", c => c.HasName("CK_LargeId")); + + Assert.Equal( + RelationalStrings.DuplicateCheckConstraint("LargeId", nameof(Child), nameof(ChildBase)), + Assert.Throws( + () => modelBuilder.Entity().HasCheckConstraint("LargeId", "Id > 1000")).Message); + } + + [ConditionalFact] + public void Adding_conflicting_check_constraint_to_derived_type_before_base_throws() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity() + .HasBaseType(null) + .HasCheckConstraint("LargeId", "Id > 1000"); + modelBuilder.Entity() + .HasCheckConstraint("LargeId", "Id > 100", c => c.HasName("CK_LargeId")); + + Assert.Equal( + RelationalStrings.DuplicateCheckConstraint("LargeId", nameof(Child), nameof(ChildBase)), + Assert.Throws( + () => modelBuilder.Entity().HasBaseType()).Message); + } + public class Parent { public int Id { get; set; } @@ -743,16 +807,16 @@ public virtual void Owned_type_collections_can_be_mapped_to_different_tables() public override void Can_configure_owned_type() { var modelBuilder = CreateModelBuilder(); - var model = modelBuilder.Model; - var entityBuilder = modelBuilder.Entity().OwnsOne(c => c.Details) - .ToTable("CustomerDetails"); - entityBuilder.Property(d => d.CustomerId); - entityBuilder.HasIndex(d => d.CustomerId); - entityBuilder.WithOwner(d => d.Customer) + var ownedBuilder = modelBuilder.Entity().OwnsOne(c => c.Details) + .ToTable("CustomerDetails") + .HasCheckConstraint("CK_CustomerDetails_T", "AlternateKey <> 0", c => c.HasName("CK_Guid")); + ownedBuilder.Property(d => d.CustomerId); + ownedBuilder.HasIndex(d => d.CustomerId); + ownedBuilder.WithOwner(d => d.Customer) .HasPrincipalKey(c => c.AlternateKey); - modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); var owner = model.FindEntityType(typeof(Customer)); Assert.Equal(typeof(Customer).FullName, owner.Name); @@ -762,7 +826,12 @@ public override void Can_configure_owned_type() Assert.Equal("CustomerAlternateKey", ownership.Properties.Single().Name); Assert.Equal(nameof(Customer.AlternateKey), ownership.PrincipalKey.Properties.Single().Name); var owned = ownership.DeclaringEntityType; - Assert.Same(entityBuilder.OwnedEntityType, owned); + Assert.Same(ownedBuilder.OwnedEntityType, owned); + Assert.Equal("CustomerDetails", owned.GetTableName()); + var checkConstraint = owned.GetCheckConstraints().Single(); + Assert.Equal("CK_CustomerDetails_T", checkConstraint.ModelName); + Assert.Equal("AlternateKey <> 0", checkConstraint.Sql); + Assert.Equal("CK_Guid", checkConstraint.Name); Assert.Single(owned.GetForeignKeys()); Assert.Equal(nameof(CustomerDetails.CustomerId), owned.GetIndexes().Single().Properties.Single().Name); Assert.Equal( diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs index d12e8da9843..17fa9c9e9f6 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerTestModelBuilderExtensions.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata.Builders; +#nullable enable + namespace Microsoft.EntityFrameworkCore.ModelBuilding { public static class SqlServerTestModelBuilderExtensions diff --git a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs index 0cfb966ed40..e142a3877a1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Migrations/MigrationsSqliteTest.cs @@ -53,7 +53,7 @@ public override async Task Create_table_all_settings() ""SSN"" TEXT COLLATE NOCASE NOT NULL, CONSTRAINT ""AK_People_SSN"" UNIQUE (""SSN""), - CONSTRAINT ""CK_EmployerId"" CHECK (""EmployerId"" > 0), + CONSTRAINT ""CK_People_EmployerId"" CHECK (""EmployerId"" > 0), CONSTRAINT ""FK_People_Employers_EmployerId"" FOREIGN KEY (""EmployerId"") REFERENCES ""Employers"" (""Id"") ON DELETE RESTRICT );"); } @@ -321,7 +321,7 @@ public override async Task Add_column_with_check_constraint() @"CREATE TABLE ""ef_temp_People"" ( ""DriverLicense"" INTEGER NOT NULL, ""Id"" INTEGER NOT NULL, - CONSTRAINT ""CK_Foo"" CHECK (""DriverLicense"" > 0) + CONSTRAINT ""CK_People_Foo"" CHECK (""DriverLicense"" > 0) );", @"INSERT INTO ""ef_temp_People"" (""DriverLicense"", ""Id"") SELECT ""DriverLicense"", ""Id"" @@ -797,7 +797,7 @@ public override async Task Add_check_constraint_with_name() @"CREATE TABLE ""ef_temp_People"" ( ""DriverLicense"" INTEGER NOT NULL, ""Id"" INTEGER NOT NULL, - CONSTRAINT ""CK_Foo"" CHECK (""DriverLicense"" > 0) + CONSTRAINT ""CK_People_Foo"" CHECK (""DriverLicense"" > 0) );", @"INSERT INTO ""ef_temp_People"" (""DriverLicense"", ""Id"") SELECT ""DriverLicense"", ""Id"" @@ -816,7 +816,7 @@ public override async Task Alter_check_constraint() @"CREATE TABLE ""ef_temp_People"" ( ""DriverLicense"" INTEGER NOT NULL, ""Id"" INTEGER NOT NULL, - CONSTRAINT ""CK_Foo"" CHECK (""DriverLicense"" > 1) + CONSTRAINT ""CK_People_Foo"" CHECK (""DriverLicense"" > 1) );", @"INSERT INTO ""ef_temp_People"" (""DriverLicense"", ""Id"") SELECT ""DriverLicense"", ""Id""