Valtuutus.Data.Caching 2.0.0

dotnet add package Valtuutus.Data.Caching --version 2.0.0
                    
NuGet\Install-Package Valtuutus.Data.Caching -Version 2.0.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="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Valtuutus.Data.Caching" Version="2.0.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 2.0.0
                    
#r "nuget: Valtuutus.Data.Caching, 2.0.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@2.0.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=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Valtuutus.Data.Caching&version=2.0.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
// page.SnapToken — the snap token the page was read at

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

Leave SnapToken unset to read each page at the latest snapshot, or pass the previous page's SnapToken to read every page from the same one.

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 lookup query reads. See How lookups run.

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 makes the graph/step lookup engines the only lookup engines, and makes every engine result report the snap token it was read at. As a result, enabling caching no longer costs a round trip to resolve the latest token. It also fixes queries left running by a finished check failing once the request scope is disposed.

## Breaking changes

* **The V2 lookup engines are the only lookup engines** (#328).  registers them. The recursive engines (, ) and the opt-in methods  and  are removed. Delete those calls; nothing else changes for callers. The engines return the same results; see the v1.6.0 notes for how they run and how they compare.
* **Engine results carry the snap token they were read at** (#329):

 | Method | 1.6 | 2.0 |
 |---|---|---|
 |  |  |  (, ) |
 |  |  |  (, ) |
 |  |  |  (, ) |
 |  |  | , plus a  on the page |
 |  |  | , plus a  on the result |

 Read ,  or  where you used the value directly. Await each call once, and use  where you need a , for example for . A result's token is null only when the request carried none and the answer read no stored data.
* **Requests are immutable** (#329). ,  and  are -only; use  to derive one request from another.
* **Removed public types and members:**
 * ,  (#328)
 *  (#329)
 *  on ,  and  (#330)
* **Custom engines** (, ,  implementations) should report the token they read at.  caches a result under that token, and does not cache a result without one.

See Upgradingfrom1.6 in [Using the Engines](https://github.com/valtuutus/valtuutus/blob/main/Using the Engines.md#upgrading-from-16) for the full list with examples.

## New: snap tokens on results

Pass a result's  to a later request to read the same snapshot, for example to make a check and a lookup agree. When paging :
* pass each page's  to the next request to read every page from one snapshot;
* or leave it unset to read each page at the latest snapshot.

## Fixes

* **Caching no longer spends a round trip resolving the latest token** (#329).
 * **Before:** when the cached latest token had expired (5-minute lifetime), the caching decorators called  as a separate query before every check or lookup. The engine then always got a token, so it never resolved the token inside its own first round trip.
 * **Now:** without a known token, the decorator calls the engine tokenless and caches the result under the token the engine reports. It records that token as the latest unless the cache already knows a newer one.
* **Queries a check leaves running keep working after the request scope is disposed** (#330). A check returns as soon as its answer is known, while queries of branches it no longer needs keep running. Disposing the request scope disposed the reader's concurrency semaphore under them. A leftover query that then needed it threw , or hung if it was already waiting for a slot.

## Performance

and the new result structs, measured against v1.6.0 plus #328 (net10.0 BenchmarkDotNet, median of 3 interleaved rounds):

| Provider | Time | Allocated per call |
|---|---|---|
| InMemory | within ±4% everywhere | Check unchanged; SubjectPermission and LookupSubject −72 B; LookupEntity −64 B |
| Postgres | −13%..+11%, no consistent direction | +72..+113 B |
| SQL Server | −2%..+7.5%, within this provider's run-to-run spread | +72..+113 B |

* A cache-hit  completes without allocating a task, where  would cost 88 B.
* A database-backed call carries the larger result through its async chain, about 1% of what it already allocates.

## Pull requests

* #328 chore!: make the V2 lookup engines the only lookup engines
* #329 feat!: report the snap token every engine result was read at
* #330 fix(data): keep serving stragglers after the request scope is disposed
* #319 Build(deps): Bump EnricoMi/publish-unit-test-result-action