Valtuutus.Data.Caching 1.6.0

There is a newer version of this package available.
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
                    
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="Valtuutus.Data.Caching" Version="1.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Valtuutus.Data.Caching" Version="1.6.0" />
                    
Directory.Packages.props
<PackageReference Include="Valtuutus.Data.Caching" />
                    
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 Valtuutus.Data.Caching --version 1.6.0
                    
#r "nuget: Valtuutus.Data.Caching, 1.6.0"
                    
#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 Valtuutus.Data.Caching@1.6.0
                    
#: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=Valtuutus.Data.Caching&version=1.6.0
                    
Install as a Cake Addin
#tool nuget:?package=Valtuutus.Data.Caching&version=1.6.0
                    
Install as a Cake Tool

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.

NuGet Version

<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 the Check function.
    • What permissions entity U have in resource Z? For that, use the SubjectPermission function.
    • Why did that permission check succeed or fail? For that, use the Explain function — 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 the Lookup function.
  • ILookupEntityEngine: The engine that can answer: Which resources of type T can entity:X have permission Y? For that, use the LookupEntity function. 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 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. 
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
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
Loading failed

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