Home Services Work About Blog Contact Let's Talk
BlogPostgreSQL & .NET
🖵 PostgreSQL & .NET

PostgreSQL 17 for .NET Developers: EF Core, Npgsql & Performance Patterns

PostgreSQL has been quietly winning the "which database do you use for your new .NET SaaS?" question for the past three years. Lower licensing cost than SQL Server, better JSON support, exceptional extension ecosystem (PostGIS, pgvector, TimescaleDB), and now in version 17 — meaningful performance improvements that close several remaining gaps with SQL Server in enterprise scenarios.

This post covers what's new in PostgreSQL 17 that actually matters for .NET teams, how to configure EF Core 9 + Npgsql for production workloads, and a decision matrix for choosing between PostgreSQL and SQL Server for your next enterprise project.

What's New in PostgreSQL 17 That Matters for .NET Teams

PostgreSQL 17 was released in September 2024. The headline features from a .NET application developer's perspective:

Incremental Sort Improvements

Sort operations on partially-ordered data (common in paginated queries) are significantly faster. Affects any query with ORDER BY on indexed columns that aren't the leftmost index key.

New JSON_TABLE Function

SQL/JSON path language gets JSON_TABLE(), enabling you to query JSON arrays as relational rows without custom functions. Native in SQL — no app-side shredding.

Logical Replication Improvements

Failover slots survive primary switchover. Logical replication from slots now survives high-availability failover — critical for CDC pipelines and read replica setups.

VACUUM Performance

VACUUM now uses 20x less I/O on large tables by scanning only pages that need vacuuming. Reduces contention during autovacuum on high-write-volume tables.

Merge Command Enhancements

MERGE gains RETURNING clause and WHEN NOT MATCHED BY SOURCE. Now functionally equivalent to SQL Server's MERGE — useful for upsert patterns in EF Core migrations.

Parallel Query in More Places

Window functions and DISTINCT aggregates can now run in parallel. Complex analytical queries that previously ran single-threaded benefit immediately.

EF Core 9 + Npgsql Setup for Production

Npgsql is the .NET data provider for PostgreSQL. The EF Core provider (Npgsql.EntityFrameworkCore.PostgreSQL) wraps Npgsql and adds EF Core-specific query translation. For .NET 9 + PostgreSQL 17:

// Package references
// dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 9.*
// dotnet add package Npgsql --version 9.*

// Program.cs — minimal production setup
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        npgsqlOptions =>
        {
            npgsqlOptions.EnableRetryOnFailure(
                maxRetryCount: 5,
                maxRetryDelay: TimeSpan.FromSeconds(30),
                errorCodesToAdd: null);
            npgsqlOptions.CommandTimeout(60);
        })
    .UseSnakeCaseNamingConvention()); // Maps PascalCase C# to snake_case PG

Connection pooling: Npgsql vs PgBouncer

Npgsql has built-in connection pooling that works well for most .NET apps. For high-concurrency SaaS apps, you may hit PostgreSQL's per-connection overhead (each PG connection is a separate OS process). The recommendation in 2026:

  • Under 200 concurrent users: Npgsql's built-in pool is sufficient. Set MinPoolSize=5;MaxPoolSize=50 in your connection string.
  • 200–1,000 concurrent users: Consider PgBouncer in transaction pooling mode as a sidecar. Reduces PostgreSQL connections by 5–10x.
  • 1,000+ concurrent users: PgBouncer is essential. Alternatively, use Azure Database for PostgreSQL Flexible Server which includes built-in PgBouncer.
// Connection string with Npgsql pooling tuned
"DefaultConnection": "Host=myserver;Database=mydb;Username=app;Password=***;
  MinPoolSize=5;MaxPoolSize=50;ConnectionIdleLifetime=300;
  ConnectionPruningInterval=10;Include Error Detail=true"

JSON in PostgreSQL + .NET: The Practical Patterns

