Provider-specific features
This page covers behaviour that is provider-dependent rather than portable: the active dialect has to advertise the matching
Supports*capability, and the providers that do not opt in reject the construct withNotSupportedException(FOR JSON/FOR XMLon SQL Server, row locking on PostgreSQL/MySQL/MariaDB/SQL Server). Features that every provider implements — even when the storage or the spelling differs — are documented on concept pages instead, for example range columns (a native range type on PostgreSQL, a pair of scalar columns elsewhere) or JSON support across providers. The closing table lists spelling differences that do not gate a feature at all.
JSON output (SQL Server)
ForJson is a terminal operator: it executes the query and returns the
whole result set as one JSON document (SupportsForJson). Because it is
terminal it applies no implicit TOP 1, so the document covers every row; the query element type is
irrelevant since the database returns a single document column:
string? json = dataContext.From<IComplexEntity>()
.Select(e => new { e.Id, e.String })
.ForJson(ForJsonMode.Path, root: "items", includeNullValues: true);
select id, somestring
from complex_entity for json path, root('items'), include_null_values
Path shapes the document from the projection aliases, Auto from the table
structure. ForJson returns null when the query produces no rows (SQL Server returns SQL NULL for an
empty FOR JSON result). The clause is placed after ORDER BY and before a trailing OPTION (...);
other providers throw NotSupportedException. Use
WithForJson to only attach the clause and keep the command composable (for further
hints or SQL inspection).
XML output (SQL Server)
ForXml is the XML counterpart and terminal (SupportsForXml); it supports
RAW, AUTO, EXPLICIT and PATH, with an optional row element name, a ROOT('...') wrapper and the
ELEMENTS flag:
string? xml = dataContext.From<IComplexEntity>()
.Select(e => new { e.Id })
.ForXml(ForXmlMode.Raw, elementName: "row", root: "items", elements: true);
select id
from complex_entity for xml raw('row'), root('items'), elements
Like ForJson, ForXml returns null for an empty result set, and
WithForXml attaches the clause without executing. FOR JSON and FOR XML are mutually
exclusive; combining them throws NotSupportedException.
Row locking (FOR UPDATE / FOR SHARE)
ForUpdate and
ForShare lock the selected rows until the surrounding
transaction ends. PostgreSQL, MySQL and MariaDB emit a trailing clause placed last, after WHERE,
ORDER BY and a page request; SQL Server attaches a WITH (updlock)/WITH (holdlock) table hint to
every physical source of the statement:
var rows = await dataContext.From<SimpleEntity>()
.Where(x => x.Id > 5)
.ForUpdate()
.ToListAsync();
-- PostgreSQL / MySQL / MariaDB
select id
from simple_entity where (id > 5) for update
-- SQL Server
select id
from simple_entity with (updlock) where (id > 5)
ForUpdate() locks rows exclusively; ForShare takes a
shared lock — PostgreSQL renders for share, MySQL/MariaDB render lock in share mode, and SQL Server
renders holdlock (shared) versus updlock for ForUpdate. The whole-query clause is implemented by
PostgreSQL, MySQL, MariaDB and SQL Server (Lock; SQL Server uses
ILockRenderer.UsesTableHints);
every other provider throws NotSupportedException when the SQL is built.
Whole-query vs source-level lock
A whole-query ForUpdate()/ForShare() locks the statement; a source-level lock targets one
physical source and is configured through that source's options:
- the primary source through
FromOptions.ForUpdate/FromOptions.ForSharepassed toFrom<T>(o => ...)orFrom("table", o => ...); - a join through
JoinOptions.ForUpdate/JoinOptions.ForSharepassed to the trailing options argument ofJoin/LeftJoin/RightJoin/FullJoin.
The source is addressed by its SQL alias, so the lock never leaks to a neighbouring join, to another instance of the same CLR type or to another alias:
var rows = await dataContext.From<SimpleEntity>(o => o.ForUpdate())
.Join<ComplexEntity>((s, c) => s.Id == c.Id, j => j.ForShare())
.Select(p => new { p.Item1.Id })
.ToListAsync();
-- PostgreSQL
select t1.id
from simple_entity as "t1"
join complex_entity as "t2" on cast(t1.id as bigint) = t2.id
for update of "t1"
for share of "t2"
-- MySQL
select t1.id
from simple_entity as `t1`
join complex_entity as `t2` on cast(t1.id as signed) = t2.id
for update of `t1`
for share of `t2`
PostgreSQL and MySQL render one targeted FOR UPDATE OF <alias>/FOR SHARE OF <alias> clause per
locked source. Because LOCK IN SHARE MODE has no OF form, a MySQL source-level share always uses the
newer FOR SHARE OF spelling. A whole-query and a source-level lock are mutually exclusive — combining
them throws InvalidOperationException — and a source-level lock is only valid on a physical table
source: FromSql, a query/builder subquery and a CTE source throw NotSupportedException.
SQL Server. A whole-query
ForUpdate()/ForShare()places the lock hint on everyFROM/JOINphysical source of the statement. A source-level lock places the hint on the chosen source only.
MariaDB has no
FOR UPDATE OF/FOR SHARE OFgrammar, so a source-level lock is not supported: it is rejected deterministically withNotSupportedException(fail-closed) instead of emitting an unsupportedOF, substituting a whole-query lock or silently ignoring the target. Whole-queryForUpdate()/ForShare()keep MariaDB's own grammar (for update,lock in share mode).
Pass a LockWaitMode to control what happens when another transaction
already holds the row: NoWait fails immediately instead of
waiting, and SkipLocked leaves the locked rows out of the
result — the standard way to build a queue or worker pool:
var claimed = await dataContext.From<Job>()
.Where(x => x.State == "pending")
.ForUpdate(LockWaitMode.SkipLocked)
.ToListAsync();
-- PostgreSQL / MySQL / MariaDB
select id
from job where (state = 'pending') for update skip locked
-- SQL Server (READPAST approximates SKIP LOCKED)
select id
from job with (updlock, readpast) where (state = 'pending')
PostgreSQL and MySQL append the trailing nowait/skip locked (FOR UPDATE/FOR SHARE [NOWAIT | SKIP LOCKED]); a shared lock with a wait mode switches MySQL from lock in share mode to for share, because
LOCK IN SHARE MODE takes no lock option. MariaDB appends the mode to both for update and lock in share mode (NOWAIT on 10.3+, SKIP LOCKED on 10.6+). SQL Server adds nowait or readpast to the
same table hint (with (updlock, nowait) / with (updlock, readpast)); readpast skips any locked row,
not only a row locked by another writer, so it approximates rather than exactly matches SKIP LOCKED.
Wait is the default and keeps the blocking behaviour.
Provider differences
| Provider | Behaviour |
|---|---|
| SQLite | Column aliases are single-quoted (as 'Calc'); derived tables do not require an alias. |
| SQL Server | Column aliases are bracket-quoted (as [Calc]); every derived table must be aliased (as [t1]). String concatenation uses +. |
| PostgreSQL | Column aliases are double-quoted (as "Calc"); derived tables must be aliased (as "t1"). |
| MySQL | Column aliases are backtick-quoted (as `Calc`); every derived table must be aliased (as `t1`). String concatenation uses concat(a, b). |
| MariaDB | Same as MySQL: backtick-quoted aliases, aliased derived tables and concat(a, b) concatenation. |
| ClickHouse | Column aliases are backtick-quoted (as `Calc`); every derived table must be aliased (as `t1`). String concatenation uses concat(a, b). |
| In-memory | No SQL is generated; the projection delegates are compiled and run over in-memory objects. |
See also
- Provider-specific SQL — constructs exclusive to a single provider
- Range columns — a portable range mapping for providers without a native range type