fix(data-access): correct self-referential SQL in WorkCenter filter
The WHERE clause was comparing Code to itself instead of the aliased table reference, which would always be true.
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user