TCIS.Pluggable.Persistence.Oracle 1.0.8-rc.7

This is a prerelease version of TCIS.Pluggable.Persistence.Oracle.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package TCIS.Pluggable.Persistence.Oracle --version 1.0.8-rc.7
                    
NuGet\Install-Package TCIS.Pluggable.Persistence.Oracle -Version 1.0.8-rc.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TCIS.Pluggable.Persistence.Oracle" Version="1.0.8-rc.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TCIS.Pluggable.Persistence.Oracle" Version="1.0.8-rc.7" />
                    
Directory.Packages.props
<PackageReference Include="TCIS.Pluggable.Persistence.Oracle" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TCIS.Pluggable.Persistence.Oracle --version 1.0.8-rc.7
                    
#r "nuget: TCIS.Pluggable.Persistence.Oracle, 1.0.8-rc.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TCIS.Pluggable.Persistence.Oracle@1.0.8-rc.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TCIS.Pluggable.Persistence.Oracle&version=1.0.8-rc.7&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=TCIS.Pluggable.Persistence.Oracle&version=1.0.8-rc.7&prerelease
                    
Install as a Cake Tool

TCIS.Pluggable.Persistence.Oracle

Oracle provider for the Pluggable multi-tenant data layer. Supports all three tiers — Virtual Private Database, schema per tenant, and database per tenant.

Shared behaviour — model composition, query filters, audit stamping — lives in TCIS.Pluggable.Persistence.EntityFrameworkCore.


1. Registration

dotnet add package TCIS.Pluggable.Persistence.Oracle
builder.Services.AddPluggableOracle<TosDbContext>(commandTimeoutSeconds: 30);

Dapper only, no DbContext (a worker that reads existing tables with raw SQL):

builder.Services.AddPluggableOracleConnections(commandTimeoutSeconds: 30);
// inject IDapperContext

Both register IDbConnectionFactory per tenant and IDapperContext, and use commandTimeoutSeconds as the default timeout for every Dapper call that does not pass its own.

Call Connection string from the Tenant Store
Query* ReadConnectionString, else ConnectionString
Execute*, ExecuteCustom* ConnectionString only — never the read one

Read a scalar (SELECT COUNT(*)) with QuerySingleOrDefaultAsync<T>, not ExecuteScalarAsync: Execute* is the write path, so on a read-only tenant it is refused. Blank ReadConnectionString ("" or whitespace) counts as not set and falls back to ConnectionString.

A tenant that must only be read (for example a site running in parallel for comparison) declares only ReadConnectionString with a read-only database user: reads work, and every write is refused before it reaches the database (Oracle write connection string is missing).

Do not pass a connection string. It is resolved per request from the Tenant Store through IMultiTenantContextAccessor (the non-generic accessor, so a host declaring its own tenant type such as MyTenantInfo : TenantInfo works too).


2. Tier support

Tier Mechanism Identifier used
Tier 1 — Row level (VPD) TCIS_CTX_PKG.SET_TENANT(…) / TCIS_CTX_PKG.CLEAR_TENANT — your trusted package, see §3 TenantInfo.Id (GUID)
Tier 2 — Schema per tenant ALTER SESSION SET CURRENT_SCHEMA = "<IDENTIFIER IN UPPER CASE>" TenantInfo.Identifier (site code)
Tier 3 — Database per tenant no-op initializer —

Tier 2 takes the schema name from TenantInfo.Identifier, not TenantInfo.Id. The identifier becomes an Oracle schema name and must pass the identifier whitelist; a GUID contains - and is always rejected.

In Oracle a schema is a user, so Tier 2 means one database user per tenant. Grant the application account the privileges to switch into each of them, and nothing more.

The identifier is upper-cased. Users created without quotes are stored in upper case, and the schema name is quoted, so tci would otherwise miss schema TCI (ORA-01435). On close the session is switched back to the login user's schema before it returns to the pool.


3. Tier 1 — application context and VPD

OracleRlsConnectionInitializer sets the tenant on Open() and clears it on Close() by calling your trusted package TCIS_CTX_PKG. Oracle lets DBMS_SESSION.SET_CONTEXT run only inside the package named in CREATE CONTEXT … USING — that is what stops a session from choosing its own tenant — so the library cannot call DBMS_SESSION directly (doing so raises ORA-01031).

Create the package in the application schema (or give the application account a synonym named TCIS_CTX_PKG):

CREATE OR REPLACE PACKAGE TCIS_CTX_PKG AS
    PROCEDURE SET_TENANT(p_tenant_id VARCHAR2);
    PROCEDURE CLEAR_TENANT;
