Casl.Net
0.1.0
dotnet add package Casl.Net --version 0.1.0
NuGet\Install-Package Casl.Net -Version 0.1.0
<PackageReference Include="Casl.Net" Version="0.1.0" />
<PackageVersion Include="Casl.Net" Version="0.1.0" />
<PackageReference Include="Casl.Net" />
paket add Casl.Net --version 0.1.0
#r "nuget: Casl.Net, 0.1.0"
#:package Casl.Net@0.1.0
#addin nuget:?package=Casl.Net&version=0.1.0
#tool nuget:?package=Casl.Net&version=0.1.0
CASL .NET
An independent, community-built .NET implementation of CASL's rule format and semantics. Not affiliated with or endorsed by CASL or its author.
CASL-compatible authorization for .NET. Write permission rules once, in the CASL format, and use them three ways:
- In memory:
ability.Can("update", workItem) - In the database:
db.WorkItems.AccessibleBy(ability, "read"). Rules become a LINQ expression that EF Core translates to SQL, with rule values sent as SQL parameters. - In the browser: send
ability.ToCaslRules()to the frontend and check them with the real@casl/ability.
All three give the same answers. That isn't a promise; three sets of tests check it on every build:
- CASL's own test suite, ported to C#: 172 of CASL's 187 runtime tests; the other 15 test JavaScript-only behaviour (details).
- Database vs memory: 400 random rule sets × 2 actions × 120 entities on a real EF Core database, under both rule semantics. The SQL filter and the in-memory check return the same rows every time.
- CASL compatibility: 24,000 decisions on 150 random rule sets, made by CASL .NET and by
@casl/ability7, all identical.
It has no dependencies beyond .NET itself, so you can use it in any .NET 8 or later application, with or without EF Core. It was first built for Msar, an open-source work management platform, and is maintained by Tekthar as a standalone project.
Quick start
Everything lives in the Casl namespace: using Casl;.
dotnet add package Casl.Net
For the browser: npm install casl-net @casl/ability.
If you know CASL, it works the way you expect:
var ability = new AbilityBuilder()
.Can("read", "Post")
.Can("update", "Post", new { authorId = user.Id })
.Cannot("delete", "Post", new { published = true }).Because("Published posts can't be deleted")
.Build();
ability.Can("update", post); // the subject type of a Post object is "Post"
ability.Can("delete", "Post"); // "on at least some posts?"
When rules come from users (an admin screen, a database), use a strict registry, so rules can only filter on members you list:
// 1. Say which types are subjects, and which members rules may filter on.
var subjects = new SubjectRegistry()
.Add<WorkItem>("WorkItem", s => s
.ConditionFields("projectId", "assigneeId", "status", "priority", "labels",
"project.orgUnitId", "comments.authorId")
.Fields("summary", "status", "assigneeId", "priority"));
// 2. Build one user's ability from rules: from code, from your database, from role grants.
var ability = new AbilityBuilder(subjects)
.WithVariables(new { user = new { id = currentUserId } })
.Can("read", "WorkItem", new { projectId = new Dictionary<string, object> { ["$in"] = new[] { 1, 2 } } })
.AddRange(RawRule.ParseMany(rulesJsonFromDatabase), variables: new { grant = new { projectIds = new[] { 7 } } })
.Cannot("update", "WorkItem", new { status = "Done" }, reason: "Done items are frozen", "status")
.Build();
// 3. Use it.
ability.Can("update", item); // one object
ability.Can("update", item, "status"); // one field
ability.PermittedFields("update", item); // ["summary", "assigneeId", "priority"]
ability.EnsureCan("update", item, "status"); // throws ForbiddenException("Done items are frozen")
var visible = await db.WorkItems.AccessibleBy(ability, "read").ToListAsync(); // filtered in SQL
var forBrowser = ability.ToCaslRules(); // JSON for @casl/ability
Rules are plain JSON in CASL's format, so they can live in a database table and be edited by admins:
{ "action": "update", "subject": "WorkItem",
"conditions": { "projectId": { "$in": "${grant.projectIds}" }, "assigneeId": "${user.id}" },
"fields": ["summary", "status"] }
In the browser:
import { createAbility } from 'casl-net';
import { subject } from '@casl/ability';
const ability = createAbility(rulesFromServer);
ability.can('update', subject('WorkItem', item));
Conditions
MongoDB query syntax, the same subset CASL supports, plus logical operators:
| Kind | Operators |
|---|---|
| Comparison | $eq (or a plain value), $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists |
| Text | $regex, with $options (i, m, s, x) |
| Logical | $and, $or, $nor, $not |
| Arrays | { labels: "bug" } means "labels contains bug"; also $all, $size, $elemMatch, and dot notation through collections ("comments.authorId": "u2" means "any comment by u2") |
| Paths | Dot notation through navigations ("project.orgUnitId") and array indexes ("authors.0") |
| Placeholders | "${user.id}", "${grant.projectIds}"; a whole-string placeholder can be an array or a number |
How they behave:
- Safe by default with a strict registry. Only member paths registered with
ConditionFieldsare accepted, so rule authors can't filter onPasswordHash. Unknown operators, unknown fields, unknown placeholders and values of the wrong type fail when the ability is built. They are never skipped, because skipping a "cannot" rule would widen access. - Values become SQL parameters, so EF Core caches one query plan for every user, not one per user.
- Missing related objects read as null, the way a LEFT JOIN does:
{ "project.archived": false }doesn't match an item with no project. - Ordering operators aren't allowed on text,
Guid,boolor enums, because databases and .NET order text differently.
Rule semantics
- CASL's by default: the last matching rule wins.
Can("read", "Post")followed byCannot("read", "Post", new { secret = true })hides secret posts; in the other order, the laterCanlets them through. - Or deny-overrides, with
new AbilityOptions { Semantics = RuleSemantics.DenyOverrides }: access is granted when an allow rule matches and no "cannot" rule matches, whatever order the rules come in. Choose this when rules are combined from several roles or grants, where order means nothing.ToCaslRules()then puts "cannot" rules last, so@casl/abilityreaches the same answers. manageandallmean every action and every subject, as in CASL. Both can be renamed (AnyAction,AnySubjectType).- Field-level rules follow CASL. A check without a field ignores "cannot" rules limited to fields and counts allow rules limited to fields. A field-limited "cannot" doesn't hide rows from
AccessibleBy; it only restricts those fields. Can(action, "Subject")without an instance answers "could this user do it to some instance?", for menus and buttons.- Validate rules before saving them:
AbilityBuilder.Validate(rule, subjects)checks paths, operators and fields without needing placeholder values. - Abilities are immutable. Build a new one when rules change. One ability can be shared across threads, and it caches its compiled checks and SQL filters.
Everything else CASL has
| Action aliases | new AbilityOptions { Aliases = ActionAliases.Create(new Dictionary<string, string[]> { ["modify"] = ["update", "delete"] }) } |
| Field patterns | .OnFields("author.*"), "author.**", "*.name", "street*" |
| Claim rules | Can("login"), checked with ability.Can("login") |
| Class subjects | Can<Post>("read"); ability.Can("read", typeof(Post)) |
| Tagged objects | ability.Can("read", Subject.Of("Article", dto)) |
| Custom subject detection | AbilityOptions.DetectSubjectType |
| Error messages | .Because("…"), EnsureCan(…, message: …), AbilityOptions.ForbiddenMessage; ForbiddenException carries Action, Subject, SubjectType, Field, Reason |
| Rule lookup | RulesFor, PossibleRulesFor, RelevantRuleFor, ActionsFor |
| Fields and defaults | PermittedFields (permittedFieldsOf), DefaultValues (rulesToFields) |
| Queries | Filter<T> and AccessibleBy for EF Core; ToMongoQuery for document stores; Ability.RulesToCondition for anything else |
| Compact transport | PackedRules.Pack / Unpack, compatible with CASL's packRules / unpackRules |
docs/casl-spec-port.md maps each CASL function to its .NET name.
Documentation
| Guide | What it covers |
|---|---|
| Getting started | Install, a first ability, rules from a database, one ability per request in ASP.NET Core |
| Rules | The rule format, actions and subjects, placeholders, how rules combine, validating rules before saving them |
| Conditions | Every operator, with examples, and what is refused and why |
| Subjects and fields | Strict and open registries, condition paths, field-level rules, field patterns and families, FieldMatch.Named |
| EF Core and SQL | AccessibleBy and Filter<T>, what the SQL looks like, navigations and collections, nulls, other stores |
| The browser | Sending rules to @casl/ability with the casl-net npm package, packed rules, checking fields in the UI |
| Coming from CASL | Each CASL function's .NET name, and which of CASL's tests are ported |
The changelog lists what each release changed. The API reference is in the package's XML documentation, so your editor shows it.
Repository
src/Casl.Net/ the library (package Casl.Net, namespace Casl; net8.0, net10.0)
packages/casl-net/ casl-net on npm, the browser companion
tests/Casl.Net.Tests/ unit tests, CASL's ported specs (CaslSpec/) and the database-vs-memory test (EF Core + SQLite)
tests/casl-compat/ the CASL compatibility test (Node, real @casl/ability)
dotnet test
cd tests/casl-compat && npm install && npm test # after dotnet test, which writes the fixture
Status
0.1.0, the first release. Every build checks its decisions against CASL and against a real database, but the API may still change before 1.0: minor versions (0.2, 0.3) may break it, and the changelog will say how. Planned next:
- custom operators, for example
$underfor org-unit subtrees through a closure table - ordering operators on enums
- an ASP.NET Core package for per-request abilities and authorization policies
Questions and ideas are welcome as issues; security problems go through private reporting.
Credits and licence
Apache License 2.0. CASL .NET is an independent implementation of CASL's rule format and semantics. CASL (stalniy/casl) is MIT-licensed, and its design is what this library follows. A few files are translations of CASL code (alias resolution, field patterns, rule packing, rulesToCondition), and the CASL tests are ported from CASL's specs; they are listed, with CASL's licence, in THIRD-PARTY-NOTICES.md.
| 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 was computed. 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
- No dependencies.
-
net8.0
- No dependencies.
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 |
|---|---|---|
| 0.1.0 | 112 | 10/10/2026 |