API Reference
Queries API
XQuery execution, parameters, results, and error handling
#Query API
Queries run against one container through IContainer.QueryAsync (or IReadTransaction.QueryAsync, which takes the container's ContainerId). The engine is the PhoenixmlDb XQuery 4.0 engine.
IAsyncEnumerable<object> QueryAsync(
string query,
IReadOnlyDictionary<string, object>? variables = null,
CancellationToken cancellationToken = default);
IAsyncEnumerable<object> QueryAsync(
string query,
IReadOnlyDictionary<string, object>? variables,
Predicate<string>? documentNameFilter,
CancellationToken cancellationToken = default);
#Basic Queries
#Execute Query
var products = await db.OpenOrCreateContainerAsync("products");
await foreach (var name in products.QueryAsync("//product/name/text()"))
{
Console.WriteLine(name);
}
#How a query is evaluated
A query is classified when it is compiled:
-
Row-wise queries — a per-document filter, map or projection such as
//product[price > 100]/name— run once per document in the container, with that document as the context item, so path expressions need nodoc()orcollection()prefix. Results from all documents are concatenated in document-name order. -
Cross-document queries — global aggregation (
sum,avg,min,max),count(collection()),distinct-valuesacross documents, or a FLWORorder by/group by— run once, withfn:collection()spanning every document in the container.
// One result: cross-document
await foreach (var n in products.QueryAsync("count(collection())"))
Console.WriteLine(n); // "3"
// One result per document: row-wise
await foreach (var n in products.QueryAsync("count(/product)"))
Console.WriteLine(n); // "1", "1", "1"
Because a row-wise query runs per document, an expression that does not depend on the context document — doc('p1.xml')/product/name, say — returns its result once for every document in the container.
fn:collection() with no argument is the container. fn:doc() and fn:collection() with a URI name a single document in the same container, by its bare name (doc('p1.xml')); there is no way to reach another container from a query.
#Query a Single Value
There is no typed single-value helper. Take the first result and convert it yourself:
string? total = null;
await foreach (var r in orders.QueryAsync("sum(collection()//order/total)"))
{
total = (string)r;
break;
}
decimal value = decimal.Parse(total!, CultureInfo.InvariantCulture);
#Parameterized Queries
#Basic Parameters
Values in variables are bound as external variables. Declare each one in the query prolog:
var variables = new Dictionary<string, object> { ["maxPrice"] = 100.0 };
await foreach (var name in products.QueryAsync("""
declare variable $maxPrice external;
/product[price < $maxPrice]/name/string()
""", variables))
{
Console.WriteLine(name);
}
#Multiple Parameters
var variables = new Dictionary<string, object>
{
["category"] = "Electronics",
["minPrice"] = 50m,
["maxPrice"] = 500m
};
await foreach (var item in products.QueryAsync("""
declare variable $category external;
declare variable $minPrice external;
declare variable $maxPrice external;
/product[category = $category and price >= $minPrice and price <= $maxPrice]
""", variables))
{
Console.WriteLine(item);
}
#Parameter Types
Pass ordinary .NET values (string, int, long, decimal, double, bool). Variable names are matched in no namespace. When a declaration carries a type (declare variable $n as xs:integer external;) and the supplied value is a string, the string is cast to the declared type; a value that does not match the declared type raises a type error. A declared external variable that is not supplied, and has no default, raises XPDY0002.
#Query Results
QueryAsync returns IAsyncEnumerable<object>. Every item is a string: nodes are serialized as markup, and atomic values as their lexical form, using the adaptive output method.
await foreach (var item in products.QueryAsync("/product[price > 100]"))
{
string xml = (string)item; // <product id="2"><name>Gadget</name>...</product>
}
For typed access to XDM nodes and values, use the XQuery engine directly (PhoenixmlDb.XQuery); the container surface is string-based.
#Filtering documents
The four-argument overload restricts the query to documents whose name passes documentNameFilter. The filter applies to fn:collection() and fn:doc() as well as to the per-document loop:
await foreach (var item in products.QueryAsync(
"count(collection())", variables: null,
documentNameFilter: name => name.StartsWith("2024/", StringComparison.Ordinal)))
{
Console.WriteLine(item);
}
#XQuery Update
The embedded query surface does not apply XQuery Update Facility expressions to stored documents: an updating query against a container changes nothing. Modify documents with PutDocumentAsync (which replaces the whole document) and DeleteDocumentAsync, directly or inside a write transaction.
#In Transactions
IReadTransaction.QueryAsync and the inherited method on IWriteTransaction take the container's id:
using (var read = db.BeginRead())
{
await foreach (var item in read.QueryAsync(products.Id, "count(collection())"))
Console.WriteLine(item);
}
A transaction's query sees committed data. It does not see writes buffered in an uncommitted IWriteTransaction, including the transaction's own. A query in a read transaction from BeginRead reads the committed state of BeginRead, as every other call on that transaction does; a query in a write transaction reads the committed state at the time it starts. See Transaction API and Reads During Writes: Queries.
#Static Namespaces
Prefixes bound in ContainerOptions.DefaultNamespaces are available to every query on the container, in addition to the predeclared prefixes (fn, xs, map, array, math, phx, ...):
var orders = await db.CreateContainerAsync("orders",
opts => opts.DefaultNamespaces["o"] = "urn:orders");
await foreach (var id in orders.QueryAsync("/o:order/o:id/string()"))
Console.WriteLine(id);
#Cancellation
Pass a CancellationToken to QueryAsync (or with WithCancellation) to stop a long-running query; cancellation surfaces as OperationCanceledException. There is no built-in query timeout or result limit; apply your own with a token from CancellationTokenSource.CancelAfter. The number of queries running at one moment is limited (LmdbStorageOptions.MaxConcurrentQueries): a query that starts at the limit fails with ReaderLimitException and does not wait. See Reads During Writes: Queries.
#Error Handling
try
{
await foreach (var item in products.QueryAsync(xquery))
Console.WriteLine(item);
}
catch (PhoenixmlDb.XQuery.Functions.XQueryException ex)
{
// Compilation failed: raised before any document is read
Console.WriteLine($"Static error: {ex.ErrorCode}");
Console.WriteLine($"Message: {ex.Message}");
}
catch (PhoenixmlDb.XQuery.Execution.XQueryRuntimeException ex)
{
// Dynamic error during evaluation
Console.WriteLine($"Runtime error: {ex.ErrorCode}");
Console.WriteLine($"Message: {ex.Message}");
}
Errors surface when the sequence is enumerated, not when QueryAsync is called. A stored document that cannot be loaded for a query is skipped rather than failing the query.
#Common Error Codes
|
Code |
Description |
|---|---|
|
|
Static error (syntax) |
|
|
Undefined variable |
|
|
Unknown function |
|
|
Type error |
|
|
External variable not bound |
|
|
Division by zero |
|
|
Invalid value for cast |
#Best Practices
-
Use parameters - Never concatenate user input into queries; bind it through
variables -
Know which shape you wrote - Aggregate over
collection()for one answer; a bare path expression answers per document -
Limit results - Use
[position() <= $limit]inside a cross-document query, or stop enumerating early -
Declare indexes - Index frequently filtered paths (see Index API)
-
Handle errors - Catch both the static and the runtime exception types
#Next Steps
|
Language Reference |
Performance |
Transactions |
|---|---|---|
|
XQuery Guide XQuery language reference |
Index API Optimize queries with indexes |
Transaction API Transactional queries |