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
                    
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="PrimusSaaS.Rbac.Dapper" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PrimusSaaS.Rbac.Dapper" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="PrimusSaaS.Rbac.Dapper" />
                    
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 PrimusSaaS.Rbac.Dapper --version 1.0.1
                    
#r "nuget: PrimusSaaS.Rbac.Dapper, 1.0.1"
                    
#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 PrimusSaaS.Rbac.Dapper@1.0.1
                    
#: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=PrimusSaaS.Rbac.Dapper&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=PrimusSaaS.Rbac.Dapper&version=1.0.1
                    
Install as a Cake Tool

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:

  1. choose your database driver
  2. configure ConnectionStrings:Rbac
  3. create the RBAC schema
  4. implement IRbacDbConnectionFactory
  5. register AddPrimusRbacDapper(...)
  6. seed initial RBAC data
  7. 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
IRbacStoreRbacDapperStore 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: RbacDapperStore also implements IRbacStoreAsync, which RbacService.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 TEXT for all string/JSON/datetime columns and INTEGER instead of BOOLEAN / BIT. SQLite has no VARCHAR length 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:

  1. Remove AddPrimusRbacEfCore<TContext>() and the EF Core package reference.
  2. Add IRbacDbConnectionFactory registration + AddPrimusRbacDapper().
  3. You no longer need modelBuilder.AddPrimusRbac() in your DbContext.
  4. 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; uses DbConnection.OpenAsync when the provider exposes it.
  • Seeding — same SeedMode option as EFCore (none / minimal / sample). Seeds only when all tables are empty.
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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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.

Version Downloads Last Updated
1.0.1 142 4/19/2026
1.0.0 176 4/1/2026