Oracle
The Shiny.DocumentDb.Oracle package provides an Oracle Database-backed document store using Oracle.ManagedDataAccess.Core (ODP.NET). Documents are stored as IS JSON-checked CLOB columns with full LINQ-to-SQL translation and function-based JSON indexes.
When to Use
Section titled “When to Use”- Existing Oracle estate on 23ai+
- ASP.NET / server workloads using Oracle as primary storage
- Full feature parity with the other SQL providers (except spatial, vector, native change feeds, and
Backup())
Installation
Section titled “Installation”dotnet add package Shiny.DocumentDb.Oracle-
Direct instantiation
using Shiny.DocumentDb;using Shiny.DocumentDb.Oracle;var store = new DocumentStore(new DocumentStoreOptions{DatabaseProvider = new OracleDatabaseProvider("User Id=myuser;Password=pass;Data Source=localhost:1521/FREEPDB1")}); -
Dependency injection
services.AddDocumentStore(opts =>{opts.DatabaseProvider = new OracleDatabaseProvider("User Id=myuser;Password=pass;Data Source=localhost:1521/FREEPDB1");});
Storage Layout
Section titled “Storage Layout”CREATE TABLE "documents" ( Id VARCHAR2(255) NOT NULL, TypeName VARCHAR2(255) NOT NULL, Data CLOB CHECK (Data IS JSON), CreatedAt TIMESTAMP(6) WITH TIME ZONE NOT NULL, UpdatedAt TIMESTAMP(6) WITH TIME ZONE NOT NULL, CONSTRAINT pk_documents PRIMARY KEY (Id, TypeName));The Data column is a CLOB with an IS JSON check constraint. Property access translates to JSON_VALUE(Data, '$.path') (with RETURNING NUMBER for numeric comparisons).
Dialect Adaptation
Section titled “Dialect Adaptation”The core library authors SQL with @name placeholders and name-bound parameters — conventions Oracle does not accept. The provider wraps every connection in a dialect adapter that rewrites commands just before execution:
@nameplaceholders become:name- parameters bind by name (ODP.NET defaults to positional binding)
- trailing semicolons are stripped (illegal in plain SQL through ODP.NET; preserved for PL/SQL blocks)
FROM-lessSELECT CASE ...expressions gainFROM DUAL- strings past the
VARCHAR2bind limit are bound asCLOB
This is invisible in normal use — it only matters when reading the Logging callback output, which shows the pre-rewrite SQL.
Upsert (RFC 7396 Deep Merge)
Section titled “Upsert (RFC 7396 Deep Merge)”Oracle exposes the native JSON_MERGEPATCH function, so Upsert runs entirely server-side with true RFC 7396 deep merge semantics:
MERGE INTO "documents" tUSING (SELECT :id AS Id, :typeName AS TypeName FROM DUAL) srcON (t.Id = src.Id AND t.TypeName = src.TypeName)WHEN MATCHED THEN UPDATE SET t.Data = JSON_MERGEPATCH(t.Data, :data RETURNING CLOB), t.UpdatedAt = :nowWHEN NOT MATCHED THEN INSERT (Id, TypeName, Data, CreatedAt, UpdatedAt) VALUES (:id, :typeName, :data, :now, :now);Null properties are stripped recursively before sending — unset nullable C# properties do not accidentally delete stored fields.
SetProperty / RemoveProperty
Section titled “SetProperty / RemoveProperty”Oracle’s JSON_TRANSFORM only accepts literal path expressions — it cannot take the path as a bind variable. The provider creates two small PL/SQL helper functions alongside each table (shiny_json_set, shiny_json_remove) that inline the path into a dynamic JSON_TRANSFORM statement:
UPDATE "documents"SET Data = shiny_json_set(Data, :path, :value), UpdatedAt = :nowWHERE Id = :id AND TypeName = :typeName;Function creation is automatic and idempotent (CREATE OR REPLACE); the connecting user needs CREATE PROCEDURE privilege.
Indexes
Section titled “Indexes”CreateIndexAsync<T>(x => x.Prop) emits a function-based index on the extracted property. Index names are quoted to preserve the library’s lowercase naming (unquoted Oracle identifiers fold to uppercase, which would break index discovery):
CREATE INDEX "idx_json_User_name" ON "documents" (JSON_VALUE(Data, '$.name'));Composite indexes emit one JSON_VALUE expression per path in a single index. Oracle has no partial-index equivalent, so the index spans all rows in the table rather than being filtered by TypeName.
Raw SQL
Section titled “Raw SQL”Write @name parameters as with every other SQL provider — the dialect adapter rewrites them to :name before execution:
var results = await store.Query<User>( "JSON_VALUE(Data, '$.name') = @name", parameters: new { name = "Alice" });Limitations
Section titled “Limitations”- Oracle 23ai or later only — older versions fail at table creation or batch insert.
- No spatial —
WithinRadius,WithinBoundingBox,NearestNeighborsthrowNotSupportedException. Oracle has native spatial (SDO_GEOMETRY) but the provider does not wrap it. - No vector search —
NearestVectorsthrowsNotSupportedException. Oracle 23ai has a nativeVECTORtype that the provider does not yet wrap. - No native change feed —
SubscribeChanges<T>throwsNotSupportedException. In-process change monitoring (NotifyOnChange) is fully supported. - No
Backup()— use RMAN or Data Pump from your operations tooling.
- Identifier quoting uses double quotes (
"); table names are case-preserved. - Optimistic concurrency works via
MapVersionPropertyonDocumentStoreOptions. - Connections are pooled per-operation like the other server SQL providers — a single store instance serves concurrent callers without a semaphore.
- Multi-tenancy (
TenantIdAccessor) is fully supported.