Skip to content

Add the hook contract and the hook pipeline - #2734

Merged
pournasserian merged 2 commits into
mainfrom
feat/2694-hook-pipelines-6d8480
Nov 2, 2026
Merged

pournasserian merged 2 commits into
mainfrom
feat/2694-hook-pipelines-6d8480

Conversation

@pournasserian

@pournasserian pournasserian commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Closes #2694

Adds the hook contract to the SDK (IBeforeValidateHook<T> and the Before/After pairs for Create, Update, Delete, Publish, Unpublish and Restore, HookContext<T>, HookOrderAttribute, HookRejectedException, ContentDocument) and the pipeline in FluentCMS.Core that resolves hooks for a content class and for ContentDocument, orders them by [HookOrder], runs them, and throws HookRejectedException at the first cancellation. ContentChanges.Between computes the changed field paths in the dotted form validation uses; the pipeline recomputes them before each before hook, so a hook sees the changes of the hooks before it. After hooks work on a deep copy of the entry, because only before hooks may change it. The content service (#2693) will call the pipeline inside its transaction; the tests in this PR play that part against a SQLite CmsDbContext. The validation pipeline already ran in the order the issue lists (#2685), including Required/RequiredWhen on publish, so it is unchanged; a test now pins that one result lists every failing field across declarative rules, field validators and entry validators.

Dependencies

None.

Acceptance

  • Hooks fire in the documented order with the OperationContext and change set available
  • A cancelling before hook rolls back the write and surfaces its code and message
  • Validation errors list every failing field in one result
  • A hook registered for all types receives events for every type

Beyond the spec

  • Namespace. Spec 4.1 puts the hook interfaces and HookContext in FluentCMS.Hooks, so they are there, together with HookOrderAttribute and HookRejectedException, which the spec does not place. ContentDocument is in FluentCMS.Content.
  • No IAfterValidateHook. The spec lists IBeforeValidateHook<T> only, so there are 13 hook interfaces.
  • Hook shape. HookContext<T>; one method per interface (BeforeCreateAsync, ...) so a class can implement a Before/After pair. Cancel records the cancellation and the pipeline throws after the hook returns; a hook can also throw HookRejectedException itself. An after hook can cancel too, which rolls the write back.
  • Interleaving. Hooks for ContentDocument and for the content class (and its base classes, such as ContentItem) form one sequence ordered by [HookOrder], then by tiebreak rank. If they still tie, all-types hooks run first, then base classes, then the content class, each in registration order.
  • Tiebreak. IHookTiebreaker.Rank(Type) is an optional service for the plugin host's dependency order. Without it, registration order decides.
  • IHookEntryMapper. ContentDocument is the canonical entry, so hooks for a content class go through this seam (ToEntry, CopyTo), which the content service (IContentService and IContentService<T> with optimistic concurrency #2693) provides. A typed before hook's changes are copied back into the document. Without a mapper, a registered typed hook throws InvalidOperationException.
  • Pipeline owns the changed paths. They are ContentChanges.Between(previous, entry). An operation that changes no field, such as publish, passes the stored state as previous.
  • ContentDocument.Get<T>. Converts only between numeric types, and only when the value fits; anything else throws InvalidCastException.

@pournasserian
pournasserian merged commit 5934ec8 into main Nov 2, 2026
5 checks passed
@pournasserian
pournasserian deleted the feat/2694-hook-pipelines-6d8480 branch November 2, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validation and lifecycle hook pipelines

1 participant