ChronoSafe.Npgsql
1.1.2
dotnet add package ChronoSafe.Npgsql --version 1.1.2
NuGet\Install-Package ChronoSafe.Npgsql -Version 1.1.2
<PackageReference Include="ChronoSafe.Npgsql" Version="1.1.2" />
<PackageVersion Include="ChronoSafe.Npgsql" Version="1.1.2" />
<PackageReference Include="ChronoSafe.Npgsql" />
paket add ChronoSafe.Npgsql --version 1.1.2
#r "nuget: ChronoSafe.Npgsql, 1.1.2"
#:package ChronoSafe.Npgsql@1.1.2
#addin nuget:?package=ChronoSafe.Npgsql&version=1.1.2
#tool nuget:?package=ChronoSafe.Npgsql&version=1.1.2
ChronoSafe.Npgsql
Automatically converts DateTime values to UTC for PostgreSQL — so you never see Cannot write DateTime with Kind=Local again, and every value you read back is guaranteed to be UTC.
Works with raw Npgsql, Dapper, and EF Core.
The problem
PostgreSQL stores timestamptz in UTC and returns values without timezone info. .NET's DateTime has a Kind property (Utc, Local, Unspecified) that Npgsql must respect — but your app almost certainly mixes kinds, especially when deserialising from JSON or reading from a form. This causes:
InvalidCastException: Cannot write DateTime with Kind=Local to column of type 'timestamp with time zone'- Silent data corruption when
Kind=Unspecifiedis written as-is - UTC values read back with
Kind=Unspecified, breaking comparisons
ChronoSafe.Npgsql fixes all of this at the data layer — one line of setup, zero changes to your domain code.
Installation
dotnet add package ChronoSafe.Npgsql
Usage
Raw Npgsql
var dataSource = new NpgsqlDataSourceBuilder(connectionString)
.UseChronoSafe() // ← one line
.Build();
// Everything after this point just works.
await using var conn = await dataSource.OpenConnectionAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = "INSERT INTO events (occurred_at) VALUES (@t)";
// Kind=Local → converted to UTC automatically
cmd.Parameters.AddWithValue("t", DateTime.Now);
await cmd.ExecuteNonQueryAsync();
EF Core
Call UseChronoSafe() inside OnModelCreating:
public class AppDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseChronoSafe(); // ← one line, covers every DateTime property
}
}
Dapper
Register the global type handlers once at startup (e.g. Program.cs):
ChronoSafeExtensions.AddDapperHandlers();
// Then use Dapper as normal — conversion is automatic.
var events = await conn.QueryAsync<MyEvent>(
"SELECT occurred_at FROM events WHERE id = @id",
new { id = 42 });
Configuration
All three integrations accept an optional ChronoSafeOptions delegate:
// EF Core
modelBuilder.UseChronoSafe(opts =>
{
opts.UnspecifiedHandling = UnspecifiedKindHandling.AssumeLocal;
opts.TimeZoneId = "Asia/Manila"; // optional — convert reads to local time
});
// Dapper
ChronoSafeExtensions.AddDapperHandlers(opts =>
{
opts.UnspecifiedHandling = UnspecifiedKindHandling.AssumeUtc;
opts.TimeZoneId = "Asia/Manila";
});
UnspecifiedKindHandling values
| Value | Behaviour |
|---|---|
AssumeUtc (default) |
Keeps the tick value, sets Kind = Utc |
AssumeLocal |
Converts from local time to UTC |
Throw |
Throws InvalidOperationException — use this to catch bugs early |
Reading back in local time (TimeZoneId)
By default, values read from PostgreSQL are returned as UTC. Set TimeZoneId to automatically
convert every DateTime read to your server's local timezone — no manual conversion needed anywhere in your code.
modelBuilder.UseChronoSafe(opts =>
{
opts.UnspecifiedHandling = UnspecifiedKindHandling.AssumeLocal;
opts.TimeZoneId = "Asia/Manila"; // UTC+8, DST-aware
});
Setting an invalid ID throws TimeZoneNotFoundException at startup, so misconfiguration is caught immediately.
Finding your timezone ID
Use the IANA timezone database ID for your region. These are cross-platform and work on Linux, macOS, and Windows (.NET 6+).
⚠️ Do not use Windows-style IDs like
"Singapore Standard Time"— they break on Linux where most servers run.
Common IANA IDs:
| Region | ID |
|---|---|
| Philippines | Asia/Manila |
| Singapore | Asia/Singapore |
| Thailand | Asia/Bangkok |
| Japan / Korea | Asia/Tokyo / Asia/Seoul |
| India | Asia/Kolkata |
| UAE / Gulf | Asia/Dubai |
| UK | Europe/London |
| Central Europe | Europe/Paris |
| US Eastern | America/New_York |
| US Central | America/Chicago |
| US Pacific | America/Los_Angeles |
| Australia Eastern | Australia/Sydney |
| New Zealand | Pacific/Auckland |
Full list: https://www.iana.org/time-zones
How it works
| Integration | Write path | Read path |
|---|---|---|
| Raw Npgsql | NpgsqlDataSourceBuilder |
Pass-through |
| EF Core | ValueConverter<DateTime, DateTime> on every DateTime property — converts to UTC |
Reverse converter applies TimeZoneId offset when set |
| Dapper | SqlMapper.TypeHandler<DateTime> sets DbType.DateTime2 and converts to UTC |
Parse applies TimeZoneId offset when set |
FAQ
Does this affect DateTimeOffset?
No. DateTimeOffset always carries offset information and Npgsql handles it correctly. This package only touches DateTime.
What about timestamp without time zone columns?
Those columns store no timezone data. If you're using timestamptz (recommended), this package is all you need.
Is there a performance overhead?
Negligible. The TimeZoneInfo is resolved once at startup and cached — no per-query lookup.
Why Kind=Unspecified on the read result?
After converting UTC → local time, the value is no longer UTC and shouldn't be treated as Local either (that would trigger another OS-level conversion). Unspecified means "already in target timezone — display as-is."
License
MIT © Johann Christopher Desepeda
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Dapper (>= 2.1.79)
- Microsoft.EntityFrameworkCore (>= 8.0.30)
- Npgsql (>= 8.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v1.1.2
- Fix: re-release of 1.1.0 with correct build. Both 1.1.0 and 1.1.1 were accidentally
packed from old source and were missing the TimeZoneId feature entirely.
No API changes — just the correct 1.1.0 code properly published.
v1.1.0
- New: TimeZoneId option on ChronoSafeOptions accepts any IANA timezone ID (e.g. "Asia/Manila",
"America/Los_Angeles") and automatically converts all DateTime values from UTC to the target
timezone on read. DST-aware. Validated at startup — throws TimeZoneNotFoundException immediately
on an unrecognised ID. Applies to both EF Core and Dapper.
v1.0.1
- Fixed: PostgreSQL connection error "invalid command-line argument for server process: TimeZone=UTC".
The startup parameter now correctly uses the -c TimeZone=UTC format required by PostgreSQL.
Resolves compatibility issues with Neon, Supabase, and direct PostgreSQL connections.