From 4c838ce65ff411d8adb135583e0710dbc5cf0fd1 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Fri, 5 Aug 2022 09:53:25 -0700 Subject: [PATCH] Add pre-convention configuration for conventions Part of #214 --- src/EFCore/Infrastructure/ModelSource.cs | 4 +- .../Metadata/Builders/ConventionsBuilder.cs | 100 ++++++++++++++++++ src/EFCore/ModelConfigurationBuilder.cs | 23 ++-- .../RelationalModelValidatorTest.cs | 2 +- .../TestUtilities/TestHelpers.cs | 95 +++++++++-------- .../TestUtilities/TestModelSource.cs | 4 +- .../ModelBuilding/NonRelationshipTestBase.cs | 61 +++++++++++ 7 files changed, 231 insertions(+), 58 deletions(-) create mode 100644 src/EFCore/Metadata/Builders/ConventionsBuilder.cs diff --git a/src/EFCore/Infrastructure/ModelSource.cs b/src/EFCore/Infrastructure/ModelSource.cs index 15593abf4f1..2b70c7dd084 100644 --- a/src/EFCore/Infrastructure/ModelSource.cs +++ b/src/EFCore/Infrastructure/ModelSource.cs @@ -94,7 +94,9 @@ protected virtual IModel CreateModel( { Check.DebugAssert(context != null, "context == null"); - var modelConfigurationBuilder = new ModelConfigurationBuilder(conventionSetBuilder.CreateConventionSet()); + var modelConfigurationBuilder = new ModelConfigurationBuilder( + conventionSetBuilder.CreateConventionSet(), + context.GetInfrastructure()); context.ConfigureConventions(modelConfigurationBuilder); diff --git a/src/EFCore/Metadata/Builders/ConventionsBuilder.cs b/src/EFCore/Metadata/Builders/ConventionsBuilder.cs new file mode 100644 index 00000000000..b19780faa44 --- /dev/null +++ b/src/EFCore/Metadata/Builders/ConventionsBuilder.cs @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace Microsoft.EntityFrameworkCore.Metadata.Builders; + +/// +/// Provides a simple API surface for configuring conventions. +/// +/// +/// 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. +/// +/// +/// See Modeling entity types and relationships for more information and examples. +/// +public class ConventionsBuilder +{ + private readonly IServiceProvider _serviceProvider; + private readonly ConventionSet _conventionSet; + + /// + /// 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 ConventionsBuilder(ConventionSet conventionSet, IServiceProvider serviceProvider) + { + Check.NotNull(conventionSet, nameof(conventionSet)); + + _conventionSet = conventionSet; + _serviceProvider = serviceProvider; + } + + /// + /// Replaces an existing convention with a derived convention. Also registers the new convention for any + /// convention types not implemented by the existing convention. + /// + /// The type of the old convention. + /// The factory that creates the new convention. + public virtual void Replace(Func conventionFactory) + where TImplementation : IConvention + { + var convention = conventionFactory(_serviceProvider); + _conventionSet.Replace(convention); + } + + /// + /// Adds a convention to the set. + /// + /// The factory that creates the convention. + public virtual void Add(Func conventionFactory) + { + var convention = conventionFactory(_serviceProvider); + _conventionSet.Add(convention); + } + + /// + /// Removes the convention of the given type. + /// + /// The convention type to remove. + public virtual void Remove(Type conventionType) + { + _conventionSet.Remove(conventionType); + } + + #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/ModelConfigurationBuilder.cs b/src/EFCore/ModelConfigurationBuilder.cs index 5a1dc815fc7..4b6349eaf90 100644 --- a/src/EFCore/ModelConfigurationBuilder.cs +++ b/src/EFCore/ModelConfigurationBuilder.cs @@ -25,20 +25,21 @@ public class ModelConfigurationBuilder { private readonly ModelConfiguration _modelConfiguration = new(); private readonly ConventionSet _conventions; - + private readonly ConventionsBuilder _conventionsBuilder; + /// - /// Initializes a new instance of the . + /// 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. /// - /// - /// See Pre-convention model building in EF Core for more information and - /// examples. - /// - /// The conventions to be applied during model building. - public ModelConfigurationBuilder(ConventionSet conventions) + [EntityFrameworkInternal] + public ModelConfigurationBuilder(ConventionSet conventions, IServiceProvider serviceProvider) { Check.NotNull(conventions, nameof(conventions)); _conventions = conventions; + _conventionsBuilder = new ConventionsBuilder(conventions, serviceProvider); } /// @@ -51,6 +52,12 @@ public ModelConfigurationBuilder(ConventionSet conventions) protected virtual ModelConfiguration ModelConfiguration => _modelConfiguration; + /// + /// Gets the builder for the conventions that will be used in the model. + /// + public virtual ConventionsBuilder Conventions + => _conventionsBuilder; + /// /// Prevents the conventions from the given type from discovering properties of the given or derived types. /// diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 2ef844b692b..502c51d1e00 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -3432,7 +3432,7 @@ protected virtual TestHelpers.TestModelBuilder CreateModelBuilderWithoutConventi => TestHelpers.CreateConventionBuilder( CreateModelLogger(sensitiveDataLoggingEnabled), CreateValidationLogger(sensitiveDataLoggingEnabled), modelConfigurationBuilder => ConventionSet.Remove( - modelConfigurationBuilder.Conventions.ModelFinalizingConventions, + modelConfigurationBuilder.ConventionSet.ModelFinalizingConventions, typeof(T))); protected override TestHelpers TestHelpers diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs index a67233bc1e3..b5caea75ba0 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestHelpers.cs @@ -152,7 +152,8 @@ public TestModelBuilder CreateConventionBuilder( var modelCreationDependencies = contextServices.GetRequiredService(); var modelConfigurationBuilder = new TestModelConfigurationBuilder( - modelCreationDependencies.ConventionSetBuilder.CreateConventionSet()); + modelCreationDependencies.ConventionSetBuilder.CreateConventionSet(), + contextServices); configure?.Invoke(modelConfigurationBuilder); @@ -412,20 +413,20 @@ public IModel FinalizeModel(bool designTime = false, bool skipValidation = false public class TestModelConfigurationBuilder : ModelConfigurationBuilder { - public TestModelConfigurationBuilder(ConventionSet conventions) - : base(conventions) + public TestModelConfigurationBuilder(ConventionSet conventionSet, IServiceProvider serviceProvider) + : base(conventionSet, serviceProvider) { - Conventions = conventions; + ConventionSet = conventionSet; } - public ConventionSet Conventions { get; } + public ConventionSet ConventionSet { get; } public TestModelBuilder CreateModelBuilder( ModelDependencies modelDependencies, IModelRuntimeInitializer modelRuntimeInitializer, IDiagnosticsLogger validationLogger) => new( - Conventions, + ConventionSet, modelDependencies, ModelConfiguration.IsEmpty() ? null : ModelConfiguration.Validate(), modelRuntimeInitializer, @@ -433,47 +434,47 @@ public TestModelBuilder CreateModelBuilder( public void RemoveAllConventions() { - Conventions.EntityTypeAddedConventions.Clear(); - Conventions.EntityTypeAnnotationChangedConventions.Clear(); - Conventions.EntityTypeBaseTypeChangedConventions.Clear(); - Conventions.EntityTypeIgnoredConventions.Clear(); - Conventions.EntityTypeMemberIgnoredConventions.Clear(); - Conventions.EntityTypePrimaryKeyChangedConventions.Clear(); - Conventions.EntityTypeRemovedConventions.Clear(); - Conventions.ForeignKeyAddedConventions.Clear(); - Conventions.ForeignKeyAnnotationChangedConventions.Clear(); - Conventions.ForeignKeyDependentRequirednessChangedConventions.Clear(); - Conventions.ForeignKeyOwnershipChangedConventions.Clear(); - Conventions.ForeignKeyPrincipalEndChangedConventions.Clear(); - Conventions.ForeignKeyPropertiesChangedConventions.Clear(); - Conventions.ForeignKeyRemovedConventions.Clear(); - Conventions.ForeignKeyRequirednessChangedConventions.Clear(); - Conventions.ForeignKeyUniquenessChangedConventions.Clear(); - Conventions.IndexAddedConventions.Clear(); - Conventions.IndexAnnotationChangedConventions.Clear(); - Conventions.IndexRemovedConventions.Clear(); - Conventions.IndexUniquenessChangedConventions.Clear(); - Conventions.IndexSortOrderChangedConventions.Clear(); - Conventions.KeyAddedConventions.Clear(); - Conventions.KeyAnnotationChangedConventions.Clear(); - Conventions.KeyRemovedConventions.Clear(); - Conventions.ModelAnnotationChangedConventions.Clear(); - Conventions.ModelFinalizedConventions.Clear(); - Conventions.ModelFinalizingConventions.Clear(); - Conventions.ModelInitializedConventions.Clear(); - Conventions.NavigationAddedConventions.Clear(); - Conventions.NavigationAnnotationChangedConventions.Clear(); - Conventions.NavigationRemovedConventions.Clear(); - Conventions.PropertyAddedConventions.Clear(); - Conventions.PropertyAnnotationChangedConventions.Clear(); - Conventions.PropertyFieldChangedConventions.Clear(); - Conventions.PropertyNullabilityChangedConventions.Clear(); - Conventions.PropertyRemovedConventions.Clear(); - Conventions.SkipNavigationAddedConventions.Clear(); - Conventions.SkipNavigationAnnotationChangedConventions.Clear(); - Conventions.SkipNavigationForeignKeyChangedConventions.Clear(); - Conventions.SkipNavigationInverseChangedConventions.Clear(); - Conventions.SkipNavigationRemovedConventions.Clear(); + ConventionSet.EntityTypeAddedConventions.Clear(); + ConventionSet.EntityTypeAnnotationChangedConventions.Clear(); + ConventionSet.EntityTypeBaseTypeChangedConventions.Clear(); + ConventionSet.EntityTypeIgnoredConventions.Clear(); + ConventionSet.EntityTypeMemberIgnoredConventions.Clear(); + ConventionSet.EntityTypePrimaryKeyChangedConventions.Clear(); + ConventionSet.EntityTypeRemovedConventions.Clear(); + ConventionSet.ForeignKeyAddedConventions.Clear(); + ConventionSet.ForeignKeyAnnotationChangedConventions.Clear(); + ConventionSet.ForeignKeyDependentRequirednessChangedConventions.Clear(); + ConventionSet.ForeignKeyOwnershipChangedConventions.Clear(); + ConventionSet.ForeignKeyPrincipalEndChangedConventions.Clear(); + ConventionSet.ForeignKeyPropertiesChangedConventions.Clear(); + ConventionSet.ForeignKeyRemovedConventions.Clear(); + ConventionSet.ForeignKeyRequirednessChangedConventions.Clear(); + ConventionSet.ForeignKeyUniquenessChangedConventions.Clear(); + ConventionSet.IndexAddedConventions.Clear(); + ConventionSet.IndexAnnotationChangedConventions.Clear(); + ConventionSet.IndexRemovedConventions.Clear(); + ConventionSet.IndexUniquenessChangedConventions.Clear(); + ConventionSet.IndexSortOrderChangedConventions.Clear(); + ConventionSet.KeyAddedConventions.Clear(); + ConventionSet.KeyAnnotationChangedConventions.Clear(); + ConventionSet.KeyRemovedConventions.Clear(); + ConventionSet.ModelAnnotationChangedConventions.Clear(); + ConventionSet.ModelFinalizedConventions.Clear(); + ConventionSet.ModelFinalizingConventions.Clear(); + ConventionSet.ModelInitializedConventions.Clear(); + ConventionSet.NavigationAddedConventions.Clear(); + ConventionSet.NavigationAnnotationChangedConventions.Clear(); + ConventionSet.NavigationRemovedConventions.Clear(); + ConventionSet.PropertyAddedConventions.Clear(); + ConventionSet.PropertyAnnotationChangedConventions.Clear(); + ConventionSet.PropertyFieldChangedConventions.Clear(); + ConventionSet.PropertyNullabilityChangedConventions.Clear(); + ConventionSet.PropertyRemovedConventions.Clear(); + ConventionSet.SkipNavigationAddedConventions.Clear(); + ConventionSet.SkipNavigationAnnotationChangedConventions.Clear(); + ConventionSet.SkipNavigationForeignKeyChangedConventions.Clear(); + ConventionSet.SkipNavigationInverseChangedConventions.Clear(); + ConventionSet.SkipNavigationRemovedConventions.Clear(); } } } diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs b/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs index 531bea13753..41ddbec93d9 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestModelSource.cs @@ -23,7 +23,9 @@ protected override IModel CreateModel( IConventionSetBuilder conventionSetBuilder, ModelDependencies modelDependencies) { - var modelConfigurationBuilder = new ModelConfigurationBuilder(conventionSetBuilder.CreateConventionSet()); + var modelConfigurationBuilder = new ModelConfigurationBuilder( + conventionSetBuilder.CreateConventionSet(), + context.GetInfrastructure()); _configureConventions?.Invoke(modelConfigurationBuilder); var modelBuilder = modelConfigurationBuilder.CreateModelBuilder(modelDependencies); diff --git a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs index 99abb9588ac..2819a617960 100644 --- a/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/NonRelationshipTestBase.cs @@ -404,6 +404,67 @@ public virtual void Properties_can_be_ignored_by_type() Assert.Null(entityType.FindProperty(nameof(Customer.AlternateKey))); } + [ConditionalFact] + public virtual void Conventions_can_be_added() + { + var modelBuilder = CreateModelBuilder(c => c.Conventions.Add(s => new TestConvention())); + + var model = modelBuilder.FinalizeModel(); + + Assert.Equal("bar", model["foo"]); + } + + [ConditionalFact] + public virtual void Conventions_can_be_removed() + { + var modelBuilder = CreateModelBuilder(c => + { + c.Conventions.Add(s => new TestConvention()); + c.Conventions.Remove(typeof(TestConvention)); + }); + + var model = modelBuilder.FinalizeModel(); + + Assert.Null(model["foo"]); + } + + [ConditionalFact] + public virtual void Conventions_can_be_replaced() + { + var modelBuilder = CreateModelBuilder(c => + c.Conventions.Replace(s => + new TestDbSetFindingConvention(s.GetService()))); + + var model = modelBuilder.FinalizeModel(); + + Assert.Equal("bar", model["foo"]); + } + + protected class TestConvention : IModelInitializedConvention + { + public void ProcessModelInitialized( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + modelBuilder.HasAnnotation("foo", "bar"); + } + } + + protected class TestDbSetFindingConvention : DbSetFindingConvention + { + public TestDbSetFindingConvention(ProviderConventionSetBuilderDependencies dependencies) + : base(dependencies) + { + } + + public override void ProcessModelInitialized( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + modelBuilder.HasAnnotation("foo", "bar"); + } + } + [ConditionalFact] public virtual void Int32_cannot_be_ignored() => Assert.Equal(