← Back to Component Index

Dynamic Feed — Template Resolution, Content Adapters & DynamicPageOrchestrator

The async/IO step between fetching a raw PageDto and handing it to PageFactory
Companion docs: Dynamic Feed — API Contract & Rendering Pipeline (Page/Container/Item levels)  |  Dynamic Feed — Site, Theme & Template Adapter Gaps (the analysis that first called this piece out as missing)  |  Work Item: AB#1821, superseded/extended by AB#2845 — Dynamic Feed Orchestrator Refactor (Phases 0–6 merged, PR #2640)  |  Status: DynamicPageOrchestrator and seven content adapters (PilotCardFactory, LecLinkFactory, CommitteeListFactory, CommitteeMembersFactory, DocumentHeroFactory, PodcastEpisodeFactory, MecHotelDirectoryFactory) implemented and registered; a non-localized IDynamicPageLoader now sits in front of the orchestrator for every page below; DynamicListFactory alone remains implemented-but-unregistered, still blocked on IItemsQueries  |  Updated: 2026-09-29 14:55 ET  |  Audited: 2026-09-29 14:55 ET

Where this fits: the API-contract doc covers the static PageDto → ContainerDto[] → ItemDto[] shape and its pure mapping into ViewModels. This doc covers what happens before that mapping runs, when an ItemDto carries a Template instead of static display fields — the piece the adapter-gaps doc originally flagged as a total gap ("no PageTemplateResolver exists"). The orchestrator class that fills that gap ended up named DynamicPageOrchestrator, not PageResolver as first proposed.

Wire shape: ItemDto.Template arrives from the backend as a raw JSON string (per the Mobile Content API docs), not a structured object. TemplateDto throughout this doc is the client's own typed, parsed representation of that string — deserialized once at the top of this pipeline, not a backend DTO.

2026-09-29 update: the Dynamic Feed Orchestrator Refactor (Phases 0–6) added a page-loading layer in front of what this doc originally described (IDynamicPageLoader/DynamicPageLoader, a shared DynamicContentHost page host, and a widened ContentResolutionContext passed into IFeedTemplateContentFactory). This page has been updated to reflect that; see §1, §2, and §4 below for what changed and why the orchestrator itself is otherwise unchanged.

1. Where template resolution sits in the pipeline

All template-resolution types live in ALPAMobile.Presentation/Adapters/ (content-resolver factories now specifically under Adapters/ContentResolvers/, the orchestrator under Adapters/Orchestration/) — deliberately split out of ALPAMobile.Presentation/Components/, which holds pure, service-free mappers only. DynamicPageOrchestrator and every IFeedTemplateContentFactory implementation are the app's designated async/IO boundary (RawRepresentationFactory rule): they're the only pieces in this pipeline allowed to resolve a service and await it.

IDynamicPageLoader.LoadAsync
→
IHomeFeedQueries.GetPageAsync
→
DynamicPageOrchestrator.ResolveAsync
→
PageFactory.Create
→
ContainerFactory / ItemComponentFactory
→
PageViewModel

IDynamicPageLoader/DynamicPageLoader (Pages/DynamicFeed/DynamicPageLoader.cs) is the common page-loading entry point added by the Orchestrator Refactor: it owns fetching the PageDto by page key, invoking DynamicPageOrchestrator, and returning a DynamicPageLoadResult (or, for the progressive per-container path, a ProgressiveDynamicPageLoadResult) — plus not-found/error handling and force-refresh/cancellation. LocalizedDynamicPageLoader now delegates to it after its own EN/FR page-selection step, rather than duplicating that workflow itself.

