For twenty years the .NET default was obvious: SQL Server, because it came from the same company as the runtime and SSMS was the best free database tool on earth. That default has quietly stopped being automatic. This post is the decision, written by someone who has run both in production behind .NET apps — including a PostgreSQL on the very server that delivered this page — and who still likes SQL Server. The differences that matter are money, ecosystem and a handful of habits, not capability.
The short answer: for a new .NET application in 2026, PostgreSQL is the better
default. It's free with no edition matrix, the Npgsql EF Core provider is
first-class, it runs anywhere from a Docker container to every cloud's managed service, and
jsonb plus the extension ecosystem give you things SQL Server charges for or
doesn't have. Stay on SQL Server when you're already deep in it — T-SQL
procedures, SSIS packages, SSRS reports, Agent jobs, Windows authentication — when Azure SQL
is your platform, or when your team's fluency in SSMS is a real asset you'd be throwing
away. Both run .NET beautifully; the question is what else you're paying for.
Licensing and cost
SQL Server has a free tier and a bill. Express is free and capped (ten gigabytes per
database is the cap people hit); Developer is free but not for production; Standard and
Enterprise are licensed per core (Standard also sells as server-plus-CALs), and in the
cloud, where cores are metered by the hour,
that line item grows with your success. Managed SQL Server on AWS or Azure bakes the license
into the hourly price, which is why the same instance size costs markedly more than its
PostgreSQL neighbor. PostgreSQL is released under its own permissive license: no editions,
no client access licenses, no audit. Every cloud runs it managed — Azure Database for
PostgreSQL, Amazon RDS and Aurora, Google Cloud SQL, plus newer platforms built entirely
around it — and a five-dollar VPS runs it too. ClinicLive's development database is one
Docker line, and its tests run against a real postgres:18 container in CI.
Part 1 of the migration series makes
the fuller case.
EF Core: Npgsql versus SqlClient
This is the row .NET developers worry about most and the one that has aged best.
Microsoft.EntityFrameworkCore.SqlServer is Microsoft's own provider on top of
Microsoft.Data.SqlClient, and it's the one the EF team tests first.
Npgsql.EntityFrameworkCore.PostgreSQL is open source, maintained by the Npgsql
team, and ships in step with each EF Core release; its lead maintainer also works on EF Core
itself, which is as close to first-party as a community provider gets. ClinicLive uses it
without ceremony — the only PostgreSQL-specific line in the registration is the naming
convention:
// Factory, not plain AddDbContext: interactive Blazor components outlive a request,
// so each operation needs its own short-lived context.
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString).UseSnakeCaseNamingConvention());
What differs in practice: identifiers fold to lowercase in PostgreSQL, so the idiomatic
schema is snake_case and the EFCore.NamingConventions package
maps your OrderItem class to order_items without touching a
single entity; migrations regenerate because they contain provider SQL; and Npgsql exposes
PostgreSQL's richer types — jsonb, arrays, ranges, citext — as
ordinary C# properties. One rule catches everyone once: a DateTime mapped to
timestamptz must have Kind set to UTC, or Npgsql throws. LINQ
translates the same on both; raw SQL is where the dialect shows.
Part 8 walks through the swap.
Tooling: SSMS versus pgAdmin and psql
| Job | SQL Server | PostgreSQL |
|---|---|---|
| The everyday GUI | SSMS — free, Windows-only, superb | pgAdmin 4 — free, cross-platform, adequate; DBeaver or DataGrip if you want SSMS-like comfort |
| Quick scripting | SSMS query window, sqlcmd | psql — the tool you'll resist and then love |
| Execution plans | Graphical plans in SSMS | EXPLAIN ANALYZE text, graphical in pgAdmin and DBeaver |
| Who's running what | Activity Monitor, sp_who2 | pg_stat_activity |
| In the editor | Visual Studio's SQL tools, the VS Code MSSQL extension | Rider and VS Code extensions; every editor speaks Postgres |
Opinion, and I'll own it: SSMS is still the best free database GUI ever shipped, and
nothing on the PostgreSQL side is quite as complete in one window. The PostgreSQL answer is
that you stop needing one window. psql with \d orders describes a
table faster than any tree view, DBeaver covers both engines if you must keep one GUI, and
every editor has a decent Postgres extension. Budget a week of mild grumpiness, not a
month.
Hosting options
SQL Server runs on Windows, on Linux, and in the official Docker image, and it's a
first-class citizen on Azure — Azure SQL Database and Managed Instance are genuinely
excellent, and if your organization already lives there, that is a strong reason to stay.
AWS offers it managed too, license included. PostgreSQL runs everywhere the same way, plus
the tiny end of the market SQL Server never served: a container on a laptop, a shared VPS,
a free tier on a serverless Postgres platform. The practical difference is the floor. The
cheapest production SQL Server is a license decision; the cheapest production PostgreSQL is
an apt install.
JSON: jsonb versus JSON functions
PostgreSQL's jsonb is a real column type: binary-stored, validated on write,
indexable with a GIN index, and queried with operators the planner understands. SQL Server's
approach, since 2016, has been functions over text — JSON_VALUE,
JSON_QUERY, OPENJSON, FOR JSON — on an
NVARCHAR(MAX) column, with a native json type arriving in the
newest release and Azure SQL. The everyday query shows the difference:
-- PostgreSQL: a real type, an index the query can use
CREATE INDEX ix_events_data ON events USING gin (data);
SELECT * FROM events WHERE data @> '{"status": "active"}';
-- SQL Server: functions over a string column
SELECT * FROM Events WHERE JSON_VALUE(Data, '$.status') = 'active';
Both work. The PostgreSQL version indexes the containment query directly; the SQL Server
version needs a computed column and an index on that to avoid a scan. On the EF side, both
providers map a C# object graph to a JSON column and let LINQ query inside it; Npgsql also
maps a bare jsonb column to a POCO or a JsonDocument and exposes
the containment operator through EF.Functions. If your schema has a
"properties bag" column,
Part 3 explains why it should become
jsonb rather than be ported one-to-one.
Full-text search
SQL Server's Full-Text Search is a separate feature with its own catalogs, its own
population process and the CONTAINS and FREETEXT predicates;
it's powerful, and it's a component you install, configure and wait for. PostgreSQL's
full-text search is built into the core engine: a tsvector column, a GIN index
on it, and the match operator:
ALTER TABLE articles ADD COLUMN search tsvector
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX ix_articles_search ON articles USING gin (search);
SELECT title FROM articles WHERE search @@ plainto_tsquery('english', 'migration plan');
The index updates with the row, in the same transaction, with nothing to schedule. For
fuzzy matching and fast LIKE '%term%', the pg_trgm extension adds
trigram indexes, which SQL Server has no direct equivalent for. The Npgsql EF provider
exposes full-text search through EF.Functions, so it stays in LINQ.
Procedures: T-SQL versus PL/pgSQL
This is the row that decides most migrations, because it's the one with real work in it.
T-SQL is one big tent — a procedure returns result sets, sets output parameters and manages
transactions in the same batch. PL/pgSQL splits it: functions return values and
can be used inside a SELECT; procedures (called with
CALL) can commit mid-flight but don't return results the same way. Bodies are
dollar-quoted, variables have no @ prefix, PRINT becomes
RAISE NOTICE, TRY/CATCH becomes an EXCEPTION block,
and there's no GO. The behavior that bites hardest isn't syntax: in PostgreSQL,
any error aborts the whole transaction until you roll back or use a savepoint, where SQL
Server by default carries on past a failed statement. Written down, it's a week of
translation; Part 5 does one
real procedure line by line. My advice for either engine, offered as opinion: keep business
logic in C# where it has tests, and let procedures be thin.
The side-by-side
| SQL Server | PostgreSQL | Edge | |
|---|---|---|---|
| Cost | Free Express (capped) and Developer; per-core Standard and Enterprise | Free, everywhere, no editions | PostgreSQL |
| EF Core provider | Microsoft's own | Npgsql, open source, ships with each EF release | Tie |
| Tooling | SSMS | pgAdmin, psql, DBeaver | SQL Server, on polish |
| Hosting | Windows, Linux, Docker, Azure SQL, RDS | Everything, down to a $5 VPS | PostgreSQL, on range |
| JSON | Functions over text; native type in the newest release | jsonb with GIN indexes and operators | PostgreSQL |
| Full-text search | Separate feature, catalogs, population | Built in, transactional, plus pg_trgm | PostgreSQL |
| Procedures | T-SQL, one tent | PL/pgSQL, functions and procedures | Familiarity: SQL Server |
| Concurrency | Locking by default; snapshot isolation opt-in | MVCC by default; readers never block writers; VACUUM to maintain | Depends on your habits |
| Around the engine | Agent, SSIS, SSRS, linked servers, Windows auth | pg_cron, foreign data wrappers, external tools | SQL Server, if you use them |
| Extensions | CLR, limited | PostGIS, pgvector, pg_trgm, pg_cron, and hundreds more | PostgreSQL |
What hurts in a migration
- Case. Unquoted identifiers fold to lowercase, and quoted ones are case-sensitive forever; string comparison is case-sensitive by default where SQL Server's usual collations are not. The fixes are
snake_case,citextorILIKE, and an index onlower(email)— Part 2 and Part 4. - Timestamps.
DATETIME2becomestimestamportimestamptz, and choosing wrong is the number-one migration bug — Part 3. - No cross-database queries.
OtherDb.dbo.Tabledoesn't exist; schemas in one database, or a foreign data wrapper. - The procedures, as above, and everything dynamic or cursor-shaped inside them.
- The things around the engine. SQL Agent, SSIS, SSRS and linked servers have answers but not drop-in replacements; "we only have two SSIS packages" is the longest line item in most plans.
- Instincts. There is no clustered index, readers don't block writers, and VACUUM is a thing you now own — Part 6. Backups are
pg_dumpand WAL archiving, not.bakfiles — Part 7.
You don't have to guess how much of that applies to you. Paste a scripted database into the free migration assessment tool: it inventories tables, views, procedures, functions, triggers and indexes, runs the translator over the mechanical parts, and flags what genuinely needs a person — cursors, dynamic SQL, linked servers, Agent jobs. Then let the T-SQL to PostgreSQL translator do the phrasebook work, with every change explained and linked to the part that teaches it. Both run in-process and store nothing.
What you gain
- The bill, and with it the freedom to run a real database on every laptop, in every CI job and on every tiny server.
- Types you'll wonder how you lived without:
jsonb, arrays, ranges, a realboolean,uuidv7()built in. - Phrasing that says what it means:
RETURNINGinstead ofSCOPE_IDENTITY(),INSERT … ON CONFLICTfor upserts,LIMITthat reads like English. - A wider index shelf: partial and expression indexes, GIN for JSON and search, BRIN for huge append-only tables, all covered in Part 6.
- Extensions instead of features waiting for the next major version: PostGIS for geography, pgvector for embeddings, pg_trgm for fuzzy search, pg_cron for scheduling.
- Portability. Skills, schemas and dumps that move between clouds and employers unchanged.
Honestly, it depends
The strongest case for SQL Server isn't a feature; it's inertia, and inertia is a real cost. A team with a decade of T-SQL, SSRS reports the finance department lives in, Agent jobs nobody remembers writing and an Azure SQL bill already approved has nothing to gain from a migration this year and plenty to lose. Azure SQL in particular is a superb managed service with automatic tuning and point-in-time restore that just works, and if your identity story is Entra and Windows authentication end to end, SQL Server fits it like a glove. The migration series exists for teams that have decided; this post is for teams that are still deciding, and "stay" is a legitimate answer.
Two more honest notes. A SQL Server developer's first month on PostgreSQL feels like a downgrade — the GUI is plainer, errors are terser, and the case-folding rule will bite on day one; the series' Part 1 promises the differences are learnable in a focused week, and that has matched my experience, but the week is real. And the decision is independent of your front end: whether the app is Blazor Server, WebAssembly or a MAUI app — see Blazor Server vs WebAssembly vs Hybrid — the database under it faces exactly the choice above.
If you only remember one thing: both engines run .NET well, so decide on what surrounds the engine. If you're paying for cores or planning to, PostgreSQL. If you're paying in T-SQL, SSIS and SSRS you can't replace, SQL Server. Everything else — EF Core, LINQ, performance for an ordinary app — is a wash.
Frequently asked
- Is PostgreSQL better than SQL Server for .NET applications?
- For a new .NET application, PostgreSQL is usually the better default: it is free with no edition limits, the Npgsql EF Core provider is mature and ships with each EF Core release, and it runs anywhere from a Docker container to every cloud. SQL Server remains the right choice when a team already depends on T-SQL procedures, SSIS, SSRS, SQL Agent or Azure SQL, because both engines run .NET equally well and the difference is cost and ecosystem rather than capability.
- Does Entity Framework Core work well with PostgreSQL?
- Yes. The Npgsql.EntityFrameworkCore.PostgreSQL provider supports migrations, LINQ translation, JSON columns, arrays and full-text search, and is maintained in step with EF Core. Switching from SQL Server is mostly changing UseSqlServer to UseNpgsql, adding the snake_case naming convention package, regenerating migrations and translating any raw SQL.
- What is the hardest part of migrating from SQL Server to PostgreSQL?
- Stored procedures and everything around the engine. T-SQL procedures must be rewritten in PL/pgSQL, and SQL Agent jobs, SSIS packages, SSRS reports and linked servers have alternatives but no drop-in replacements. The schema and queries are mostly mechanical, with case-sensitive identifiers and string comparison and the timestamp versus timestamptz choice as the classic bugs.
If the answer for you is PostgreSQL, the eight-part SQL Server to PostgreSQL series is the translation guide, written for people who already know SQL Server deeply, and the two free tools above will tell you how big your particular move is before you commit to it. If the answer is SQL Server, you've lost nothing by checking — and you'll know exactly what you're paying for.