All notable changes to the DKNet Framework will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
DKNet.AspCore.Extensions can now fill a request member from a named HTTP request header.
[FromRequestHeader("Idempotency-Key")] on a request property — one required constructor argument, the
header name, exposed as HeaderName — is populated before validation and before the handler runs by the
same AddContextualRequestPopulation() registration that already powers [FromClaim], so it needs no
second registration and no route-level code. Header-name matching is case-insensitive, a header sent more
than once fills the member with the first value sent, a caller-supplied body value for the member is always
overwritten, and the property needs a set or init or startup throws — the same rules [FromClaim]
follows. Three differences are worth knowing before you build on it. A missing header is not a refusal:
the member holds its type’s default and the request is still dispatched, so requiring a header stays the job
of a filter. A header-filled member takes the configured SystemAccountFallback wherever a claim-filled
one would (a fallback is set and the group’s RequireAuthorization is false), which means an absent header
leaves a constant value there rather than an empty one — something a service using the member as an
idempotency key needs to account for (this describes the mechanism as it stood when this note was written;
that knob is removed later in this same [Unreleased] cycle, see Removed below). And a header
is never an authorization signal: unlike a claim it is supplied by the caller, so the declaration is a
binding convenience only, never proof of identity. The published OpenAPI operation declares the header as an
in: header parameter — unlike [FromClaim], which is hidden entirely, the caller has to know to send it —
while the member itself stays absent from the published
request body. See DKNet.AspCore.Extensions.DKCRUDGEN010 (Info) when a [CrudCreate]/[CrudUpdate]/[CrudAction]
member already resolves to the name Delete{Entity}Request. That collision has always made the generator skip the
generated delete request and fall back to the request-less MapDeleteById<TEntity, TKey>(); the diagnostic names the
entity, the colliding member and the request name it took, so the fallback is no longer silent. Severity is Info:
the build still succeeds and the entity’s DELETE route keeps working unchanged — only the delete request you could
hang a validation rule on is missing, which renaming the member restores. See
DKNet.SlimBus.Generators.DKNet.EfCore.AuditLogs can now stamp CreatedBy/UpdatedBy from the signed-in user. Implement the new
ICurrentUserProvider (one member, string? GetCurrentUser(), returning null/empty when there is no user) and
register it with services.AddCurrentUserProvider<TDbContext, TProvider>() — an application-wide, scoped,
first-caller-wins registration that attaches EfCoreAuditHook to TDbContext itself, so it needs no publisher
and no second registration call, and never overwrites an AuditLogOptions an earlier
AddEfCoreAuditHook/AddEfCoreAuditLogs registered, whichever order the two run in. The hook stamps
CreatedBy/CreatedOn on Added entries and UpdatedBy/UpdatedOn on Modified ones before it captures
the audit entries, so a published entry carries the same values the row was saved with; a value a domain method
recorded in the same change set is never replaced. DKNet.EfCore.DataAuthorization’s DataOwnerHook takes the
same provider and stamps OwnedBy alone for any save where it returns a non-empty user — with no provider
registered, or one that returns nothing for that save, the ownership key keeps filling the audit fields exactly
as before, so existing applications need no change. Both providers are optional and each works without the
other. The returned value reaches every registered IAuditLogPublisher unmasked, so an application under a
personal-data rule should return a stable non-personal identifier (for example a subject id) rather than an email
address. See DKNet.EfCore.AuditLogs.CrudMapOptions.Exclude(params string[] routeNames) withdraws the one route carrying each name and leaves
the entity’s other routes of the same kind published at their existing addresses — so a single
[CrudUpdate]/[CrudAction] member can be dropped without taking Update or Action down wholesale. The
names are the same ones Configure(string, …) takes, and they go through the same validation:
Map{Entity}Crud throws ArgumentException at registration for a name the entity has no route for, so an
exclusion that matches nothing fails loudly instead of leaving the route live. Nothing is excluded by default.CrudMapOptions.Configure(CrudOp, Action<RouteHandlerBuilder>) applies a setting — RequireAuthorization
first of all — to every generated route of that operation kind, and
CrudMapOptions.Configure(string routeName, Action<RouteHandlerBuilder>) applies one to a single named
route. Both are additive and chainable; for a given route, operation-kind settings run before name
settings. A route’s name is GetById, GetList, Create or Delete for the four fixed operations and the
[CrudUpdate]/[CrudAction] member’s own C# method name for the rest. Map{Entity}Crud validates every
configured name before mapping anything, so an unknown name throws ArgumentException at registration
rather than silently dropping the setting; a name belonging to an excluded operation is validated and then
dropped with the route. Two routes of one entity resolving to the same name is the new DKCRUDGEN009.[SensitiveData] now accepts optional role names ([SensitiveData("pricing", "audit")], Roles never null).
DKNet.EfCore.DtoGenerator carries the declaration from the entity onto the generated response model, and
JsonSerializerOptions.UseRoleAwareSensitiveData(ISensitiveDataPrincipalAccessor) in
DKNet.EfCore.Extensions lets a host omit the property from JSON responses for callers outside those roles —
absent from the payload, not null or masked. Opt-in per options instance and fail-closed (no identity, or an
unauthenticated one, withholds the property); naming no role admits any authenticated caller. Audit-log
redaction and hosts that do not opt in are unchanged. The DtoGenerator no longer emits an uncompilable
new[] { } for an empty params array attribute argument.[RaisesEvent] convention forms now accept Exclude/Include named arguments to shape the automatically
composed payload record — mutually exclusive, resolved against the entity’s properties at build time
(DKRAISEVT009/DKRAISEVT010/DKRAISEVT011), and never affecting the composed event name.docs/ folderBase64StringExtensions’ five methods (DKNet.Svc.Encryption) are now this string extension methods;
existing static-style call sites still compile.AddErrorResponses (DKNet.AspCore.Extensions) now also shapes an unhandled exception’s response, with no
second registration call: it registers an IExceptionHandler and wires UseExceptionHandler() for the host.
ErrorResponseOptions.UnhandledError supplies a custom body in place of the library’s own (a fixed message
outside Development, the exception’s own message inside it); ErrorSource.Unhandled and
ErrorResponseContext.Exception let StatusCode/Customize see and react to it the same way they already
react to a failed command or refused input. The call is idempotent — a second one registers nothing twice.
Endpoints the fluent mappers registered are covered by the endpoint filter ProducesCommons() adds; an endpoint
registered outside those mappers is covered in Production but still answers with the developer-exception page
in Development. See
One error-response setting.AddIdempotentKey(Action<IdempotencyOptions>? config = null) (DKNet.AspCore.Idempotency) enables
idempotency with no store named and no infrastructure at all — no database, cache, Redis, or connection string.
It registers a new in-process store that reserves each key atomically within the process, adds no options
(it reuses Expiration and InFlightReservationTimeout) and no package reference, and never serves a key past
its Expiration. Expired entries are evicted by a sweep that runs every 256 writes, so an idle process can hold
up to 255 already-expired entries until the next sweep reclaims them. Keys are process-local, lost on restart,
and not shared between instances, so it is for local
development and unit tests only; the app logs one startup warning saying so while it is the store serving
requests. An explicitly named store replaces it whichever order the two registrations run in, so existing
registrations keep behaving exactly as before.DKNet.EfCore.AuditLogs now redacts a property carrying [Encrypted]
(DKNet.EfCore.Encryption.Attributes.EncryptedAttribute) in the same way as [SensitiveData] — unconditionally,
even alongside [AuditLog] on the same property — so a value encrypted at rest no longer reaches an audit
publisher as plaintext. Matching is by attribute type name, not type identity, so any attribute of your own
named EncryptedAttribute is redacted too. Separately, the error log written when an IAuditLogPublisher
throws no longer serializes the failed batch’s entries; it now names the publisher, the entity name(s), and the
entry count instead. A consumer parsing entry values out of that publisher-failure log line must switch to the
publisher’s own retry/outbox path. See DKNet.EfCore.AuditLogs.[RaisesEvent] convention-form payloads now honour the project-wide
DtoGeneratorExclusions MSBuild list (the same list [GenerateDto] DTOs already respect), so composed
event payloads narrow in any project that configures it — unless overridden by a non-empty Include.EfCoreEncryptionSetup.AddEfCoreEncryption<T>() now takes and returns
IServiceCollection instead of the concrete ServiceCollection, so it is callable on builder.Services.
Source-compatible for existing callers; pre-compiled assemblies referencing the old signature must be recompiled.AddEncryptionServices() no longer registers IRsaEncryption — it previously resolved to a new,
throwaway random key pair on every DI resolution, so keys never survived across resolutions. Callers that need
RSA must opt in explicitly with services.AddRsaEncryption(privateKeyBase64), which registers IRsaEncryption
as a singleton built from a caller-supplied key.AddEncryptionServices() no longer registers IAesGcmEncryption or the obsolete IAesEncryption
either — both were transients over a constructor that generates a fresh random key per instance. Every injection
therefore got a different key, the key was persisted nowhere, and any ciphertext written through one resolution
became permanently unreadable, silently. Callers must now opt in with
services.AddAesGcmEncryption(base64Key) — a singleton IAesGcmEncryption over a plain Base64 128/192/256-bit
key — or, for migration only, services.AddAesEncryption(keyString), a singleton IAesEncryption over the
combined Base64 key:iv value that AesEncryption.Key returns (that method is itself [Obsolete]). Both throw ArgumentException on a null, empty,
or whitespace key. AddEncryptionServices() now registers only the keyless IShaHashing and IHmacHashing
transients; new AesGcmEncryption() still generates an ephemeral key and stays valid for data that never
outlives the process.EndpointRegistrationOptions.EnableRequestValidation and EndpointRegistrationOptions.SystemAccountName
have been removed from DKNet.AspCore.Extensions, and UseEndpointConfigs() no longer stamps RequestBase.ByUser
or applies FluentValidation auto-validation on its own. A consumer that had set either setting gets a compile
failure; one relying on defaults loses automatic validation silently, and — because the stamping filter is gone —
a caller-supplied ByUser — regardless of binding source (query, [AsParameters], or JSON body) — now reaches
the handler unchanged, so a host that does not re-add stamping via ConfigureGroup must treat ByUser as
caller-influenced. Supply both
through the new EndpointRegistrationOptions.ConfigureGroup callback instead. Versioning is now a switch
(EnableVersioning, default true) and IEndpointConfig.Version is optional (defaults to 1).DKNet.SlimBus.Extensions.RequestBase — and its [JsonIgnore] string? ByUser property — has been
removed. The package never populated it, and with UseEndpointConfigs() no longer stamping ByUser either (see the
entry above), the base record carried nothing while still making the acting user look like a framework-supplied
value. A request type that derived from it no longer compiles. Migration: drop the : RequestBase base, declare the
acting-user property on the request itself, mark it with an IContextualSource attribute — for example
[FromClaim(ClaimTypes.Name)] from DKNet.AspCore.Extensions — and register
services.AddContextualRequestPopulation(), which stamps the property before validation and before the handler
runs. See DKNet.SlimBus.Extensions.AddDataOwnerProvider<TDbContext, TProvider>() in DKNet.EfCore.DataAuthorization now constrains
TDbContext to DbContext, IDataOwnerDbContext. This is source-breaking: a consumer whose DbContext does not
implement IDataOwnerDbContext no longer compiles. Previously it compiled and silently lost row-level ownership
isolation at runtime in Release builds. Migration: implement IDataOwnerDbContext (supply AccessibleKeys;
override IsUnrestrictedAccess only for admin/system contexts) on the DbContext type you register — see the
Migration Guide.SpecRepoExtensions.ToListAsync, ModelSpecRepoExtensions.ToListAsync
and both SpecRepoExtensions.ToKeysetPageAsync overloads (DKNet.EfCore.Specifications) now return
Task<List<T>> instead of Task<IList<T>>. Existing call sites compile unchanged; a precompiled assembly
referencing the old return type must be recompiled.BlobServiceOptions.IncludedExtensions (DKNet.Svc.BlobStorage.Abstractions) is now
IReadOnlyList<string> instead of IEnumerable<string>. A caller assigning a lazy query to it no longer compiles.EfCoreExceptionHandler (DKNet.EfCore.Extensions) gained an optional ILogger<EfCoreExceptionHandler>?
constructor parameter; parameterless construction still works.NextSeqValue/NextSeqValueWithFormat (DKNet.EfCore.Extensions) gained an optional CancellationToken.MapGetList, DKNet.AspCore.Extensions) is subject to is now
host-configurable and defaults to 1000, replacing the hard-coded private const MaxPageSize = 100 on
ListQueryRequest. A consumer who upgrades and configures nothing therefore serves up to 1,000 rows per list
request where 100 was previously the worst case. Bind the DKNet:ListQuery:MaxPageSize configuration key, or call
services.AddListQueryOptions(o => o.MaxPageSize = 100), to keep the old ceiling. The clamp semantics are
unchanged: an oversized pageSize is still served trimmed to the ceiling, never rejected with a 400. See
Page-size defaults and ceiling.MapGetList,
DKNet.AspCore.Extensions) serves when the caller asks for none is now 1000 instead of 20, matching the
ceiling. A consumer who upgrades and configures nothing therefore answers a bare list request with up to 1,000
items where it previously answered with 20. Everything else about paging is unchanged: MaxPageSize still wins
whenever it is lower than the default in force, an oversized explicit pageSize is still trimmed rather than
rejected, and an absent or below-one pageNumber is still the first page. Set
DKNet:ListQuery:DefaultPageSize to 20, or call services.AddListQueryOptions(o => o.DefaultPageSize = 20),
to keep the old default. See
Page-size defaults and ceiling.MapGetList, DKNet.AspCore.Extensions) over records
that carry audit timestamps (IAuditedProperties) now answers a bare request with the last 3 months of
activity instead of all history, and accepts two new query parameters — fromDate and toDate — that bound a
listing by when a record was last active (either its CreatedOn or its UpdatedOn moment inside the bounds).
A caller who names either bound replaces the default window entirely, open-ended on the side they leave out, so
fromDate=0001-01-01T00:00:00Z asks for all history; a fromDate later than toDate is a 400. The window is
applied by the database, so it narrows TotalItemCount as well as the returned items, and a listed type with no
audit timestamps ignores the bounds rather than rejecting them. The window length is the new
ListQueryOptions.DefaultActivityWindowMonths: bind DKNet:ListQuery:DefaultActivityWindowMonths, or call
services.AddListQueryOptions(o => o.DefaultActivityWindowMonths = 0), to switch the default window off and keep
today’s unbounded listing. See
Default recent-activity window.DKNet.AspCore.Extensions) no longer carries Detail, and its
errors extension is now an ErrorItem[] ({ message, code?, field? }) instead of a flat message-string list —
for every failure kind Response()/Response<T>() answer, whether or not AddErrorResponses is registered. type is now always the final response status’
HttpStatusCode name (recomputed after a StatusCode callback runs) for all three failure kinds, and every
body carries a traceId. A consumer parsing errors as strings, or Detail for the failure message, must read
errors[].message instead. See the
Migration guide and
One error-response setting.DKNet.EfCore.Repos and DKNet.EfCore.Repos.Abstractions packages, and the Mapster.EFCore
central package entry they alone used. Use DKNet.EfCore.Specifications + AddSpecRepo<TDbContext>() instead —
see the Migration Guide and
Migrating-Repos-To-Specifications.md.AsyncEnumerableExtensions.ToListAsync(this IAsyncEnumerable<T>) (DKNet.Fw.Extensions). Use .NET
10’s System.Linq.AsyncEnumerable.ToListAsync, which also accepts a CancellationToken.Base65StringExtensions (DKNet.Svc.Encryption), the misspelled duplicate of
Base64StringExtensions. Use Base64StringExtensions.IAesEncryption, AesEncryption, and AddAesEncryption (DKNet.Svc.Encryption).
The cipher was AES-CBC with a fixed IV embedded in the key, so identical plaintexts always produced identical
ciphertext. Migrate to IAesGcmEncryption / AddAesGcmEncryption; there is no automated conversion of existing
ciphertext.EncryptionKeyProvider abstract class (DKNet.EfCore.Encryption). Implement
IEncryptionKeyProvider directly — the abstract class re-declared the interface’s only member with no shared
implementation.IShaHashing and IHmacHashing (DKNet.Svc.Encryption) no longer extend IDisposable — both were
stateless wrappers over static hashing calls, and Dispose() did nothing.HmacHashing.VerifySha256/VerifySha512 (DKNet.Svc.Encryption) lost their ignoreCase
parameter — it was read but could not change the result, since hex decoding is already case-insensitive.IRepositorySpec.DeleteRange<TEntity> (DKNet.EfCore.Specifications). Use BulkDeleteAsync.ISpecification<TEntity>.OrderByQueries/.OrderByDescendingQueries, and the equivalent properties
on Specification<TEntity> (DKNet.EfCore.Specifications). Ordering is now a single declared-sequence model.
An ISpecification<TEntity> implementation that does not derive from Specification<TEntity> is no longer
supported, and the legacy “all ascending clauses applied first, then all descending” fallback ordering is gone —
that path produced different SQL than the declared-sequence path for the same specification.IdempotencyDistributedCacheStore (DKNet.AspCore.Idempotency), the IDistributedCache-backed store whose
check-then-act reservation was never atomic. Not a breaking change: the type was internal and no public
registration selected it. The parameterless AddIdempotentKey() overload that used to default to it is not
removed — it now registers the atomic in-process store described under Added.EnumExtensions.GetEumInfos<T>()/GetEumInfo() (DKNet.Fw.Extensions) renamed to
GetEnumInfos<T>()/GetEnumInfo() — the old names were a typo.ToProblemDetails(this IResultBase, HttpStatusCode), ToProblemDetails(this ModelStateDictionary),
and Response(this IResultBase/IResult<T>, ErrorResponseOptions?, …) (DKNet.AspCore.Extensions) — none of them
read a host’s registered error-response setting. ToProblemDetails(this IResultBase, ErrorResponseOptions?) is
now internal for the same reason. Use Response()/Response<T>(), the only public path onto the standard
body: they resolve the registered ErrorResponseOptions from the container on their own, so an endpoint no
longer needs to name it. See the
Migration guide.ContextualPopulationOptions and its SystemAccountFallback knob (DKNet.AspCore.Extensions)
are gone, and AddContextualRequestPopulation() no longer takes a configure delegate. A declared contextual
member the registered resolvers cannot resolve now always holds its type’s default — there is no built-in
substitute value. Migrate by registering your own IContextualValueResolver before
AddContextualRequestPopulation() — but note that CanResolve keys on the declaration’s attribute type, and
population consults only the first matching resolver, so registering one ahead of the built-in
ClaimValueResolver/RequestHeaderValueResolver replaces it entirely for every member declaring that
attribute, not only the ones missing a value. Your resolver must perform the built-in lookup itself
(httpContext.User.FindFirst(claimType)?.Value ?? "system-account", or the header equivalent) before
substituting, or it silently overrides every caller who does have the claim or header. See
DKNet.AspCore.Extensions.[CrudAction] member that takes no parameters no longer requires a
request body (DRK-1436). DKNet.SlimBus.Generators now picks the mapper by the action method’s parameter
count: a parameterless action is registered with the new
DKNet.AspCore.Extensions mapper MapParameterlessActionById<TCommand, TKey, TResponse>(endpoint, httpMethod),
which binds the target id from the route and nothing from the body, so a caller that sends no body and no
Content-Type at all is dispatched instead of rejected with 400. A body sent anyway is still accepted and
ignored — an existing caller posting {} keeps working — and the published OpenAPI operation for such a
route declares no requestBody. An action that takes parameters is unchanged: it still maps through
MapActionById and still requires its body.DbContext type registry (DKNet.SlimBus.Extensions) was a static set shared by every
service provider in the process, so providers built concurrently could throw
InvalidOperationException: Operations that change non-concurrent collections must have exclusive access out of
AddSlimBusEfCoreInterceptor<TDbContext>(), and one provider’s auto-save could reach for a DbContext type only
another provider had registered. The registry now lives in the IServiceCollection — one registration per
TDbContext, deduplicated, resolved from the request’s own provider — so providers no longer share it. No
public API change.DKNet.EfCore.DtoGenerator and DKNet.SlimBus.Generators now emit attribute arguments as valid C# literals
when carrying an entity’s attributes onto generated code. Previously a float argument was emitted
without its f suffix, a non-finite double (NaN, positive/negative infinity) as a bare
NaN/Infinity word rather than double.NaN/double.PositiveInfinity/double.NegativeInfinity, a ' or
\ char without an escape, and a string containing a backslash without escaping it — each produced
generated code that did not compile. Finite double emission is unchanged (an unsuffixed decimal literal
already is a double).[RaisesEvent] convention-form composed payloads no longer pull a navigation/complex-type property into the
record when a non-empty Include names it — Include narrowed the entity’s own scalar properties but was
silently reusing the property as-is when it named a navigation, shipping every property of the referenced type.
Navigation properties are now omitted unconditionally, matching Exclude/no-filter behaviour.[GenerateDto]’s Exclude/Include and [RaisesEvent]’s narrowing params argument, when written as
new[] { nameof(...) } (a classic array-initializer, as opposed to the [nameof(...)] collection-expression
form), previously resolved to an empty filter and silently kept the named property instead of dropping/narrowing
it. This is now resolved like every other form: an affected [GenerateDto] DTO narrows as declared, and an
affected [RaisesEvent] narrowing list both narrows the raise condition and changes the composed event name
(e.g. OrderUpdatedEvent → OrderStatusUpdatedEvent) — re-check any declaration using this exact array syntax.IDataSeedingConfiguration, DKNet.EfCore.Extensions) compared entities by reference equality, so
every seed row was re-inserted on every application start. Comparison is now by primary key; existing databases
that accumulated duplicate seed rows are not cleaned up automatically.DisableHooks() (DKNet.EfCore.Hooks) suppressed hooks process-wide via a static dictionary keyed by DbContext
type name, so one request’s using (db.DisableHooks()) silently disabled audit logging, event dispatch, and
owner stamping for every concurrent request against that DbContext type. Suppression is now scoped to the
logical call context via AsyncLocal.TransformerService (DKNet.Svc.Transformation) cached token values on the instance keyed only by token text, so
a second Transform/TransformAsync call on the same instance with different parameters returned the first
call’s values. The cache is now local to each call.ListItemsAsync (DKNet.Svc.BlobStorage.AzureStorage) returned the searched-for prefix as every item’s
name instead of the item’s own name, so a folder listing came back with every result sharing one name —
including through BlobService.GetItemAsync.ListItemsAsync (DKNet.Svc.BlobStorage.AwsS3) never read IsTruncated/NextContinuationToken, so any
prefix with more than 1,000 objects silently dropped the rest; it also classified a 0-byte or 1-byte file as a
directory. Listing now paginates fully and classifies by key shape, not size.LocalBlobService’s relative-path computation (DKNet.Svc.BlobStorage.Local) used string.Replace against the
root folder name, which strips every occurrence — a file under a subfolder that happens to repeat the root
folder’s name resolved to the wrong relative path. Now uses Path.GetRelativePath.TableExistsAsync (DKNet.EfCore.Relational.Helpers) treated any DbException from a probe query as “table
does not exist”, so a permissions failure or timeout silently reported false. It now queries
INFORMATION_SCHEMA.TABLES directly and lets infrastructure errors propagate.EfCoreExceptionHandler (DKNet.EfCore.Extensions) decided retryability by matching EF Core’s English exception
text; it now inspects exception.Entries, which retries a slightly broader set of concurrency conflicts.TypeExtractor’s fluent API (DKNet.Fw.Extensions) mutated shared predicate state, so branching one extractor
(e.g. var abstracts = e.Abstract(); var concretes = e.NotAbstract(); off the same instance) made both branches
empty. Each fluent call now returns an independent extractor.DateTimeExtensions.LastDayOfMonth (DKNet.Fw.Extensions) reconstructed the DateTime and hardcoded
DateTimeKind.Local, silently converting a UTC input to Local and dropping sub-millisecond precision. It now
shifts the date with AddDays, preserving Kind and full precision.EnumExtensions.GetEnumInfos/GetEnumInfo (DKNet.Fw.Extensions) filtered the enum backing field by assuming an
int backing type, so a byte- or long-backed enum leaked a spurious value__ entry into the results.GetEntityKeyValues (DKNet.EfCore.Extensions) read primary-key values via PropertyInfo!.GetValue(...), which
threw for shadow properties and backing-field-only keys — reached from audit logging. It now reads through EF’s
CurrentValues, with no reflection and no crash case.DKNet.AspCore.Idempotency.Relational) now runs once at application startup via a
hosted service instead of on the first incoming request; the per-request check remains as a defensive fallback.IRsaEncryption resolving to an unmanaged, silently discarded random key pair per resolution
(DKNet.Svc.Encryption).IAesGcmEncryption and IAesEncryption resolving to a random, never-persisted key per resolution, which
made every value they encrypted unrecoverable (DKNet.Svc.Encryption).DKNet.EfCore.DataAuthorization now fails closed when a DbContext does not implement IDataOwnerDbContext.
DataOwnerAuthQuery.HasQueryFilter previously guarded that case with Debug.Fail(...) and returned null;
Debug.Fail is compiled out in Release, and a null filter means “apply nothing”, so in Release builds every
IOwnedBy entity was left with no ownership filter and every caller could read every owner’s rows — a complete
row-level isolation bypass. It now throws InvalidOperationException at model-build time, and the tightened
AddDataOwnerProvider constraint (see Changed) stops the mistake at compile time.IAesGcmEncryption’s Encrypt/Decrypt overloads that take a base64Key now compare it against the instance’s
own key with CryptographicOperations.FixedTimeEquals instead of a plain equality check, so a wrong key is
rejected in constant time regardless of how much of it was right. One behaviour consequence: a base64 string that
decodes to the same bytes as the instance’s key is now accepted even if the two strings differ textually.DKNet.Fw.Extensions: 1.0.0+ (Core framework extensions)DKNet.RandomCreator: 1.0.0+ (Random data generation utilities)DKNet.EfCore.Abstractions: 1.0.0+DKNet.EfCore.Extensions: 1.0.0+DKNet.EfCore.Repos: 1.0.0+DKNet.EfCore.Repos.Abstractions: 1.0.0+DKNet.EfCore.Hooks: 1.0.0+DKNet.EfCore.Events: 1.0.0+DKNet.EfCore.DataAuthorization: 1.0.0+DKNet.EfCore.Specifications: 1.0.0+DKNet.EfCore.Relational.Helpers: 1.0.0+DKNet.SlimBus.Extensions: 1.0.0+DKNet.Svc.BlobStorage.Abstractions: 1.0.0+DKNet.Svc.BlobStorage.AzureStorage: 1.0.0+DKNet.Svc.BlobStorage.AwsS3: 1.0.0+DKNet.Svc.BlobStorage.Local: 1.0.0+DKNet.Svc.Transformation: 1.0.0+Aspire.Hosting.ServiceBus: 1.0.0+This represents a complete rewrite of the framework with focus on:
For existing users of legacy DKNet packages:
All packages include security enhancements:
For detailed package-specific changes, see:
📝 Note: This consolidated changelog provides an overview of the entire framework. For detailed, package-specific changes, please refer to the individual package changelogs linked above.