Skip to content
Merged
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
13 changes: 13 additions & 0 deletions conceptual/Npgsql/release-notes/10.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <xref:Npgsql.NpgsqlCommand.ExecuteScalarAsync*> and <xref:Npgsql.NpgsqlDataReader.GetValue*?displayProperty=nameWithType>; you can still read `DateTime` and `TimeSpan` via the generic <xref:Npgsql.NpgsqlDataReader.GetFieldValue%2A>.

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.
Expand Down
20 changes: 11 additions & 9 deletions conceptual/Npgsql/types/datetime.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (Utc<sup>1</sup>) | DateTimeOffset (Offset=0)<sup>2</sup>
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 (<sup>3</sup>) | <xref:NpgsqlTypes.NpgsqlInterval>
PostgreSQL type | Default .NET type | Non-default .NET types
--------------------------- | ---------------------------- | ----------------------
timestamp with time zone | DateTime (Utc<sup>1</sup>) | DateTimeOffset (Offset=0)<sup>2</sup>
timestamp without time zone | DateTime (Unspecified) |
date | DateOnly (10.0+)<sup>3</sup> | DateTime
time without time zone | TimeOnly (10.0+)<sup>3</sup> | TimeSpan
time with time zone | DateTimeOffset |
interval | TimeSpan (<sup>4</sup>) | <xref:NpgsqlTypes.NpgsqlInterval>

<sup>1</sup> 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).

<sup>2</sup> 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.

<sup>3</sup> 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 <xref:NpgsqlTypes.NpgsqlInterval>.
<sup>3</sup> 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).

<sup>4</sup> 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 <xref:NpgsqlTypes.NpgsqlInterval>.

## Detailed Behavior: Sending values to the database

Expand Down
2 changes: 1 addition & 1 deletion docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"resource":
[
{
"files": [ "img/**", "styles/**", "CNAME" ]
"files": [ "img/**", "styles/**", "static/**", "CNAME" ]
},
{
"files": [ "**" ],
Expand Down
126 changes: 126 additions & 0 deletions static/LegacyDateAndTimeResolverFactory.cs
Original file line number Diff line number Diff line change
@@ -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<DateTime>(Date,
static (options, mapping, _) => options.GetTypeInfo(typeof(DateTime), new DataTypeName(mapping.DataTypeName))!,
matchRequirement: MatchRequirement.DataTypeName);

mappings.AddStructType<TimeSpan>(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<DateTime>(Date);
mappings.AddStructArrayType<TimeSpan>(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<NpgsqlRange<DateTime>>(DateRange,
static (options, mapping, _) => options.GetTypeInfo(typeof(NpgsqlRange<DateTime>), 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<NpgsqlRange<DateTime>>(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<NpgsqlRange<DateTime>[]>(DateMultirange,
static (options, mapping, _) => options.GetTypeInfo(typeof(NpgsqlRange<DateTime>[]), 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<NpgsqlRange<DateTime>[]>(DateMultirange);

return mappings;
}
}
}