Skip to main content

Kandra.Persistence

Kandra.Persistence.Accounting.AccountBalanceUpdater

Applies the source=credit/destination=debit convention: for each line, increments (or, when reverting, decrements) the destination account's DebitTurnover and the source account's CreditTurnover by Amount. The steady-state path uses atomic ExecuteUpdateAsync (translated to SQL UPDATE ... SET X = X + @delta), never a read-then-write round trip, so concurrent postings to the same account+subconto combination can't lose an update without needing explicit locks. Like IPostingRepository, this never calls SaveChangesAsync itself - that's the calling use case's scope, the same DbContext/ transaction it's already saving its own document changes through.

Constructors

ConstructorDescription
AccountBalanceUpdater(DbContext)Applies the source=credit/destination=debit convention: for each line, increments (or, when reverting, decrements) the destination account's DebitTurnover and the source account's CreditTurnover by Amount. The steady-state path uses atomic ExecuteUpdateAsync (translated to SQL UPDATE ... SET X = X + @delta), never a read-then-write round trip, so concurrent postings to the same account+subconto combination can't lose an update without needing explicit locks. Like IPostingRepository, this never calls SaveChangesAsync itself - that's the calling use case's scope, the same DbContext/ transaction it's already saving its own document changes through.

Kandra.Persistence.Accounting.PostingLineContentComparer

Compares exactly the fields that make a line "the same posting" - explicitly excluding Id, BatchId, IsActive, CreatedAtUtc, DeprecatedAtUtc, and the Batch navigation, none of which are part of a line's business content. Overriding Equals/GetHashCode on PostingTransactionRecord itself isn't safe - it's an EF-tracked entity, and EF Core's change tracker, DbSet operations, and LINQ translation all assume the default reference-based identity for tracked entities. This comparer is a separate object used only for in-memory List/HashSet comparison, and never touches how EF itself identifies or tracks the entity.

Kandra.Persistence.Accounting.SubcontoQueryExpressions

Methods

MethodDescription
BuildSlotPredicate<T0>(IReadOnlyList<ValueTuple<String, String>>, IReadOnlyDictionary<Int32, IReadOnlyList<AccountCode>>, IReadOnlyList<Guid>)columns: one (accountCodeProperty, slotPropertyPrefix) pair per place a match can occur - one pair for AccountBalance, two pairs (source, destination) for PostingTransactionRecord. entityIds: OR'd together (an "any of these" filter for one subconto type).

Kandra.Persistence.Blobs.BlobGarbageCollectorService

Background sweep (docs/blob-storage-architecture.md §8): tombstones expired-but-never-promoted ephemeral references, purges tombstoned references past their grace period, then deletes blobs left with zero remaining references. Runs its own unit of work each cycle - nothing else owns a transaction around it.

Constructors

ConstructorDescription
BlobGarbageCollectorService(IServiceScopeFactory, IOptions<BlobStorageOptions>)Background sweep (docs/blob-storage-architecture.md §8): tombstones expired-but-never-promoted ephemeral references, purges tombstoned references past their grace period, then deletes blobs left with zero remaining references. Runs its own unit of work each cycle - nothing else owns a transaction around it.

Kandra.Persistence.Blobs.LocalDiskBlobContentStore

Local-disk IBlobContentStore. Keys are sharded (AD-12): the first 2 hex chars of the key become a subfolder, spreading files across 256 buckets instead of one flat directory.

Constructors

ConstructorDescription
LocalDiskBlobContentStore(IOptions<BlobStorageOptions>)Local-disk IBlobContentStore. Keys are sharded (AD-12): the first 2 hex chars of the key become a subfolder, spreading files across 256 buckets instead of one flat directory.

Kandra.Persistence.Contexts.PlatformDbContext

