Skip to content

refactor: Deduplicate the block preview rendering pipeline - #329

Open
rickbutterfield wants to merge 7 commits into
v5/devfrom
v5/refactor/preview-pipeline-dedup
Open

refactor: Deduplicate the block preview rendering pipeline#329
rickbutterfield wants to merge 7 commits into
v5/devfrom
v5/refactor/preview-pipeline-dedup

Conversation

@rickbutterfield

Copy link
Copy Markdown
Owner

Summary

Extracts the duplicated pipeline logic in BlockPreviewApiController and BlockPreviewService into three focused, independently testable services, without changing any public behavior.

  • IMarkupSanitizer — the markup-cleanup step previously inlined in the controller.
  • IPreviewContentResolver — content/culture/published-request resolution, previously duplicated across the controller's actions.
  • IPreviewRequestExecutor — the shared "check models exist → resolve → enrich → render → enrich → catch" pipeline, previously repeated across all four preview endpoints.
  • BlockPreviewService's four Render*Block methods now share one internal tail (RenderTypedBlockAsync<TBlockItem>) instead of duplicating ~60 lines each.

All three new services follow the existing "Replaceable Services" pattern already used by IBlockModelFactory/IBlockViewRenderer/IBlockDataConverter, and are documented in docs/advanced-customization.md alongside them.

No public behavior changes. IBlockPreviewService's interface and its three documented overridable methods (GetViewResult, CreateViewDataAsync, GetStylesheetPaths) are byte-identical to before. The controller's HTTP contract (routes, params, response shapes) is unchanged. The obsolete 14-parameter fallback constructor was removed (an intentional, suppressed APICompat break — see CompatibilitySuppressions.xml, following the existing precedent for AddInternal's visibility change).

Both the controller and the service have unit test coverage for the first time.

Test plan

  • dotnet test — 91/91 passing
  • Two rounds of automated code review (task-level + whole-branch), both approved with fixes applied
  • Manual backoffice smoke test (all four block editor types) — not run this cycle, no browser access available; recommend running before a release tag, particularly around Block Grid area rendering with row/column spans

🤖 Generated with Claude Code

Rick Butterfield and others added 7 commits August 4, 2026 12:01
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moves GetPublishedContent, GetCurrentCulture, and SetupPublishedRequest
into a standalone, Moq-testable PreviewContentResolver service. Behavior
is preserved verbatim; the controller is not yet wired to use it (Task 4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pulls the "check models exist -> resolve content/culture -> enrich request
-> render -> enrich response -> catch and format error" pipeline shared by
the four preview/* controller actions into a standalone, Moq-testable
IPreviewRequestExecutor. The block-type-specific render call is injected
as a delegate so the executor stays decoupled from IBlockPreviewService.
Nothing wires into the controller yet; that's Task 4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nto IPreviewRequestExecutor

Rewrites the controller's constructor and PreviewGridBlock/PreviewListBlock/
PreviewSingleBlock/PreviewRichTextMarkup to delegate to IPreviewRequestExecutor
and IMarkupSanitizer (Tasks 1-3), dropping nine constructor parameters it only
used to build those services directly. Deletes CleanUpMarkup, GetCurrentCulture,
SetupPublishedRequest, CheckGeneratedModelsExist, and the 3-arg
GetPublishedContent overload (logic moved to PreviewContentResolver/
PreviewRequestExecutor/MarkupSanitizer). The 2-arg GetPublishedContent overload
is kept, now delegating to IPreviewContentResolver, so the seven stylesheet
actions compile unchanged. Removes the obsolete StaticServiceProvider-forwarding
constructor, which only existed to bridge a prior IBlockPreviewResponseEnricher
migration and has no external callers since the controller is DI-activated.

Adds a CP0002 APICompat suppression for the removed constructor, following the
existing suppression pattern for intentional breaking changes in this package.

Adds the controller's first unit tests.
… shared tail

RenderGridBlock, RenderListBlock, RenderSingleBlock, and RenderRichTextBlock repeated an
identical ~60-line tail (convert content data to element, find block types, create the
block instance, build ViewData, render markup). Extracted that into a private generic
RenderTypedBlockAsync<TBlockItem> helper; each public method now keeps only its
block-type-specific deserialize/key-parsing/grid-config head.

Grid's layout matching (GetMatchingGridLayout) previously mutated the block instance's
RowSpan/ColumnSpan directly, but that instance no longer exists until inside the shared
helper. Reworked GetMatchingGridLayout to match by content key instead of block instance
and report the matched spans via out params, applied via the helper's `configure`
callback (same formulas, same relative ordering as before, verified equivalent since
CreateBlockInstance sets ContentKey directly from the same contentData.Key).

Adds BlockPreviewService's first unit tests, covering all four render methods through the
shared tail's guard clauses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…edup

Closes the gaps found in the final cross-commit review of the preview
rendering pipeline refactor:

- Document IMarkupSanitizer, IPreviewContentResolver, and
  IPreviewRequestExecutor in docs/advanced-customization.md's
  Replaceable Services section, alongside the pipeline's other three
  extracted services.
- Drop the dead `culture` parameter from
  IPreviewContentResolver.SetupPublishedRequestAsync (it was never
  read, and the doc comment claiming it affected downstream culture
  was inaccurate).
- Add render-delegate wiring assertions for PreviewGridBlock,
  PreviewListBlock, and PreviewSingleBlock so a method-name swap
  between block types would fail a test instead of silently compiling.
- Add test coverage for BlockPreviewService's Grid `configure`
  callback (RowSpan/ColumnSpan threading), by making the private
  FindBlockType protected virtual so a test subclass can bypass the
  unmockable BlockEditorConverter.
- Restore XML doc comments lost during the controller action rewrite,
  and add the missing summary/param tags flagged on
  IPreviewRequestExecutor.ExecuteAsync, PreviewContentResolver's
  constructor, and PreviewRenderRequest.
- Remove three genuinely-unused usings from the controller.
- Assert the logger actually receives an error log in
  PreviewRequestExecutorTests, and reference
  Constants.ErrorMessages.RenderError instead of a hardcoded string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- CustomPreviewContentResolver example now injects ContextCultureService and calls SetCulture() with a required comment
- CustomPreviewRequestExecutor example now includes missing using statements (IPublishedContent and ControllerContext)
- IPreviewContentResolver XML doc cref to ContextCultureService now fully qualified to resolve CS1574 warning
- BlockPreviewServiceTests comment updated: six tests now (not five), sixth test uses TestableBlockPreviewService

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(string), 200)]
public async Task<IActionResult> PreviewGridBlock(
public Task<IActionResult> PreviewGridBlock(
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(string), 200)]
public async Task<IActionResult> PreviewListBlock(
public Task<IActionResult> PreviewListBlock(
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(string), 200)]
public async Task<IActionResult> PreviewRichTextMarkup(
public Task<IActionResult> PreviewSingleBlock(
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(string), 200)]
public async Task<IActionResult> PreviewSingleBlock(
public Task<IActionResult> PreviewRichTextMarkup(
}
catch (Exception ex)
{
_logger.LogError(ex, string.Format(Constants.ErrorMessages.LoggerError, request.ContentElementAlias));
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.

2 participants