PostgreSQL's JSON support (json and jsonb types) is one of the main reasons .NET teams choose it over SQL Server for SaaS products. jsonb is stored in a binary format, indexed with GIN indexes, and is far more performant than json for query use cases.

Storing and querying JSONB with EF Core

// Entity with JSONB column
public class Order
{
    public int Id { get; set; }
    public string CustomerCode { get; set; } = "";
    public JsonDocument Metadata { get; set; } = null!; // maps to jsonb
}

// EF Core configuration
modelBuilder.Entity<Order>()
    .Property(o => o.Metadata)
    .HasColumnType("jsonb");

// Querying JSONB in EF Core with Npgsql
// Find orders where metadata contains a specific region
var results = await context.Orders
    .Where(o => EF.Functions.JsonContains(
        o.Metadata, @"{""region"": ""APAC""}"))
    .ToListAsync();

PostgreSQL 17's JSON_TABLE in raw SQL

-- Query a JSONB array as relational rows (PostgreSQL 17+)
SELECT p.order_id, item.product_code, item.quantity
FROM orders o,
     JSON_TABLE(
         o.metadata, '$.line_items[*]'
         COLUMNS (
             product_code TEXT PATH '$.sku',
             quantity INT PATH '$.qty'
         )
     ) AS item(product_code, quantity),
     (SELECT o.id AS order_id) AS p;

-- Run via EF Core with raw SQL
var items = await context.Database
    .SqlQuery<OrderLineItem>($"SELECT ...")
    .ToListAsync();

Performance Patterns for .NET + PostgreSQL

1. Use compiled queries for hot paths

// Compiled query — evaluated once, reused across calls
private static readonly Func<AppDbContext, int, Task<Order?>> GetOrderById =
    EF.CompileAsyncQuery((AppDbContext ctx, int id) =>
        ctx.Orders
           .Include(o => o.Lines)
           .FirstOrDefault(o => o.Id == id));

// In your service
var order = await GetOrderById(context, orderId);

2. Batch inserts with Npgsql's COPY protocol

// Npgsql COPY is 10–50x faster than INSERT for bulk data
await using var writer = await conn.BeginBinaryImportAsync(
    "COPY orders (customer_code, amount, created_at) FROM STDIN (FORMAT BINARY)");

foreach (var order in ordersToInsert)
{
    await writer.StartRowAsync();
    await writer.WriteAsync(order.CustomerCode);
    await writer.WriteAsync(order.Amount);
    await writer.WriteAsync(order.CreatedAt);
}
await writer.CompleteAsync();

3. GIN indexes for JSONB and full-text search

-- GIN index for JSONB containment queries
CREATE INDEX CONCURRENTLY idx_orders_metadata_gin
ON orders USING GIN (metadata);

-- GIN index for full-text search
CREATE INDEX CONCURRENTLY idx_products_search
ON products USING GIN (to_tsvector('english', name || ' ' || description));

-- Full-text search in EF Core
var results = await context.Products
    .Where(p => EF.Functions.ToTsVector("english", p.Name + " " + p.Description)
                .Matches(EF.Functions.ToTsQuery("english", searchTerm)))
    .ToListAsync();

4. Streaming large result sets

// Stream large result sets to avoid loading everything into memory
await foreach (var batch in context.Orders
    .Where(o => o.CreatedAt >= cutoff)
    .AsAsyncEnumerable()
    .Chunk(1000))
{
    await ProcessBatchAsync(batch);
}

PostgreSQL 17 vs SQL Server 2022: Enterprise Decision Guide

