PrimusSaaS.Rbac.Dapper
1.0.1
dotnet add package PrimusSaaS.Rbac.Dapper --version 1.0.1
NuGet\Install-Package PrimusSaaS.Rbac.Dapper -Version 1.0.1
<PackageReference Include="PrimusSaaS.Rbac.Dapper" Version="1.0.1" />
<PackageVersion Include="PrimusSaaS.Rbac.Dapper" Version="1.0.1" />
<PackageReference Include="PrimusSaaS.Rbac.Dapper" />
paket add PrimusSaaS.Rbac.Dapper --version 1.0.1
#r "nuget: PrimusSaaS.Rbac.Dapper, 1.0.1"
#:package PrimusSaaS.Rbac.Dapper@1.0.1
#addin nuget:?package=PrimusSaaS.Rbac.Dapper&version=1.0.1
#tool nuget:?package=PrimusSaaS.Rbac.Dapper&version=1.0.1
PrimusSaaS.Rbac.Dapper
Dapper storage adapter for PrimusSaaS.Rbac. A lightweight, provider-agnostic alternative to the EF Core adapter that gives you full control over SQL. No DbContext, no migrations, no EF dependency.
Start here
- First runnable Dapper + SQLite path:
/docs/modules/rbac/verified-dapper-quickstart - Existing host integration path:
/docs/modules/rbac/integration-guide - Core RBAC concepts and package map:
/docs/modules/rbac - Exact repository implementation reference:
examples/RbacDapperQuickstart
Installation
# Core RBAC + Dapper adapter
dotnet add package PrimusSaaS.Rbac
dotnet add package PrimusSaaS.Rbac.Dapper
# Pick your ADO.NET driver (the adapter has no opinion on provider)
dotnet add package Microsoft.Data.SqlClient # SQL Server
dotnet add package Npgsql # PostgreSQL
dotnet add package Microsoft.Data.Sqlite # SQLite
Quickstart
Follow steps 1-5 in order for the minimum runnable setup. Step 6 is optional programmatic seeding. Steps 1 and 2 (schema + config) must be done before the app starts because the adapter checks for seed data on startup.
First-time integration order
If you are using this package for the first time, follow this order exactly:
- choose your database driver
- configure
ConnectionStrings:Rbac - create the RBAC schema
- implement
IRbacDbConnectionFactory - register
AddPrimusRbacDapper(...) - seed initial RBAC data
- test one allow case and one deny case
The main difference from the EF Core adapter is step 3: the schema must already exist before the store is first resolved.
1 — Add the connection string
// appsettings.json
{
"ConnectionStrings": {
"Rbac": "Server=localhost;Database=MyApp;Trusted_Connection=True;"
},
"Rbac": {
"SeedMode": "none"
}
}
SeedMode options: none (production default), minimal (one catch-all permission), sample (demo roles and groups for local dev).
2 — Create the schema
Run the DDL once against your database before first launch. See the full DDL section below for the complete script.
3 — Implement IRbacDbConnectionFactory
This is the one class you write — it tells the adapter how to get a connection:
using System.Data;
using Microsoft.Data.SqlClient;
using PrimusSaaS.Rbac.Dapper;
public class RbacConnectionFactory : IRbacDbConnectionFactory
{
private readonly string _connectionString;
public RbacConnectionFactory(IConfiguration config)
=> _connectionString = config.GetConnectionString("Rbac")!;
public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
}
For PostgreSQL, swap SqlConnection for NpgsqlConnection. For SQLite, use SqliteConnection. The adapter works with any IDbConnection.
4 — Register in Program.cs
using PrimusSaaS.Rbac;
using PrimusSaaS.Rbac.Dapper;
// Register the connection factory (singleton — connection pooling is handled by the driver)
builder.Services.AddSingleton<IRbacDbConnectionFactory, RbacConnectionFactory>();
// Register RBAC with Dapper store
builder.Services.AddPrimusRbacDapper(opts =>
builder.Configuration.GetSection("Rbac").Bind(opts));
// Optional: enable PostgreSQL session variable initializer for DB RLS policies
builder.Services.AddPrimusRbacPostgresRls();
What gets registered in DI:
| Service | Lifetime | Purpose |
|---|---|---|
IRbacService |
Scoped | Main entry point — evaluate access, manage roles/permissions |
IRbacStore → RbacDapperStore |
Scoped | Dapper-backed store |
IRbacAuditSink |
Singleton | Logs RBAC decisions (override to send elsewhere) |
IRbacDecisionCache |
Singleton | No-op by default (plug in Redis etc.) |
IRbacDbConnectionFactory |
Singleton | Your implementation |
Note:
RbacDapperStorealso implementsIRbacStoreAsync, whichRbacService.EvaluateAsync()discovers at runtime via cast — no separate DI registration is needed.
When using PostgreSQL Row-Level Security policies that reference
current_setting('app.rbac_actor'), app.rbac_roles, or app.rbac_tenant,
call AddPrimusRbacPostgresRls() so IRbacRlsInitializer resolves to
PostgresRbacRlsInitializer instead of the default no-op implementation.
5 — Protect your endpoints
Option A — attribute-based (AspNetCore package required):
dotnet add package PrimusSaaS.Rbac.AspNetCore
// On your endpoint or controller
[PrimusRbacAuthorize("orders", "read")]
public IActionResult GetOrders() { ... }
No extra AddPrimusRbacAspNetCore() registration method exists. Referencing the package is enough; the integration surface is the attribute and the .RequirePrimusRbac(...) endpoint filter.
app.MapGet("/orders", () => Results.Ok())
.RequireAuthorization()
.RequirePrimusRbac("orders", "read");
Option B — programmatic check via IRbacService:
public class OrdersController : ControllerBase
{
private readonly IRbacService _rbac;
public OrdersController(IRbacService rbac) => _rbac = rbac;
public async Task<IActionResult> GetOrders()
{
var decision = await _rbac.EvaluateAsync(new RbacAccessRequest
{
PrincipalId = User.FindFirstValue(ClaimTypes.NameIdentifier)!,
Resource = "orders",
Action = "read",
TenantId = User.FindFirstValue("tid")
});
if (!decision.Allowed)
return Forbid();
return Ok(await _orderService.GetAllAsync());
}
}
6 — Seed initial roles and permissions (optional)
Use SeedPrimusRbac after app.Build() to create your initial data programmatically:
var app = builder.Build();
app.Services.SeedPrimusRbac(rbac =>
{
rbac.UpsertPermission(new RbacPermission(
Id: "perm:orders:read",
Action: "read",
Resource: "orders",
Effect: RbacEffect.Allow,
Scope: new RbacScope(ApplicationId: "my-app")));
rbac.UpsertRole(new RbacRole(
Id: "role:viewer",
Name: "Viewer",
Scope: new RbacScope(ApplicationId: "my-app"),
PermissionIds: new[] { "perm:orders:read" }));
});
Schema DDL
Column names match the EF Core adapter so you can swap providers without migrating data. Run the script once before first launch.
SQL Server
CREATE TABLE RbacRoles (
Id NVARCHAR(120) NOT NULL PRIMARY KEY,
Name NVARCHAR(120) NOT NULL,
ApplicationId NVARCHAR(100) NULL,
TenantId NVARCHAR(100) NULL,
Environment NVARCHAR(50) NULL, -- maps to RbacScope.Qualifier
PermissionIdsJson NVARCHAR(MAX) NOT NULL DEFAULT '[]',
InheritsJson NVARCHAR(MAX) NULL,
Description NVARCHAR(MAX) NULL,
MetadataJson NVARCHAR(MAX) NULL
);
CREATE INDEX IX_RbacRoles_Name ON RbacRoles (Name);
CREATE INDEX IX_RbacRoles_Scope ON RbacRoles (ApplicationId, TenantId, Environment);
CREATE TABLE RbacPermissions (
Id NVARCHAR(120) NOT NULL PRIMARY KEY,
Action NVARCHAR(80) NOT NULL,
Resource NVARCHAR(200) NOT NULL,
Effect INT NOT NULL,
ApplicationId NVARCHAR(100) NULL,
TenantId NVARCHAR(100) NULL,
Environment NVARCHAR(50) NULL,
Description NVARCHAR(MAX) NULL,
ConditionsJson NVARCHAR(MAX) NULL,
ConditionGroupsJson NVARCHAR(MAX) NULL,
MetadataJson NVARCHAR(MAX) NULL
);
CREATE INDEX IX_RbacPermissions_ActionResource ON RbacPermissions (Action, Resource);
CREATE INDEX IX_RbacPermissions_Scope ON RbacPermissions (ApplicationId, TenantId, Environment, Action, Resource);
CREATE TABLE RbacAssignments (
Id NVARCHAR(140) NOT NULL PRIMARY KEY,
PrincipalId NVARCHAR(120) NOT NULL,
PrincipalType NVARCHAR(40) NOT NULL,
RoleId NVARCHAR(120) NOT NULL,
ApplicationId NVARCHAR(100) NULL,
TenantId NVARCHAR(100) NULL,
Environment NVARCHAR(50) NULL,
ExpiresAt DATETIME2 NULL,
Enabled BIT NOT NULL DEFAULT 1,
AttributesJson NVARCHAR(MAX) NULL
);
CREATE INDEX IX_RbacAssignments_PrincipalId ON RbacAssignments (PrincipalId);
CREATE INDEX IX_RbacAssignments_RoleId ON RbacAssignments (RoleId);
CREATE INDEX IX_RbacAssignments_PrincipalScope ON RbacAssignments (PrincipalId, ApplicationId, TenantId, Environment);
CREATE TABLE RbacGroups (
Id NVARCHAR(140) NOT NULL PRIMARY KEY,
Name NVARCHAR(120) NOT NULL,
Type NVARCHAR(80) NOT NULL,
ApplicationId NVARCHAR(100) NULL,
TenantId NVARCHAR(100) NULL,
Environment NVARCHAR(50) NULL,
ParentId NVARCHAR(140) NULL,
Description NVARCHAR(MAX) NULL,
MetadataJson NVARCHAR(MAX) NULL
);
CREATE INDEX IX_RbacGroups_ParentId ON RbacGroups (ParentId);
CREATE INDEX IX_RbacGroups_Type ON RbacGroups (Type);
CREATE INDEX IX_RbacGroups_Scope ON RbacGroups (ApplicationId, TenantId, Environment);
CREATE TABLE RbacMemberships (
Id NVARCHAR(160) NOT NULL PRIMARY KEY,
GroupId NVARCHAR(140) NOT NULL,
PrincipalId NVARCHAR(120) NOT NULL,
PrincipalType NVARCHAR(40) NOT NULL,
ApplicationId NVARCHAR(100) NULL,
TenantId NVARCHAR(100) NULL,
Environment NVARCHAR(50) NULL,
ExpiresAt DATETIME2 NULL,
Enabled BIT NOT NULL DEFAULT 1,
AttributesJson NVARCHAR(MAX) NULL
);
CREATE INDEX IX_RbacMemberships_GroupId ON RbacMemberships (GroupId);
CREATE INDEX IX_RbacMemberships_PrincipalId ON RbacMemberships (PrincipalId);
CREATE INDEX IX_RbacMemberships_GroupPrincipal ON RbacMemberships (GroupId, PrincipalId);
CREATE INDEX IX_RbacMemberships_Scope ON RbacMemberships (PrincipalId, ApplicationId, TenantId, Environment);
PostgreSQL
CREATE TABLE "RbacRoles" (
"Id" VARCHAR(120) NOT NULL PRIMARY KEY,
"Name" VARCHAR(120) NOT NULL,
"ApplicationId" VARCHAR(100) NULL,
"TenantId" VARCHAR(100) NULL,
"Environment" VARCHAR(50) NULL,
"PermissionIdsJson" TEXT NOT NULL DEFAULT '[]',
"InheritsJson" TEXT NULL,
"Description" TEXT NULL,
"MetadataJson" TEXT NULL
);
CREATE INDEX "IX_RbacRoles_Name" ON "RbacRoles" ("Name");
CREATE INDEX "IX_RbacRoles_Scope" ON "RbacRoles" ("ApplicationId", "TenantId", "Environment");
CREATE TABLE "RbacPermissions" (
"Id" VARCHAR(120) NOT NULL PRIMARY KEY,
"Action" VARCHAR(80) NOT NULL,
"Resource" VARCHAR(200) NOT NULL,
"Effect" INT NOT NULL,
"ApplicationId" VARCHAR(100) NULL,
"TenantId" VARCHAR(100) NULL,
"Environment" VARCHAR(50) NULL,
"Description" TEXT NULL,
"ConditionsJson" TEXT NULL,
"ConditionGroupsJson" TEXT NULL,
"MetadataJson" TEXT NULL
);
CREATE INDEX "IX_RbacPermissions_ActionResource" ON "RbacPermissions" ("Action", "Resource");
CREATE INDEX "IX_RbacPermissions_Scope" ON "RbacPermissions" ("ApplicationId", "TenantId", "Environment", "Action", "Resource");
CREATE TABLE "RbacAssignments" (
"Id" VARCHAR(140) NOT NULL PRIMARY KEY,
"PrincipalId" VARCHAR(120) NOT NULL,
"PrincipalType" VARCHAR(40) NOT NULL,
"RoleId" VARCHAR(120) NOT NULL,
"ApplicationId" VARCHAR(100) NULL,
"TenantId" VARCHAR(100) NULL,
"Environment" VARCHAR(50) NULL,
"ExpiresAt" TIMESTAMPTZ NULL,
"Enabled" BOOLEAN NOT NULL DEFAULT TRUE,
"AttributesJson" TEXT NULL
);
CREATE INDEX "IX_RbacAssignments_PrincipalId" ON "RbacAssignments" ("PrincipalId");
CREATE INDEX "IX_RbacAssignments_RoleId" ON "RbacAssignments" ("RoleId");
CREATE INDEX "IX_RbacAssignments_PrincipalScope" ON "RbacAssignments" ("PrincipalId", "ApplicationId", "TenantId", "Environment");
CREATE TABLE "RbacGroups" (
"Id" VARCHAR(140) NOT NULL PRIMARY KEY,
"Name" VARCHAR(120) NOT NULL,
"Type" VARCHAR(80) NOT NULL,
"ApplicationId" VARCHAR(100) NULL,
"TenantId" VARCHAR(100) NULL,
"Environment" VARCHAR(50) NULL,
"ParentId" VARCHAR(140) NULL,
"Description" TEXT NULL,
"MetadataJson" TEXT NULL
);
CREATE INDEX "IX_RbacGroups_ParentId" ON "RbacGroups" ("ParentId");
CREATE INDEX "IX_RbacGroups_Type" ON "RbacGroups" ("Type");
CREATE INDEX "IX_RbacGroups_Scope" ON "RbacGroups" ("ApplicationId", "TenantId", "Environment");
CREATE TABLE "RbacMemberships" (
"Id" VARCHAR(160) NOT NULL PRIMARY KEY,
"GroupId" VARCHAR(140) NOT NULL,
"PrincipalId" VARCHAR(120) NOT NULL,
"PrincipalType" VARCHAR(40) NOT NULL,
"ApplicationId" VARCHAR(100) NULL,
"TenantId" VARCHAR(100) NULL,
"Environment" VARCHAR(50) NULL,
"ExpiresAt" TIMESTAMPTZ NULL,
"Enabled" BOOLEAN NOT NULL DEFAULT TRUE,
"AttributesJson" TEXT NULL
);
CREATE INDEX "IX_RbacMemberships_GroupId" ON "RbacMemberships" ("GroupId");
CREATE INDEX "IX_RbacMemberships_PrincipalId" ON "RbacMemberships" ("PrincipalId");
CREATE INDEX "IX_RbacMemberships_GroupPrincipal" ON "RbacMemberships" ("GroupId", "PrincipalId");
CREATE INDEX "IX_RbacMemberships_Scope" ON "RbacMemberships" ("PrincipalId", "ApplicationId", "TenantId", "Environment");
SQLite: Use
TEXTfor all string/JSON/datetime columns andINTEGERinstead ofBOOLEAN/BIT. SQLite has noVARCHARlength enforcement but specifying it is harmless.
Migrating from PrimusSaaS.Rbac.EFCore
The two adapters share the same table names, column names, and JSON encoding. Swap is straightforward:
- Remove
AddPrimusRbacEfCore<TContext>()and the EF Core package reference. - Add
IRbacDbConnectionFactoryregistration +AddPrimusRbacDapper(). - You no longer need
modelBuilder.AddPrimusRbac()in yourDbContext. - Schema stays the same — no data migration needed.
Behaviour notes
- Per-operation connections — each store method opens a fresh connection from
IRbacDbConnectionFactory.CreateConnection(). ADO.NET connection pooling handles the actual socket reuse. - Upsert strategy — SELECT EXISTS then INSERT or UPDATE (cross-database portable, no dialect-specific MERGE/ON CONFLICT).
- Async — implements
IRbacStoreAsync; usesDbConnection.OpenAsyncwhen the provider exposes it. - Seeding — same
SeedModeoption as EFCore (none/minimal/sample). Seeds only when all tables are empty.
| 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 is compatible. 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 is compatible. 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. |
-
net10.0
- Dapper (>= 2.1.35)
- Microsoft.Data.SqlClient (>= 5.2.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Options (>= 10.0.5)
- PrimusSaaS.Rbac (>= 2.0.1)
-
net8.0
- Dapper (>= 2.1.35)
- Microsoft.Data.SqlClient (>= 5.2.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Options (>= 10.0.5)
- PrimusSaaS.Rbac (>= 2.0.1)
-
net9.0
- Dapper (>= 2.1.35)
- Microsoft.Data.SqlClient (>= 5.2.2)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- Microsoft.Extensions.Options (>= 10.0.5)
- PrimusSaaS.Rbac (>= 2.0.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on PrimusSaaS.Rbac.Dapper:
| Package | Downloads |
|---|---|
|
PrimusSaaS.Rbac.Dapper.MySql
MySQL connection factory for PrimusSaaS.Rbac.Dapper. Drop-in replacement for the SQL Server factory. |
GitHub repositories
This package is not used by any popular GitHub repositories.