TCIS.Pluggable.Persistence.Oracle
1.0.8-rc.7
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
<PackageReference Include="TCIS.Pluggable.Persistence.Oracle" Version="1.0.8-rc.7" />
<PackageVersion Include="TCIS.Pluggable.Persistence.Oracle" Version="1.0.8-rc.7" />
<PackageReference Include="TCIS.Pluggable.Persistence.Oracle" />
paket add TCIS.Pluggable.Persistence.Oracle --version 1.0.8-rc.7
#r "nuget: TCIS.Pluggable.Persistence.Oracle, 1.0.8-rc.7"
#:package TCIS.Pluggable.Persistence.Oracle@1.0.8-rc.7
#addin nuget:?package=TCIS.Pluggable.Persistence.Oracle&version=1.0.8-rc.7&prerelease
#tool nuget:?package=TCIS.Pluggable.Persistence.Oracle&version=1.0.8-rc.7&prerelease
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, notTenantInfo.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
tciwould otherwise miss schemaTCI(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_CONTEXTfrom 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 | Versions 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. |
-
net8.0
- Dapper (>= 2.1.66)
- Microsoft.EntityFrameworkCore (>= 8.0.13)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.13)
- Microsoft.Extensions.Caching.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.Configuration (>= 9.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.0)
- Microsoft.Extensions.DependencyInjection (>= 9.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Http (>= 9.0.0)
- Microsoft.Extensions.Logging (>= 9.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 8.0.0)
- Oracle.EntityFrameworkCore (>= 8.23.60)
- System.Diagnostics.DiagnosticSource (>= 10.0.0)
- TCIS.Core (>= 1.0.8-rc.7)
- TCIS.MultiTenancy (>= 1.0.8-rc.7)
- TCIS.Pluggable.Persistence.EntityFrameworkCore (>= 1.0.8-rc.7)
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 |