Callers: MecHomePage.razor.cs and LecPage.razor.cs go through LocalizedDynamicPageLoader → DynamicPageLoader and render via the shared DynamicContentHost component (loading skeleton, not-found/error states, favorite context, and the component loop all live in the host now, not in each page). DynamicFeedOrchestratorPage.razor.cs (route /dynamic-feed-orchestrator?page={pageKey}, CMS deep links) calls IDynamicPageLoader.LoadProgressivelyAsync directly but keeps its own inline shell markup rather than using DynamicContentHost, because it needs a FixedContent filter-bar slot (hotel directory) the host doesn't support yet. AlpaIHomePage.razor.cs (renamed from HomeAlpaPage) was deliberately not migrated onto either IDynamicPageLoader or DynamicContentHost — it still calls DynamicPageOrchestrator.ResolveAsync directly, because its signed-out CMS branch, dual refresh-pill overlays, and WatchForStuckContainers-after-every-reassignment shape are real page-specific behavior the shared host doesn't model (see the refactor plan's Phase 4 for the full reasoning). HomePreviewPage, previously documented here as the one caller still bypassing template resolution, no longer exists in the codebase.

Everything left of PageFactory.Create can still touch a raw PageDto with templated items in it. Everything at or right of it only ever sees fully static items — ContainerFactory and ItemComponentFactory are unmodified by this work and never learn templates exist.

2. IFeedTemplateContentFactory — the per-content-type adapter contract

MemberPurpose
string ContentType { get; }The TemplateDto.ContentType value this adapter resolves (e.g. "DynamicList", "PilotCard", "LecLink"). DynamicPageOrchestrator dispatches on this string.
Task<IReadOnlyList<ItemDto>> ResolveAsync(TemplateDto, CancellationToken)Fetches and maps whatever data the template describes. Returns an empty list — never throws — when the template can't be resolved (missing/malformed Parameters key, empty result set).
Task<IReadOnlyList<ItemDto>> ResolveAsync(ContentResolutionContext, CancellationToken) NewWidened overload (Orchestrator Refactor Phase 5) carrying the outer templated SourceItem, the current User, and the owning PageId/ContainerType, not just Template. Has a default implementation that forwards to the template-only overload, so every pre-existing factory keeps compiling and running unchanged; a factory only overrides this when it actually needs the wider context (e.g. to read the source item's own typography tokens instead of relying on the orchestrator's post-hoc copy — see §4).

One implementation is registered per ContentType string. An unrecognized ContentType is not an error at the DynamicPageOrchestrator level — it's the same forward-compatibility rule as an unknown ItemType or ContainerType elsewhere in this pipeline: the templated item is dropped, the rest of the page renders normally.

Shared helpers: FeedTemplateResolutionHelpers (Adapters/ContentResolvers/FeedTemplateResolutionHelpers.cs) centralizes the two pieces of factory logic that were byte-for-byte duplicated across implementations before Phase 6: render-type stamping (RenderTypeOrDefault for factories with a sensible fallback shape, RequireRenderType for factories that must drop the item when the CMS omits renderType) and the "isFavoritable defaults true unless explicitly false" check (ResolveIsFavoritable). It's deliberately not a base class — a factory with genuinely different semantics (e.g. DynamicListFactory's favorite-value preservation) keeps its own logic instead of being forced onto these helpers.

2b. Single-row adapters — PilotCardFactory & LecLinkFactory

Both live in ALPAMobile.Presentation/Adapters/ alongside DynamicListFactory, and both resolve a template to exactly one ItemDto rather than expanding to a collection.

