TCIS.Pluggable.Persistence.SqlServer
1.0.0-rc.19
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
<PackageReference Include="TCIS.Pluggable.Persistence.SqlServer" Version="1.0.0-rc.19" />
<PackageVersion Include="TCIS.Pluggable.Persistence.SqlServer" Version="1.0.0-rc.19" />
<PackageReference Include="TCIS.Pluggable.Persistence.SqlServer" />
paket add TCIS.Pluggable.Persistence.SqlServer --version 1.0.0-rc.19
#r "nuget: TCIS.Pluggable.Persistence.SqlServer, 1.0.0-rc.19"
#:package TCIS.Pluggable.Persistence.SqlServer@1.0.0-rc.19
#addin nuget:?package=TCIS.Pluggable.Persistence.SqlServer&version=1.0.0-rc.19&prerelease
#tool nuget:?package=TCIS.Pluggable.Persistence.SqlServer&version=1.0.0-rc.19&prerelease
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
CASTmatters. The application binds the value as aUNIQUEIDENTIFIERwhenever 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 bothFILTERandBLOCKpredicates. - The application account must not be able to disable it.
ALTER SECURITY POLICY … STATE = OFFshould 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 | 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.Bcl.Memory (>= 10.0.7)
- Microsoft.Data.SqlClient (>= 5.2.2)
- Microsoft.EntityFrameworkCore (>= 8.0.13)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.13)
- Microsoft.EntityFrameworkCore.SqlServer (>= 8.0.0)
- 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)
- Microsoft.IdentityModel.JsonWebTokens (>= 8.4.0)
- System.IdentityModel.Tokens.Jwt (>= 8.4.0)
- System.Text.Encodings.Web (>= 9.0.0)
- System.Text.Json (>= 9.0.0)
- TCIS.Core (>= 1.0.0-rc.19)
- TCIS.MultiTenancy (>= 1.0.0-rc.19)
- TCIS.Pluggable.Persistence.EntityFrameworkCore (>= 1.0.0-rc.19)
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 |