Valtuutus.Data.Caching
1.6.0
See the version list below for details.
dotnet add package Valtuutus.Data.Caching --version 1.6.0
NuGet\Install-Package Valtuutus.Data.Caching -Version 1.6.0
<PackageReference Include="Valtuutus.Data.Caching" Version="1.6.0" />
<PackageVersion Include="Valtuutus.Data.Caching" Version="1.6.0" />
<PackageReference Include="Valtuutus.Data.Caching" />
paket add Valtuutus.Data.Caching --version 1.6.0
#r "nuget: Valtuutus.Data.Caching, 1.6.0"
#:package Valtuutus.Data.Caching@1.6.0
#addin nuget:?package=Valtuutus.Data.Caching&version=1.6.0
#tool nuget:?package=Valtuutus.Data.Caching&version=1.6.0
Valtuutus
Valtuutus is a high-performance Relationship-Based Access Control (ReBAC) authorization library for .NET, inspired by Google Zanzibar.
It enables fine-grained authorization for ASP.NET Core and .NET applications — modeling permissions and access control through relationships instead of relying only on roles.
Features:
- Google Zanzibar-inspired permission model
- High-performance authorization engine
- ASP.NET Core / .NET integration
- Strongly typed schema support (source generator)
- Multiple evaluation engines (Check, LookupEntity, LookupSubject, SubjectPermission, Explain)
- Low-allocation execution
- Native AOT compatible
The implementation is inspired on permify and other ReBAC open source projects.
<a href="https://bencher.dev/perf/valtuutus?clear=true&key=true&reports_per_page=4&branches_per_page=8&testbeds_per_page=8&benchmarks_per_page=8&plots_per_page=8&reports_page=1&branches_page=1&testbeds_page=1&benchmarks_page=1&plots_page=1&tab=branches&measures=b549a9dd-6ff0-4525-b90a-c9e3af815580&branches_search=main&branches=1bffa1be-8399-4560-814d-30231501957f%2C9e4cbdcf-9fee-4cd3-ada1-62aefe433145&heads=deb5918c-87b4-4bdb-9b46-25e563bdba14%2C5bdd1841-0a0f-4532-84b7-87ef3d065302&testbeds=072da3db-e609-4676-99a6-5b9262df6086&benchmarks_search=check_complex&benchmarks=86537617-e761-40b8-bd7a-8ad4b6559b47%2C177b6be7-d449-45cb-928a-04f792c80c43%2C8dc1fb40-6499-45e0-be05-23bbf539cd7e%2Cfa90791e-1700-480d-a973-a5df9b695431&start_time=1782864000000&utm_medium=share&utm_source=bencher&utm_content=img&utm_campaign=perf%2Bimg&utm_term=valtuutus"><img src="https://api.bencher.dev/v0/projects/valtuutus/perf/img?branches=1bffa1be-8399-4560-814d-30231501957f%2C9e4cbdcf-9fee-4cd3-ada1-62aefe433145&heads=deb5918c-87b4-4bdb-9b46-25e563bdba14%2C5bdd1841-0a0f-4532-84b7-87ef3d065302&testbeds=072da3db-e609-4676-99a6-5b9262df6086&specs=&benchmarks=86537617-e761-40b8-bd7a-8ad4b6559b47%2C177b6be7-d449-45cb-928a-04f792c80c43%2C8dc1fb40-6499-45e0-be05-23bbf539cd7e%2Cfa90791e-1700-480d-a973-a5df9b695431&measures=b549a9dd-6ff0-4525-b90a-c9e3af815580&start_time=1782864000000" title="valtuutus" alt="valtuutus - Bencher" /></a>
Functionality
The library is designed to be simple and easy to use. Each subset of functionality is divided in engines. The engines are:
- ICheckEngine: The engine that handles the answering of three questions:
Can entity U perform action Y in resource Z? For that, use theCheckfunction.What permissions entity U have in resource Z? For that, use theSubjectPermissionfunction.Why did that permission check succeed or fail?For that, use theExplainfunction — returns a full resolution tree showing every evaluated relation, attribute, and expression.
- ILookupSubjectEngine: The engine that can answer:
Which subjects of type T have permission Y on entity:X?For that, use theLookupfunction. - ILookupEntityEngine: The engine that can answer:
Which resources of type T can entity:X have permission Y?For that, use theLookupEntityfunction. Supports scoped queries and cursor pagination — see below. - IDataWriterProvider: This is the provider that can write your relational or attribute data.
- IDbDataWriterProvider: Works similarly to
IDataWriterProvider, with the addition of accepting a connection and transaction as parameters. - Read here about how the relational data is stored.
- Read here for engine usage examples (Check, Explain, SubjectPermission, LookupSubject, LookupEntity).
LookupEntity — scoped queries and pagination
LookupEntity returns a LookupEntityPage:
LookupEntityPage page = await lookupEntityEngine.LookupEntity(
new LookupEntityRequest("task", "view", "user", "alice"),
cancellationToken);
// page.EntityIds — IReadOnlyList<string>
// page.ContinuationToken — null if no more pages
Scope — constrain results to a parent entity
Use EntityScope when you need to answer a scoped question like
"which tasks in project X can this user view?" — the same query you'd back a
GET /projects/{projectId}/tasks endpoint with.
Without scope, LookupEntity returns all tasks the user can view across the entire system.
With scope, results are limited to tasks that have the specified relation to the given parent entity —
so only tasks belonging to project-1 are considered.
var page = await lookupEntityEngine.LookupEntity(
new LookupEntityRequest("task", "view", "user", "alice")
{
Scope = new EntityScope(
Relation: "parent", // the relation on "task" that points to its parent
SubjectType: "project", // the parent entity type
SubjectId: "project-1" // the specific parent to scope to
)
},
cancellationToken);
Pagination
string? token = null;
do
{
var page = await lookupEntityEngine.LookupEntity(
new LookupEntityRequest("task", "view", "user", "alice")
{
Scope = new EntityScope("parent", "project", "project-1"),
PageSize = 50,
ContinuationToken = token
},
cancellationToken);
Process(page.EntityIds);
token = page.ContinuationToken;
} while (token is not null);
Documentation
| Guide | Description |
|---|---|
| Getting Started | End-to-end quickstart — install, configure, write data, check permissions |
| Modeling Authorization | Schema DSL walkthrough with the GitHub example |
| Schema Reference | Complete reference for every keyword, operator, and type in the DSL |
| Authorization Patterns | Ready-made patterns: RBAC, hierarchical RBAC, ABAC, multi-tenancy |
| Using the Engines | Code examples for Check, Explain, SubjectPermission, LookupSubject, LookupEntity, depth |
| Storing Data | Writing, deleting, snap tokens |
| Source Generator | Build-time schema constants and compiled fn functions |
| Testing | Unit-testing your authorization model with the InMemory provider |
| Caching | Reducing database load with FusionCache |
| Telemetry | OpenTelemetry activity sources, emitted spans, and what to monitor |
Usage
Install the package from NuGet:
If using Postgres:
dotnet add package Valtuutus.Data.Postgres
If using SqlServer:
dotnet add package Valtuutus.Data.SqlServer
Requires SQL Server 2016 or later (database compatibility level 130+), Azure SQL Database or Azure SQL Managed Instance.
If you prefer using an InMemory provider:
dotnet add package Valtuutus.Data.InMemory
Adding to DI:
builder.Services.AddValtuutusCore(c =>
...
See examples of how to define your schema here.
If using Postgres:
builder.Services
.AddPostgres(_ => () => new NpgsqlConnection(builder.Configuration.GetConnectionString("PostgresDb")!));
If using SqlServer:
builder.Services
.AddSqlServer(_ => () => new SqlConnection(builder.Configuration.GetConnectionString("SqlServerDb")!));
If using InMemory:
builder.Services
.AddInMemory();
Database migrations
If you are using a DB provider to store your data, please look at the scripts that create the tables that Valtuutus require to function.
Schema and table name customization
Both relational providers accept an optional options object to customise the database schema and table names. Pass it as the second argument to AddPostgres or AddSqlServer:
// Postgres — defaults: schema="public", tables="transactions", "relation_tuples", "attributes"
builder.Services.AddValtuutusCore(/* schema */)
.AddPostgres(
_ => () => new NpgsqlConnection(connectionString),
new ValtuutusPostgresOptions(
schema: "authz",
transactionsTableName: "transactions",
relationsTableName: "relation_tuples",
attributesTableName: "attributes"));
// SQL Server — defaults: schema="dbo", same table names
builder.Services.AddValtuutusCore(/* schema */)
.AddSqlServer(
_ => () => new SqlConnection(connectionString),
new ValtuutusSqlServerOptions(
schema: "authz",
transactionsTableName: "transactions",
relationsTableName: "relation_tuples",
attributesTableName: "attributes"));
Make sure the migration script targets the same schema and table names you configure here.
ValtuutusPostgresOptions also exposes two Npgsql-specific properties for automatic prepared statements:
| Property | Default | Meaning |
|---|---|---|
MaxAutoPrepare |
64 |
Maximum number of statements Npgsql will auto-prepare |
AutoPrepareMinUsages |
2 |
Minimum executions before a statement is prepared |
These map directly to Npgsql's prepared statement feature and can improve performance for repeated queries under load.
Both options types also expose LookupHopsPerQuery (default 2): how many levels of a recursive relation one query reads when the opt-in V2 lookup engines are enabled. See Lookup engines V2.
Using query concurrent limiting
It is expected that you don't want to allow Valtuutus to expand queries while it has resources. The default limit is 5 concurrent queries for the same request. To change that, you can use the AddConcurrentQueryLimit method, for example:
builder.Services
.AddPostgres(_ => () => new NpgsqlConnection(builder.Configuration.GetConnectionString("PostgresDb")!)) // Replace this with any provider you want
.AddConcurrentQueryLimit(10);
Change your data provider according to your database.
Caching
Valtuutus supports caching the calls to the engines through the Valtuutus.Data.Caching package. To use it, install like:
dotnet add package Valtuutus.Data.Caching
In your DI setup, add the caching component:
builder.Services
.AddPostgres(_ => () => new NpgsqlConnection(builder.Configuration.GetConnectionString("PostgresDb")!)) // Replace this with any provider you want
.AddCaching(); // <-- This line
This packages requires that you set up the amazing FusionCache library. Click here for more information.
Telemetry
The library uses OpenTelemetry to provide telemetry data. To enable it, just add a source with the name "Valtuutus":
builder.Services
.AddOpenTelemetry()
.WithTracing(telemetry =>
{
telemetry
.AddSource("Valtuutus")
...
| 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. net11.0 is compatible. |
-
net10.0
- Valtuutus.Core (>= 1.6.0)
- Valtuutus.Data (>= 1.6.0)
- ZiggyCreatures.FusionCache (>= 2.1.0)
-
net11.0
- Valtuutus.Core (>= 1.6.0)
- Valtuutus.Data (>= 1.6.0)
- ZiggyCreatures.FusionCache (>= 2.1.0)
-
net8.0
- Valtuutus.Core (>= 1.6.0)
- Valtuutus.Data (>= 1.6.0)
- ZiggyCreatures.FusionCache (>= 2.1.0)
-
net9.0
- Valtuutus.Core (>= 1.6.0)
- Valtuutus.Data (>= 1.6.0)
- ZiggyCreatures.FusionCache (>= 2.1.0)
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 |
|---|---|---|
| 2.0.0 | 48 | 10/16/2026 |
| 1.6.0 | 64 | 10/16/2026 |
| 1.5.0 | 46 | 10/14/2026 |
| 1.4.0 | 96 | 10/3/2026 |
| 1.3.0 | 153 | 7/30/2026 |
| 1.2.0 | 140 | 7/24/2026 |
| 1.1.0 | 124 | 7/17/2026 |
| 1.0.0 | 960 | 7/11/2026 |
| 0.11.1-beta | 220 | 7/4/2026 |
| 0.11.0-beta | 165 | 6/26/2026 |
| 0.10.2-beta | 237 | 6/19/2026 |
| 0.10.1-beta | 136 | 6/15/2026 |
| 0.10.0-beta | 110 | 6/9/2026 |
| 0.9.0-beta | 347 | 4/20/2026 |
| 0.8.2-beta | 117 | 4/17/2026 |
| 0.8.1-beta | 119 | 4/17/2026 |
| 0.8.0-beta | 121 | 4/13/2026 |
| 0.7.3.1-beta | 330 | 4/3/2025 |
| 0.7.3-beta | 238 | 3/20/2025 |
| 0.7.2-beta | 194 | 12/30/2024 |
This release adds opt-in V2 engines for both lookups, makes SQL Server's lookups and checks much cheaper by replacing table-valued parameters with JSON id lists, and fixes lookups that could grant what Check denies.
## Breaking changes
* **SQL Server 2016+ required** (#309). Id and name lists are passed as JSON read with , which needs database compatibility level 130+ (SQL Server 2016 or later, Azure SQL Database, Azure SQL Managed Instance). No migration: the type stays in the database but is no longer used.
* ** lookup reads take a subject relation** (#315). , , , , and take a non-null ( = direct subject); a null no longer means anyrelation. Custom provider implementations must filter on it. The InMemory, Postgres and SQL Server providers are updated.
* **Lookup results can change** where they were wrong:
* Lookups no longer return entities or subjects reached through a tuple whose subject relation the schema path doesn't name (#315). Example: with , a tuple on no longer grants 's members.
* A terminal userset read through the joined fast path now spends a level of depth, as the tuple-to-userset path already did (#313 for LookupSubject, #314 for LookupEntity). Results change only at the exact depth limit; depth keeps counting schema hops, as documented.
## New: opt-in LookupSubject and LookupEntity V2 engines
Enable them with and (after , before ). The default engines are unchanged; V2 becomes the default in a later release.
* **Graph/step engines** (#304, #317). Each permission compiles once into a plan per (entity type, permission, subject type) (#310 for LookupSubject); a request binds its subject, depth, scope and context to the cached plans and runs every query a step waits on together. Results match the default engines: the shared suites compare V1 and V2 on InMemory, Postgres and SQL Server.
* **One round trip per step** on Postgres and SQL Server (#307, #320). A step's queries run as one , and a tokenless request resolves its snap token inside the first batch and pins it for the rest of the request.
* **Pooled per-request state** (#310, #318), so V2 allocates less than V1.
* **Fewer steps on recursive relations:**
* LookupSubject reads several userset levels per query (#313).
* LookupEntity walks a self-recursive relation such as from the subject outward and stops at the first hop that finds nothing new, instead of unrolling to the request's depth (#322). It reads several hops per query (#323), the first one together with the direct grants (#324).
* The number of levels per query is on / (default ; reads one level per step).
* **Extension points** for providers: , , , (Core), and , (Data.Db). is a default-implemented member (), so existing option types keep compiling.
V1 → V2 on this release (dev cedf814), net10.0 BenchmarkDotNet, median of 3 interleaved rounds:
| Scenario | Postgres | SQL Server |
|---|---|---|
| LookupSubject | 69 → 53 µs (−23%) | 175 → 138 µs (−21%) |
| LookupSubject_Diamond | 69 → 52 µs (−24%) | 180 → 137 µs (−24%) |
| LookupSubject_SiblingBatch | 123 → 70 µs (−43%) | 203 → 179 µs (−12%) |
| LookupSubject_NestedGroup | 346 → 258 µs (−26%) | 605 → 377 µs (−38%) |
| LookupEntity | 389 → 373 µs (−4%) | 736 → 663 µs (−10%) |
| LookupEntity_SiblingBatch | 152 → 66 µs (−57%) | 199 → 167 µs (−16%) |
| LookupEntity_ConstrainedSiblingBatch | 131 → 70 µs (−47%) | 275 → 198 µs (−28%) |
| LookupEntity_NegateSiblingBatch | 129 → 48 µs (−63%) | 225 → 129 µs (−43%) |
| LookupEntity_Diamond | 66 → 61 µs (−7%) | 205 → 153 µs (−25%) |
| LookupEntity_NestedGroup | 1128 → 94 µs (−92%) | 1198 → 343 µs (−71%) |
| LookupEntity_Scoped | 502 → 532 µs (+6%) | 1017 → 1002 µs (tie) |
Allocations drop in every scenario (e.g. LookupEntity_NestedGroup 90 → 11 KB on Postgres) except LookupEntity and LookupEntity_Scoped on SQL Server (+5%). LookupEntity_Scoped on Postgres is the one scenario where V2 is slower: its first step's two scoped reads run one after the other in a batch, where V1 overlaps them on two connections.
## Performance (default engines too)
* **SQL Server id lists as JSON** (#309). Each parameter cost ~0.5–1 ms per use; costs ~50–80 µs. Check −7% to −39%, SubjectPermission −12% to −25%, LookupEntity −6% to −81%, LookupSubject −45% to −96%.
* **SQL Server cached plans that hold for every caller:**
* The entity-ids relation reads no longer recompile on every execution (#308): LookupSubject_SiblingBatch −70%.
* The joined lookup's inner rows seek by the listed ids (#309): LookupSubject_SiblingBatch another −88%.
* Single-subject relation reads seek by subject (#325). A plan compiled for a sparse relation could read every tuple of a dense one later (up to ~14 ms per read); every compile order now costs under 40 µs.
* **Scoped lookups seek the scope per candidate** (#321), on both providers and both engines: LookupEntity_Scoped SQL Server 1680 → 951 µs (V1), 2331 → 981 µs (V2); Postgres V2 765 → 547 µs.
* **Postgres lookup relation reads seek under generic plans** (#312): a nested lookup on V2 went from ~14 ms to ~0.55 ms.
## Benchmarks
* New nested-userset scenarios for both lookups (#311, #316).
## Pull requests
* #304 feat: add LookupSubjectEngineV2 graph/step engine (opt-in)
* #307 perf(lookup): run LookupSubject V2 steps as one DbBatch on Postgres/SqlServer
* #308 perf(sqlserver): drop OPTION (RECOMPILE) from the entity-ids relation queries
* #309 perf(sqlserver): pass id lists as JSON read with OPENJSON
* #310 perf(lookup): compile LookupSubject V2 plans and pool its per-request state
* #311 bench(lookup): add nested userset LookupSubject scenario
* #312 perf(postgres): let lookup relation reads seek under generic plans
* #313 perf(lookup): read several userset levels per LookupSubject V2 query
* #314 fix(lookup): spend a level of depth on LookupEntity's joined userset path
* #315 fix(lookup): read only tuples whose subject relation the schema path names
* #316 bench(lookup): add nested userset LookupEntity scenario
* #317 feat(lookup): add LookupEntityEngineV2 graph/step engine (opt-in)
* #318 perf(lookup): pool LookupEntity V2 per-request state
* #320 perf(lookup): run LookupEntity V2 steps as one DbBatch on Postgres/SqlServer
* #321 perf(data): seek the scope probe per candidate in scoped relation reads
* #322 perf(lookup): stop LookupEntity V2's self-recursive walks when a hop finds nothing new
* #323 perf(lookup): walk LookupEntity V2's self-recursive relations several hops per query
* #324 perf(lookup): read LookupEntity V2's direct grants in the first levels query
* #325 perf(data): seek single-subject relation reads by subject on SqlServer