Releases: xshaheen/headless-framework
Release list
0.11.0
This release carries the post-0.10.0 public API cleanup program, the blob and SMS contract redesigns, named-client registration across more provider families, TUS Azure conformance work, and the package-quality/readiness fixes needed before publishing.
⚠️ Pre-1.0 software: breaking changes land in minor versions. Review the Breaking Changes section before upgrading.
⚠️ Breaking Changes
Public API compatibility pass across the framework (#623)
0.11.0 applies the repo-wide public API compatibility review while the framework is still pre-v1.0. The biggest migration theme is namespace and package ownership:
Headless.Api.Abstractionsnow owns its namesake namespace;IProblemDetailsCreatorand the newIAbsoluteUrlFactorylive there.- Feature packages now keep their registration surface in their own package/family namespace instead of foreign framework namespaces. Update
usingstatements to the package namespace, for exampleHeadless.Api,Headless.Api.Idempotency,Headless.Sms,Headless.Tus, and provider family roots. - BCL-colliding holder names were renamed to
Headless*or package-specific names, including HTTP helpers and antiforgery registration holders. Headless.Messaging.Internalis gone; cross-package SPI moved toHeadless.Messaging.Runtimeand the rest was internalized.Headless.Urls,Headless.Primitives, andHeadless.Permissions.Testingare now separate packages.Headless.Extensionsstill references the extracted primitives/URL surface transitively.
Several contract-shape fixes also landed in this program: Jobs source-generator tuple ABI moved to named structs/contexts, IConnectionStringChecker returns ConnectionCheckResult, several collection-returning APIs moved to read-only shapes, behavior-selecting enums now have explicit values, and Paymob money/ID fields use safer numeric types.
Migration: expect compile-time fixes: add the correct package/family using, replace renamed holder/types, and consume the new named DTO/record return types where tuple or loose helper contracts were removed.
Blob storage operations use BlobLocation, token paging, and explicit capabilities (#550)
The IBlobStorage operational contract was redesigned to make provider behavior consistent across AWS, Azure, Cloudflare R2, FileSystem, Redis, and SFTP:
string[] containerplusblobNameis replaced byBlobLocation(Container, Path).- Listing is token-based:
ListAsync(BlobQuery) -> BlobPage(Items, ContinuationToken?); streaming is available throughGetBlobsAsync. RenameAsyncis nowMoveAsync;DeleteAllAsync(container, glob)is nowDeleteAllAsync(BlobQuery)with prefix-based provider deletes and client-side glob support.- Container management moved out of
IBlobStoragetoIBlobContainerManager, resolved as an optional DI capability. Uploads no longer auto-create missing containers. - Bulk operations return
IReadOnlyList<BlobBulkResult>, metadata values are non-null strings, andIPresignedUrlBlobStoragetakesBlobLocation.
Migration: construct BlobLocation/BlobQuery at call sites, resolve IBlobContainerManager only for providers that support bucket/container management, and update custom blob providers to the new operation signatures.
SMS is single-recipient by default, with bulk and named clients as explicit capabilities (#549, #587, #589)
SMS was split into a clean single-recipient core plus optional bulk and named-client capabilities:
ISmsSender.SendAsyncnow sends to exactly oneSmsRequestDestination;SendSingleSmsRequest.DestinationsandIsBatchare gone.- Bulk-capable providers implement
IBulkSmsSender.SendBulkAsync(SendBulkSmsRequest)with per-recipientSmsRecipientResultvalues. AWS SNS and Twilio intentionally do not registerIBulkSmsSender. - Registration moved to the new
Headless.Sms.Corepackage.Headless.Sms.Abstractionsis now contracts-only. ISmsProviderOptionsExtensionwas removed. Providers register throughAddHeadlessSms(setup => setup.Use*(...))and named instances usesetup.AddNamed("name", i => i.Use*(...)).UseDevwas renamed toUseDevelopmentfor Emails parity.
Migration: use one Destination for single sends, inject IBulkSmsSender only where bulk is required, add Headless.Sms.Core, and move provider setup calls to the new default/named builder form.
Named-DI registration is unified across provider families (#589, #593)
The named-client model now covers Emails, SMS, Caching, Blobs, Captcha, and Push Notifications with consistent provider factories and RegisteredNames where applicable.
- Emails and SMS now allow named-only hosts: zero or one default provider plus any number of named providers. The unkeyed sender is not registered when no default is configured.
IEmailSenderProvider,ISmsSenderProvider, andICacheProviderexposeRegisteredNamesfor validation and discovery.- Captcha registration moved to the new
Headless.Captcha.Corepackage; Abstractions is contracts-only. - Push Notifications moved setup/factory machinery into
Headless.PushNotifications.Core, addedIPushNotificationServiceProvider, and supports default plus named Firebase/Noop services.
Migration: add the relevant .Core package when using registration APIs, move named/default registration to the setup builder, and validate externally supplied names with RegisteredNames rather than probing by exception.
API primitives and request contracts were renamed or tightened (#596, #597, #623)
CollectionEnvelop,DataEnvelop,IdEnvelop,MessageEnvelop,Operations*Envelop, andValueEnvelopare now*Envelope.IProblemDetailsCreator.UnprocessableEntitynow acceptsIReadOnlyDictionary<string, IReadOnlyList<ErrorDescriptor>>.- JWT creation now uses
JwtTokenRequestinstead of long parameter lists. HttpCurrentUserno longer reports principal-derived values when unauthenticated.- API idempotency and service-default types moved into their package-owned namespaces.
Migration: rename envelope types, update JWT call sites to construct JwtTokenRequest, and add package namespaces for API idempotency/service-default helpers.
✨ Features
Provider registration and package surfaces
- New
Headless.PushNotifications.Corewith default and named Firebase/Noop registration plusIPushNotificationServiceProvider(#593) - New
Headless.Captcha.Core,Headless.Sms.Core,Headless.Urls,Headless.Primitives,Headless.Permissions.Testing, andHeadless.Sql.Corepackage surfaces (#589, #623) RegisteredNamessupport on sender/cache providers for external name validation (#589)
Blobs
BlobLocation,BlobQuery,BlobPage,BlobBulkResult, andIBlobContainerManageras first-class contracts (#550)- Universal metadata support for FileSystem and SFTP via hidden sidecar files, plus provider-conformance coverage for metadata and token paging (#550)
SMS
IBulkSmsSender,SendBulkSmsRequest,SendBulkSmsResponse, andSmsRecipientResultfor providers with true or aggregate bulk sends (#549)- Named SMS clients across AWS SNS, Twilio, Cequens, Connekio, Infobip, VictoryLink, Vodafone, Dev, and Noop (#587)
- Shared SMS failure classification for transport, resilience, and provider-specific error contracts (#549)
TUS and uploads
AddTusAzureStoreDI registration with the standard configuration/action overload trio (#586)DeletePartialFilesOnConcat, TUS CORS defaults, expired-upload cleanup service, and a runnabledemo/Headless.Tus.Demowith a Vite/React client (#586)
API and validation
IAbsoluteUrlFactory,JwtTokenRequest, DataProtection key-ring startup validation, and DataProtection health checks (#623)- FluentValidation package coverage for API contracts and a broader
Headless.FluentValidationvalidator suite (#564, #623)
Maintenance tooling
- Bounded NuGet advisory/deprecation audit tooling:
scripts/audit-nuget-advisories.sh,make nuget-advisory-audit, andmake dependency-security-audit PROJECT=<path>(#636)
⚡ Performance
- Delayed messaging waits until the next due message instead of polling every 50ms (#635)
- Monthly count helpers try database-side grouping/counting before falling back to client aggregation (#635)
- Kafka publishing reuses whole-array message bodies where possible (#635)
- PostgreSQL advisory-lock key hashing avoids transient byte arrays and caps the process-wide hash cache (#635)
- Messaging/cache/API/extensions hot paths were reduced across the repository review and follow-up passes (#599, #609, #617)
🐛 Fixes
Caching
- Hybrid and Redis caching reliability fixes: eager-refresh fail-closed behavior, recovery-drain signaling, bounded cold-read fan-out, expiration validation, Redis marker and numeric semantics, NUL-byte key rejection, and auto-recovery warnings (#548, #540)
- Hybrid invalidation consumer registration is now order-independent when a backplane bus is present (#511, #599)
Messaging and Jobs
- Redis Streams reject handling now requeues before acknowledging, stale pending entries recover through XAUTOCLAIM, and startup pause failures clean up failed clients (#638)
- Jobs execution is fenced to rows still owned by the current node; active async executions count against scheduler concurrency and drain behavior (#637)
- Messaging consumer durability, retry behavior, NATS/ASB/AWS provider tests, and dashboard testing gaps were hardened (#610, #614, #616, #621)
TUS Azure
- Deferred-length uploads, completed-upload expiration, byte-for-byte
Upload-Metadataecho, non-ASCII metadata, checksum-trailer rollback, duplicate partial concatenation, andValidateIdenforcement now align with tus 1.0.0/reference-store behavior (#586) BlobMaxChunkSizedefault is lower to avoid 100 MB per concurrent large upload buffering (#586)
Payments, API, ORM, and core utilities
- Paymob CashOut/CashIn now deserialize unknown...
0.10.0
This release consolidates everything since 0.5.3 — a large span covering the messaging consumer model, the distributed-locks and coordination substrates, the caching resilience program, the new commit-coordination primitive, and a broad hot-path performance pass. The headline change in 0.10.0 itself is the buffer-first ISerializer redesign.
⚠️ Pre-1.0 software: breaking changes land in minor versions. Review the Breaking Changes section before upgrading.
⚠️ Breaking Changes
Serializer is now buffer-first (#534)
ISerializer no longer takes or returns Stream. The core contract is buffer-based:
// before (Stream-first)
void Serialize<T>(T value, Stream output);
T? Deserialize<T>(Stream data);
// after (buffer-first)
void Serialize<T>(T value, IBufferWriter<byte> output);
T? Deserialize<T>(ReadOnlyMemory<byte> data);
T? Deserialize<T>(in ReadOnlySequence<byte> data);Migration: most call sites are unaffected — byte[], string, and Stream helpers (SerializeToBytes, SerializeToString, Deserialize<T>(byte[]), Deserialize<T>(string?), Serialize<T>(T, Stream), Deserialize<T>(Stream)) live on as SerializerExtensions (C# 14 extension members). Only custom ISerializer implementations must be rewritten to the buffer overloads. Bonus: SerializeToBytes no longer pays a MemoryStream + ToArray() copy, and byte-array reads happen in place.
Emails: unified provider builder + named senders (#504, #516)
The four bespoke registration extensions are removed in favor of the framework's unified setup-builder grammar (matching Caching / Coordination / Settings / …), and a new Azure Communication Services provider is added:
// before — one bespoke extension per provider
services.AddMailKitEmailSender(config);
services.AddAwsSesEmailSender(awsOptions);
services.AddDevEmailSender("emails.txt");
// after — one entry, exactly one default provider per call
services.AddHeadlessEmails(setup => setup.UseMailkit(config));
services.AddHeadlessEmails(setup => setup.UseAwsSes(awsOptions));
services.AddHeadlessEmails(setup => setup.UseDevelopment("emails.txt"));
services.AddHeadlessEmails(setup => setup.UseAzure(o => o.ConnectionString = "…")); // new ACS provider| Removed | Replacement |
|---|---|
AddMailKitEmailSender |
setup.UseMailkit |
AddAwsSesEmailSender |
setup.UseAwsSes |
AddDevEmailSender |
setup.UseDevelopment |
AddNoopEmailSender |
setup.UseNoop |
Zero, multiple, or repeated AddHeadlessEmails on the same IServiceCollection now throws InvalidOperationException at registration (a host resolves exactly one default IEmailSender). Named senders are supported alongside the required default and resolved via the new IEmailSenderProvider:
services.AddHeadlessEmails(setup =>
{
setup.UseAwsSes(awsOptions); // default (required)
setup.AddNamed("marketing", i => i.UseMailkit(smtpSection)); // named, keyed
});
// resolve: IEmailSenderProvider.GetSender("marketing") or [FromKeyedServices("marketing")] IEmailSenderThe IEmailSender send contract is unchanged. Custom-provider authors: IEmailProviderOptionsExtension is dropped (use the captured-Action<IServiceCollection> contribution model); MailkitSmtpOptions properties changed init → set.
Captcha: provider abstraction + package rename (#503)
The single hard-wired Headless.ReCaptcha package is retired and split into an abstraction + providers:
// before (package: Headless.ReCaptcha)
services.AddReCaptchaV3(o => { … });
// inject IReCaptchaSiteVerifyV3
// after (packages: Headless.Captcha.Abstractions + Headless.Captcha.ReCaptcha [+ .Turnstile])
services.AddHeadlessCaptcha(b => b.UseReCaptchaV3(o => { … }));
// inject IReCaptchaV3Verifier (score) or ICaptchaVerifier (pass/fail)- Package id
Headless.ReCaptcha→Headless.Captcha.ReCaptcha; newHeadless.Captcha.Turnstile(Cloudflare). - Removed (no shim):
AddReCaptchaV2/AddReCaptchaV3,IReCaptchaSiteVerifyV2/IReCaptchaSiteVerifyV3. - Call sites should depend on
ICaptchaVerifier(pass/fail); the reCAPTCHA v3 numeric score stays onIReCaptchaV3Verifier. At most one default provider plus any number of named providers resolved viaICaptchaProvider.GetVerifier(name).
Domain: non-boxing equality hooks on EqualityBase (#517)
The boxing EqualityComponents() contract is replaced by two typed hooks — removing per-probe boxing and enumerator allocations on every dictionary / HashSet / EF change-tracker key compare:
// before
protected abstract IEnumerable<object?> EqualityComponents();
// after
protected abstract bool EqualityComponentsEqual(T other); // typed compare, early-exit, no box
protected abstract void BuildHashCode(ref HashCode hash); // typed hash, no box- Value objects now derive from
ValueObject<TSelf>(CRTP — no cast in overrides):sealed class Money : ValueObject<Money>. The non-genericValueObjectbase is replaced by theIValueObjectmarker for heterogeneous references. - Entities that overrode
EqualityComponents()move to the two hooks.Entity<TId>/GetKeys()and EF key mapping are unchanged (the composite-keyEntity<TId>de-box is a tracked follow-up).
✨ Features
Caching
- Resilience program: timeouts, sliding expiration, eager + conditional refresh, tagging, distributed stampede protection, named caches, auto-recovery (#437)
- Distributed, tag-aware
IOutputCacheStoreadapter (#451) - BCL
IDistributedCacheadapter (#446) - Serve stale values on factory failure (#408)
- Redis cache entry envelope (#398) and cache entry options envelope (#389)
Messaging
- Split bus and queue delivery intents (#340)
ForMessageregistration API (#362) and configure scanned consumers (#387)- Universal provider knobs — Cluster 0.4 (#410)
- Monitored retry lock leases (#430); recover retry rows from dead coordination owners (#421); dead-only reclaim + shared coordination recovery bridge (#449)
Distributed Locks
- Fencing tokens + Redis semaphores (#368) and Redis reader/writer locks (#342)
- Providers: PostgreSQL advisory (#391), in-memory + Postgres with shared conformance harness (#393), SQL Server (#406)
- Distinguish safety-deadline stall from contention in non-blocking acquire (#444)
Coordination & Commit Coordination
- Provider-agnostic commit coordinator, replacing the ambient-transaction approach (#428)
- Membership substrate (#416); migrate Jobs node membership & dead-node recovery onto
Headless.Coordination(#422)
Jobs
- Atomic job enqueue via commit coordination (#455)
OnNodeDeathconsumer input API + per-job policy and lease/status vocabulary alignment (#454, #456)- Optional
IDistributedLockguard for cron-seed migration (#468)
Blobs & Storage
- Multiple named blob storages in one DI container (#495)
- Cloudflare R2 backend + presigned URLs (#476)
- Unified storage provider setup (#354)
⚡ Performance
- Buffer-first serializer removes intermediate
MemoryStream/ToArray()copies on serialize and reads byte arrays in place (#534) - Replace dynamic SQL IN-lists with TVPs + messaging hot-path allocations (#352 via #448)
- Coordination: targeted single-node liveness via
ReadNodeLivenessAsyncSPI (#447); co-locate incarnation to drop O(N) Redis GETs (#425) - Messaging: drop per-publish payload copy (ASB) and LINQ attribute dict (SQS/SNS) (#529)
serializer-json: cache derived options inCollectionItemJsonConverter(#531)- Assorted hot-path allocation fixes across packages (#513, #518, #530, #533)
🐛 Fixes
- Caching: review-driven reliability, perf & correctness hardening (#512)
- Jobs: honest renewal-loop signals — membership skip + fenced-success flag (#470)
- Messaging: reject more than one storage provider at bootstrap (#450); publish callback response values (#395); hourly monitoring bugs (#423); integration-test reliability (#363)
- Coordination: retry SQL Server membership deadlocks (#435)
- Postgres: harden advisory savepoint detection (#432)
- Blobs/TUS: Azure blob & TUS review fixes (#496)
- Dashboards: harden SPA build automation (#494); vue-tsc 3 import fix (#497)
♻️ Refactoring
- ORM/EF: internalize context runtime (#433); two-tier event dispatch — domain-event bus + integration-event outbox bridge (#407)
- Messaging: broker-neutral message names (#361); order-independent consumer registration (#420);
MessageOptionsrename (#394) - Core: backend-keyed storage GUIDs (#414)
- Distributed locks: builder-only setup API (#415)
- Review hardening pass across Blobs, NATS, PostgreSQL messaging storage, Emails (#506)
🧪 Tests & 📚 Docs
- Distributed-locks provider test plan + fencing-token / race-condition regression coverage (#413, #412, #452, #409)
- Messaging consume-dispatch benchmark harness (#514)
- Conform
docs/llms+ package READMEs to the authoring shape; add provider tables + end-to-end examples (#532)
🔧 Build & CI
- Route hooks and CI through Make (#457); pre-push build/format guardrails + CI format gate (#434)
- Automate dashboard SPA build + npm dependency updates (#479)
- Prune prerelease packages workflow; CodeQL on PRs only
⬆️ Dependencies & Security
- Patch shell-quote (GHSA-w7jw-789q-3m8p) and MessagePack (GHSA-hv8m-jj95-wg3x) (#443)
- Patch 11 Dependabot npm alerts in dashboards (#477); axios + brace-expansion advisories (#401)
- Update packages within-major + tools; fix NU1903 SQLitePCLRaw (#475)
- Routine dependency bumps: dotnet-sdk, @microsoft/signalr, vue-tsc, npm-minor-patch group, GitHub Actions (#453, #486, #488, #491, #485, #482, #483, #400)
Contributors: @xshaheen, with dependency updates from @dependabot.
Full Changelog: 0.5.3...0.10.0
0.5.3
What's Changed
- fix(caching-memory): align InMemoryCache set key-type predicate by @xshaheen in #325
- docs(orm): document HeadlessDbContext is intentionally not poolable by @xshaheen in #326
- feat(distributed-locks): improve lock acquisition ergonomics by @xshaheen in #322
- feat(management): configure EF storage schema by @xshaheen in #327
- feat(distributed-locks): add lease monitoring by @xshaheen in #330
Full Changelog: 0.5.2...0.5.3
0.4.18
- feat(idempotency): rewrite as Stripe-style replay in new Headless.Api.Idempotency package by @xshaheen (#286)
- feat(messaging): replace filters with typed middleware pipeline (+ distributed-locks docs) by @xshaheen (#285)
- refactor(api): move tenant enforcement to authorization by @xshaheen (#281)
- docs(messaging): refresh Phase 1 LLM doc + provider capability matrix by @xshaheen (#277)
- feat(messaging-otel): add IActivityTagEnricher tag-enrichment pipeline by @xshaheen (#275)
🧰 Maintenance
- build(deps): bump dotnet-sdk from 10.0.203 to 10.0.300 by @dependabot[bot] (#301)
Contributors
@dependabot[bot], @xshaheen and dependabot[bot]
0.4.17
- No changes
Contributors
No contributors
0.4.16
0.4.15
- No changes
Contributors
No contributors
0.4.14
0.4.13
What's Changed
- ci(deps): bump release-drafter/release-drafter from 7.2.1 to 7.3.0 in the actions-minor-patch group by @dependabot[bot] in #247
- build(deps): bump dotnet-sdk from 10.0.100 to 10.0.203 by @dependabot[bot] in #246
- refactor(orm): simplify headless save pipeline by @xshaheen in #248
Full Changelog: 0.4.12...0.4.13