EricksonLopez.DapperExtensions.PostgreSQL 1.0.1

dotnet add package EricksonLopez.DapperExtensions.PostgreSQL --version 1.0.1
                    
NuGet\Install-Package EricksonLopez.DapperExtensions.PostgreSQL -Version 1.0.1
                    
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="EricksonLopez.DapperExtensions.PostgreSQL" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EricksonLopez.DapperExtensions.PostgreSQL" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="EricksonLopez.DapperExtensions.PostgreSQL" />
                    
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 EricksonLopez.DapperExtensions.PostgreSQL --version 1.0.1
                    
#r "nuget: EricksonLopez.DapperExtensions.PostgreSQL, 1.0.1"
                    
#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 EricksonLopez.DapperExtensions.PostgreSQL@1.0.1
                    
#: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=EricksonLopez.DapperExtensions.PostgreSQL&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=EricksonLopez.DapperExtensions.PostgreSQL&version=1.0.1
                    
Install as a Cake Tool

EricksonLopez.DapperExtensions.PostgreSQL

NuGet CI License: MIT .NET

High-performance Dapper extensions for PostgreSQL. The core feature is bulk insert and upsert via UNNEST — a PostgreSQL-native strategy that's 10-30x faster than row-by-row inserts with a single round-trip to the database.

Also includes: paginated queries returning PagedList<T>, transaction helpers, and JSONB type handler.


Why this exists

Dapper is fast, but inserting collections row-by-row (even with ExecuteAsync) is slow:

  • 100 rows row-by-row: ~15ms
  • 100 rows UNNEST: ~3ms
  • 10,000 rows row-by-row: ~1,300ms
  • 10,000 rows UNNEST: ~45ms (~29x faster)

Multi-value INSERT ... VALUES (...),(...) is limited by PostgreSQL's ~65,535 parameter count.
UNNEST has no such limit. 1,000,000 rows? One round-trip.


Installation

dotnet add package EricksonLopez.DapperExtensions.PostgreSQL

Requires: EricksonLopez.SharedKernel (automatically pulled as a dependency)


Quick Start

Bulk Insert (UNNEST)

// Build the typed array parameters
var parameters = BulkParameters.From(products)
    .Add("Ids",       p => p.Id,        NpgsqlDbType.Uuid)
    .Add("Names",     p => p.Name,      NpgsqlDbType.Text)
    .Add("Prices",    p => p.Price,     NpgsqlDbType.Numeric)
    .Add("CreatedAts",p => p.CreatedAt, NpgsqlDbType.TimestampTz)
    .Build();

// Execute — one round-trip regardless of how many rows
var rowsInserted = await connection.BulkInsertAsync(
    """
    INSERT INTO products (id, name, price, created_at)
    SELECT * FROM UNNEST(@Ids, @Names, @Prices, @CreatedAts)
    """,
    parameters);

Bulk Upsert (ON CONFLICT DO UPDATE)

var parameters = BulkParameters.From(products)
    .Add("Ids",    p => p.Id,    NpgsqlDbType.Uuid)
    .Add("Names",  p => p.Name,  NpgsqlDbType.Text)
    .Add("Prices", p => p.Price, NpgsqlDbType.Numeric)
    .Build();

await connection.BulkUpsertAsync(
    """
    INSERT INTO products (id, name, price)
    SELECT * FROM UNNEST(@Ids, @Names, @Prices)
    ON CONFLICT (id) DO UPDATE
        SET name  = EXCLUDED.name,
            price = EXCLUDED.price
    """,
    parameters);

Paginated Query

Returns PagedList<T> from EricksonLopez.SharedKernel — includes TotalCount, TotalPages, HasNextPage, etc.

var page = await connection.QueryPagedAsync<ProductDto>(
    sql:      "SELECT id, name, price FROM products WHERE active = @Active ORDER BY name",
    countSql: "SELECT COUNT(*) FROM products WHERE active = @Active",
    pagination: PaginationParameters.Of(page: 1, pageSize: 20),
    param:    new { Active = true });

// page.Items        — IReadOnlyList<ProductDto> for this page
// page.TotalCount   — total matching rows
// page.TotalPages   — total pages
// page.HasNextPage  — true if not on last page

Single round-trip variant (query + count in one call):

var page = await connection.QueryPagedMultipleAsync<ProductDto>(
    sql: """
        SELECT id, name FROM products WHERE active = @Active ORDER BY name
        LIMIT @Limit OFFSET @Offset;
        SELECT COUNT(*) FROM products WHERE active = @Active;
        """,
    pagination: PaginationParameters.Of(page: 1, pageSize: 20),
    param: new { Active = true, Limit = 20, Offset = 0 });

Transactions (Unit of Work)

// Void overload — commits on success, rolls back on exception
await connection.ExecuteInTransactionAsync(async trx =>
{
    await connection.ExecuteAsync(insertOrderSql,    orderParams,    trx);
    await connection.ExecuteAsync(insertLinesSql,    linesParams,    trx);
    await connection.ExecuteAsync(updateInventorySql,inventoryParams, trx);
});

// T-returning overload
var newId = await connection.ExecuteInTransactionAsync(async trx =>
{
    await connection.ExecuteAsync(insertSql, param, trx);
    return await connection.ExecuteScalarAsync<Guid>(selectIdSql, param, trx);
});

JSONB Type Handler

// Register once at startup (Program.cs or DI setup)
NpgsqlTypeHandlerRegistrar.RegisterJsonbHandler<AddressDto>();
NpgsqlTypeHandlerRegistrar.RegisterJsonbHandler<MetadataDto>();

// Then use normally — Dapper handles serialization/deserialization
var result = await connection.QueryAsync<Customer>(
    "SELECT id, name, address FROM customers");
// customer.Address is automatically deserialized from JSONB

NpgsqlDbType Reference

Common mappings for BulkParameters.Add():

C# Type NpgsqlDbType
Guid NpgsqlDbType.Uuid
string NpgsqlDbType.Text
int NpgsqlDbType.Integer
long NpgsqlDbType.Bigint
decimal NpgsqlDbType.Numeric
bool NpgsqlDbType.Boolean
DateTime (UTC) NpgsqlDbType.TimestampTz
DateOnly NpgsqlDbType.Date
object (JSONB) NpgsqlDbType.Jsonb
string[] NpgsqlDbType.Array \| NpgsqlDbType.Text

Architecture Decisions


License

MIT © Erickson López

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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.0.1 68 7/21/2026
1.0.0 85 7/16/2026