MongoDbRepositories 10.2.0.1
MongoDbRepositories
Generic, tenant-scoped MongoDB repository for entities deriving from EntityBase
(SeliseBlocks.Genesis.Framework.PDS.Entity). Intended to be shared across projects that need
plain EntityBase persistence without any domain-specific entity handling.
Row-level and column-level security are enforced inside the query — a server-side $and on
the filter and a server-side projection — so a caller cannot widen their own access by changing
what they ask for. Type-level create permission and column security are opt-in; tenant isolation
and audit stamping are not.
The tenant always comes from the security context. No method takes a tenant id, so no call site can reach another tenant's data by passing a different one.
Multi-targets net8.0 and net10.0, so services that have not yet migrated to .NET 10 can
consume it.
Integrating this package into an application? Start with INTEGRATION.md — the full reference: every option, every collection schema with example documents, a worked end-to-end example and a troubleshooting table. This README explains why each rule is the way it is; the guide explains how to wire it up.
The two targets differ only in dependency pins, not behaviour. The SeliseBlocks framework
packages publish a net10.0-only 10.x line, so each target framework pins its own:
| Dependency | net8.0 | net10.0 |
|---|---|---|
SeliseBlocks.Genesis.Framework |
8.0.1.1 | 10.0.0.2 |
SeliseBlocks.Genesis.Framework.PDS.Entity |
8.0.1.9 | 10.0.0.6 |
Microsoft.Extensions.* |
8.0.x | 10.0.11 |
FinMongoDbDataContext |
10.1.1.1 | 10.1.1.1 |
MongoDB.Driver / MongoDB.Bson |
3.11.0 | 3.11.0 |
EntityBase carries the same 17 properties in both the 8.x and 10.x PDS.Entity lines, and
SecurityContext exposes the same members, so no source is conditionally compiled and there are
no #if directives.
One pin is not obvious: on net8.0, Microsoft.Extensions.Logging.Abstractions is 8.0.3, not
8.0.2. SeliseBlocks.Genesis.Framework 8.0.1.1 pulls Azure.Identity -> Azure.Core ->
System.ClientModel, which requires >= 8.0.3; pinning 8.0.2 fails restore with NU1605.
Relationship to FinMongoDbRepositories
This package is the hierarchy-free counterpart of FinMongoDbRepositories. It keeps the same
method surface and the same tenant-per-database access through FinMongoDbDataContext, and drops
everything that is specific to the BURO domain:
| FinMongoDbRepositories | MongoDbRepositories |
|---|---|
applyHierarchy on every read, filtering Hid / Path / Depth via IHierarchy |
applyRowLevelSecurity, filtering RolesAllowedTo* / IdsAllowedTo* on EntityBase |
IFiscalYearInfo / BuroFiscalYear resolution, Redis-cached |
removed |
BuroEntityBase.BusinessDate resolution with fallbacks |
removed |
IEmployeeActionContext and delegation-source lookup |
removed |
ChangePropagator notifications on update |
removed |
RowLevelPermission, SecurityClaims, SeliseBlocks.Entities dependencies |
removed; the permission filter and entity stamping are implemented here |
applyHierarchy branches on typeof(IHierarchy).IsAssignableFrom(typeof(T)) |
T stays unconstrained; row-level permission applies when T derives from EntityBase, and plain documents pass through untouched |
Registration
AddMongoDbRepositoryServices registers the repository, the permission filter, the entity
stamper and the collection-name resolver, and registers FinMongoDbDataContext if the host has
not already done so. The host is responsible for the Genesis security context
(ISecurityContextProvider) and tenant registration (ITenants).
services.AddMongoDbRepositoryServices();
Every collaborator is registered with TryAdd, so a host can replace any of them by registering
its own implementation first.
Options
services.AddMongoDbRepositoryServices(options =>
{
options.CollectionNameResolver = type => $"{type.Name}s";
options.ApplyRowLevelSecurityByDefault = true;
options.StampAuditFieldsOnUpdate = true;
options.SeedCreatorPermissionsOnInsert = true;
options.FailOpenOnSecurityContextFailure = false;
options.CreatePermission = CreatePermissionMode.Off; // Off | Shadow | Enforce
options.RefusePermissionFieldWrites = false;
options.SettingsCacheTtl = TimeSpan.FromSeconds(60);
options.ExcludeSoftDeletedByDefault = false;
options.SingularCollectionTypes = new HashSet<Type>();
options.SystemRoles = new HashSet<string>(
["admin", "tenantadmin", "tenantmanager"],
StringComparer.OrdinalIgnoreCase);
});
ExcludeSoftDeletedByDefault makes every read also exclude IsMarkedToDelete documents. Off
by default: a host already filtering them in its own query code would otherwise get the predicate
twice, and one that was not would silently start. It applies to reads only — an update or delete
naming a soft-deleted document still finds it, which is what makes RestoreAsync work.
SingularCollectionTypes names the types whose collection has no plural suffix. It is consulted
before CollectionNameResolver, so one unusual collection does not mean reimplementing naming for
every other type.
The repository surface
Beyond the usual find / insert / update / delete, all of it carrying the row filter and the field mask:
| Group | Members |
|---|---|
| Composable query | Query<T>() → Where, OrderBy, Skip, Take, IncludeDeleted, then ToListAsync / FirstOrDefaultAsync / CountAsync / AnyAsync / SelectAsync / DistinctAsync |
| By collection name | FindBsonAsync, FindOneBsonAsync, FindBsonPagedAsync, AggregateBsonAsync |
| Bulk | BulkWriteAsync<T> → BulkWriteOutcome |
| Upsert / save | UpsertAsync, UpsertManyAsync, SaveAsync, SaveManyAsync |
| Soft delete | SoftDeleteAsync, RestoreAsync |
| Projection | FindPagedWithProjectionAsync, FindWithFieldsAsync, FindOneWithFieldsAsync |
| Aggregation | Aggregate<T>() (fluent), AggregateAsync<T,TResult>(pipeline) |
| Other | ExistsAllAsync |
Three of these needed a decision rather than a signature:
Query<T>returns a builder, notIQueryable<T>. AnIQueryableis a handle the caller keeps composing after the repository has stopped looking, and the ways out of that — handing it to another library, calling a provider method the repository does not know — all run queries the filter never saw. Every terminal goes back through the repository's own read funnel instead, and a test asserts the interface exposes noIQueryableat all.- Reads addressed by collection name resolve the name back to an entity type, because both
halves of the security machinery are keyed on a
Type: the mask needs a schema, and the row filter asked ofBsonDocumentconcludes there are no permission arrays and does nothing. A name matching no discovered type is refused while field security is enforcing. - Upsert is safe here and still refused as a raw option.
UpsertAsynctakes the whole entity, so the create check runs and the document is stamped before it can be inserted;UpdateOptions { IsUpsert = true }stays refused because the driver builds the new document from an update definition and neither can run.
Entity types versus plain documents
T is not constrained to EntityBase. The repository stores plain documents — reference data,
lookup tables, value documents — alongside entities, and those have no permission arrays.
The rule is runtime, not compile time:
Tderives fromEntityBase-> row-level permission is applied, and inserts are stamped and seeded.Tdoes not -> filters pass through unchanged, nothing is stamped, and no audit fields are appended.
EntityPermissionFilter.IsPermissionedEntity(Type) is the single predicate behind that decision.
Because T is unconstrained, the permission filter and the audit stamper build their definitions
from field names rather than typed lambdas; the driver resolves those to element names when the
filter or update is rendered.
Why there is no row-level Insert permission
EntityPermission covers Read, Write, Update and Delete, but not Insert, and that is
deliberate.
A row-level filter restricts a query against the permission arrays of documents that already exist. An insert has no existing document. The only arrays available are the ones the caller just populated on the object being inserted, so filtering on them would be self-authorizing: the caller sets the very value that would grant the permission.
Insert is therefore gated in two other places:
- Field level —
FieldOperation.Insertcontrols which fields a caller may populate. - Type level — whether a caller may create documents of a type at all is a policy question, not a property of a document that does not yet exist.
Write maps to RolesAllowedToWrite / IdsAllowedToWrite. Those arrays exist on EntityBase but
were never consulted by FinMongoDbRepositories; they are now available as a general write check.
Row-level security
Where a method takes applyRowLevelSecurity, the supplied filter is ANDed with a permission
filter built from the current security context:
- reads, counts, projections and aggregations match
RolesAllowedToRead/IdsAllowedToRead - updates match
RolesAllowedToUpdate/IdsAllowedToUpdate - deletes match
RolesAllowedToDelete/IdsAllowedToDelete
Callers holding one of the configured SystemRoles are not filtered. Passing null (the default)
uses ApplyRowLevelSecurityByDefault; pass false explicitly for calls that are legitimately
unscoped, such as a background job reading across users.
Two behaviours differ from the older repositories on purpose:
- Writes are filtered.
FinMongoDbRepositoriesapplies its filter to reads only, so an update or delete reaches any document the caller's own filter matches. Here the update and delete permission sets are enforced as well. - The filter fails closed.
FinMongoDbRepositorieswraps filter construction incatch (Exception) { }and returns the unfiltered query, so an unreadable security context silently widens the query to the whole collection. Here that throwsInvalidOperationException. SetFailOpenOnSecurityContextFailureto restore the old behaviour.
Insert stamping
InsertOne/InsertMany and their async forms stamp each entity before writing:
ItemIdwhen empty,TenantId,CreatedBy,LastUpdatedByCreateDateandLastUpdateDatewhen stillDateTime.MinValueLanguagewhen empty, from the security context- the six
RolesAllowedTo*/IdsAllowedTo*arrays for Read, Update and Delete, resolved in this order:Caller-supplied arrays win. Whatever the caller populated is kept as is.
Settings defaults fill the gaps. For each array the caller left empty, the tenant's
EntityDefaultPermissionSettingssrow for that entity fills it, withOWNER/OWNERROLEresolved by the framework handler.Creator seeding is the last resort, and all-or-nothing. The creating user id is placed in
IdsAllowedToRead/IdsAllowedToUpdate/IdsAllowedToDeleteonly when all six role/id arrays are still empty after step 2, andSeedCreatorPermissionsOnInsertis on. If step 1 or step 2 populated any one of the six, the creator is not seeded: a populated array means access has been taken ownership of, and an empty sibling is a deliberate "admin only".The question is "does anything grant access", not "was there a settings row". A settings row that carries only
RolesAllowedToWrite/IdsAllowedToWrite- which is exactly how type-level create permission is granted, below - configures no read, update or delete access at all, so it does not suppress seeding. Keying off the row's mere existence wrote documents with six empty arrays that nobody, including the creator, could reach.
Correction. Earlier versions of this README said the creator id was added "when the caller left them empty". That was never true. The old code appended the creator id unconditionally, to arrays the caller had already populated, which widened access in ways the caller never asked for: it decided who could read, update and delete the document. Step 3 above is the actual rule, and it is narrower on purpose.
The seeding matters: with row-level read filtering on and no permissions set, a document would
otherwise be invisible to the user who just wrote it. Hosts that resolve default permissions from
their own configuration should turn SeedCreatorPermissionsOnInsert off and set the arrays
themselves.
Updates append LastUpdateDate and LastUpdatedBy to the caller's update definition unless
StampAuditFieldsOnUpdate is false.
UpdateOneAsync(expression, entity) writes only the properties the caller actually populated:
- a property still at its declared default is skipped — null for a reference type, and
0,falseorDateTime.MinValuefor a value type — so a partially populated entity does not blank or zero the fields it left unset. A nullable value type is exempt, because an explicit0on adecimal?is distinguishable from an unset one. Use theUpdateDefinition<T>or dictionary overload to write a default deliberately. ItemId,TenantId,CreateDate,CreatedBy,LastUpdateDate,LastUpdatedByand all eightRolesAllowedTo*/IdsAllowedTo*arrays are never written from an entity. Identity and authorship are fixed at insert, the audit fields are stamped separately, and permissions should change deliberately rather than as a side effect of saving an entity.
That matters more than it looks: without it, saving an entity that was built in memory rather than loaded from Mongo sets every permission array to null, and with read filtering on the document becomes invisible to everyone, including an admin-less service account.
Type-level create permission
Whether a caller may create documents of an entity type is a type-level question, answered from
the tenant's EntityDefaultPermissionSettingss document for that entity.
Which fields carry it. That document inherits EntityBase, so it has RolesAllowedToWrite and
IdsAllowedToWrite. For this purpose those two fields are repurposed to mean who may create
documents of this entity: a caller is allowed if they hold any listed role (compared
case-insensitively) or their user id is listed (compared exactly). Null and empty arrays both grant
nobody. Callers holding a SystemRoles role bypass the check before the settings row is read.
OWNER and OWNERROLE are a configuration error here, not an allow. In a row-level array
those tokens mean "the document's creator". In a create check there is no document yet, so they
would resolve to "is the caller the caller", which is always true, silently granting everyone.
They are therefore reported as a configuration error and never grant.
Literal user ids are matched. This differs from the framework's GetInterprettedIds, which
keeps only OWNER; a literal id listed in IdsAllowedToWrite allows that user to create.
Rollout. CreatePermission has three modes, default Off:
| Mode | Behaviour |
|---|---|
Off |
Nothing is checked and no settings are read. |
Shadow |
Evaluates and logs what would be refused, but allows the insert. |
Enforce |
Refuses the insert. |
Run Shadow first, read the log for refusals you did not intend, correct the settings, then move
to Enforce. The check runs on all four insert entry points of this interface - InsertOne,
InsertOneAsync, InsertMany, InsertManyAsync - keyed on the runtime type, once per
distinct type for InsertMany, after the empty-sequence return and before any write. Settings are
cached per tenant for SettingsCacheTtl (default 60 seconds), including the absence of a row.
Two things it does not cover, by construction:
GetCollection<T>()andGetUnrestrictedCollection<T>(reason)bypass it, along with row-level and field-level security. Anything written through the raw driver collection they hand back is unchecked and unstamped.GetCollection<T>()is refused outright while field security is enforcing;GetUnrestrictedCollectionis the deliberate, logged escape hatch.- Upserts cannot create.
UpdateOneAsyncwithnew UpdateOptions { IsUpsert = true }can insert a document, and that insert runs neither this check nor insert stamping - the driver builds the new document from the update alone, so it would carry noTenantId, noItemId, noCreatedByand null permission arrays. WhileCreatePermissionisEnforcesuch a call is refused withInvalidOperationException; while it isShadowit is logged and allowed; while it isOffnothing changes. Unsecured types are exempt, so the settings row itself stays upsertable. Insert explicitly when you mean to create.
The guard must be present. Constructing MongoDbRepository with no ICreatePermissionGuard
while CreatePermission is not Off throws at construction rather than leaving the check
silently inert. Use AddMongoDbRepositoryServices, or pass a guard to the constructor yourself.
FailOpenOnSecurityContextFailure does not apply to this check. Enforce always fails closed:
it is explicit opt-in, so there is no legacy behaviour to restore. A missing settings row, a
configuration error and an unreadable security context all refuse. Check that every entity type
you insert has a settings row before leaving Shadow.
The per-document pair is reserved and inert. EntityBase.RolesAllowedToWrite /
IdsAllowedToWrite on an ordinary document are still not consulted by anything. They are not
connected to the identically-named fields on the settings document, even though the names match.
At insert they are never templated from settings; on a secured type they are only normalised to
empty arrays so the stored document is uniform. On an unsecured type nothing is normalised:
that path returns before permission resolution, which is the point of the exemption, so an
unsecured row persists with whatever the caller left on it, null included.
Unsecured types
UnsecuredTypes (a set of Type) and UnsecuredTypeNames (a set of simple type names) list entity
types whose own rows are not access-controlled: no row filter on reads, updates or deletes, no
create-permission check and no permission-array resolution at insert. Audit stamping still applies.
This is needed because the settings row that defines an entity's permissions is itself an
EntityBase; row-filtering it would need the permissions it defines.
These are the only options on this branch whose defaults ship enabled: the four field-policy types
of this package in UnsecuredTypes, and EntityDefaultPermissionSettings in UnsecuredTypeNames
(that type lives in a net6.0-only package this one cannot reference, hence a second set).
- Name matching is by simple name. An unrelated host type with the same simple name is also exempted. That is a deliberate trade-off for not referencing the type; pick names no host type uses.
- Exact type only. A subclass of an unsecured type is not exempt.
- Add to the sets, do not replace them. Both properties are settable. Assigning a new set drops
the defaults, including the settings exclusion, and the settings row becomes row-filtered by its
own arrays. Use
options.UnsecuredTypes.Add(...). RefusePermissionFieldWrites(below) does not apply to unsecured types. On such a type the permission arrays are configuration, not that row's own security: create permission is granted by writingRolesAllowedToWrite/IdsAllowedToWriteon the settings row, and refusing those writes would leave an operator able to create a settings row but never able to change who may create afterwards.- Insert stamping skips array normalisation, so an unsecured row persists with whatever the
caller left on the eight arrays,
nullincluded. - The exemption is keyed on the type the call site knows. Row filtering, the permission-write
refusal and the upsert refusal are generic in
Tand test the declared type; insert stamping and the create check test the entity's runtime type. CallingUpdateOne<EntityBase>against a collection of an unsecured type therefore does not pick the exemption up. Call these with the concrete type.
Refusing permission-field writes
With RefusePermissionFieldWrites = true, the dictionary and UpdateDefinition update overloads
throw InvalidOperationException naming the field if they write any of the eight
RolesAllowedTo* / IdsAllowedTo* arrays of a permissioned entity. Unsecured types are exempt,
so create permission stays administrable once this is on. Pipeline updates cannot be inspected and
are refused outright. Field names are resolved through the driver's class map, so a
host naming convention (for example camelCase) cannot make the check fail open. Default false, so
hosts that grant permissions through the repository keep working; turn it on once nothing
legitimate does.
Designing permission arrays
Roles are for classes of principals; ids are for individuals. Owned data carries
RolesAllowedTo* = [] and grants by id. The row filter is an $or across the role and id
arrays, so a broad role such as appuser in RolesAllowedToRead grants every authenticated user
and makes the id leg decorative.
Stripping appuser / anonymous from the filter is deliberately not done. An entity meant to
be readable by all authenticated users carries that role on purpose, and stripping it would make
such an entity readable by nobody. The rule is applied when permissions are written, not by
second-guessing them when they are read.
Other differences worth knowing
- Mutating methods return the affected document count instead of
void/Task. DeleteMany<T>runs synchronously. TheFinMongoDbRepositoriesversion callsDeleteManyAsyncwithout awaiting it, so failures are swallowed and the caller cannot tell whether the delete happened.UpdateOneAsync/UpdateManyAsyncwith anIDictionarybuild a single$setdocument. TheFinMongoDbRepositoriesversion adds a duplicate$setkey per field into aBsonDocument(allowDuplicateNames: true), which Mongo rejects once more than one field is passed.- Counts return
longrather than being cast down toint. GetDistinctValueByFieldAsyncis generic in the field type instead of being fixed toBsonDocument.- Async methods accept a
CancellationToken. FindPagedAsyncreturns a page and its total count from one call.GetCollection<T>()exposes the tenant collection for operations this interface does not wrap. Nothing issued through it is filtered, so it is refused while field security is enforcing; see below.
Field-level security
Configuration is authored as one document per entity through
IFieldPolicyAdmin.ApplyEntityPolicyAsync, which validates the whole document, expands it into
the three policy collections and bumps the version once; ExportEntityPolicyAsync reads it back.
The storage stays normalised because that is what keeps policy size independent of field count -
one grant row covers a whole classification, and a field added in code inherits its entity's
default rather than needing a row of its own. The sections below describe what that document
expands into.
Off by default, and fail closed once on: with EnableFieldLevelSecurity = true a field no
rule mentions is denied, so a host that wants the permissive posture states it with one global
rule carrying Enabled = false and enables entities from there.
EnableFieldLevelSecurity = false means no policy is read, no projection is built
and behaviour is byte-for-byte what it was before.
Field policy lives in the tenant database, not in appsettings, so access can be granted and
revoked without a redeploy. Three collections carry it:
| Collection | Holds |
|---|---|
FieldClassifications |
Global, entity and field rules in one collection: what each field is, and which scopes require configuration |
FieldClassificationFeatures |
classification -> feature + operation |
The indirection is the point. Policy grants a classification to a feature, never a field to a role, so its size is independent of how many roles and users exist:
200 fields -> ~10 classifications = 200 rows
500 roles -> features, already granted and revoked by the host
Features are read from the cache the host already populates, so a role gaining or losing a feature
takes effect through machinery that already exists rather than a second system to keep in sync.
Point FeatureSource at it:
options.FeatureSource.KeyPattern = "{sessionId}-{userId}-FeaturePermissions";
options.FeatureSource.FeatureIdProperty = "FeatureId";
Enablement is separate from the master switch
EnableFieldLevelSecurity decides whether the subsystem runs at all. Whether a given field
requires configuration is a per-scope setting in the database, resolved field -> entity -> global:
Effective Enabled |
Behaviour |
|---|---|
true |
Readable only if classified and that classification is granted to a feature the caller holds. Unclassified means denied |
false |
Always accessible |
A null inherits from the enclosing scope. That is what allows a graduated rollout: switch the subsystem on globally while it is inert, then lock down one entity, or one field, at a time.
Rolling it out
- Turn the subsystem on in
Shadowmode and leave it. It computes what would be denied and logs it under[field-security:shadow], throttled, changing no query and no result. - Classify fields and grant classifications to features until the shadow log is quiet.
- Switch to
Enforce.
Shadow and enforce resolve identical masks, so step 1 genuinely predicts step 3.
What enforcement does
- Reads apply a Mongo projection, so denied fields never leave the server. A projection the
caller supplies is merged with that one rather than replacing it: the driver's
Projectreplaces whatever projection is already on a query, so the two have to be combined before the query is sent. Including a parent of a denied leaf drops the parent, since a subdocument carries its leaves out with it. - A computed projection —
Project(e => new { ... })and anything else whose keys name its output rather than the document's — has nothing to merge into, so what it reads is verified instead and it is refused when that includes a denied field. A projection reaching the whole document through$$ROOTis refused outright. - Updates and inserts are checked against the write mask, and refused or stripped per
WriteViolation. An insert is always refused rather than stripped: stripping would persist a document the caller never asked for. The insert check walks nested paths, because the schema indexes leaves —HomeAddress.Streetis a field andHomeAddressis not — and a value inside a collection counts as populated. - A write also requires read access to the same field. A field redacted on the way out would otherwise be written back over the stored value as a type default.
- Distinct and aggregation over a denied field are refused, since a projection cannot express a restriction on a field that is the subject of the query.
The escape hatch
GetCollection<T>() returns a collection that bypasses both row-level and field-level security.
While enforcing, it throws. While shadowing, it works and logs a warning naming the call site, so
the code that enforcement will break can be found first.
Two sanctioned alternatives:
Aggregate<T>()returns a pipeline that already has the row filter and the field projection as its first stages. Later stages cannot reach documents or fields the caller is not entitled to.GetUnrestrictedCollection<T>(reason)always works and logs the reason at warning level, for migrations and bulk maintenance that genuinely need raw access.
UnrestrictedCollectionAccess = Allow restores the old behaviour for hosts that have audited their
own call sites.
Known limits
$lookupdoes not carry the joined collection's policy.- Synchronous reads resolve the mask without awaiting. The warm path completes synchronously; only the first call per caller per feature-cache lifetime blocks. Blocking is chosen over skipping because skipping would return unfiltered data.
- Policy changes propagate to other instances within
PolicyVersionPollInterval. Feature revocation propagates at whatever rate the host invalidates its own feature cache, which is the same window the host's endpoint authorization already has.
Release notes
12.2.0.0
A larger repository surface, so a host migrating from a richer interface does not have to give up capabilities to gain row and column security. Purely additive.
Query<T>— a composable builder, deliberately notIQueryable<T>.- Reads addressed by collection name:
FindBsonAsync,FindOneBsonAsync,FindBsonPagedAsync,AggregateBsonAsync, secured by resolving the name back to its entity type through the newICollectionNameResolver.ResolveEntityType. BulkWriteAsync— every model rewritten before it is sent: inserts create-checked and stamped, updates and replaces row-filtered and audit-stamped, deletes row-filtered. An unrecognised model type is refused rather than passed through. Upserts inside models are refused.UpsertAsync/UpsertManyAsync/SaveAsync/SaveManyAsync,SoftDeleteAsync/RestoreAsync,AggregateAsyncover a raw pipeline,FindPagedWithProjectionAsync,FindWithFieldsAsync,FindOneWithFieldsAsync,ExistsAllAsync.- New options:
ExcludeSoftDeletedByDefault,SingularCollectionTypes. IEntityPermissionFiltergained an overload taking the entity type explicitly, so a filter can be built asBsonDocumentwhile deciding from the stored type.
SaveManyAsync is named apart from SaveAsync rather than overloaded: as an overload,
SaveAsync(list) binds to SaveAsync<List<T>> just as happily and would try to upsert the list
itself as one document.
12.1.0.0
AddEntityColumnPermissionPolicySource()— an opt-inIFieldPolicyStorethat serves field policy straight from legacy per-column permission documents (EntityColumnPermisssions) by projecting them into a snapshot in memory. A host that already maintains one hand-editable document per entity keeps exactly that and stores no second, normalised copy, so the two cannot drift. Classifications are derived from each column's token set at load time and never persisted.- While that source is active,
IFieldPolicyAdminwrites do not apply — they target collections it never reads.
12.0.0.0
Supersedes 11.0.0.0, which was published from an unfinished build and should not be used.
Breaking:
FieldSecurityScopeandFieldClassificationMapare merged intoFieldClassification. TheFieldSecurityScopesandFieldClassificationMapscollections become oneFieldClassificationscollection.FieldSecurityScope.Sealedis removed, along with thesealedScopeparameter ofSetScopeAsync. It was stored and never enforced.FieldPolicySnapshottakes one rule collection instead of two.- Field-level security fails closed. With
EnableFieldLevelSecurityon, a field no rule mentions is denied. A global rule carryingEnabled = falserestores the permissive baseline.
Added:
IFieldPolicyAdmin.ApplyEntityPolicyAsyncandExportEntityPolicyAsync: one document per entity, validated as a whole, applied in a single version bump.
11.0.0.0
A major bump. Several of these changes are breaking, and none is offered behind a compatibility switch: another option to carry forever costs more than it saves, and a major version is honest about the change where 10.2.0.0 would have read as compatible.
- Breaking:
IEntityStampergained a member.StampForInsertAsyncis new, so any external implementer of that interface no longer compiles and must add it. Source- and binary-breaking. Hosts that only use the registered default are unaffected. - Breaking: insert stamping behaviour changes on upgrade, with no switch to restore the old
behaviour. The permission arrays are now resolved in a defined order: caller-supplied values
win, the tenant's
EntityDefaultPermissionSettingssrow fills the arrays left empty, and the creator is seeded only when nothing grants access to anyone after that. Previously the creator id was appended unconditionally, including to arrays the caller had already populated. Review any entity whose permissions you set at the call site, and any tenant that already has settings rows, before upgrading. - Breaking when opted into: an upsert is refused while
CreatePermissionisEnforce.UpdateOneAsyncwithIsUpsert = truecould insert a document without the create check or insert stamping.Offhosts are unaffected;Shadowlogs instead of refusing. - Breaking when opted into: constructing
MongoDbRepositorywithout anICreatePermissionGuardwhileCreatePermissionis notOffnow throws, instead of leaving the check silently inert. - Added type-level create permission (
CreatePermission, defaultOff) andSettingsCacheTtl. - Added
RefusePermissionFieldWrites(defaultfalse). Unsecured types are exempt from it, so create permission stays administrable. - Added
UnsecuredTypes/UnsecuredTypeNames, enabled by default, excluding the field-policy types and the entity settings row from row-level filtering.
Packing
PacknPush.bat / PacknPush.sh read the feed key from the NUGET_API_KEY environment variable
rather than carrying it in the file. Publishing is the package owner's call; this repository's
agent instructions forbid pushing on the user's behalf.
No packages depend on MongoDbRepositories.
.NET 10.0
- FinMongoDbDataContext (>= 10.1.1.1)
- SeliseBlocks.Genesis.Framework.PDS.Entity (>= 10.0.0.6)
- SeliseBlocks.Genesis.Framework (>= 10.0.0.2)
- MongoDB.Driver (>= 3.11.0)
- MongoDB.Bson (>= 3.11.0)
- Microsoft.Extensions.Options (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- SeliseBlocks.Genesis.Framework.RowLevelSecurity.Contracts (>= 10.0.0.2)
.NET 8.0
- SeliseBlocks.Genesis.Framework.RowLevelSecurity.Contracts (>= 8.0.1.1)
- SeliseBlocks.Genesis.Framework.PDS.Entity (>= 8.0.1.9)
- SeliseBlocks.Genesis.Framework (>= 8.0.1.1)
- MongoDB.Driver (>= 3.11.0)
- MongoDB.Bson (>= 3.11.0)
- Microsoft.Extensions.Options (>= 8.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3)
- FinMongoDbDataContext (>= 10.1.1.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)