/DB

Changelog

What changed in Socigy.OpenSource.DB. NativeAOT support (string-named projection / `keep` overloads and a reflection-based predicate folder replacing `Expression.Compile`, so the library and a consuming app publish with `PublishAot`), plus Vault/OpenBao encryption fixes — per-column profiles activate, `UseSocigyVaultEncryption()` primes before migrations, and background rotation no longer overflows its timer — in 0.3.6; a single `InsertFields` enum (plus a `keep` selector) replacing the per-call insert booleans on the context, static, and bulk paths, plus fixes making migration apply idempotent across app restarts and safe across concurrent replicas, cancellation-token support on the generated query and write APIs, and a binary-COPY UTC-timestamp fix, in 0.3.5; modular-monolith and multi-project fixes (`required` members, flowing Npgsql/Bcl dependencies, a `contextName` for lowercase databases, and per-call `[Default]` control on every insert path) in 0.3.4; Binary COPY bulk insert, scalar/affected/DTO procedure returns, database-first scaffolding, and Transit data-key envelope encryption with per-column profiles and OpenBao support in 0.3.3; runtime-named typed tables ([TableType] and DynamicTable) in 0.3.2; the database-context bulk insert plus scalar and aggregate API in 0.3.1; and 0.3.0's field encryption, rotating credentials, and HashiCorp Vault package.

updated 28 Jul 202661 min readv0.3.6View as Markdown
29 June 2026
Socigy.OpenSource.DBv0.3.6stable
Added
  • `await app.UseSocigyVaultEncryption()` activates Vault encryption before your first data access. AddSocigyVault*Encryption only registers the encryptors; they were primed exclusively by a startup IHostedService, which does not run until app.Run(). The documented quickstart does await app.EnsureLatest{Db}Migration() between Build() and Run(), so any migration, bootstrap, or seed touching an [Encrypted] column threw "no IFieldEncryptor is configured" and the process died at boot — invisible with the local key provider, which configures its key synchronously. The new awaitable entry point primes and activates every registered profile up front; it extends IHost (so it works on a WebApplication) and has an IServiceProvider overload for hostless apps. It is idempotent, so the startup priming afterwards does not contact Vault twice, and a failed attempt (a sealed/unreachable Vault) is retried rather than cached.
  • `SocigyFieldEncryption.IsProfileConfigured(profile)`. IsConfigured only ever reported the *default* profile, so it could not tell you whether an [Encrypted(Profile = "…")] column was ready — a missing profile stayed silent until the first read/write of such a column threw. The new overload reports any profile (null/empty means the default).
  • NativeAOT: the library publishes with `PublishAot=true`. The convenience keep selector, the query Select / OrderBy / OrderByDesc, the update WithFields / ExceptFields, and the join OrderBy / OrderByDesc previously took only an Expression<Func<T, object?[]>>, whose new object?[] { ... } body emits Expression.NewArrayInit ([RequiresDynamicCode], IL3050), and the WHERE / UPDATE translator folded operands with Expression.Compile() (also [RequiresDynamicCode]), so dotnet publish -p:PublishAot=true failed with dozens of IL3050 errors plus trim/AOT warnings on Core. Each array-selector API now has an AOT-safe params string[] / string[] overload naming columns by string (a property name such as nameof(Row.Id), or a DB column name), and the translator folds operands through a reflection interpreter instead of Compile(). The library and a consuming app now publish AOT-clean (no IL3050 / IL3053); the Expression overloads are unchanged for JIT callers.
Fixed
  • A per-column encryption profile registered via DI is actually activated. Registering an envelope (default) encryptor alongside AddSocigyVaultTransitEncryption(o => o.Profile = "…") activated only the first: both registered their primer with AddHostedService, which goes through TryAddEnumerable and de-duplicates by implementation type, so the second primer was silently dropped and the first write to an [Encrypted(Profile = "…")] column threw — recommending the very DI helper that had been called. The priming work is now a per-registration service (which DI never de-duplicates) collected by a single hosted service, so the default and every named profile are primed. A transit-only registration was never affected; it is precisely the documented default-plus-profile combination that broke.
  • Background key rotation no longer kills the host at startup. EnableBackgroundRotation = true with the default RotationInterval (90 days) threw ArgumentOutOfRangeException: dueTime ('7776000000') must be less than or equal to '4294967294' out of StartAsync: System.Threading.Timer caps a dueTime at ~49.7 days. No unusual configuration was needed — only turning the documented feature on. Rotation now arms in clamped hops and rotates once the full interval has really elapsed, so any interval works. The token/credential renewal scheduler is clamped the same way (a lease TTL over ~74.6 days, or a long RefreshInterval, could throw on a timer thread where only ObjectDisposedException was caught).
  • `EnableBackgroundRotation` works in EaaS-direct (Transit) mode. AddSocigyVaultTransitEncryption never read EnableBackgroundRotation / RotationInterval, so enabling rotation there was a silent no-op even though both options are offered on its type. It now registers a rotator, and a rotator per mode coexists (envelope + transit both rotating no longer drops one).
  • `x.Col ?? null` emits a `COALESCE` again. The literal-null rewrite to IS NULL / IS NOT NULL ran for any operator, not just == / !=, so a coalesce whose right side is the null literal was rewritten to "col" IS NOT NULL — a boolean where a value belongs. The rewrite is now gated to (in)equality, matching the captured-null rewrite beside it.
  • Predicate folding preserves the operand CLR type (NativeAOT interpreter). With Expression.Compile() removed for AOT, parameter-independent sub-expressions are folded by a reflection interpreter that now follows C# numeric promotion so the bound parameter keeps the compiled-delegate type: negating a value stays its own integral/floating type instead of collapsing to double (which also lost precision for a large long), ~ on an unsigned value keeps the unsigned bit pattern, a cast from a floating/decimal value to an integral type truncates toward zero instead of rounding, and a lifted nullable operation with a null operand folds to null (a null relational comparison to false). Each shape is verified against the compiled-delegate result.
  • The AOT-safe string `Select` / `OrderBy` column names are quote-escaped. A name that does not resolve to a known column is taken to be a DB column name and quoted as-is; an embedded double-quote is now doubled (""") so a dynamically supplied name cannot break out of the quoted identifier, on both the single-table and join OrderBy string paths.