diff --git a/conceptual/Npgsql/release-notes/10.0.md b/conceptual/Npgsql/release-notes/10.0.md index 4d4f2bdd..f9891b6b 100644 --- a/conceptual/Npgsql/release-notes/10.0.md +++ b/conceptual/Npgsql/release-notes/10.0.md @@ -72,6 +72,19 @@ With .NET 6 being out of support since November 2024, Npgsql 10.0 also drops sup The PostgreSQL `date` and `time` types are now read as .NET [`DateOnly`](https://learn.microsoft.com/dotnet/api/system.dateonly) and [`TimeOnly`](https://learn.microsoft.com/dotnet/api/system.timeonly), instead of [`DateTime`](https://learn.microsoft.com/dotnet/api/system.datetime) and [`TimeSpan`](https://learn.microsoft.com/dotnet/api/system.timespan) by default, respectively. This affects non-generic read methods which return `object`, such as and ; you can still read `DateTime` and `TimeSpan` via the generic . +If your code relies on Npgsql returning `DateTime`/`TimeSpan` by default, and you are unable to either call `GetFieldValue()` or change your code to handle `DateOnly`/`TimeOnly` instead, then you can revert Npgsql's behavior to the pre-10.0 behavior. To do this, drop the [LegacyDateAndTimeResolverFactory.cs](/static/LegacyDateAndTimeResolverFactory.cs) into your project, and register it with your `NpgsqlDataSourceBuilder` as follows: + +```c# +NpgsqlDataSourceBuilder build = ...; +builder.AddTypeInfoResolverFactory(new LegacyDateAndTimeResolverFactory()); +``` + +Alternatively, if you're not yet using `NpgsqlDataSource`, register it with the global type mapper: + +```c# +NpgsqlConnection.GlobalTypeMapper.AddTypeInfoResolverFactory(new LegacyDateAndTimeResolverFactory()); +``` + ### `cidr` is now mapped to `IPNetwork` With .NET 6 no longer supported by Npgsql, the PostgreSQL `cidr` type is now mapped to `IPNetwork` by default instead of `NpgsqlCidr`. In addition, `NpgsqlCidr` is now obsolete and will be removed in the future. diff --git a/conceptual/Npgsql/types/datetime.md b/conceptual/Npgsql/types/datetime.md index ce8a8672..6718df18 100644 --- a/conceptual/Npgsql/types/datetime.md +++ b/conceptual/Npgsql/types/datetime.md @@ -48,20 +48,22 @@ Note: in versions prior to 6.0, the connection string parameter `Convert Infinit ## Detailed Behavior: Reading values from the database -PostgreSQL type | Default .NET type | Non-default .NET types ---------------------------- | -------------------------- | ---------------------- -timestamp with time zone | DateTime (Utc1) | DateTimeOffset (Offset=0)2 -timestamp without time zone | DateTime (Unspecified) | -date | DateTime | DateOnly (6.0+) -time without time zone | TimeSpan | TimeOnly (6.0+) -time with time zone | DateTimeOffset | -interval | TimeSpan (3) | +PostgreSQL type | Default .NET type | Non-default .NET types +--------------------------- | ---------------------------- | ---------------------- +timestamp with time zone | DateTime (Utc1) | DateTimeOffset (Offset=0)2 +timestamp without time zone | DateTime (Unspecified) | +date | DateOnly (10.0+)3 | DateTime +time without time zone | TimeOnly (10.0+)3 | TimeSpan +time with time zone | DateTimeOffset | +interval | TimeSpan (4) | 1 In versions prior to 6.0 (or when `Npgsql.EnableLegacyTimestampBehavior` is enabled), reading a `timestamp with time zone` returns a Local DateTime instead of Utc. [See the breaking change note for more info](../release-notes/6.0.md#major-changes-to-timestamp-mapping). 2 In versions prior to 6.0 (or when `Npgsql.EnableLegacyTimestampBehavior` is enabled), reading a `timestamp with time zone` as a DateTimeOffset returns a local offset based on the timezone of the server where Npgsql is running. -3 PostgreSQL intervals with month or year components cannot be read as TimeSpan. Consider using NodaTime's [Period](https://nodatime.org/3.0.x/api/NodaTime.Period.html) type, or . +3 In versions prior to 10.0, reading `date` and `time` returned .NET `DateTime` and `TimeSpan` by default (although reading `DateOnly` and `TimeOnly` was possible). To switch back to the old behavior, see the [10.0 breaking change note](../release-notes/10.0.md#date-and-time-are-now-mapped-to-dateonly-and-timeonly). + +4 PostgreSQL intervals with month or year components cannot be read as TimeSpan. Consider using NodaTime's [Period](https://nodatime.org/3.0.x/api/NodaTime.Period.html) type, or . ## Detailed Behavior: Sending values to the database diff --git a/docfx.json b/docfx.json index ada147bb..0f8d4884 100644 --- a/docfx.json +++ b/docfx.json @@ -73,7 +73,7 @@ "resource": [ { - "files": [ "img/**", "styles/**", "CNAME" ] + "files": [ "img/**", "styles/**", "static/**", "CNAME" ] }, { "files": [ "**" ], diff --git a/static/LegacyDateAndTimeResolverFactory.cs b/static/LegacyDateAndTimeResolverFactory.cs new file mode 100644 index 00000000..2762baa3 --- /dev/null +++ b/static/LegacyDateAndTimeResolverFactory.cs @@ -0,0 +1,126 @@ +using System; +using Npgsql.Internal; +using Npgsql.Internal.Postgres; +using NpgsqlTypes; + +sealed class LegacyDateAndTimeResolverFactory : PgTypeInfoResolverFactory +{ + public override IPgTypeInfoResolver CreateResolver() => new Resolver(); + public override IPgTypeInfoResolver CreateArrayResolver() => new ArrayResolver(); + public override IPgTypeInfoResolver CreateRangeResolver() => new RangeResolver(); + public override IPgTypeInfoResolver CreateRangeArrayResolver() => new RangeArrayResolver(); + public override IPgTypeInfoResolver CreateMultirangeResolver() => new MultirangeResolver(); + public override IPgTypeInfoResolver CreateMultirangeArrayResolver() => new MultirangeArrayResolver(); + + const string Date = "pg_catalog.date"; + const string Time = "pg_catalog.time"; + const string DateRange = "pg_catalog.daterange"; + const string DateMultirange = "pg_catalog.datemultirange"; + + class Resolver : IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new()); + + public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null; + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddStructType(Date, + static (options, mapping, _) => options.GetTypeInfo(typeof(DateTime), new DataTypeName(mapping.DataTypeName))!, + matchRequirement: MatchRequirement.DataTypeName); + + mappings.AddStructType(Time, + static (options, mapping, _) => options.GetTypeInfo(typeof(TimeSpan), new DataTypeName(mapping.DataTypeName))!, + isDefault: true); + + return mappings; + } + } + + sealed class ArrayResolver : Resolver, IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings)); + + public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null; + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddStructArrayType(Date); + mappings.AddStructArrayType(Time); + + return mappings; + } + } + + class RangeResolver : IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new()); + + public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null; + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddStructType>(DateRange, + static (options, mapping, _) => options.GetTypeInfo(typeof(NpgsqlRange), new DataTypeName(mapping.DataTypeName))!, + matchRequirement: MatchRequirement.DataTypeName); + + return mappings; + } + } + + sealed class RangeArrayResolver : RangeResolver, IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings)); + + public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null; + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddStructArrayType>(DateRange); + + return mappings; + } + } + + class MultirangeResolver : IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + protected TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new()); + + public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null; + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddType[]>(DateMultirange, + static (options, mapping, _) => options.GetTypeInfo(typeof(NpgsqlRange[]), new DataTypeName(mapping.DataTypeName))!, + matchRequirement: MatchRequirement.DataTypeName); + + return mappings; + } + } + + sealed class MultirangeArrayResolver : MultirangeResolver, IPgTypeInfoResolver + { + TypeInfoMappingCollection? _mappings; + new TypeInfoMappingCollection Mappings => _mappings ??= AddMappings(new(base.Mappings)); + + public new PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options) + => type == typeof(object) ? Mappings.Find(type, dataTypeName, options) : null; + + static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings) + { + mappings.AddArrayType[]>(DateMultirange); + + return mappings; + } + } +}