Non-generic companion to PlatformDbContext (same pairing pattern as e.g. Task/Task<T>) - holds the subconto composite indexes that can't be expressed via HasIndex in PlatformDbContext's OnModelCreating (EF Core 10.0.10 can't index a nested complex-type property, see the comments there). Kept here, next to that model configuration, as the one place the actual DDL lives; each provider's AddAccSubcontoIndexes migration (under KandraWms.Persistence.Databases/Migrations/{Sqlite,SqlServer,PostgreSql}/) just calls these two methods instead of repeating the 15 CreateIndex/DropIndex calls three times.

Kandra.Persistence.Registers.BalanceEntityConfiguration<T0, T1, T2>

Reusable EF configuration for a register's balance entity: surrogate PK, the concurrency-token counter, and owned-type mapping of Dimensions/Resources. A concrete register's config class inherits this and adds the unique dimension index plus any register- specific bits via the protected hooks.

Kandra.Persistence.Registers.BalanceReader<T0, T1, T2, T3, T4>

Generic implementation of IBalanceReader. Unlike TurnoverReader, computing the as-of overload needs TDimensions/TResources closed in (dimension equality, resource arithmetic) — so, like DocumentInfoRegisterReader, this reader is registered per register (see AddBalanceRegister) rather than as a true open generic.

Kandra.Persistence.Registers.BalanceRegisterWriter<T0, T1, T2, T3, T4>

Generic record-set commit pipeline for a balance register (design §8.2). Fully reusable — a register's only per-register code is its entity classes (design §14.1); this class is closed over a register's concrete types via a one-line DI registration in the domain-configuration project (see e.g. KandraWms.Persistence/ConfigureServices.cs). Concurrency: balance deltas are applied via tracked-entity load/mutate/ SaveChanges guarded by Version (a manually incremented concurrency token, portable across SQL Server/ SQLite/PostgreSql — unlike a DB-generated rowversion). A concurrent-write conflict throws DbUpdateConcurrencyException, which is retried a bounded number of times rather than resolved via a lock-free atomic increment (deviation from design §10.1, agreed scoped-down approach for this pass).

Kandra.Persistence.Registers.DocumentInfoRecordEntityConfiguration<T0, T1, T2>

Reusable EF configuration for a document-bound information register's record entity: PK, engine headers, owned-type mapping of Dimensions/Details, and the record-set identity index. A concrete register's config class inherits this and adds only register-specific bits (table name, FK relationships, precision, extra indexes) via the protected hooks — mirrors MovementEntityConfiguration minus the resources component.

Kandra.Persistence.Registers.DocumentInfoRegisterReader<T0, T1, T2>

Generic implementation of IInfoRegisterReader for a document-bound information register. Unlike TurnoverReader, this reader needs TDimensions closed in to group by dimensions for SliceOfLast — the DI registration (see AddDocumentInfoRegister) closes over the register's concrete types even though the public interface is only parameterized by TRecord.

Kandra.Persistence.Registers.DocumentInfoRegisterWriter<T0, T1, T2>

Generic record-set commit pipeline for a document-bound information register — the same replace-by-recorder mechanics as BalanceRegisterWriter but with no balance table to maintain, so no delta math: post = delete the document's old rows, insert the new ones; unpost = deactivate; delete = unpost + physical removal.

Kandra.Persistence.Registers.InfoRegisterReader<T0, T1, T2>

Generic implementation of IInfoRegisterReader for the periodic information register's independent write mode — records have no Active flag (nothing to unpost), so every row counts.

Kandra.Persistence.Registers.InfoRegisterWriter<T0, T1, T2>

Generic implementation of IInfoRegisterWriter — the periodic information register's independent write mode (design §11): rows are upserted directly by (dimensions, period), no recorder involved.

Kandra.Persistence.Registers.MovementEntityConfiguration<T0, T1, T2, T3>

Reusable EF configuration for a register's movement entity: PK, engine headers, owned-type mapping of Dimensions/Resources/Details, and the record-set identity index. A concrete register's config class inherits this and adds only register-specific bits (table name, FK relationships, precision, extra indexes) via the protected hooks.

Kandra.Persistence.Registers.PeriodicInfoRecordEntityConfiguration<T0, T1, T2>

Reusable EF configuration for a periodic information register's record entity (independent write mode, design §11): PK, Period index, and owned-type mapping of Dimensions/Details — no recorder, no record-set identity index. A concrete register's config class inherits this and adds the unique (dimensions + Period) index plus any register-specific bits via the protected hooks — mirrors DocumentInfoRecordEntityConfiguration minus the document binding.

Kandra.Persistence.Registers.RegisterMaintenance<T0, T1, T2, T3, T4>

Rebuild/verify the balance table from the movements log (design §8.5 — "the movements log is the single source of truth"). Aggregation is done client-side: an accepted simplification at this engine's current data volumes (same judgment call as the report service's IAsyncEnumerable→List change), not a SQL-side GROUP BY.

Kandra.Persistence.Registers.RegisterServiceCollectionExtensions

Methods

MethodDescription
AddBalanceRegister<T0, T1, T2, T3, T4>(IServiceCollection)Registers the generic engine's reader, writer, and maintenance service closed over one balance register's concrete entity types — the only per-register wiring a domain configuration needs to write by hand (design §14.1: registers contribute zero handwritten service code; this one line is the manual stand-in for what a source generator would otherwise emit into the register's partial class, §6.4). The balance reader needs TDimensions/TResources closed in for Balances(DateTime) (design §9.1), so — unlike ITurnoverReader, which stays a true open generic registered once, engine-wide, by Kandra.Persistence.ConfigureServices — it is registered here.
AddDocumentInfoRegister<T0, T1, T2>(IServiceCollection)Registers the generic engine's reader and writer closed over one document-bound information register's concrete entity types — the one-line wiring a domain configuration needs (mirrors AddBalanceRegister). Unlike the balance/turnover readers, the info-register reader needs TDimensions closed in (to group by dimensions for SliceOfLast), so it is registered here rather than as an engine-wide open generic in Kandra.Persistence.ConfigureServices.
AddPeriodicInfoRegister<T0, T1, T2>(IServiceCollection)Registers the generic engine's reader and writer closed over one periodic information register's concrete entity types — the independent write mode (design §11): unlike accumulation/document-bound writers, this one is meant to be resolved directly by application code (imports, admin UI, dictionary behaviors), not only from posting code.
AddTurnoverRegister<T0, T1, T2, T3>(IServiceCollection)Registers the generic engine's writer closed over one turnover register's concrete entity types — the only per-register wiring a domain configuration needs for a register with no balance projection (mirrors AddBalanceRegister minus the balance table). Readers (ITurnoverReader) are true open generics and registered once, engine-wide, by Kandra.Persistence.ConfigureServices.

Kandra.Persistence.Registers.TurnoverReader<T0>

Generic, fully reusable — true open-generic DI registration works for any register.

Kandra.Persistence.Registers.TurnoverRegisterWriter<T0, T1, T2, T3>

Generic record-set commit pipeline for a turnover register (design §8.1): the same replace-by-recorder mechanics as BalanceRegisterWriter but with no balance table to maintain — commit only replaces the movements log. Fully reusable — a register's only per-register code is its entity classes; this class is closed over a register's concrete types via a one-line DI registration (see AddTurnoverRegister).

Kandra.Persistence.Repositories.DatabaseSeeder<T0, T1>

Methods

MethodDescription
SeedJobsSchedulerUserAsyncSeeded with real credentials (for audit/ModifierUserId attribution on jobs it runs) but permanently locked out, so it can never complete an interactive login (checked in LoginService.Login via LockoutEnabled/LockoutEnd).