ChronoSafe.Npgsql 1.1.2

dotnet add package ChronoSafe.Npgsql --version 1.1.2
                    
NuGet\Install-Package ChronoSafe.Npgsql -Version 1.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ChronoSafe.Npgsql" Version="1.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ChronoSafe.Npgsql" Version="1.1.2" />
                    
Directory.Packages.props
<PackageReference Include="ChronoSafe.Npgsql" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ChronoSafe.Npgsql --version 1.1.2
                    
#r "nuget: ChronoSafe.Npgsql, 1.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ChronoSafe.Npgsql@1.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ChronoSafe.Npgsql&version=1.1.2
                    
Install as a Cake Addin
#tool nuget:?package=ChronoSafe.Npgsql&version=1.1.2
                    
Install as a Cake Tool

ChronoSafe.Npgsql

NuGet License: MIT

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=Unspecified is 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.2 103 8/14/2026
1.1.1 93 8/14/2026
1.1.0 100 8/14/2026
1.0.1 103 8/4/2026
1.0.0 104 8/4/2026

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.