← 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  |  Status: DynamicPageOrchestrator and three content adapters (PilotCardFactory, LecLinkFactory, DynamicListFactory) implemented; called live from MecHomePage, HomeAlpaPage & DynamicFeedOrchestratorPage; DynamicListFactory alone still not wired into DI  |  Updated: 2026-08-29 07:30 ET  |  Audited: 2026-08-14 14:27 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.

1. Where template resolution sits in the pipeline

All template-resolution types live in ALPAMobile.Presentation/Adapters/ — 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.

IHomeFeedQueries.GetPageAsync
DynamicPageOrchestrator.ResolveAsync
PageFactory.Create
ContainerFactory / ItemComponentFactory
PageViewModel

Callers: MecHomePage.razor.cs (route /home-mec, live IHomeFeedQueriesDynamicContentApiClientgatewayapi.alpa.org), HomeAlpaPage.razor.cs (route /home-alpa, backed by MockAlpaHomeFeedService), and DynamicFeedOrchestratorPage.razor.cs (route /dynamic-feed-orchestrator?page={pageKey}, CMS deep links) — each awaits Feed.GetPageAsync(pageKey) then Orchestrator.ResolveAsync(page) in OnInitializedAsync, wrapped in try/catch falling back to an empty component list. HomePreviewPage.razor.cs (route /home-preview) is the one exception — it still calls PageFactory.Create directly, skipping template resolution, as the legacy shape for pages that haven't rolled up onto templates yet.

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).

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.

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.

5. Source map & status

FileStatus
ALPAMobile.Presentation/Adapters/IFeedTemplateContentFactory.csDone
ALPAMobile.Presentation/Adapters/PilotCardFactory.csDone — registered, running live
ALPAMobile.Presentation/Adapters/LecLinkFactory.csDone — registered, running live
ALPAMobile.Presentation/Adapters/DynamicListFactory.csImplemented (sort gap noted above) — Not registered
ALPAMobile.Presentation/Adapters/DynamicPageOrchestrator.csDone — registered as a singleton, resolves the IFeedTemplateContentFactory set via DI collection injection
ALPAMobile.Application/Abstractions/HomeFeed/IItemsQueries.csInterface only — no implementation, blocks registering DynamicListFactory
DI registration (ALPAMobile/HeadServiceCollectionExtensions.csAddHeadServices)Wired for DynamicPageOrchestrator, PilotCardFactory, LecLinkFactory, PageFactory, ContainerFactory, ItemComponentFactory  |  Not wired for DynamicListFactory
Call sites — MecHomePage.razor.cs, HomeAlpaPage.razor.cs, DynamicFeedOrchestratorPage.razor.cs all invoke DynamicPageOrchestrator.ResolveAsync from OnInitializedAsyncWiredHomePreviewPage.razor.cs still bypasses it and calls PageFactory.Create directly (legacy shape)