Release v2.0.0
Highlights
IProtectedRepository<T> is now a closed ACL-aware contract. The interface no longer extends IRepository<T>, so handlers that inject the protected repository can no longer accidentally call an unprotected method and bypass object-level access control. The unprotected surface is reachable only through a new explicit, audited escape hatch: UseInnerRepositoryAsync, which logs every entry with caller member, file, and line at Information level.
This is the only breaking change in the release. Everything else is additive or internal.
Breaking changes
IProtectedRepository<T> no longer extends IRepository<T>
| Before | After |
|---|---|
IProtectedRepository<T> : IRepository<T> |
IProtectedRepository<T> (standalone) |
Why: an inherited unprotected surface meant any handler holding IProtectedRepository<T> could type repo.DeleteAsync(value, ct) and bypass the ACL check with no compiler warning. The type system now enforces an explicit, audited verb for every non-ACL operation.
Migration paths:
-
If your handler only needs ACL-aware operations — no change required. The permission-aware methods (
GetAsync(id, permission, ct),CreateAsync(value, parentResourceId, permission, ct), etc.) work exactly as before. -
If your handler needs an unprotected operation (system maintenance, bulk projections, cross-cutting reads, queries that can't be expressed via ACL) — wrap the call in
UseInnerRepositoryAsync:// Before (relied on inherited unprotected method): await protectedRepo.UpdatePartialAsync(id, ops => ops.Set(d => d.Indexed, true), ct: ct); // After (explicit, audited escape hatch): await protectedRepo.UseInnerRepositoryAsync(async (inner, token) => { await inner.UpdatePartialAsync(id, ops => ops.Set(d => d.Indexed, true), cancellationToken: token); }, ct);
-
If your handler genuinely needs the unprotected repository as its primary surface — inject
IRepository<T>directly instead ofIProtectedRepository<T>. Both registrations remain available via DI.
Every UseInnerRepositoryAsync call emits a structured log event (EventIds.CosmosInnerRepositoryScopeOpenedId = 20_101, Information level) with the entity type and caller location — making ACL-bypass entry points trivially auditable.
Notable additions
UseInnerRepositoryAsync escape hatch
Two overloads on IProtectedRepository<T>:
ValueTask UseInnerRepositoryAsync(
Func<IRepository<TEntity>, CancellationToken, ValueTask> action,
CancellationToken cancellationToken = default,
[CallerMemberName] string callerMember = "",
[CallerFilePath] string callerFile = "",
[CallerLineNumber] int callerLine = 0);
ValueTask<TResult> UseInnerRepositoryAsync<TResult>(
Func<IRepository<TEntity>, CancellationToken, ValueTask<TResult>> action,
CancellationToken cancellationToken = default,
[CallerMemberName] string callerMember = "",
[CallerFilePath] string callerFile = "",
[CallerLineNumber] int callerLine = 0);The caller-info attributes are declared on the interface (not just the impl) so the compiler binds values at the actual call site through interface dispatch.
Soft-delete on the protected repository
The four-overload DeleteAsync pattern from IRepository<T> is now mirrored on IProtectedRepository<T>:
ValueTask DeleteAsync(TEntity value, Permission permission, CancellationToken ct = default);
ValueTask DeleteAsync(string id, Permission permission, CancellationToken ct = default);
ValueTask<TEntity> DeleteAsync(TEntity value, Permission permission, CancellationToken ct, bool softDelete = true);
ValueTask<TEntity> DeleteAsync(string id, Permission permission, CancellationToken ct, bool softDelete = true);softDelete = true is the default destructive path — hard-delete requires explicit opt-in.
Dependency updates
| Package | From | To |
|---|---|---|
Cirreum.Core |
3.0.4 |
4.0.0 |
Microsoft.Azure.Cosmos |
3.58.0 |
3.59.0 |
Cirreum.ServiceProvider |
1.0.14 |
1.0.15 |
Internal cleanup
- Removed
DefaultProtectedRepository.delegated.cs(197 lines) — every member existed solely to satisfy the dropped inheritance. - Removed dead
IsEmulatorConnectionStringhelper andEmulatorAccountKeyconstant fromCosmosRegistrationExtensions. - Refactored
DefaultRepository<T>.DeleteAsync(value, ct, softDelete)— collapsed an unreachable defensiveelsebranch and reordered the hard-delete branch before soft-delete for readability.
Documentation
The README now covers the protected-resources surface, soft-delete semantics, and the UseInnerRepositoryAsync escape hatch with copy-pasteable handler examples. The Quick Start example now uses primary-constructor style for consistency with later examples.
Versioning note
.gitattributes now enforces CRLF line endings via * text=auto eol=crlf, with *.sh text eol=lf as a defensive override for shell scripts. This eliminates the "LF will be replaced by CRLF" warnings from contributors using different core.autocrlf settings.