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
                    
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="BccCode.Privacy.EntityFrameworkCore" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BccCode.Privacy.EntityFrameworkCore" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="BccCode.Privacy.EntityFrameworkCore" />
                    
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 BccCode.Privacy.EntityFrameworkCore --version 0.1.0
                    
#r "nuget: BccCode.Privacy.EntityFrameworkCore, 0.1.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 BccCode.Privacy.EntityFrameworkCore@0.1.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=BccCode.Privacy.EntityFrameworkCore&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=BccCode.Privacy.EntityFrameworkCore&version=0.1.0
                    
Install as a Cake Tool

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 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. 
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
0.1.0 0 11/14/2026