PhoenixmlDb

Metadata

Unlimited namespace-key-value metadata on every document

#Metadata

Every document in PhoenixmlDb can carry unlimited metadata — arbitrary key-value pairs stored alongside the document content. Metadata is stored in a dedicated LMDB database, indexed for fast lookups, and participates in ACID transactions.

The metadata model uses a namespace-key-value pattern inspired by Oracle Berkeley DB XML. The namespace dimension allows the same key name under different namespaces — critical for enterprise integrations where multiple systems attach metadata to the same document.

#Storing Metadata

#Simple Key-Value

csharp
// Set metadata with a flat key
await container.SetMetadataAsync("invoice.xml", "status", "pending");
await container.SetMetadataAsync("invoice.xml", "priority", "high");
// Retrieve
var status = await container.GetMetadataAsync("invoice.xml", "status");
// "pending"
                                  

#Namespaced Keys

When multiple systems need to attach metadata to the same document, use namespaced keys to avoid collisions:

csharp
// BizTalk context properties
await container.SetMetadataAsync("message.xml", "biztalk", "status", "received");
await container.SetMetadataAsync("message.xml", "biztalk", "port", "ReceivePort1");
// Custom application metadata
await container.SetMetadataAsync("message.xml", "app", "status", "processed");
await container.SetMetadataAsync("message.xml", "app", "processor", "OrderService");
// Both "status" keys coexist — different namespaces
var btStatus = await container.GetMetadataAsync("message.xml", "biztalk", "status");
// "received"
var appStatus = await container.GetMetadataAsync("message.xml", "app", "status");
// "processed"
                                  

#How It Works

Namespaced keys are stored as "namespace:key" strings in LMDB. The colon is the separator. This means:

  • SetMetadataAsync("doc", "source", "type", "api") stores key "source:type" with value "api"

  • SetMetadataAsync("doc", "status", "active") stores key "status" with value "active" (flat key, no namespace)

  • Both forms coexist on the same document

#Retrieving Metadata

#Single Key

csharp
// Flat key
var value = await container.GetMetadataAsync("doc.xml", "status");
// Namespaced key
var value = await container.GetMetadataAsync("doc.xml", "source", "type");
                                  

#All Metadata

csharp
var allMeta = await container.GetAllMetadataAsync("doc.xml");
// Returns: { "status": "active", "source:type": "api", "source:path": "/data/imports" }
                                  

#Filter by Namespace

csharp
var sourceMeta = await container.GetMetadataByNamespaceAsync("doc.xml", "source");
// Returns only keys starting with "source:":
// { "source:type": "api", "source:path": "/data/imports" }
                                  

#Querying by Metadata

Find documents that have a specific metadata value:

csharp
// Find all documents with status = "pending"
await foreach (var doc in container.QueryMetadataAsync("status", "pending"))
{
    Console.WriteLine(doc.Name);
}
// Find documents in a namespace
await foreach (var doc in container.QueryMetadataAsync("biztalk:status", "received"))
{
    Console.WriteLine(doc.Name);
}
                                  

#Metadata in Transactions

Metadata operations participate in ACID transactions:

csharp
await using var txn = await db.BeginWriteAsync();
// Store document and set metadata atomically
await txn.PutDocumentAsync(containerId, "order.xml", orderXml);
await txn.SetMetadataAsync(containerId, "order.xml", "workflow", "status", "new");
await txn.SetMetadataAsync(containerId, "order.xml", "workflow", "step", "validation");
await txn.CommitAsync();
// Both document and metadata are committed together — or neither is
                                  

#Metadata Indexing

Metadata keys can be indexed for fast lookups. AddMetadataIndex takes a qualified XdmQName (PhoenixmlDb.Xdm), not a bare or colon-separated string — the namespace dimension that keeps two systems' status keys apart in storage is the same one the index is keyed on:

csharp
using PhoenixmlDb.Xdm;
var biztalkNs = db.GetOrCreateNamespaceId("urn:example:biztalk");
var workflowNs = db.GetOrCreateNamespaceId("urn:example:workflow");
var container = await db.OpenOrCreateContainerAsync("orders", opts =>
{
    opts.Indexes
        .AddMetadataIndex(new XdmQName(NamespaceId.None, "status"), XdmValueType.XdmString)
        .AddMetadataIndex(new XdmQName(biztalkNs, "status"), XdmValueType.XdmString)
        .AddMetadataIndex(new XdmQName(workflowNs, "step"), XdmValueType.XdmString);
});
                                  

Indexed metadata queries use the index instead of scanning all documents.

#Accessing Metadata in XQuery

The phx:metadata() function retrieves metadata from within XQuery expressions. The engine binds the phx prefix on every query path, so no prolog declaration is needed. Every form takes the node whose document you are asking about — there is no single-argument key-only form:

xquery
(: Get metadata for the current document :)
phx:metadata(., 'status')
(: A system key: dbxml: here is a literal key prefix, not a namespace :)
phx:metadata(., 'dbxml:name')
(: Any namespace, written in full :)
phx:metadata(., 'Q{https://example.com/biztalk}status')
(: Filter documents by metadata :)
for $doc in collection('orders')
where phx:metadata($doc, 'workflow:status') = 'pending'
return $doc
                                  

A key written as prefix:local resolves prefix through the container's ContainerOptions.DefaultNamespaces. A prefix that is not bound there raises FONS0004 — it does not quietly return nothing. Use Q{uri}local when you do not control the container's bindings, and an unprefixed key for the container's default metadata namespace.

#Use Cases

#Enterprise Integration (BizTalk Migration)

BizTalk message context properties map directly to namespaced metadata:

csharp
// Store BizTalk context properties as metadata
await container.SetMetadataAsync("msg.xml", "BTS", "MessageType", messageType);
await container.SetMetadataAsync("msg.xml", "BTS", "ReceivePortName", portName);
await container.SetMetadataAsync("msg.xml", "BTS", "InboundTransportType", "FILE");
await container.SetMetadataAsync("msg.xml", "APP", "CorrelationId", correlationId);
                                  

#Document Workflow

Track document lifecycle without modifying the document content:

csharp
await container.SetMetadataAsync("report.xml", "workflow", "status", "draft");
await container.SetMetadataAsync("report.xml", "workflow", "author", "alice");
await container.SetMetadataAsync("report.xml", "workflow", "created", DateTime.UtcNow);
// Later...
await container.SetMetadataAsync("report.xml", "workflow", "status", "reviewed");
await container.SetMetadataAsync("report.xml", "workflow", "reviewer", "bob");
                                  

#Content Classification

Attach tags and categories without schema changes:

csharp
await container.SetMetadataAsync("article.xml", "taxonomy", "category", "technology");
await container.SetMetadataAsync("article.xml", "taxonomy", "tags", "xml,database,dotnet");
await container.SetMetadataAsync("article.xml", "audit", "imported-from", "legacy-cms");
await container.SetMetadataAsync("article.xml", "audit", "import-date", DateTime.UtcNow);
                                  

#Best Practices

  1. Use namespaces for metadata from different systems or concerns

  2. Index frequently queried keys — unindexed metadata queries scan all documents

  3. Keep values small — metadata is serialized as JSON bytes in LMDB

  4. Use transactions for multi-key updates that must be atomic

  5. Prefer string keys — the colon convention is simple and readable

#Next Steps

Storage

Querying

Extensions

Documents & Storage Document operations

Indexing Index optimization

Database Extensions phx:metadata() function