protected override void OnConfiguring(DbContextOptionsBuilder builder) =>
builder
.UseNpgsql(
_connectionString,
options => options.SetPostgresVersion(18, 3).UseNodaTime()
)
.ReplaceService<IConventionSetBuilder, CustomNpgsqlConventionSetBuilder>()
.ReplaceService<IModelValidator, CustomNpgsqlModelValidator>();
public sealed class CustomNpgsqlConventionSetBuilder(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies,
IRelationalTypeMappingSource typeMappingSource,
INpgsqlSingletonOptions npgsqlSingletonOptions
)
: NpgsqlConventionSetBuilder(
dependencies,
relationalDependencies,
typeMappingSource,
npgsqlSingletonOptions
),
IConventionSetBuilder
{
private readonly NpgsqlTypeMappingSource _typeMappingSource =
(NpgsqlTypeMappingSource)typeMappingSource;
private readonly IReadOnlyList<EnumDefinition> _enumDefinitions =
npgsqlSingletonOptions.EnumDefinitions;
public override ConventionSet CreateConventionSet()
{
var set = base.CreateConventionSet();
var existingFinalizingConvention = set
.ModelFinalizingConventions.OfType<NpgsqlPostgresModelFinalizingConvention>()
.SingleOrDefault();
if (existingFinalizingConvention is not null)
{
set.ModelFinalizingConventions.Remove(existingFinalizingConvention);
}
set.ModelFinalizingConventions.Add(
new CustomNpgsqlPostgresModelFinalizingConvention(_typeMappingSource, _enumDefinitions)
);
var existingRuntimeModelConvention = set
.ModelFinalizedConventions.OfType<NpgsqlRuntimeModelConvention>()
.SingleOrDefault();
if (existingRuntimeModelConvention is not null)
set.ModelFinalizedConventions.Remove(existingRuntimeModelConvention);
set.ModelFinalizedConventions.Add(
new CustomNpgsqlRuntimeModelConvention(Dependencies, RelationalDependencies)
);
return set;
}
}
/// <inheritdoc />
public sealed class CustomNpgsqlRuntimeModelConvention(
ProviderConventionSetBuilderDependencies dependencies,
RelationalConventionSetBuilderDependencies relationalDependencies
) : NpgsqlRuntimeModelConvention(dependencies, relationalDependencies)
{
/// <inheritdoc />
protected override void ProcessPropertyAnnotations(
Dictionary<string, object?> annotations,
IProperty property,
RuntimeProperty runtimeProperty,
bool runtime
)
{
base.ProcessPropertyAnnotations(annotations, property, runtimeProperty, runtime);
if (
(property.IsKey() || property.IsForeignKey())
&& property.FindTypeMapping() is IntervalRangeMapping
)
{
runtimeProperty.SetCurrentValueComparer(
new EntryCurrentValueComparer(runtimeProperty, new IntervalCurrentValueComparer())
);
}
}
}
/// <inheritdoc />
public sealed class CustomNpgsqlPostgresModelFinalizingConvention(
NpgsqlTypeMappingSource typeMappingSource,
IReadOnlyList<EnumDefinition> enumDefinitions
) : NpgsqlPostgresModelFinalizingConvention(typeMappingSource, enumDefinitions)
{
/// <inheritdoc />
protected override void SetRangeCurrentValueComparer(
IConventionProperty property,
RelationalTypeMapping typeMapping
)
{
base.SetRangeCurrentValueComparer(property, typeMapping);
if (
(property.IsKey() || property.IsForeignKey())
&& typeMapping is IntervalRangeMapping
&& property is PropertyBase propertyBase
)
{
propertyBase.SetCurrentValueComparer(
new EntryCurrentValueComparer(
(IProperty)property,
new IntervalCurrentValueComparer()
)
);
}
}
}
/// <summary>
/// Current-value comparer for NodaTime.Interval with range-like semantics.
/// Designed to be used as EF Core ValueComparer for mapped Interval properties.
/// </summary>
public sealed class IntervalCurrentValueComparer
: ValueComparer<Interval>,
IComparer,
IComparer<Interval>
{
public static readonly IntervalCurrentValueComparer Instance = new();
public IntervalCurrentValueComparer()
: base((x, y) => EqualsCore(x, y), v => GetHashCodeCore(v), v => v) { }
private static bool EqualsCore(Interval x, Interval y)
{
// Interval is a value type with value-based equality in NodaTime,
// but we make intent explicit and robust here.
return x.Start == y.Start && x.End == y.End;
}
private static int GetHashCodeCore(Interval v)
{
unchecked
{
var h = 17;
h = (h * 31) + v.Start.GetHashCode();
h = (h * 31) + v.End.GetHashCode();
return h;
}
}
public override Interval Snapshot(Interval v)
// Interval is immutable, so direct return is a valid snapshot.
=>
v;
/// <summary>
/// Optional ordering helper equivalent to range ordering intent:
/// start first, then end.
/// </summary>
public int Compare(Interval x, Interval y)
{
var c = x.Start.CompareTo(y.Start);
return c != 0 ? c : x.End.CompareTo(y.End);
}
/// <summary>
/// Non-generic comparer implementation.
/// </summary>
int IComparer.Compare(object? x, object? y)
{
return ReferenceEquals(x, y) ? 0
: x is null ? -1
: y is null ? 1
: x is Interval ix && y is Interval iy ? Compare(ix, iy)
: throw new ArgumentException(
$"Both arguments must be of type {typeof(Interval).FullName}."
);
}
}
public sealed class CustomNpgsqlModelValidator(
ModelValidatorDependencies dependencies,
RelationalModelValidatorDependencies relationalDependencies,
INpgsqlSingletonOptions npgsqlSingletonOptions
) : NpgsqlModelValidator(dependencies, relationalDependencies, npgsqlSingletonOptions)
{
private readonly Version _postgresVersion = npgsqlSingletonOptions.PostgresVersion;
protected override void ValidateKey(
IKey key,
IDiagnosticsLogger<DbLoggerCategory.Model.Validation> logger
)
{
// Keep all default validations first
ValidateShadowKey(key, logger);
ValidateMutableKey(key, logger);
ValidateDefaultValuesOnKey(key, logger);
ValidateValueGeneration(key, logger);
if (key.GetWithoutOverlaps() == true)
{
ValidateWithoutOverlapsKeyAllowingInterval(key);
}
}
private void ValidateWithoutOverlapsKeyAllowingInterval(IKey key)
{
var keyName = key.IsPrimaryKey()
? "primary key"
: $"alternate key {key.Properties.Format()}";
var entityType = key.DeclaringEntityType;
// Keep PG18 requirement
if (_postgresVersion < new Version(18, 0))
{
throw new InvalidOperationException(
NpgsqlStrings.WithoutOverlapsRequiresPostgres18(keyName, entityType.DisplayName())
);
}
// Keep "last property must be range-like" requirement, but allow Interval+conversion
var lastProperty = key.Properties[^1];
var typeMapping = lastProperty.FindTypeMapping();
if (typeMapping is not NpgsqlRangeTypeMapping && typeMapping is not IntervalRangeMapping)
{
throw new InvalidOperationException(
NpgsqlStrings.WithoutOverlapsRequiresRangeType(
keyName,
entityType.DisplayName(),
lastProperty.Name,
lastProperty.ClrType.ShortDisplayName()
)
);
}
}
}
Hello there.
I am trying out the version 11 preview to get the
.WithoutOverlaps()extension for temporal keys, along with the NodaTime types. It seems it is not possible to use the Interval NodaTime type to configure WITHOUT OVERLAPS. I get the following error when generating a migration:I have made the following overrides to get it working again:
Overrides
Override these services in the DbContext:With the following:
It would be nice to have support for this.