diff --git a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs index d9143e0ef..66f43025d 100644 --- a/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs +++ b/src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs @@ -40,6 +40,10 @@ private static readonly MethodInfo ModelHasPostgresRangeMethodInfo2 nameof(NpgsqlModelBuilderExtensions.HasPostgresRange), typeof(ModelBuilder), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string)); + private static readonly MethodInfo ModelUseNoIdentityGenerationMethodInfo + = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( + nameof(NpgsqlModelBuilderExtensions.UseNoIdentityGeneration), typeof(ModelBuilder)); + private static readonly MethodInfo ModelUseSerialColumnsMethodInfo = typeof(NpgsqlModelBuilderExtensions).GetRequiredRuntimeMethod( nameof(NpgsqlModelBuilderExtensions.UseSerialColumns), typeof(ModelBuilder)); @@ -393,8 +397,11 @@ public override IReadOnlyList GenerateFluentApiCalls( }); } case NpgsqlValueGenerationStrategy.None: - return new MethodCallCodeFragment( - ModelHasAnnotationMethodInfo, NpgsqlAnnotationNames.ValueGenerationStrategy, NpgsqlValueGenerationStrategy.None); + return onModel + ? new MethodCallCodeFragment(ModelUseNoIdentityGenerationMethodInfo) + : new MethodCallCodeFragment( + ModelHasAnnotationMethodInfo, NpgsqlAnnotationNames.ValueGenerationStrategy, + NpgsqlValueGenerationStrategy.None); default: throw new ArgumentOutOfRangeException(strategy.ToString()); diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs index 6650eb824..2d532e897 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs @@ -125,6 +125,29 @@ public static ModelBuilder UseSerialColumns( #region Identity + /// + /// Configures the model to not use any Npgsql-specific value generation strategy for properties + /// marked as , when targeting PostgreSQL. This disables all + /// automatic identity generation, serial columns, sequences, and hi-lo patterns; all identity + /// values must be provided by the application. + /// + /// The model builder. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseNoIdentityGeneration(this ModelBuilder modelBuilder) + { + Check.NotNull(modelBuilder, nameof(modelBuilder)); + + var model = modelBuilder.Model; + + model.SetValueGenerationStrategy(NpgsqlValueGenerationStrategy.None); + model.SetSequenceNameSuffix(null); + model.SetSequenceSchema(null); + model.SetHiLoSequenceName(null); + model.SetHiLoSequenceSchema(null); + + return modelBuilder; + } + /// /// /// Configures the model to use the PostgreSQL IDENTITY feature to generate values for properties diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs index 32b057f81..56b5f7c6e 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlValueGenerationConvention.cs @@ -9,19 +9,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; /// or were configured to use a . /// It also configures properties as if they were configured as computed columns. /// -public class NpgsqlValueGenerationConvention : RelationalValueGenerationConvention +public class NpgsqlValueGenerationConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + : RelationalValueGenerationConvention(dependencies, relationalDependencies) { - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - /// Parameter object containing relational dependencies for this convention. - public NpgsqlValueGenerationConvention( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies) - : base(dependencies, relationalDependencies) - { - } /// /// Called after an annotation is changed on a property. @@ -88,7 +80,9 @@ public override void ProcessPropertyAnnotationChanged( /// The identifier of the store object. /// The store value generation strategy to set for the given property. public static new ValueGenerated? GetValueGenerated(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) - => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) + => ShouldSuppressValueGeneration(property, storeObject) + ? GetNonStrategyValueGenerated(property, storeObject) + : RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) ?? (property.GetValueGenerationStrategy(storeObject) != NpgsqlValueGenerationStrategy.None ? ValueGenerated.OnAdd : null); @@ -97,8 +91,31 @@ public override void ProcessPropertyAnnotationChanged( IReadOnlyProperty property, in StoreObjectIdentifier storeObject, ITypeMappingSource typeMappingSource) - => RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) - ?? (property.GetValueGenerationStrategy(storeObject, typeMappingSource) != NpgsqlValueGenerationStrategy.None + => ShouldSuppressValueGeneration(property, storeObject) + ? GetNonStrategyValueGenerated(property, storeObject) + : RelationalValueGenerationConvention.GetValueGenerated(property, storeObject) + ?? (property.GetValueGenerationStrategy(storeObject, typeMappingSource) != NpgsqlValueGenerationStrategy.None + ? ValueGenerated.OnAdd + : null); + + /// + /// Returns whether should be suppressed for the given property because the + /// model has been configured with and the property has no + /// explicit value generation strategy override. + /// + private static bool ShouldSuppressValueGeneration(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => property.DeclaringType.Model.GetValueGenerationStrategy() is NpgsqlValueGenerationStrategy.None + && property.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy) is null + && property.FindOverrides(storeObject)?.FindAnnotation(NpgsqlAnnotationNames.ValueGenerationStrategy) is null; + + /// + /// Returns value generation based on non-strategy sources (default values, computed columns) when + /// the Npgsql value generation strategy has been suppressed. + /// + private static ValueGenerated? GetNonStrategyValueGenerated(IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => property.GetComputedColumnSql(storeObject) is not null + ? ValueGenerated.OnAddOrUpdate + : property.TryGetDefaultValue(storeObject, out _) || property.GetDefaultValueSql(storeObject) is not null ? ValueGenerated.OnAdd - : null); + : null; } diff --git a/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs b/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs index ec9cff986..9feba3815 100644 --- a/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs +++ b/test/EFCore.PG.Tests/Design/Internal/NpgsqlAnnotationCodeGeneratorTest.cs @@ -173,6 +173,22 @@ public void GenerateFluentApi_IProperty_works_with_IdentityAlways() Assert.Empty(result.Arguments); } + [ConditionalFact] + public void GenerateFluentApi_IModel_works_with_NoIdentityGeneration() + { + var generator = CreateGenerator(); + var modelBuilder = new ModelBuilder(NpgsqlConventionSetBuilder.Build()); + modelBuilder.UseNoIdentityGeneration(); + + var annotations = modelBuilder.Model.GetAnnotations().ToDictionary(a => a.Name, a => a); + var result = generator.GenerateFluentApiCalls(((IModel)modelBuilder.Model), annotations).Single(); + + Assert.Equal("UseNoIdentityGeneration", result.Method); + Assert.Equal("NpgsqlModelBuilderExtensions", result.DeclaringType); + + Assert.Empty(result.Arguments); + } + [ConditionalFact] public void GenerateFluentApi_IModel_works_with_HiLo() { diff --git a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs b/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs index 9115f5f73..01eef1957 100644 --- a/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs +++ b/test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs @@ -514,6 +514,21 @@ public void TryGetSequence_with_schema_returns_sequence_model_is_marked_for_sequ Assert.Equal("R", property.FindHiLoSequence().Schema); } + [ConditionalFact] + public void UseNoIdentityGeneration_suppresses_ValueGenerated_OnAdd_for_int_pk() + { + var modelBuilder = GetModelBuilder(); + modelBuilder.UseNoIdentityGeneration(); + + var property = modelBuilder + .Entity() + .Property(e => e.Id) + .Metadata; + + Assert.Equal(NpgsqlValueGenerationStrategy.None, property.GetValueGenerationStrategy()); + Assert.NotEqual(ValueGenerated.OnAdd, property.ValueGenerated); + } + private static ModelBuilder GetModelBuilder() => NpgsqlTestHelpers.Instance.CreateConventionBuilder();