FactorPostgreSQL 17SQL Server 2022
Licensing cost Free (open source) Expensive — per core for Enterprise
Azure managed option Azure DB for PostgreSQL Flexible Server Azure SQL Database / Managed Instance
JSON / semi-structured data Excellent — JSONB + GIN indexes Good — JSON functions, no JSONB equivalent
Full-text search Native tsvector + GIN Full-Text Search service (heavier setup)
Microsoft 365 / Azure AD integration Manual configuration needed Native AAD auth, Microsoft Fabric integration
SSMS / developer tooling pgAdmin, DBeaver, DataGrip, TablePlus SSMS, Azure Data Studio — excellent
Extension ecosystem PostGIS, pgvector, TimescaleDB, Citus Limited — fewer open-source extensions
EF Core support Excellent via Npgsql provider Excellent via Microsoft.EntityFrameworkCore.SqlServer
Multi-tenancy patterns Row-level security (RLS) — native Row-level security supported but more verbose
Existing .NET enterprise teams Small learning curve Zero learning curve — familiar tooling

Our recommendation: For new SaaS products targeting multi-cloud deployment, cost sensitivity, or workloads needing pgvector (AI embeddings) or PostGIS (geospatial), PostgreSQL 17 is the clear choice. For enterprise .NET apps deeply integrated with Azure Active Directory, Microsoft Fabric, or existing SQL Server infrastructure, SQL Server 2022 avoids migration friction and tooling retraining.

Multi-Tenancy with PostgreSQL Row-Level Security

For multi-tenant SaaS, PostgreSQL's Row-Level Security (RLS) is a powerful pattern that enforces tenant isolation at the database level — even if your application code contains a bug that forgets to filter by tenant ID.

-- Enable RLS on tenant-shared tables
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy: users can only see their own tenant's rows
CREATE POLICY tenant_isolation_policy ON orders
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Set the tenant context in your .NET connection
await context.Database.ExecuteSqlRawAsync(
    $"SET LOCAL app.current_tenant_id = '{tenantId}'");

-- Now all queries on orders automatically filter by tenant
var orders = await context.Orders.ToListAsync(); // returns only current tenant's rows

Npgsql + RLS tip: Use NpgsqlConnection.ReloadTypes() after changing the PostgreSQL search path in multi-tenant schemas. For connection pool scenarios, set the tenant context in a SaveChanges interceptor rather than per-query to avoid per-request overhead.

pgvector: PostgreSQL as Your AI Embedding Store

One of the most compelling reasons to choose PostgreSQL in 2026 is pgvector — the open-source extension that adds vector similarity search to PostgreSQL. If you're building AI features (semantic search, recommendation systems, RAG pipelines), pgvector lets you store and query embeddings directly in PostgreSQL alongside your relational data — no separate vector database needed.

// Install pgvector extension
CREATE EXTENSION vector;

// EF Core entity with vector column
public class Document
{
    public int Id { get; set; }
    public string Content { get; set; } = "";
    public Vector Embedding { get; set; } = null!; // 1536 dims for text-embedding-3-small
}

// Query: find 10 most similar documents to a query embedding
var queryEmbedding = await openAiClient.GetEmbeddingAsync(userQuery);
var results = await context.Documents
    .OrderBy(d => d.Embedding.L2Distance(queryEmbedding))
    .Take(10)
    .ToListAsync();

Summary

PostgreSQL 17 is a mature, production-ready database for .NET enterprise applications in 2026. Key takeaways:

  • PostgreSQL 17 brings meaningful performance improvements — better sort, vacuum, and parallel query — that close remaining gaps with SQL Server in enterprise-scale scenarios.
  • Npgsql 9 + EF Core 9 provide a first-class .NET experience. Snake_case naming, JSONB support, compiled queries, and COPY-protocol batch inserts are all production-ready.
  • Choose PostgreSQL for new SaaS products, multi-cloud strategies, cost-sensitive environments, or workloads needing JSON, full-text, PostGIS, or pgvector.
  • Choose SQL Server when deeply integrated with Azure AD/Entra, Microsoft Fabric, SSRS, or an existing SQL Server estate — the tooling and integration advantages are real.
  • Row-Level Security makes PostgreSQL particularly powerful for multi-tenant SaaS — tenant isolation enforced at the database engine level.