TL;DR: Plan (Epic #2081 / WI-2086, Phase 4 of the N-Tier refactor) to decompose theDataManager(~2,403 lines) andRestService(~2,073 lines) singletons into 8 focused Infrastructure services, one per domain. The Application-layer interfaces and DI aliases already exist, so each migration is an alias swap in DI — callers see no change. Work runs in 4 batches ordered low → high risk, ending with DataManager/RestService deletion; completing WI-2086 unblocks WI-2087 (Presentation Extraction).
Epic #2081 / WI-2086 (Phase 4)
DataManager.cs (~2,403 lines) and RestService.cs (~2,073 lines) are both registered as application-wide singletons and together handle every cache-aside operation and HTTP call in the app. This plan decomposes them into 8 focused Infrastructure service implementations, each owning one domain's caching logic and its corresponding HTTP calls.
Why now: All Application-layer interfaces are already extracted (ALPAMobile.Application/Abstractions/). The DI aliases (IDocumentsQueries → IDataManager, etc.) are in place in ALPAMobile/HeadServiceCollectionExtensions.cs. The split is a substitution: remove the alias, register a real implementation. No callers need to change; they already depend on the interface.
Target state:
DataManager retained as a shell during migration; deleted once all 8 services are registeredRestService internals absorbed into each domain service; IRestService registration removed when the last method migratesALPAMobile.Infrastructure/ owns all concrete HTTP + caching codeALPAMobile.Application/ contains only interfaces, DTOs, and domain events| # | Service class | Interface(s) | Key domain |
|---|---|---|---|
| 1 | ContentService | IContentQueries | Page banners, app config, company info, resource links, call-to-action, in-case-of-accident, app update status |
| 2 | MemberService | IMemberQueries | Member profile, member number lookup, PAC member info |
| 3 | MenuService | IMenuQueries | Menu items (including mock items) |
| 4 | JumpseatService | IJumpseatQueries | Airline jumpseat policies, jumpseat airlines, KCM airports, KCM airlines |
| 5 | MECService | IMECQueries | MEC/LEC events, representatives, hotel airports, committees |
| 6 | NotificationsService | INotificationsQueries, ISubscriptionsQueries | Notification list, push token registration, subscriptions toggle |
| 7 | DocumentsService | IDocumentsQueries, IDocumentsCommands | Document list, scoped docs, categories, download, local DB cache |
| 8 | FlightSearchService | IFlightSearchQueries | Airports, flight search, saved flights refresh, flight status |
Note:IFTDTQueries(GetAllDutyPeriodsList,GetSoftDeletedDutyPeriodsList) reads fromFTDTDataBase(local SQLite), not DataManager's HTTP cache. Those two methods stay inFTDTDataSyncHelper/FTDTDataBase. The FTDT remote calls (CreateDutyPeriodAsync,UpdateDutyPeriodAsync,SoftDeleteDutyPeriodAsync) migrate with RestService's internals but are not a new Infrastructure service.
The DI aliases in ALPAMobile/HeadServiceCollectionExtensions.cs are the seam. Remove one alias at a time and register its real implementation:
// Before
services.AddSingleton<IDataManager, DataManager>();
services.AddSingleton<IContentQueries>(sp => sp.GetRequiredService<IDataManager>());
// After (one interface at a time)
services.AddSingleton<IContentQueries, ContentService>();
// IDataManager alias for IContentQueries removed
Callers (ViewModels, Pages) already inject IContentQueries; they see no change.
Base: imp/blazor-hybrid (active target, WI-2086 is Phase 4). Four batches, each merging back before the next starts:
imp/blazor-hybrid
└── infra/split-batch-1 (ContentService + MemberService)
└── infra/split-batch-2 (MenuService + JumpseatService)
└── infra/split-batch-3 (MECService + NotificationsService)
└── infra/split-batch-4 (DocumentsService + FlightSearchService + DataManager teardown)
Each new service implements IAuthContextCache. Authentication.cs already resolves IEnumerable<IAuthContextCache> and calls ClearCache() on all of them on sign-out. Verify this wiring is in place before starting Batch 2.
// ALPAMobile.Infrastructure/Content/ContentService.cs
internal sealed class ContentService : IContentQueries, IAuthContextCache
{
private readonly IRequestProvider _http;
private ObservableCollection<PageBanner>? _pageBanners;
private AppConfigSettings? _appConfig;
// ... other cache fields
public ContentService(IRequestProvider http) => _http = http;
public async Task<ObservableCollection<PageBanner>?> GetPageBannersAsync(bool refresh = false)
{
if (!refresh && _pageBanners is not null)
return _pageBanners;
_pageBanners = await _http.GetAsync<ObservableCollection<PageBanner>>(ApiEndpoints.PageBanners);
return _pageBanners;
}
// IAuthContextCache — called by Authentication on sign-out
public void ClearCache()
{
_pageBanners = null;
_appConfig = null;
// ... clear all fields
}
}
// ALPAMobile/HeadServiceCollectionExtensions.cs — AFTER Batch 4 (IDataManager and IRestService removed entirely)
builder.Services.AddSingleton<IContentQueries, ContentService>();
builder.Services.AddSingleton<IMemberQueries, MemberService>();
builder.Services.AddSingleton<IMenuQueries, MenuService>();
builder.Services.AddSingleton<IJumpseatQueries, JumpseatService>();
builder.Services.AddSingleton<IMECQueries, MECService>();
// NotificationsService implements two interfaces (shared instance trick):
builder.Services.AddSingleton<NotificationsService>();
builder.Services.AddSingleton<INotificationsQueries>(sp => sp.GetRequiredService<NotificationsService>());
builder.Services.AddSingleton<ISubscriptionsQueries>(sp => sp.GetRequiredService<NotificationsService>());
builder.Services.AddSingleton<IDocumentsQueries, DocumentsService>();
builder.Services.AddSingleton<IDocumentsCommands>(sp => sp.GetRequiredService<DocumentsService>());
builder.Services.AddSingleton<IFlightSearchQueries, FlightSearchService>();
Each batch adds registrations to ALPAMobile.Infrastructure/InfrastructureServiceCollectionExtensions.cs (not MauiProgram.cs). IAuthContextCache is registered multiple times intentionally — IEnumerable<IAuthContextCache> resolution calls ClearCache() on all implementations.
grep -rn "IDataManager" ALPAMobile/ViewModels/ ALPAMobile/Pages/
Any ViewModel still injecting IDataManager directly (bypassing the interface) must be updated before the DataManager alias for that interface can be removed.
Risk: Low. Pure HTTP GET + in-memory cache; no local DB writes; no cross-service deps.
ALPAMobile.Infrastructure/Content/ContentService.csALPAMobile.Infrastructure/Member/MemberService.csRestService.cs URL strings → ApiEndpoints static classInfrastructureServiceCollectionExtensionsALPAMobile/HeadServiceCollectionExtensions.csDataManager.csdotnet build ALPAMobile.sln — confirm 0 errorsdotnet test UnitTest/UnitTest.csprojRisk: Low–Medium. Menu has a large response body with recursive category tree; verify IAuthContextCache wiring in Authentication.cs before tagging services as implementors. Smoke test: Menu navigation, KCM Airports, Jumpseat Policies.
Risk: Medium. Subscription toggle is a POST that invalidates the cache — verify ToggleSubscriptionAsync clears _subscriptions before returning. Handle dual-registration pattern for NotificationsService. Smoke test: Advocacy page, Notifications center, MEC events.
Risk: High. Documents has local DB reads/writes + file download + scope-indexed in-memory cache. Test download resumption and scope invalidation paths explicitly.
DocumentsService (depends on IDocumentDatabase / LiteDB + IRequestProvider)FlightSearchServiceIDataManager direct injectionsIDataManager registration from ALPAMobile/HeadServiceCollectionExtensions.csDataManager.cs and RestService.csIRestService registrationLayer-dependency rules already live in UnitTest/Architecture/LayerDependencyRulesTests.cs. Add a test there for each migrated batch to prevent regression:
// UnitTest/Architecture/LayerDependencyRulesTests.cs — add after each batch lands
[Test]
public void ContentService_must_not_depend_on_Application_implementations()
{
Types.InAssembly(typeof(ContentService).Assembly)
.That().HaveNameEndingWith("Service")
.Should().NotHaveDependencyOn("ALPADocs.Services")
.GetResult().IsSuccessful.Should().BeTrue();
}
ALPAMobile.Infrastructure/ sub-foldersDataManager.cs deleted (or reduced to ≤50 lines of delegating stubs)RestService.cs deletedIDataManager registration removed from ALPAMobile/HeadServiceCollectionExtensions.csIRestService registration removed from ALPAMobile/HeadServiceCollectionExtensions.csIAuthContextCache implemented on every service that holds in-memory cachedotnet build ALPAMobile.sln — 0 errors, 0 warnings addeddotnet test UnitTest/UnitTest.csproj — all passIRestEndpointProvider — DataManager currently implements this (exposes GetRestServiceBaseUrl() etc.). Should this move to a dedicated EndpointConfigService, or is it better placed in ISettingsService?FTDTDataSyncHelper — currently calls IRestService directly for create/update/delete DutyPeriods. After RestService is deleted, decide whether it calls IFTDTRemoteService or a method injected from FlightSearchService. Decide ownership before Batch 4.CacheDatabase vs in-memory — Some services cache in CacheDatabase (serialized to SQLite), others in in-memory fields. Recommendation: in-memory for session-only data (notifications, menus), CacheDatabase for data that must survive cold-start (documents list, member profile).DataManager has retry logic from RestService.RetryTransientAsync (see MauiProgram.cs comment AB#2039). RequestProvider already handles retry via Polly. Confirm no duplicate retry wrapping in new services.| Document | Location |
|---|---|
| N-Tier Architecture Refactor overview | n-tier-orchestration-handoff.html |
| Blazor Hybrid Migration Guide (service extraction roadmap) | blazor-hybrid-migration-guide.html |