BccCode.Privacy.EntityFrameworkCore
0.1.0
dotnet add package BccCode.Privacy.EntityFrameworkCore --version 0.1.0
NuGet\Install-Package BccCode.Privacy.EntityFrameworkCore -Version 0.1.0
<PackageReference Include="BccCode.Privacy.EntityFrameworkCore" Version="0.1.0" />
<PackageVersion Include="BccCode.Privacy.EntityFrameworkCore" Version="0.1.0" />
<PackageReference Include="BccCode.Privacy.EntityFrameworkCore" />
paket add BccCode.Privacy.EntityFrameworkCore --version 0.1.0
#r "nuget: BccCode.Privacy.EntityFrameworkCore, 0.1.0"
#:package BccCode.Privacy.EntityFrameworkCore@0.1.0
#addin nuget:?package=BccCode.Privacy.EntityFrameworkCore&version=0.1.0
#tool nuget:?package=BccCode.Privacy.EntityFrameworkCore&version=0.1.0
BccCode.Privacy.EntityFrameworkCore
Purpose-bound access to personal data in EF Core and PostgreSQL. A row is returned only when a basis covers its person for the organisation and process of the request; other organisations see only the data items shared with them. Queries and saves stay ordinary EF Core; the package adds a query filter, a save guard, and read views generated by your normal migrations.
Design: docs/architecture-proposal, pages 00 and 16.
Version 0.x: the API will change. Licence: Apache 2.0. Targets .NET 10 and EF Core 10, so it
runs in applications on .NET 10 and 11.
Use
public sealed class AppDbContext(DbContextOptions<AppDbContext> options, PrivacyScope scope)
: DbContext(options), IPrivacyDbContext
{
public PrivacyScope Scope { get; } = scope; // one per request; fixed at the first command
public PrivacyRules Rules { get; } = new PrivacyRules()
.Group<Membership>("members", m => m.PersonId, m => m.OrganisationId, m => m.Id, activeWhile: m => m.Ended == null)
.Process(1, new("membership.register")).Covers<PersonProfile>().Done();
protected override void OnModelCreating(ModelBuilder b)
{
b.UsePrivacy();
b.Entity<PersonProfile>(e =>
{
e.Personal(this, subject: x => x.PersonId, organisation: x => x.OrganisationId);
e.Item(Items.Name, sortable: true, x => x.FirstName, x => x.LastName); // shareable, by item
});
}
}
// options: .UseNpgsql(...).UsePrivacy()
Declare item keys once (public static readonly ItemKey Name = new("name");). A special-category
table adds .EnforcedInDatabase(): PostgreSQL row-level security, so even raw SQL sees only the
acting organisation's rows. Such a table cannot also be shared.
Supported operations
Each behaviour below is covered by a test in BccCode.Privacy.EntityFrameworkCore.Tests.
| Operation | Behaviour |
|---|---|
LINQ reads: Where, OrderBy, Skip/Take, Count, joins, projections |
Own rows, plus shared rows with only the shared items; ordering and paging cover both |
| A filter or sort on an item not shared | Reveals nothing: the value is null for that row |
| A masked item projected into a DTO | Null |
Include over a navigation between personal entities |
Own related rows; another organisation's related rows only if that table is shared too |
Tracked and AsNoTracking reads of one row in one context |
Agree; identity resolution as usual |
| Edit an own row read through a view | Saved to the table |
| Edit a row of another organisation | Refused |
| Insert a row without a basis for its person | Refused, with the fix |
Change a row's person or organisation, or write usable |
Refused |
SaveChangesAsync |
Guards run; derived state follows in the same transaction |
SaveChanges() (synchronous) |
Refused: use SaveChangesAsync |
ExecuteUpdate / ExecuteDelete |
Limited to own usable rows. They do not run the save guard: do not use them on identifiers, usable, bases or group facts (an analyser and a runtime guard follow in LIB-6) |
IgnoreQueryFilters on a shareable table |
Refused, with the fix |
IgnoreQueryFilters on other tables, raw SQL |
Not guarded yet (LIB-6); a table with EnforcedInDatabase() is still limited by PostgreSQL |
| Changing the scope after the context's first command | Refused: one context, one organisation and process |
Migrate() at every start |
Safe: the package's steps run only in the application's own migrations |
| A migration | Views the package generated are listed in privacy.read_view; only those are touched, and only when their definition changed. Added columns: replaced in place (oid, grants and dependent views kept). Otherwise dropped and created; if another object depends on the view, the migration stops and names it |
| An upgrade or rollback while others read | No errors: the migration locks the views first, so reads wait for it |
Starts and ends of bases: tested, and planned
| Behaviour | In this package | Planned |
|---|---|---|
| A basis counts only from its start to its end | Tested: one rule, an injectable clock | — |
| A start or end takes effect at its moment | Tested when PrivacyBoundaries.ApplyDueAsync is called; nothing calls it on its own |
LIB-2: a Wolverine message scheduled per moment, normally seconds; LIB-3: nightly rebuild as backstop |
| The event for a moment is not lost | Tested: stored in the outbox (privacy.basis_event) in the same transaction; PublishAsync hands it out at least once, with a stable id |
LIB-2: published through Wolverine; receivers deduplicate on the id |
| Two workers apply a moment once | Tested: an atomic claim | — |
| A changed, withdrawn or deleted basis leaves no active moment | Tested | LIB-3: the same for static group members |
| Maximum staleness when the scheduler is down | Not built | LIB-3, LIB-6: 5-minute limit, alert, shared reads stop past it |
| Bulk writes keep the derived state | Not built: the save guard covers SaveChanges only |
LIB-6: guarded bulk API, analyser, runtime guard; trusted-code assumption (page 23, F3) |
Develop
Needs the .NET SDK from global.json and Docker (the tests start PostgreSQL 18 through Aspire).
dotnet test --project libraries/dotnet/BccCode.Privacy.EntityFrameworkCore.Tests
The sample application and its migrations are in the test project (Sample/). After changing the
sample model, add a migration from libraries/dotnet:
dotnet tool restore
dotnet ef migrations add <Name> --project BccCode.Privacy.EntityFrameworkCore.Tests --output-dir Sample/Migrations
Published by .github/workflows/libraries-dotnet-publish.yml (tag prefix efcore-dotnet-v). The
version is major.minor.n, where n counts commits under libraries/dotnet since the last change of
minor version. A commit under libraries/dotnet whose subject starts with feat: or feat(<scope>):
(paulhatch/semantic-version's default) starts the next minor version, as does a tag
efcore-dotnet-v<major>.<minor>.0. The 0.1 line was started by a feat(efcore): commit.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.14)
- Microsoft.Extensions.Caching.Memory (>= 11.0.0)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.3)
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 | 0 | 11/14/2026 |