d4135e8ad3
The WHERE clause was comparing Code to itself instead of the aliased table reference, which would always be true.
228 lines
7.9 KiB
Markdown
228 lines
7.9 KiB
Markdown
# 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<T>` 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<T>
|
|
|
|
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<HttpResponseMessage> 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<SearchStatus>(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<AuthRedirectHandler>();
|
|
|
|
// Configure HttpClient with handler pipeline
|
|
builder.Services.AddScoped(sp =>
|
|
{
|
|
var navigationManager = sp.GetRequiredService<NavigationManager>();
|
|
var handler = new AuthRedirectHandler(navigationManager)
|
|
{
|
|
InnerHandler = new HttpClientHandler()
|
|
};
|
|
return new HttpClient(handler)
|
|
{
|
|
BaseAddress = new Uri(sp.GetRequiredService<IWebAssemblyHostEnvironment>().BaseAddress)
|
|
};
|
|
});
|
|
|
|
// Remove old service registrations
|
|
// - builder.Services.AddScoped<ISearchService, SearchService>();
|
|
// - builder.Services.AddScoped<ILookupService, LookupService>();
|
|
// - builder.Services.AddScoped<IAuthService, AuthService>();
|
|
// - builder.Services.AddScoped<IFileService, FileService>();
|
|
```
|
|
|
|
## 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)
|