diff --git a/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs index eece3f04c7c..4c27bf2a617 100644 --- a/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTriggerExtensions.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.Metadata.Internal; - // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore; diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index db46effec2a..90661d5ef40 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -1117,6 +1117,19 @@ protected virtual void ValidateSharedColumnsCompatibility( continue; } + if (property.DeclaringEntityType.IsAssignableFrom(duplicateProperty.DeclaringEntityType) + || duplicateProperty.DeclaringEntityType.IsAssignableFrom(property.DeclaringEntityType)) + { + throw new InvalidOperationException( + RelationalStrings.DuplicateColumnNameSameHierarchy( + duplicateProperty.DeclaringEntityType.DisplayName(), + duplicateProperty.Name, + property.DeclaringEntityType.DisplayName(), + property.Name, + columnName, + storeObject.DisplayName())); + } + ValidateCompatible(property, duplicateProperty, columnName, storeObject, logger); } diff --git a/src/EFCore.Relational/Metadata/Conventions/BlankTriggerAddingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/BlankTriggerAddingConvention.cs new file mode 100644 index 00000000000..7e617d5ff7c --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/BlankTriggerAddingConvention.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that makes sure there is a trigger on all entity types. +/// +/// +/// See Model building conventions for more information and examples. +/// +public class BlankTriggerAddingConvention : IModelFinalizingConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public BlankTriggerAddingConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Dependencies for this convention. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + /// Called when a model is being finalized. + /// + /// The builder for the model. + /// Additional information associated with convention execution. + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + var table = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table); + if (table != null + && entityType.GetDeclaredTriggers().All(t => t.GetName(table.Value) == null)) + { + entityType.Builder.HasTrigger(table.Value.Name + "_Trigger"); + } + + foreach (var fragment in entityType.GetMappingFragments(StoreObjectType.Table)) + { + if (entityType.GetDeclaredTriggers().All(t => t.GetName(fragment.StoreObject) == null)) + { + entityType.Builder.HasTrigger(fragment.StoreObject.Name + "_Trigger"); + } + } + } + } +} diff --git a/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs index 99b7f6f44b0..0d9f1a621ef 100644 --- a/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs @@ -99,7 +99,7 @@ public virtual void ProcessModelFinalizing( continue; } - RemoveDerivedEntityTypes(entityTypesMissingConcurrencyColumn); + RemoveDerivedEntityTypes(entityTypesMissingConcurrencyColumn, mappedTypes); foreach (var (conventionEntityType, exampleProperty) in entityTypesMissingConcurrencyColumn) { @@ -194,10 +194,11 @@ public static bool IsConcurrencyTokenMissing( { var declaringEntityType = mappedProperty.DeclaringEntityType; if (declaringEntityType.IsAssignableFrom(entityType) + || entityType.IsAssignableFrom(declaringEntityType) || declaringEntityType.IsInOwnershipPath(entityType) || entityType.IsInOwnershipPath(declaringEntityType)) { - // The concurrency token is on the base type or in the same aggregate + // The concurrency token is on the base type, derived type or in the same aggregate propertyMissing = false; continue; } @@ -220,21 +221,31 @@ public static bool IsConcurrencyTokenMissing( return propertyMissing; } - private static void RemoveDerivedEntityTypes(Dictionary entityTypeDictionary) + private static void RemoveDerivedEntityTypes( + Dictionary entityTypeDictionary, + List mappedTypes) { - foreach (var entityType in entityTypeDictionary.Keys) + foreach (var (entityType, property) in entityTypeDictionary) { + var removed = false; var baseType = entityType.BaseType; while (baseType != null) { if (entityTypeDictionary.ContainsKey(baseType)) { entityTypeDictionary.Remove(entityType); + removed = true; break; } baseType = baseType.BaseType; } + + if (!removed + && entityType.IsAssignableFrom(property.DeclaringEntityType)) + { + entityTypeDictionary.Remove(entityType); + } } } } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 86b0ea4614f..a3a71a97123 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -422,7 +422,7 @@ public static string DuplicateColumnNameMaxLengthMismatch(object? entityType1, o entityType1, property1, entityType2, property2, columnName, table, maxLength1, maxLength2); /// - /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different nullability settings. + /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different column nullability settings. /// public static string DuplicateColumnNameNullabilityMismatch(object? entityType1, object? property1, object? entityType2, object? property2, object? columnName, object? table) => string.Format( @@ -453,6 +453,14 @@ public static string DuplicateColumnNameProviderTypeMismatch(object? entityType1 GetString("DuplicateColumnNameProviderTypeMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(type1), nameof(type2)), entityType1, property1, entityType2, property2, columnName, table, type1, type2); + /// + /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but the properties are contained within the same hierarchy. All properties on an entity type must be mapped to unique different columns. + /// + public static string DuplicateColumnNameSameHierarchy(object? entityType1, object? property1, object? entityType2, object? property2, object? columnName, object? table) + => string.Format( + GetString("DuplicateColumnNameSameHierarchy", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table)), + entityType1, property1, entityType2, property2, columnName, table); + /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different scales ('{scale1}' and '{scale2}'). /// @@ -1575,14 +1583,6 @@ public static string StoredProcedureOutputParameterNotGenerated(object? entityTy GetString("StoredProcedureOutputParameterNotGenerated", nameof(entityType), nameof(property), nameof(sproc)), entityType, property, sproc); - /// - /// Stored procedure '{sproc]' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure. - /// - public static string StoredProcedureRowsAffectedNotPopulated(object? sproc) - => string.Format( - GetString("StoredProcedureRowsAffectedNotPopulated", nameof(sproc)), - sproc); - /// /// The property '{propertySpecification}' has specific configuration for the stored procedure '{sproc}', but it isn't mapped to a parameter or a result column on that stored procedure. Remove the specific configuration, or map an entity type that contains this property to '{sproc}'. /// @@ -1655,6 +1655,14 @@ public static string StoredProcedureResultColumnParameterConflict(object? entity GetString("StoredProcedureResultColumnParameterConflict", nameof(entityType), nameof(property), nameof(sproc)), entityType, property, sproc); + /// + /// Stored procedure '{sproc}' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure. + /// + public static string StoredProcedureRowsAffectedNotPopulated(object? sproc) + => string.Format( + GetString("StoredProcedureRowsAffectedNotPopulated", nameof(sproc)), + sproc); + /// /// The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 3eb627796b8..df48b4a53a0 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -272,7 +272,7 @@ '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different maximum lengths ('{maxLength1}' and '{maxLength2}'). - '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different nullability settings. + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different column nullability settings. '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use different column orders ('{columnOrder1}' and '{columnOrder2}'). @@ -283,6 +283,9 @@ '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured to use differing provider types ('{type1}' and '{type2}'). + + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but the properties are contained within the same hierarchy. All properties on an entity type must be mapped to unique different columns. + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different scales ('{scale1}' and '{scale2}'). @@ -1008,9 +1011,6 @@ The property '{entityType}.{property}' is mapped to an output parameter of the stored procedure '{sproc}', but it is not configured as store-generated. Either configure it as store-generated or don't configure the parameter as output. - - Stored procedure '{sproc}' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure. - The property '{propertySpecification}' has specific configuration for the stored procedure '{sproc}', but it isn't mapped to a parameter or a result column on that stored procedure. Remove the specific configuration, or map an entity type that contains this property to '{sproc}'. @@ -1038,6 +1038,9 @@ The property '{entityType}.{property}' is mapped to a result column of the stored procedure '{sproc}', but it is also mapped to an output parameter. A store-generated property can only be mapped to one of these. + + Stored procedure '{sproc}' was configured with a rows affected output parameter or return value, but a valid value was not found when executing the procedure. + The stored procedure '{sproc}' cannot be configured to return the rows affected because a rows affected parameter or a rows affected result column for this stored procedure already exists. diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs index 22a4c46891e..093687b789e 100644 --- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs @@ -308,29 +308,11 @@ private static void ValidateTemporalPeriodProperty(IEntityType temporalEntityTyp temporalEntityType.DisplayName(), periodProperty.Name)); } - if (temporalEntityType.GetTableName() is string tableName) + if (periodProperty.ValueGenerated != ValueGenerated.OnAddOrUpdate) { - var storeObjectIdentifier = StoreObjectIdentifier.Table(tableName, temporalEntityType.GetSchema()); - var periodColumnName = periodProperty.GetColumnName(storeObjectIdentifier); - - var propertiesMappedToPeriodColumn = temporalEntityType.GetProperties().Where( - p => p.Name != periodProperty.Name && p.GetColumnName(storeObjectIdentifier) == periodColumnName).ToList(); - foreach (var propertyMappedToPeriodColumn in propertiesMappedToPeriodColumn) - { - if (propertyMappedToPeriodColumn.ValueGenerated != ValueGenerated.OnAddOrUpdate) - { - throw new InvalidOperationException( - SqlServerStrings.TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate( - temporalEntityType.DisplayName(), propertyMappedToPeriodColumn.Name, nameof(ValueGenerated.OnAddOrUpdate))); - } - - if (propertyMappedToPeriodColumn.TryGetDefaultValue(out var _)) - { - throw new InvalidOperationException( - SqlServerStrings.TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue( - temporalEntityType.DisplayName(), propertyMappedToPeriodColumn.Name)); - } - } + throw new InvalidOperationException( + SqlServerStrings.TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate( + temporalEntityType.DisplayName(), periodProperty.Name, nameof(ValueGenerated.OnAddOrUpdate))); } // TODO: check that period property is excluded from query (once the annotation is added) diff --git a/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs b/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs index 65154026676..c2cbeb28dc9 100644 --- a/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Builders/TemporalPeriodPropertyBuilder.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Builders; /// Instances of this class are returned from methods when using the API /// and it is not designed to be directly constructed in your application code. /// -public class TemporalPeriodPropertyBuilder +public class TemporalPeriodPropertyBuilder : IInfrastructure { private readonly PropertyBuilder _propertyBuilder; @@ -59,6 +59,9 @@ public virtual TemporalPeriodPropertyBuilder HasPrecision(int precision) return this; } + PropertyBuilder IInfrastructure.Instance + => _propertyBuilder; + #region Hidden System.Object members /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index 05880ce5dd9..ccbf7ea4af5 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -333,14 +333,6 @@ public static string TemporalPeriodPropertyMustBeNonNullableDateTime(object? ent GetString("TemporalPeriodPropertyMustBeNonNullableDateTime", nameof(entityType), nameof(propertyName), nameof(dateTimeType)), entityType, propertyName, dateTimeType); - /// - /// Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified. - /// - public static string TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue(object? entityType, object? propertyName) - => string.Format( - GetString("TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue", nameof(entityType), nameof(propertyName)), - entityType, propertyName); - /// /// Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'. /// diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index 1e30c0c3c20..9815e413d51 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -332,9 +332,6 @@ Period property '{entityType}.{propertyName}' must be non-nullable and of type '{dateTimeType}'. - - Property '{entityType}.{propertyName}' is mapped to the period column and can't have default value specified. - Property '{entityType}.{propertyName}' is mapped to the period column and must have ValueGenerated set to '{valueGeneratedValue}'. diff --git a/src/EFCore/Extensions/Internal/DbContextExtensions.cs b/src/EFCore/Extensions/Internal/DbContextExtensions.cs new file mode 100644 index 00000000000..e747f45c422 --- /dev/null +++ b/src/EFCore/Extensions/Internal/DbContextExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable CheckNamespace + +namespace Microsoft.EntityFrameworkCore.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 static class DbContextExtensions +{ + /// + /// 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 ConfigureConventions( + this DbContext context, + ModelConfigurationBuilder configurationBuilder) + => context.ConfigureConventions(configurationBuilder); +} diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 378bb6eb950..e629cebace4 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -478,6 +478,20 @@ public virtual void Detects_incompatible_shared_columns_in_shared_table_with_dif modelBuilder); } + [ConditionalFact] + public virtual void Detects_properties_mapped_to_the_same_column_within_hierarchy() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity().Property(a => a.P0).HasColumnName(nameof(A.P0)); + modelBuilder.Entity().Property("PC").HasColumnName(nameof(A.P0)); + + VerifyError( + RelationalStrings.DuplicateColumnNameSameHierarchy( + nameof(A), nameof(A.P0), nameof(C), "PC", nameof(A.P0), nameof(A)), + modelBuilder); + } + [ConditionalFact] public virtual void Detects_incompatible_shared_columns_in_shared_table_with_different_provider_types() { @@ -869,35 +883,18 @@ public virtual void Detects_unmapped_foreign_keys_in_entity_splitting() LogLevel.Error); } - - - [ConditionalFact] - public virtual void Detects_duplicate_column_names() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder.Entity().Property(b => b.Id).HasColumnName("Name"); - modelBuilder.Entity().Property(d => d.Name).HasColumnName("Name").IsRequired(); - - VerifyError( - RelationalStrings.DuplicateColumnNameDataTypeMismatch( - nameof(Animal), nameof(Animal.Id), - nameof(Animal), nameof(Animal.Name), "Name", nameof(Animal), "default_int_mapping", "just_string(max)"), - modelBuilder); - } - [ConditionalFact] public virtual void Detects_duplicate_columns_in_derived_types_with_different_types() { var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity(); - modelBuilder.Entity().Property(c => c.Type).HasColumnName("Type").IsRequired(); - modelBuilder.Entity().Property(d => d.Type).HasColumnName("Type"); + modelBuilder.Entity().Property(c => c.Type).HasColumnName("Type").HasColumnType("someInt"); + modelBuilder.Entity().Property(d => d.Type).HasColumnName("Type").HasColumnType("default_int_mapping"); VerifyError( RelationalStrings.DuplicateColumnNameDataTypeMismatch( - nameof(Cat), nameof(Cat.Type), nameof(Dog), nameof(Dog.Type), nameof(Cat.Type), nameof(Animal), "just_string(max)", + nameof(Cat), nameof(Cat.Type), nameof(Dog), nameof(Dog.Type), nameof(Cat.Type), nameof(Animal), "someInt", "default_int_mapping"), modelBuilder); } @@ -1018,16 +1015,20 @@ public virtual void Detects_duplicate_column_names_within_hierarchy_with_differe } [ConditionalFact] - public virtual void Detects_duplicate_column_names_within_hierarchy_with_different_nullability() + public virtual void Detects_duplicate_column_names_with_different_column_nullability() { var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity(); - modelBuilder.Entity(); - modelBuilder.Entity().Property("OtherId").HasColumnName("Id"); + + modelBuilder.Entity().HasOne().WithOne(b => b.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id); + modelBuilder.Entity().HasOne().WithOne(g => g.A).HasForeignKey(a => a.Id).HasPrincipalKey(b => b.Id) + .Metadata.IsRequiredDependent = true; + modelBuilder.Entity().ToTable("Table").Ignore(a => a.P0); + modelBuilder.Entity().ToTable("Table").Property(b => b.P0).HasColumnName(nameof(A.P0)); + modelBuilder.Entity().ToTable("Table").Property(g => g.P0).HasColumnName(nameof(A.P0)).IsRequired(); VerifyError( RelationalStrings.DuplicateColumnNameNullabilityMismatch( - nameof(Animal), nameof(Animal.Id), nameof(Dog), "OtherId", nameof(Animal.Id), nameof(Animal)), + nameof(B), nameof(B.P0), nameof(G), nameof(G.P0), nameof(A.P0), "Table"), modelBuilder); } @@ -1686,7 +1687,7 @@ private class Address } [ConditionalFact] - public virtual void Detects_missing_concurrency_token_on_the_base_type_without_convention() + public virtual void Passes_with_missing_concurrency_token_on_the_base_type_without_convention() { var modelBuilder = CreateModelBuilderWithoutConvention(); modelBuilder.Entity().ToTable(nameof(Animal)) @@ -1695,9 +1696,7 @@ public virtual void Detects_missing_concurrency_token_on_the_base_type_without_c modelBuilder.Entity() .Property("Version").IsRowVersion().HasColumnName("Version"); - VerifyError( - RelationalStrings.MissingConcurrencyColumn(nameof(Animal), "Version", nameof(Animal)), - modelBuilder); + Validate(modelBuilder); } [ConditionalFact] @@ -1723,7 +1722,7 @@ public virtual void Passes_with_missing_concurrency_token_property_on_the_base_t modelBuilder.Entity() .Property("Version").IsRowVersion().HasColumnName("Version"); - var model = Validate(modelBuilder); + Validate(modelBuilder); } [ConditionalFact] diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/BlankTriggerAddingConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/BlankTriggerAddingConventionTest.cs new file mode 100644 index 00000000000..529b6084437 --- /dev/null +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/BlankTriggerAddingConventionTest.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +public class BlankTriggerAddingConventionTest +{ + [ConditionalFact] + public virtual void Adds_triggers_with_table_splitting() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity().SplitToTable("OrderDetails", s => s.Property(o => o.CustomerId)); + + var model = modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Order))!; + + Assert.Equal(new[] { "OrderDetails_Trigger", "Order_Trigger" }, entity.GetDeclaredTriggers().Select(t => t.ModelName)); + } + + [ConditionalFact] + public virtual void Does_not_add_triggers_without_tables() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder.Entity().ToView("Orders"); + modelBuilder.Entity().SplitToView("OrderDetails", s => s.Property(o => o.CustomerId)); + + var model = modelBuilder.FinalizeModel(); + + var entity = model.FindEntityType(typeof(Order))!; + + Assert.Empty(entity.GetDeclaredTriggers()); + } + + protected class Order + { + public int OrderId { get; set; } + + public int? CustomerId { get; set; } + public Guid AnotherCustomerId { get; set; } + } + + protected virtual ModelBuilder CreateModelBuilder() + => FakeRelationalTestHelpers.Instance.CreateConventionBuilder( + configureModel: + b => b.Conventions.Add( + p => new BlankTriggerAddingConvention( + p.GetRequiredService(), + p.GetRequiredService()))); +} diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs b/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs index 41ddbec93d9..fb7f21cdf7e 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.Internal; + namespace Microsoft.EntityFrameworkCore.TestUtilities; public class TestModelSource : ModelSource @@ -26,6 +28,7 @@ protected override IModel CreateModel( var modelConfigurationBuilder = new ModelConfigurationBuilder( conventionSetBuilder.CreateConventionSet(), context.GetInfrastructure()); + context.ConfigureConventions(modelConfigurationBuilder); _configureConventions?.Invoke(modelConfigurationBuilder); var modelBuilder = modelConfigurationBuilder.CreateModelBuilder(modelDependencies); diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs index 17fc19a4dcc..85d914fdc58 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerTriggersTest.cs @@ -107,12 +107,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity( eb => { - eb.ToTable(tb => - { - tb.HasTrigger("TRG_InsertProduct"); - tb.HasTrigger("TRG_UpdateProduct"); - tb.HasTrigger("TRG_DeleteProduct"); - }); eb.Property(e => e.Version) .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken(); @@ -122,6 +116,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .Property(e => e.Id).ValueGeneratedNever(); } + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.Conventions.Add(p => + new BlankTriggerAddingConvention( + p.GetRequiredService(), + p.GetRequiredService())); + } } protected class Product diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 83b527d9591..69139900baf 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -11,20 +11,6 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure; public class SqlServerModelValidatorTest : RelationalModelValidatorTest { - public override void Detects_duplicate_column_names() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder.Entity().Property(b => b.Id).HasColumnName("Name"); - modelBuilder.Entity().Property(d => d.Name).IsRequired().HasColumnName("Name"); - - VerifyError( - RelationalStrings.DuplicateColumnNameDataTypeMismatch( - nameof(Animal), nameof(Animal.Id), - nameof(Animal), nameof(Animal.Name), "Name", nameof(Animal), "int", "nvarchar(max)"), - modelBuilder); - } - public override void Detects_duplicate_columns_in_derived_types_with_different_types() { var modelBuilder = CreateConventionModelBuilder(); @@ -838,27 +824,12 @@ public void Temporal_period_property_must_be_mapped_to_datetime2() public void Temporal_all_properties_mapped_to_period_column_must_have_value_generated_OnAddOrUpdate() { var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity().Property(typeof(DateTime), "Start2").HasColumnName("StartColumn").ValueGeneratedOnAddOrUpdate(); - modelBuilder.Entity().Property(typeof(DateTime), "Start3").HasColumnName("StartColumn"); - modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => ttb.HasPeriodStart("Start").HasColumnName("StartColumn"))); + modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => + ttb.HasPeriodStart("Start").HasColumnName("StartColumn").GetInfrastructure().ValueGeneratedNever())); VerifyError( SqlServerStrings.TemporalPropertyMappedToPeriodColumnMustBeValueGeneratedOnAddOrUpdate( - nameof(Dog), "Start3", nameof(ValueGenerated.OnAddOrUpdate)), modelBuilder); - } - - [ConditionalFact] - public void Temporal_all_properties_mapped_to_period_column_cant_have_default_values() - { - var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity().Property(typeof(DateTime), "Start2").HasColumnName("StartColumn").ValueGeneratedOnAddOrUpdate(); - modelBuilder.Entity().Property(typeof(DateTime), "Start3").HasColumnName("StartColumn").ValueGeneratedOnAddOrUpdate() - .HasDefaultValue(DateTime.MinValue); - modelBuilder.Entity().ToTable(tb => tb.IsTemporal(ttb => ttb.HasPeriodStart("Start").HasColumnName("StartColumn"))); - - VerifyError( - SqlServerStrings.TemporalPropertyMappedToPeriodColumnCantHaveDefaultValue( - nameof(Dog), "Start3"), modelBuilder); + nameof(Dog), "Start", nameof(ValueGenerated.OnAddOrUpdate)), modelBuilder); } [ConditionalFact] diff --git a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs index 0a8dfe29cf1..c07162a4ba7 100644 --- a/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs +++ b/test/EFCore.SqlServer.Tests/ModelBuilding/SqlServerModelBuilderTestBase.cs @@ -1313,12 +1313,12 @@ public virtual void Temporal_table_with_explicit_properties_mapped_to_the_period })); modelBuilder.Entity() - .Property("MappedStart") + .Property("Start") .HasColumnName("PeriodStartColumn") .ValueGeneratedOnAddOrUpdate(); modelBuilder.Entity() - .Property("MappedEnd") + .Property("End") .HasColumnName("PeriodEndColumn") .ValueGeneratedOnAddOrUpdate(); @@ -1326,7 +1326,7 @@ public virtual void Temporal_table_with_explicit_properties_mapped_to_the_period var entity = model.FindEntityType(typeof(Customer))!; Assert.True(entity.IsTemporal()); - Assert.Equal(7, entity.GetProperties().Count()); + Assert.Equal(5, entity.GetProperties().Count()); Assert.Equal("HistoryTable", entity.GetHistoryTableName()); @@ -1344,12 +1344,6 @@ public virtual void Temporal_table_with_explicit_properties_mapped_to_the_period Assert.True(periodEnd.IsShadowProperty()); Assert.Equal(typeof(DateTime), periodEnd.ClrType); Assert.Equal(ValueGenerated.OnAddOrUpdate, periodEnd.ValueGenerated); - - var propertyMappedToStart = entity.GetProperty("MappedStart"); - Assert.Equal("PeriodStartColumn", propertyMappedToStart[RelationalAnnotationNames.ColumnName]); - - var propertyMappedToEnd = entity.GetProperty("MappedEnd"); - Assert.Equal("PeriodEndColumn", propertyMappedToEnd[RelationalAnnotationNames.ColumnName]); } [ConditionalFact] diff --git a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs index fb7d17922d2..fd176d9a659 100644 --- a/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs +++ b/test/EFCore.Sqlite.Tests/Infrastructure/SqliteModelValidatorTest.cs @@ -9,33 +9,6 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure; public class SqliteModelValidatorTest : RelationalModelValidatorTest { - public override void Detects_duplicate_column_names() - { - var modelBuilder = CreateConventionModelBuilder(); - - modelBuilder.Entity().Property(b => b.Id).HasColumnName("Name"); - modelBuilder.Entity().Property(d => d.Name).IsRequired().HasColumnName("Name"); - - VerifyError( - RelationalStrings.DuplicateColumnNameDataTypeMismatch( - nameof(Animal), nameof(Animal.Id), - nameof(Animal), nameof(Animal.Name), "Name", nameof(Animal), "INTEGER", "TEXT"), - modelBuilder); - } - - public override void Detects_duplicate_columns_in_derived_types_with_different_types() - { - var modelBuilder = CreateConventionModelBuilder(); - modelBuilder.Entity(); - - modelBuilder.Entity().Property(c => c.Type).IsRequired().HasColumnName("Type"); - modelBuilder.Entity().Property(d => d.Type).HasColumnName("Type"); - - VerifyError( - RelationalStrings.DuplicateColumnNameDataTypeMismatch( - typeof(Cat).Name, "Type", typeof(Dog).Name, "Type", "Type", nameof(Animal), "TEXT", "INTEGER"), modelBuilder); - } - [ConditionalFact] public virtual void Detects_duplicate_column_names_within_hierarchy_with_different_srid() { diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs index 75d7a2a33f6..46ea3e9a6a1 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTestBase.cs @@ -97,6 +97,18 @@ protected class F : D { } + protected class G + { + public int Id { get; set; } + + public int? P0 { get; set; } + public int? P1 { get; set; } + public int? P2 { get; set; } + public int? P3 { get; set; } + + public A A { get; set; } + } + protected abstract class Abstract : A { }