AdapterContentTypeWhat it does
PilotCardFactory.csPilotCardNo HTTP call. Reads IAuthentication.GetUserInfo() and builds a single greeting row — "Welcome {FirstName} {LastName}", or bare "Welcome" when signed out. Synchronous, wrapped in Task.FromResult.
LecLinkFactory.csLecLinkCtor takes IAuthentication + IMemberQueries. Reads userInfo.MEC/LEC (returns [] if the member isn't signed in), calls memberQueries.GetMemberAsync() (backed by /api/member/getuser) for the airport base, and builds a single tap-target row routed to /mec/{mec}/lec/{lec}, resolving its image filename via ActionRouteResolver.

3. DynamicListFactory — contentType: "DynamicList"

The collection-shaped adapter: resolves a server-driven collection of items — a list whose membership isn't known until the device asks for it — as opposed to the single-row adapters above.

TemplateDto.Parameters keyRead viaUsed for
scopeTryGetStringPassed through to IItemsQueries.GetItemsAsync.
categoryTryGetStringPassed through to IItemsQueries.GetItemsAsync.
filterValueTryGetStringPassed through to IItemsQueries.GetItemsAsync.
searchFilterTryGetString, then SearchFilterSpec.ParseOnly the top (take) segment is applied today — see the sort gap below.
renderType (top-level field, not a Parameters key)TemplateDto.RenderTypeStamped onto every resolved row's ItemDto.ItemType before returning, so ItemComponentFactory's existing switch picks the right ViewModel unchanged.

Data source: a new port, IItemsQueries.GetItemsAsync(scope, category, filterValue, cancellationToken), returning IReadOnlyList<ItemDto> — already component-shaped rows, not a raw DTO needing further mapping. It lives in ALPAMobile.Application/Abstractions/HomeFeed/IItemsQueries.cs, alongside IHomeFeedQueries (the two are distinct: IHomeFeedQueries fetches an already-composed PageDto; IItemsQueries fetches the flat row set a dynamic list template pulls from).

No concrete IItemsQueries implementation exists yet. DynamicListFactory is implemented but deliberately left out of the IFeedTemplateContentFactory registrations in HeadServiceCollectionExtensions.cs — registering it today would fail at startup with an unresolved-service error, since it depends on IItemsQueries. DynamicPageOrchestrator itself, PilotCardFactory, and LecLinkFactory are registered and run today; a Template.ContentType == "DynamicList" item currently falls through the "unresolvable ContentType" branch and is silently dropped. See §5.

Sort is not implemented. SearchFilterSpec.SortBy/Descending are parsed but unused — ItemDto has no generic sortable field (e.g. a date) to sort arbitrary rows by, and fabricating one wasn't in scope. Only Top (take) is applied. This mirrors Q5 in the adapter-gaps doc: the searchFilter grammar's full vocabulary isn't confirmed yet.

4. DynamicPageOrchestrator — the orchestrator

DynamicPageOrchestrator.ResolveAsync(PageDto page, CancellationToken) walks page.Containers, and within each container's Items:

  1. An item with Template is null is static — kept as-is, in place.
  2. An item with Template set is dispatched by Template.ContentType to the matching IFeedTemplateContentFactory (resolved from an IEnumerable<IFeedTemplateContentFactory> injected into the constructor and keyed into a dictionary once). The single templated item is replaced in place by however many ItemDtos the adapter's ResolveAsync returns — zero, one, or many.
  3. A missing ContentType or one with no registered adapter drops the item — never the container, never the page.

Once every container's item list is fully static, DynamicPageOrchestrator hands the same PageDto to the existing, unmodified PageFactory.Create and returns the resulting PageViewModel directly — callers get one method to await instead of having to remember to chain resolution and mapping themselves.

Task<PageViewModel> ResolveAsync(PageDto page, CancellationToken ct = default) { foreach (container in page.Containers) container.Items = await ResolveItemsAsync(container.Items, ct); return _pageFactory.Create(page); }

Order preservation: resolved items are spliced in at the position their source template item occupied — a container with [static, template, static] items keeps that same left-to-right order, with the template slot expanding to N items in place.

Any ItemDto, not just itemType: "List": the adapter-gaps doc originally scoped template expansion to a repeater on an itemType: "List" item specifically. The implementation is more general — any item with Template set gets expanded, regardless of its (otherwise-ignored) ItemType, since the resolved rows carry their own ItemType via TemplateDto.RenderType anyway. Worth confirming this matches the intended contract before more adapters are built against it.

ContentResolutionContext and InheritTypographyTokens (Orchestrator Refactor Phase 5–6): step 2 above now builds a ContentResolutionContext (source item, template, current user, page id, container type) per templated item and calls the context-aware ResolveAsync overload from §2. DynamicPageOrchestrator still runs its own post-hoc InheritTypographyTokens step afterward, copying the outer template item's typography tokens onto each resolved row — this was deliberately not removed when the wider context was added, because no factory yet reads those tokens off ContentResolutionContext.SourceItem itself. Removing the copy now would silently drop working behavior with nothing replacing it; the refactor plan tracks this as a follow-up for whenever a factory actually migrates to reading SourceItem directly.

5. Source map & status

FileStatus
ALPAMobile.Presentation/Adapters/ContentResolvers/IFeedTemplateContentFactory.csDone — includes the Phase 5 ContentResolutionContext-aware overload (§2)
ALPAMobile.Presentation/Adapters/ContentResolvers/FeedTemplateResolutionHelpers.cs NewDone — shared render-type/favoritable helpers (§2), used by 6 of the 7 registered factories
PilotCardFactory.cs, LecLinkFactory.cs, CommitteeListFactory.cs, CommitteeMembersFactory.cs, DocumentHeroFactory.cs, PodcastEpisodeFactory.cs, MecHotelDirectoryFactory.csDone — all 7 registered, running live
ALPAMobile.Presentation/Adapters/DynamicListFactory.csImplemented (sort gap noted above) — Not registered, unchanged by the Orchestrator Refactor
ALPAMobile.Presentation/Adapters/Orchestration/DynamicPageOrchestrator.csDone — registered as a singleton, resolves the IFeedTemplateContentFactory set via DI collection injection; also exposes ResolveProgressivelyAsync for the per-container progressive-load path
ALPAMobile.Presentation/Pages/DynamicFeed/DynamicPageLoader.cs (IDynamicPageLoader) NewDone — registered scoped; common page-loading entry point in front of the orchestrator (§1). LocalizedDynamicPageLoader delegates to it.
ALPAMobile.Presentation/Components/Library/DynamicContentHost.razor NewDone — shared page-hosting shell; used by MecHomePage/LecPage, not yet by DynamicFeedOrchestratorPage (needs a FixedContent slot) or AlpaIHomePage (real page-specific logic, stays dedicated)
ALPAMobile.Application/Abstractions/HomeFeed/IItemsQueries.csInterface only — no implementation, blocks registering DynamicListFactory. Unchanged, tracked risk in the Orchestrator Refactor plan's §8.
DI registration (ALPAMobile/HeadServiceCollectionExtensions.cs → AddHeadServices)Wired for DynamicPageOrchestrator, IDynamicPageLoader, all 7 factories above, PageFactory, ContainerFactory, ItemComponentFactory  |  Not wired for DynamicListFactory
Call sites — see §1 for the current per-page breakdown (MecHomePage/LecPage via DynamicContentHost; DynamicFeedOrchestratorPage via IDynamicPageLoader.LoadProgressivelyAsync with its own shell; AlpaIHomePage via DynamicPageOrchestrator.ResolveAsync directly)Wired — HomePreviewPage, previously the one caller bypassing template resolution, no longer exists in the codebase