END;
/
CREATE OR REPLACE PACKAGE BODY TCIS_CTX_PKG AS
    PROCEDURE SET_TENANT(p_tenant_id VARCHAR2) IS
    BEGIN DBMS_SESSION.SET_CONTEXT('TCIS_CTX', 'TENANT_ID', p_tenant_id); END;
    PROCEDURE CLEAR_TENANT IS
    BEGIN DBMS_SESSION.CLEAR_CONTEXT('TCIS_CTX', NULL, 'TENANT_ID'); END;
END;
/
CREATE CONTEXT TCIS_CTX USING TCIS_CTX_PKG;

Your VPD policy function reads the context back:

CREATE OR REPLACE FUNCTION sec.tenant_predicate(
    schema_name IN VARCHAR2, table_name IN VARCHAR2) RETURN VARCHAR2 AS
BEGIN
    RETURN 'TENANT_ID = SYS_CONTEXT(''TCIS_CTX'', ''TENANT_ID'')';
END;

Changed 27/10/2026: earlier versions called DBMS_SESSION.SET_CONTEXT from an anonymous block, which Oracle rejects for a context bound to a package — Tier 1 could not open a single connection. The package name and its two procedures are now the contract.

A blank tenant id runs the cleanup command, clearing any context left over from a pooled session. The predicate then matches no row: fail-closed.


4. Tier status

All three tiers run against a real Oracle (Free 23, Testcontainers) in TCIS.Pluggable.Persistence.Oracle.IntegrationTests, together with named binding, padded CHAR columns, transactions and command timeouts through the tenant connection wrapper. They need Docker; set TCIS_REQUIRE_DOCKER=true on CI so a missing daemon fails the run instead of skipping it.

Tier 3 has no initializer, so tenant resolution is its single point of failure — if the wrong connection string is resolved, nothing downstream will catch it. The tests cover the library's part (read/write split, a read-only tenant refusing writes); a rollout still needs its own check that each site resolves its own database.


5. Pitfalls

# Pitfall Consequence
1 Using a GUID as the Tier 2 identifier Whitelist rejection — use the site code
2 Granting EXECUTE on TCIS_CTX_PKG to accounts other than the application's Those accounts can set any tenant id and read through VPD
3 Granting the application account EXEMPT ACCESS POLICY VPD no longer applies to it — the defence is gone
4 Assuming the EF query filter protects Dapper queries It does not; on Tier 1 only VPD stands in the way
5 Passing commandTimeout: 0 to a Dapper call Oracle waits forever — 0 overrides the registered default
6 Deploying Tier 3 without an isolation test Tenant resolution is the only thing preventing a cross-tenant connection
7 Dapper on a library version without named binding (before the release after 1.0.8-rc.1) ODP.NET binds by position: two parameters declared in a different order than in the SQL swap values and return wrong rows silently. Fixed in the library: the tenant connection wrapper turns BindByName on. On an older version set OracleConfiguration.BindByName = true before the first connection
8 Detecting a command timeout by one exception type The type depends on the ODP.NET version (measured 27/10/2026, through the tenant wrapper): 23.6.0 raises OracleException with Number == 1013 on reads and writes; 23.26.301 raises OperationCanceledException wrapping OracleException 1013 on writes, and a bare TaskCanceledException on reads. Treat as a timeout: OracleException { Number: 1013 } or an OperationCanceledException while your own token is not cancelled. Pinned on both versions by A_Command_Over_Its_Timeout_Is_Recognisable_As_A_Timeout

6. A note on terminology

FORCE ROW LEVEL SECURITY and BYPASSRLS are PostgreSQL concepts and have no Oracle equivalent. The Oracle counterparts are VPD policies plus the EXEMPT ACCESS POLICY privilege — do not port a PostgreSQL checklist verbatim.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.8-rc.11 41 10/30/2026
1.0.8-rc.10 38 10/30/2026
1.0.8-rc.9 44 10/30/2026
1.0.8-rc.8 49 10/29/2026
1.0.8-rc.7 46 10/27/2026
1.0.8-rc.6 47 10/27/2026
1.0.8-rc.5 84 10/23/2026
1.0.8-rc.4 58 10/23/2026
1.0.8-rc.3 57 10/23/2026
1.0.8-rc.2 56 10/22/2026
1.0.8-rc.1 58 10/22/2026
1.0.6 94 10/20/2026
1.0.6-rc.1 57 10/20/2026
1.0.5 84 10/19/2026
1.0.4 94 10/13/2026
1.0.3 93 10/13/2026
1.0.2 88 10/12/2026
1.0.2-rc.1 53 10/12/2026
1.0.1-rc.10 66 10/8/2026
1.0.1-rc.9 57 10/8/2026
Loading failed