TCIS.Pluggable.Persistence.Oracle
1.0.8-rc.2
dotnet add package TCIS.Pluggable.Persistence.Oracle --version 1.0.8-rc.2
NuGet\Install-Package TCIS.Pluggable.Persistence.Oracle -Version 1.0.8-rc.2
<PackageReference Include="TCIS.Pluggable.Persistence.Oracle" Version="1.0.8-rc.2" />
<PackageVersion Include="TCIS.Pluggable.Persistence.Oracle" Version="1.0.8-rc.2" />
<PackageReference Include="TCIS.Pluggable.Persistence.Oracle" />
paket add TCIS.Pluggable.Persistence.Oracle --version 1.0.8-rc.2
#r "nuget: TCIS.Pluggable.Persistence.Oracle, 1.0.8-rc.2"
#:package TCIS.Pluggable.Persistence.Oracle@1.0.8-rc.2
#addin nuget:?package=TCIS.Pluggable.Persistence.Oracle&version=1.0.8-rc.2&prerelease
#tool nuget:?package=TCIS.Pluggable.Persistence.Oracle&version=1.0.8-rc.2&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) | DBMS_SESSION.SET_CONTEXT('TCIS_CTX', 'TENANT_ID', …) |
TenantInfo.Id (GUID) |
| Tier 2 — Schema per tenant | ALTER SESSION SET CURRENT_SCHEMA |
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.
3. Tier 1 — application context and VPD
OracleRlsConnectionInitializer sets an application context on Open() and clears it on Close(). Your VPD policy function reads it 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;
The context namespace TCIS_CTX must be created with a trusted package so that only that package can set it — otherwise any session could set its own tenant id and defeat the policy:
CREATE CONTEXT TCIS_CTX USING sec.tenant_ctx_pkg;
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
Tier 3 is the simplest design here (a no-op initializer) but has no automated test coverage yet, and tenant resolution is its single point of failure — if the wrong connection string is resolved, nothing downstream will catch it. Ironically it is also the tier most likely to be chosen by a large port.
Treat a Tier 3 rollout as needing its own isolation test before go-live.
5. Pitfalls
| # | Pitfall | Consequence |
|---|---|---|
| 1 | Using a GUID as the Tier 2 identifier | Whitelist rejection — use the site code |
| 2 | Creating TCIS_CTX without a trusted package |
Any session can set its own tenant id and bypass 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; a parameter used twice raises ORA-01008. Fixed in the library: the tenant connection wrapper turns BindByName on. On an older version set OracleConfiguration.BindByName = true before the first connection |
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.2)
- TCIS.MultiTenancy (>= 1.0.8-rc.2)
- TCIS.Pluggable.Persistence.EntityFrameworkCore (>= 1.0.8-rc.2)
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.2 | 0 | 10/22/2026 |
| 1.0.8-rc.1 | 28 | 10/22/2026 |
| 1.0.6 | 77 | 10/20/2026 |
| 1.0.6-rc.1 | 40 | 10/20/2026 |
| 1.0.5 | 79 | 10/19/2026 |
| 1.0.4 | 89 | 10/13/2026 |
| 1.0.3 | 90 | 10/13/2026 |
| 1.0.2 | 82 | 10/12/2026 |
| 1.0.2-rc.1 | 48 | 10/12/2026 |
| 1.0.1-rc.10 | 61 | 10/8/2026 |
| 1.0.1-rc.9 | 51 | 10/8/2026 |
| 1.0.1-rc.8 | 61 | 10/8/2026 |
| 1.0.1-rc.7 | 54 | 10/8/2026 |
| 1.0.1-rc.6 | 59 | 10/8/2026 |
| 1.0.1-rc.5 | 60 | 10/7/2026 |
| 1.0.1-rc.4 | 55 | 10/7/2026 |
| 1.0.1-rc.3 | 55 | 10/7/2026 |
| 1.0.1-rc.2 | 61 | 10/5/2026 |
| 1.0.1-rc.1 | 64 | 10/5/2026 |
| 1.0.0 | 97 | 10/1/2026 |