# Blazor Component Migration Design ## Purpose Migrate all Blazor components from using the old `I*Service` interfaces to the new `I*ApiClient` interfaces, implementing proper error handling with the `ApiResult` discriminated union pattern. ## Architecture ### View Model Mapping The client has its own view models (`JdeScoping.Client.Models.*`) that differ from Core view models (`JdeScoping.Core.ViewModels.*`): | Client | Core | Differences | |--------|------|-------------| | `SearchViewModel` | `SearchViewModel` | Client uses `string Status`, Core uses `SearchStatus` enum | | `SearchCriteriaViewModel` | `SearchCriteria` | Same structure, different namespaces | | `ItemViewModel` | `ItemViewModel` | Same | | `OperatorViewModel` | `JdeUserViewModel` | Different names | **Strategy**: Create mapping extension methods to convert Core -> Client view models. ### Error Handling Strategy **Hybrid approach:** - **Global 401 handling**: `AuthRedirectHandler` (DelegatingHandler) intercepts all 401 responses and redirects to `/login` - **Component-level handling**: Components use `result.Switch()` for all other cases (success, not found, validation errors, general errors) ### Why Keep Unauthorized in ApiResult Even with global 401 handling, we keep `Unauthorized` in the type for: 1. **Completeness** - Type accurately represents all possible server responses 2. **Defense in depth** - Fallback if handler fails or is bypassed 3. **Testing** - Can test unauthorized scenarios without HTTP layer 4. **Future flexibility** - May need component-specific handling later ### Pattern Usage Components use `result.Switch()` directly - no shared helper method. This provides: - Explicit handling at each call site - Flexibility for different UI patterns per component - Clear, readable code without indirection ## Components ### AuthRedirectHandler ```csharp public class AuthRedirectHandler : DelegatingHandler { private readonly NavigationManager _navigationManager; public AuthRedirectHandler(NavigationManager navigationManager) { _navigationManager = navigationManager; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); if (response.StatusCode == HttpStatusCode.Unauthorized) { var returnUrl = Uri.EscapeDataString(_navigationManager.Uri); _navigationManager.NavigateTo($"/login?returnUrl={returnUrl}", forceLoad: true); } return response; } } ``` ### View Model Mapping Extensions ```csharp public static class ViewModelMappingExtensions { public static ClientSearchViewModel ToClient(this Core.ViewModels.SearchViewModel vm) => new() { Id = vm.Id, Name = vm.Name, UserName = vm.UserName, Status = vm.Status.ToString(), SubmitDt = vm.SubmitDt, StartDt = vm.StartDt, EndDt = vm.EndDt, Criteria = vm.Criteria?.ToClientCriteria() ?? new() }; public static Core.ViewModels.SearchViewModel ToCore(this ClientSearchViewModel vm) => new() { Id = vm.Id, Name = vm.Name, UserName = vm.UserName, Status = Enum.Parse(vm.Status), SubmitDt = vm.SubmitDt, StartDt = vm.StartDt, EndDt = vm.EndDt, Criteria = vm.Criteria.ToCoreCriteria() }; } ``` ### Component Pattern Before: ```csharp @inject ISearchService SearchService var searches = await SearchService.GetUserSearchesAsync(); ``` After: ```csharp @inject ISearchApiClient SearchApi var result = await SearchApi.GetUserSearchesAsync(); result.Switch( searches => { _searches = searches.Select(s => s.ToClient()).ToList(); }, notFound => { _errorMessage = "Not found"; }, validation => { _errorMessage = validation.Message; }, unauthorized => { /* handled globally, but fallback */ }, forbidden => { _errorMessage = "Access denied"; }, error => { _errorMessage = error.Message; } ); ``` ## Files to Change ### Create - `Client/Http/AuthRedirectHandler.cs` - Global 401 redirect handler - `Client/Extensions/ViewModelMappingExtensions.cs` - Core <-> Client mapping ### Modify (12 files) **Pages:** 1. `Pages/Searches.razor` - Uses `ISearchService` 2. `Pages/SearchEdit.razor` - Uses `ISearchService`, `IFileService` 3. `Pages/SearchQueue.razor` - Uses `ISearchService` 4. `Pages/Login.razor` - Uses `IAuthService` 5. `Layout/MainLayout.razor` - Uses `IAuthService` **Filter Panels:** 6. `Components/FilterPanels/ItemNumberFilterPanel.razor` - Uses `ILookupService`, `IFileService` 7. `Components/FilterPanels/WorkCenterFilterPanel.razor` - Uses `ILookupService` 8. `Components/FilterPanels/ProfitCenterFilterPanel.razor` - Uses `ILookupService` 9. `Components/FilterPanels/OperatorFilterPanel.razor` - Uses `ILookupService` 10. `Components/FilterPanels/WorkOrderFilterPanel.razor` - Uses `IFileService` 11. `Components/FilterPanels/ComponentLotFilterPanel.razor` - Uses `IFileService` 12. `Components/FilterPanels/PartOperationFilterPanel.razor` - Uses `IFileService` ### Update - `Client/Program.cs` - Register AuthRedirectHandler, configure HttpClient, remove old services ### Delete (8 old service files) - `Client/Services/ISearchService.cs` - `Client/Services/SearchService.cs` - `Client/Services/ILookupService.cs` - `Client/Services/LookupService.cs` - `Client/Services/IAuthService.cs` - `Client/Services/AuthService.cs` - `Client/Services/IFileService.cs` - `Client/Services/FileService.cs` ### Keep (not migrating) - `IRefreshStatusService` / `RefreshStatusService` - No corresponding API client yet - `IHubConnectionService` / `HubConnectionService` - SignalR, not HTTP - `ICryptoService` / `CryptoService` - Client-side encryption ## DI Registration ```csharp // Add handler builder.Services.AddTransient(); // Configure HttpClient with handler pipeline builder.Services.AddScoped(sp => { var navigationManager = sp.GetRequiredService(); var handler = new AuthRedirectHandler(navigationManager) { InnerHandler = new HttpClientHandler() }; return new HttpClient(handler) { BaseAddress = new Uri(sp.GetRequiredService().BaseAddress) }; }); // Remove old service registrations // - builder.Services.AddScoped(); // - builder.Services.AddScoped(); // - builder.Services.AddScoped(); // - builder.Services.AddScoped(); ``` ## Migration Order 1. Create foundation files (AuthRedirectHandler, ViewModelMappingExtensions) 2. Update Program.cs to register handler 3. Migrate pages one at a time, testing each: - Searches.razor (simplest, read-only) - SearchQueue.razor (read-only) - SearchEdit.razor (most complex, read + write) - Login.razor (auth) - MainLayout.razor (logout) 4. Migrate filter panels: - ItemNumberFilterPanel (lookup + file) - WorkCenterFilterPanel (lookup only) - ProfitCenterFilterPanel (lookup only) - OperatorFilterPanel (lookup only) - WorkOrderFilterPanel (file only) - ComponentLotFilterPanel (file only) - PartOperationFilterPanel (file only) 5. Delete old service files after all components migrated 6. Final verification pass ## Acceptance Criteria - [ ] All 401 responses redirect to `/login` with return URL - [ ] Components display appropriate error messages for each error type - [ ] No old `I*Service` interfaces remain in use - [ ] Old service files deleted - [ ] All components compile and function correctly - [ ] Authentication flow works end-to-end - [ ] View model mapping works correctly (Core <-> Client)