Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/EFCore.PG/Design/Internal/NpgsqlAnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -393,8 +397,11 @@ public override IReadOnlyList<MethodCallCodeFragment> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,29 @@ public static ModelBuilder UseSerialColumns(

#region Identity

/// <summary>
/// Configures the model to not use any Npgsql-specific value generation strategy for properties
/// marked as <see cref="ValueGenerated.OnAdd" />, 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.
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
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;
}

/// <summary>
/// <para>
/// Configures the model to use the PostgreSQL IDENTITY feature to generate values for properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,11 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
/// or were configured to use a <see cref="NpgsqlValueGenerationStrategy" />.
/// It also configures properties as <see cref="ValueGenerated.OnAddOrUpdate" /> if they were configured as computed columns.
/// </summary>
public class NpgsqlValueGenerationConvention : RelationalValueGenerationConvention
public class NpgsqlValueGenerationConvention(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies)
: RelationalValueGenerationConvention(dependencies, relationalDependencies)
{
/// <summary>
/// Creates a new instance of <see cref="NpgsqlValueGenerationConvention" />.
/// </summary>
/// <param name="dependencies">Parameter object containing dependencies for this convention.</param>
/// <param name="relationalDependencies">Parameter object containing relational dependencies for this convention.</param>
public NpgsqlValueGenerationConvention(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies)
: base(dependencies, relationalDependencies)
{
}

/// <summary>
/// Called after an annotation is changed on a property.
Expand Down Expand Up @@ -88,7 +80,9 @@ public override void ProcessPropertyAnnotationChanged(
/// <param name="storeObject"> The identifier of the store object. </param>
/// <returns>The store value generation strategy to set for the given property.</returns>
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);
Expand All @@ -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);

/// <summary>
/// Returns whether <see cref="ValueGenerated.OnAdd" /> should be suppressed for the given property because the
/// model has been configured with <see cref="NpgsqlValueGenerationStrategy.None" /> and the property has no
/// explicit value generation strategy override.
/// </summary>
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;

/// <summary>
/// Returns value generation based on non-strategy sources (default values, computed columns) when
/// the Npgsql value generation strategy has been suppressed.
/// </summary>
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
15 changes: 15 additions & 0 deletions test/EFCore.PG.Tests/Metadata/NpgsqlMetadataExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Customer>()
.Property(e => e.Id)
.Metadata;

Assert.Equal(NpgsqlValueGenerationStrategy.None, property.GetValueGenerationStrategy());
Assert.NotEqual(ValueGenerated.OnAdd, property.ValueGenerated);
}

private static ModelBuilder GetModelBuilder()
=> NpgsqlTestHelpers.Instance.CreateConventionBuilder();

Expand Down