TCIS.Pluggable.Persistence.SqlServer 1.0.0-rc.19

This is a prerelease version of TCIS.Pluggable.Persistence.SqlServer.
dotnet add package TCIS.Pluggable.Persistence.SqlServer --version 1.0.0-rc.19
                    
NuGet\Install-Package TCIS.Pluggable.Persistence.SqlServer -Version 1.0.0-rc.19
                    
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.SqlServer" Version="1.0.0-rc.19" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TCIS.Pluggable.Persistence.SqlServer" Version="1.0.0-rc.19" />
                    
Directory.Packages.props
<PackageReference Include="TCIS.Pluggable.Persistence.SqlServer" />
                    
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.SqlServer --version 1.0.0-rc.19
                    
#r "nuget: TCIS.Pluggable.Persistence.SqlServer, 1.0.0-rc.19"
                    
#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.SqlServer@1.0.0-rc.19
                    
#: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.SqlServer&version=1.0.0-rc.19&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=TCIS.Pluggable.Persistence.SqlServer&version=1.0.0-rc.19&prerelease
                    
Install as a Cake Tool

TCIS.Pluggable.Persistence.SqlServer

SQL Server provider for the Pluggable multi-tenant data layer. Supports Tier 1 (Row-Level Security) and Tier 3 (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.SqlServer
builder.Services.AddPluggableSqlServer<TosDbContext>(commandTimeoutSeconds: 30);

Do not pass a connection string. It is resolved per request from the Tenant Store through IMultiTenantContextAccessor<TenantInfo> — that is what lets one process serve tenants whose databases live on different servers.

commandTimeoutSeconds bounds each EF Core command. It is the only mechanism that genuinely cancels a running query, because it sends an attention signal to the server. It does not cover the Dapper path — pass commandTimeout on those calls yourself.


2. Tier support

Tier Supported Mechanism
Tier 1 — Row-Level Security sp_set_session_context
Tier 2 — Schema per tenant Throws NotSupportedException — use Tier 1 or Tier 3
Tier 3 — Database per tenant No-op initializer; the connection string isolates

Tier 2 is refused at startup, not silently downgraded. SQL Server has no session-level default-schema switch equivalent to search_path, so a partial implementation would be worse than none.


3. How Tier 1 works

TenantAwareDbConnection wraps the connection; on Open() it applies the session context:

EXEC sp_set_session_context @key = N'TenantId', @value = @TenantId;   -- Initialize
EXEC sp_set_session_context @key = N'TenantId', @value = NULL;        -- Cleanup

Your RLS predicate then reads it back:

CREATE FUNCTION sec.fn_TenantAccessPredicate(@TenantId nvarchar(64))
RETURNS TABLE WITH SCHEMABINDING
AS RETURN
    SELECT 1 AS granted
    WHERE CAST(SESSION_CONTEXT(N'TenantId') AS NVARCHAR(64)) = @TenantId;

The CAST matters. The application binds the value as a UNIQUEIDENTIFIER whenever the tenant id parses as a GUID — param.Value = Guid.TryParse(tenantId, out var g) ? g : tenantId — precisely so the type matches and the predicate keeps its index seek. An implicit conversion here turns every filtered query into a scan.

Empty tenant → the cleanup command runs, wiping any context left over from a pooled connection. The RLS predicate then matches no row: fail-closed.


4. RLS is not a second line of defence — on the read path it is the only one

EF Core's global query filter protects the EF path. It does not touch IDapperContext, which emits raw SQL.

That was measured, not assumed: with RLS off, two tenants hitting the same Dapper-backed endpoint saw each other's rows; with RLS on, each saw only its own.

Therefore, for Tier 1:

  • RLS must be enabled on every table carrying TenantId, with both FILTER and BLOCK predicates.
  • The application account must not be able to disable it. ALTER SECURITY POLICY … STATE = OFF should fail for that login.
  • RLS filter predicates apply to sa/sysadmin as well — a sysadmin cannot bypass the predicate, only turn the policy off.

Scripts and the verification procedure: md/13 Part 2 (policy) and Part 4 (least-privilege login).


5. Pitfalls

# Pitfall Consequence
1 Configuring a tenant as Tier 2 NotSupportedException at connection time
2 Comparing SESSION_CONTEXT without a CAST in the predicate Implicit conversion kills the index seek — every filtered query scans
3 Running the application under a login that can alter the security policy The last line of defence can be switched off by the process it protects
4 Writing Dapper queries with no tenant predicate Only RLS stands in the way — and only if it is switched on
5 Relying on commandTimeoutSeconds for Dapper It applies to EF Core only
6 Storing the connection string in appsettings Bypasses the Tenant Store; the process can then serve one tenant only
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.0-rc.19 38 8/13/2026
1.0.0-rc.18 36 8/13/2026
1.0.0-rc.17 35 8/13/2026
1.0.0-rc.16 38 8/13/2026
1.0.0-rc.15 39 8/12/2026
1.0.0-rc.14 43 8/12/2026
1.0.0-rc.13 48 8/11/2026
1.0.0-rc.12 53 8/10/2026
1.0.0-rc.11 67 7/28/2026
1.0.0-rc.10 58 7/24/2026
1.0.0-rc.9 58 7/21/2026
1.0.0-rc.8 50 7/21/2026
1.0.0-rc.7 53 7/17/2026
1.0.0-rc.6 61 7/7/2026
1.0.0-rc.5 70 7/7/2026