The WHERE clause was comparing Code to itself instead of the aliased table reference, which would always be true.
31 KiB
Blazor Component Migration Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Migrate all Blazor components from old I*Service interfaces to new I*ApiClient interfaces with proper ApiResult<T> error handling.
Architecture: Create AuthRedirectHandler for global 401 handling, view model mapping extensions for Core<->Client conversion, then update each component to inject API clients and use result.Switch() pattern.
Tech Stack: Blazor WebAssembly, OneOf discriminated unions, DelegatingHandler, extension methods
Task 1: Create AuthRedirectHandler
Files:
- Create:
src/JdeScoping.Client/Http/AuthRedirectHandler.cs
Step 1: Create the handler file
using System.Net;
using Microsoft.AspNetCore.Components;
namespace JdeScoping.Client.Http;
/// <summary>
/// HTTP message handler that intercepts 401 Unauthorized responses
/// and redirects to the login page with return URL.
/// </summary>
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;
}
}
Step 2: Verify file compiles
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
Expected: Build succeeded
Step 3: Commit
git add src/JdeScoping.Client/Http/AuthRedirectHandler.cs
git commit -m "feat(client): add AuthRedirectHandler for global 401 redirect"
Task 2: Create View Model Mapping Extensions
Files:
- Create:
src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs
Step 1: Create the mapping extensions file
using JdeScoping.Client.Models;
using JdeScoping.Core.Models.Enums;
using JdeScoping.Core.Models.Search;
using CoreSearch = JdeScoping.Core.ViewModels.SearchViewModel;
using CoreItem = JdeScoping.Core.ViewModels.ItemViewModel;
using CoreWorkOrder = JdeScoping.Core.ViewModels.WorkOrderViewModel;
using CoreProfitCenter = JdeScoping.Core.ViewModels.ProfitCenterViewModel;
using CoreWorkCenter = JdeScoping.Core.ViewModels.WorkCenterViewModel;
using CoreLot = JdeScoping.Core.ViewModels.LotViewModel;
using CorePartOp = JdeScoping.Core.ViewModels.PartOperationViewModel;
using CoreJdeUser = JdeScoping.Core.ViewModels.JdeUserViewModel;
namespace JdeScoping.Client.Extensions;
/// <summary>
/// Extension methods for mapping between Core and Client view models.
/// </summary>
public static class ViewModelMappingExtensions
{
// SearchViewModel: Core -> Client
public static SearchViewModel ToClient(this CoreSearch 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()
};
// SearchViewModel: Client -> Core
public static CoreSearch ToCore(this SearchViewModel vm) => new()
{
Id = vm.Id,
Name = vm.Name,
UserName = vm.UserName,
Status = Enum.TryParse<SearchStatus>(vm.Status, out var status) ? status : SearchStatus.New,
SubmitDt = vm.SubmitDt,
StartDt = vm.StartDt,
EndDt = vm.EndDt,
Criteria = vm.Criteria.ToCoreCriteria()
};
// SearchCriteria: Core -> Client
public static SearchCriteriaViewModel ToClientCriteria(this SearchCriteria criteria)
{
var client = new SearchCriteriaViewModel
{
MinimumDt = criteria.MinimumDt,
MaximumDt = criteria.MaximumDt,
ExtractMisData = criteria.ExtractMisData
};
// Map work orders (Core has just numbers, Client has full objects)
client.WorkOrders = criteria.WorkOrderNumbers
.Select(n => new CoreWorkOrder { WorkOrderNumber = n })
.ToList();
// Map items
client.Items = criteria.ItemNumbers
.Select(n => new CoreItem { ItemNumber = n })
.ToList();
// Map profit centers
client.ProfitCenters = criteria.ProfitCenters
.Select(pc => new CoreProfitCenter { ProfitCenterCode = pc })
.ToList();
// Map work centers
client.WorkCenters = criteria.WorkCenters
.Select(wc => new CoreWorkCenter { WorkCenterCode = wc })
.ToList();
// Map operators
client.Operators = criteria.OperatorIDs
.Select(id => new OperatorViewModel { UserId = id })
.ToList();
// Map component lots (Core and Client both use LotViewModel)
client.ComponentLots = criteria.ComponentLotNumbers
.Select(l => new ComponentLotViewModel { LotNumber = l.LotNumber, ItemNumber = l.ItemNumber })
.ToList();
// Map part operations (same structure)
client.PartOperations = criteria.PartOperations.ToList();
return client;
}
// SearchCriteria: Client -> Core
public static SearchCriteria ToCoreCriteria(this SearchCriteriaViewModel criteria) => new()
{
MinimumDt = criteria.MinimumDt,
MaximumDt = criteria.MaximumDt,
ExtractMisData = criteria.ExtractMisData,
WorkOrderNumbers = criteria.WorkOrders.Select(wo => wo.WorkOrderNumber).ToList(),
ItemNumbers = criteria.Items.Select(i => i.ItemNumber).ToList(),
ProfitCenters = criteria.ProfitCenters.Select(pc => pc.ProfitCenterCode).ToList(),
WorkCenters = criteria.WorkCenters.Select(wc => wc.WorkCenterCode).ToList(),
OperatorIDs = criteria.Operators.Select(o => o.UserId).ToList(),
ComponentLotNumbers = criteria.ComponentLots
.Select(l => new CoreLot { LotNumber = l.LotNumber, ItemNumber = l.ItemNumber })
.ToList(),
PartOperations = criteria.PartOperations.ToList()
};
// JdeUserViewModel -> OperatorViewModel
public static OperatorViewModel ToClientOperator(this CoreJdeUser vm) => new()
{
AddressNumber = (int)vm.AddressNumber,
UserId = vm.UserId,
FullName = vm.FullName
};
// Collection helpers
public static List<SearchViewModel> ToClientList(this IEnumerable<CoreSearch> list) =>
list.Select(s => s.ToClient()).ToList();
public static List<OperatorViewModel> ToClientOperatorList(this IEnumerable<CoreJdeUser> list) =>
list.Select(u => u.ToClientOperator()).ToList();
}
Step 2: Verify file compiles
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
Expected: Build succeeded
Step 3: Commit
git add src/JdeScoping.Client/Extensions/ViewModelMappingExtensions.cs
git commit -m "feat(client): add ViewModelMappingExtensions for Core<->Client mapping"
Task 3: Update Program.cs with HttpClient Handler Pipeline
Files:
- Modify:
src/JdeScoping.Client/Program.cs
Step 1: Update HttpClient registration to use handler
Replace the current HttpClient registration with handler pipeline:
// OLD:
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
// NEW:
builder.Services.AddScoped<AuthRedirectHandler>();
builder.Services.AddScoped(sp =>
{
var navigationManager = sp.GetRequiredService<NavigationManager>();
var handler = new AuthRedirectHandler(navigationManager)
{
InnerHandler = new HttpClientHandler()
};
return new HttpClient(handler)
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
};
});
Step 2: Add required using statement
Add at top of file:
using JdeScoping.Client.Http;
Step 3: Verify build
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
Expected: Build succeeded
Step 4: Commit
git add src/JdeScoping.Client/Program.cs
git commit -m "feat(client): configure HttpClient with AuthRedirectHandler"
Task 4: Migrate Searches.razor
Files:
- Modify:
src/JdeScoping.Client/Pages/Searches.razor
Step 1: Update inject statement
Change:
@inject ISearchService SearchService
To:
@inject ISearchApiClient SearchApi
Step 2: Add using for extensions
Add after @page directives:
@using JdeScoping.Client.Extensions
Step 3: Update LoadSearchesAsync method
Replace the entire method with:
private async Task LoadSearchesAsync()
{
_isLoading = true;
_errorMessage = null;
try
{
var result = await SearchApi.GetUserSearchesAsync();
result.Switch(
searches => { _searches = searches.ToClientList(); },
notFound => { _errorMessage = "No searches found."; _searches = []; },
validation => { _errorMessage = validation.Message; },
unauthorized => { _errorMessage = "Session expired. Please login again."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
}
finally
{
_isLoading = false;
}
}
Step 4: Add error message field and display
Add field in @code section:
private string? _errorMessage;
Add error display in markup after loading indicator:
@if (!string.IsNullOrEmpty(_errorMessage))
{
<RadzenAlert AlertStyle="AlertStyle.Danger" ShowIcon="true" Variant="Variant.Flat" class="rz-mb-4">
@_errorMessage
</RadzenAlert>
}
Step 5: Verify build
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
Expected: Build succeeded
Step 6: Commit
git add src/JdeScoping.Client/Pages/Searches.razor
git commit -m "feat(client): migrate Searches.razor to ISearchApiClient"
Task 5: Migrate SearchQueue.razor
Files:
- Modify:
src/JdeScoping.Client/Pages/SearchQueue.razor
Step 1: Update inject statement
Change:
@inject ISearchService SearchService
To:
@inject ISearchApiClient SearchApi
Step 2: Add using for extensions
Add after @page directive:
@using JdeScoping.Client.Extensions
Step 3: Update LoadQueueAsync method
Replace with:
private async Task LoadQueueAsync()
{
_isLoading = true;
_errorMessage = null;
try
{
var result = await SearchApi.GetQueuedSearchesAsync();
result.Switch(
searches => { _searches = searches.ToClientList(); },
notFound => { _errorMessage = "Queue not found."; _searches = []; },
validation => { _errorMessage = validation.Message; },
unauthorized => { _errorMessage = "Session expired. Please login again."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
}
finally
{
_isLoading = false;
}
}
Step 4: Add error message field and display
Add field:
private string? _errorMessage;
Add error display after loading indicator.
Step 5: Verify build and commit
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
git add src/JdeScoping.Client/Pages/SearchQueue.razor
git commit -m "feat(client): migrate SearchQueue.razor to ISearchApiClient"
Task 6: Migrate SearchEdit.razor - Part 1 (Load Methods)
Files:
- Modify:
src/JdeScoping.Client/Pages/SearchEdit.razor
Step 1: Update inject statements
Change:
@inject ISearchService SearchService
@inject IFileService FileService
To:
@inject ISearchApiClient SearchApi
@inject IFileApiClient FileApi
Step 2: Add using for extensions
@using JdeScoping.Client.Extensions
Step 3: Update LoadSearchAsync method
Replace entire method with:
private async Task LoadSearchAsync()
{
_isLoading = true;
_errorMessage = null;
try
{
if (CopySearchId.HasValue)
{
var result = await SearchApi.CopySearchAsync(CopySearchId.Value);
result.Switch(
copied => {
_search = copied.ToClient();
_search.Id = 0;
_search.Status = "New";
},
notFound => { _errorMessage = "Search to copy not found."; },
validation => { _errorMessage = validation.Message; },
unauthorized => { _errorMessage = "Session expired."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
}
else if (Id.HasValue && Id.Value > 0)
{
var result = await SearchApi.GetSearchAsync(Id.Value);
result.Switch(
loaded => { _search = loaded.ToClient(); },
notFound => { _errorMessage = "Search not found."; },
validation => { _errorMessage = validation.Message; },
unauthorized => { _errorMessage = "Session expired."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
}
else
{
_search = new ClientSearchViewModel
{
Status = "New",
UserName = await AuthStateProvider.GetUsernameAsync() ?? "",
Criteria = new SearchCriteriaViewModel()
};
}
DetectSearchType();
}
finally
{
_isLoading = false;
}
}
Step 4: Add error message field
private string? _errorMessage;
Step 5: Verify build
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
Expected: Build succeeded
Step 6: Commit
git add src/JdeScoping.Client/Pages/SearchEdit.razor
git commit -m "feat(client): migrate SearchEdit.razor load methods to API clients"
Task 7: Migrate SearchEdit.razor - Part 2 (Save and Download Methods)
Files:
- Modify:
src/JdeScoping.Client/Pages/SearchEdit.razor
Step 1: Update SubmitSearchInternalAsync method
Replace the save call:
private async Task SubmitSearchInternalAsync()
{
var confirmed = await DialogService.Confirm("Are you sure you want to submit the search?", "Confirm Submit", new ConfirmOptions
{
OkButtonText = "Submit",
CancelButtonText = "Cancel"
});
if (confirmed != true)
{
return;
}
_isSubmitting = true;
try
{
var result = await SearchApi.CreateSearchAsync(_search.ToCore());
result.Switch(
id => { NavigationManager.NavigateTo($"/search/{id}"); },
notFound => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Search not found."); },
validation => { NotificationService.Notify(NotificationSeverity.Error, "Validation Error", validation.Message); },
unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
);
}
finally
{
_isSubmitting = false;
}
}
Step 2: Update DownloadResultsAsync method
Replace with:
private async Task DownloadResultsAsync()
{
var result = await SearchApi.GetResultsAsync(_search.Id);
result.Switch(
bytes =>
{
if (bytes.Length > 0)
{
_ = JSRuntime.InvokeVoidAsync("downloadFile", $"search_results_{_search.Id}.xlsx", bytes);
NotificationService.Notify(NotificationSeverity.Success, "Download", "Results downloaded successfully.");
}
else
{
NotificationService.Notify(NotificationSeverity.Warning, "Download", "No results available to download.");
}
},
notFound => { NotificationService.Notify(NotificationSeverity.Warning, "Download", "Results not found."); },
validation => { NotificationService.Notify(NotificationSeverity.Error, "Error", validation.Message); },
unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
);
}
Step 3: Verify build and commit
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
git add src/JdeScoping.Client/Pages/SearchEdit.razor
git commit -m "feat(client): migrate SearchEdit.razor save and download methods"
Task 8: Migrate Login.razor
Files:
- Modify:
src/JdeScoping.Client/Pages/Login.razor
Step 1: Update inject statement
Change:
@inject IAuthService AuthService
To:
@inject IAuthApiClient AuthApi
@inject ICryptoService CryptoService
Step 2: Update HandleLoginAsync method
Replace with:
private async Task HandleLoginAsync()
{
_isLoading = true;
_errorMessage = null;
try
{
// Encrypt credentials
var encryptedData = await CryptoService.EncryptLoginAsync(_loginModel);
var request = new EncryptedLoginRequest(encryptedData);
var result = await AuthApi.LoginAsync(request);
result.Switch(
loginResult =>
{
if (loginResult.Success && loginResult.User is not null)
{
var returnUrl = string.IsNullOrEmpty(ReturnUrl) ? "/" : ReturnUrl;
NavigationManager.NavigateTo(returnUrl);
}
else
{
_errorMessage = loginResult.ErrorMessage ?? "Login failed. Please check your credentials.";
}
},
notFound => { _errorMessage = "Authentication service not found."; },
validation => { _errorMessage = validation.Message; },
unauthorized => { _errorMessage = "Invalid credentials."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
}
catch (Exception ex)
{
_errorMessage = $"An error occurred: {ex.Message}";
}
finally
{
_isLoading = false;
}
}
Step 3: Add required using
@using JdeScoping.Core.Models
Step 4: Verify build and commit
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
git add src/JdeScoping.Client/Pages/Login.razor
git commit -m "feat(client): migrate Login.razor to IAuthApiClient"
Task 9: Migrate MainLayout.razor
Files:
- Modify:
src/JdeScoping.Client/Layout/MainLayout.razor
Step 1: Update inject statement
Change:
@inject IAuthService AuthService
To:
@inject IAuthApiClient AuthApi
Step 2: Update LogoutAsync method
Replace with:
private async Task LogoutAsync()
{
await AuthApi.LogoutAsync();
NavigationManager.NavigateTo("/login");
}
Step 3: Verify build and commit
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
git add src/JdeScoping.Client/Layout/MainLayout.razor
git commit -m "feat(client): migrate MainLayout.razor to IAuthApiClient"
Task 10: Migrate ItemNumberFilterPanel.razor
Files:
- Modify:
src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor
Step 1: Update inject statements
Change:
@inject ILookupService LookupService
@inject IFileService FileService
To:
@inject ILookupApiClient LookupApi
@inject IFileApiClient FileApi
Step 2: Update OnSearchAsync method
Replace with:
private async Task OnSearchAsync(LoadDataArgs args)
{
if (!string.IsNullOrEmpty(args.Filter) && args.Filter.Length >= 3)
{
var result = await LookupApi.FindItemsAsync(args.Filter);
result.Switch(
items => { _searchResults = items.ToList(); },
_ => { _searchResults = []; },
_ => { _searchResults = []; },
_ => { _searchResults = []; },
_ => { _searchResults = []; },
_ => { _searchResults = []; }
);
}
else
{
_searchResults = [];
}
}
Step 3: Update DownloadTemplateAsync method
Replace with:
private async Task DownloadTemplateAsync()
{
var result = await FileApi.DownloadItemsTemplateAsync(Items.AsReadOnly());
result.Switch(
bytes => { _ = JSRuntime.InvokeVoidAsync("downloadFile", "items_template.xlsx", bytes); },
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Template not found."); },
validation => { NotificationService.Notify(NotificationSeverity.Error, "Error", validation.Message); },
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
);
}
Step 4: Update OnFileSelected method
Replace with:
private async Task OnFileSelected(InputFileChangeEventArgs e)
{
if (e.File == null) return;
_isUploading = true;
try
{
using var stream = e.File.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
var result = await FileApi.UploadItemsAsync(stream, e.File.Name);
result.Switch(
items =>
{
Items.Clear();
Items.AddRange(items);
_ = ItemsChanged.InvokeAsync(Items);
NotificationService.Notify(NotificationSeverity.Success, "Upload Complete", $"Loaded {items.Count} items.");
},
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Upload endpoint not found."); },
validation => { NotificationService.Notify(NotificationSeverity.Error, "Upload Failed", validation.Message); },
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
_ => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
error => { NotificationService.Notify(NotificationSeverity.Error, "Upload Failed", error.Message); }
);
}
catch (Exception ex)
{
NotificationService.Notify(NotificationSeverity.Error, "Upload Failed", ex.Message);
}
finally
{
_isUploading = false;
}
}
Step 5: Verify build and commit
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
git add src/JdeScoping.Client/Components/FilterPanels/ItemNumberFilterPanel.razor
git commit -m "feat(client): migrate ItemNumberFilterPanel to API clients"
Task 11: Migrate WorkCenterFilterPanel.razor
Files:
- Modify:
src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor
Step 1: Update inject statement
Change @inject ILookupService LookupService to @inject ILookupApiClient LookupApi
Step 2: Update search method to use ApiResult pattern
Similar to ItemNumberFilterPanel, update the autocomplete search to use result.Switch().
Step 3: Verify build and commit
git add src/JdeScoping.Client/Components/FilterPanels/WorkCenterFilterPanel.razor
git commit -m "feat(client): migrate WorkCenterFilterPanel to ILookupApiClient"
Task 12: Migrate ProfitCenterFilterPanel.razor
Files:
- Modify:
src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor
Step 1: Update inject statement
Change @inject ILookupService LookupService to @inject ILookupApiClient LookupApi
Step 2: Update search method to use ApiResult pattern
Step 3: Verify build and commit
git add src/JdeScoping.Client/Components/FilterPanels/ProfitCenterFilterPanel.razor
git commit -m "feat(client): migrate ProfitCenterFilterPanel to ILookupApiClient"
Task 13: Migrate OperatorFilterPanel.razor
Files:
- Modify:
src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor
Step 1: Update inject statement and add using
Change @inject ILookupService LookupService to @inject ILookupApiClient LookupApi
Add: @using JdeScoping.Client.Extensions
Step 2: Update search method
Use result.Switch() and map JdeUserViewModel to OperatorViewModel using .ToClientOperatorList().
Step 3: Verify build and commit
git add src/JdeScoping.Client/Components/FilterPanels/OperatorFilterPanel.razor
git commit -m "feat(client): migrate OperatorFilterPanel to ILookupApiClient"
Task 14: Migrate WorkOrderFilterPanel.razor
Files:
- Modify:
src/JdeScoping.Client/Components/FilterPanels/WorkOrderFilterPanel.razor
Step 1: Update inject statement
Change @inject IFileService FileService to @inject IFileApiClient FileApi
Step 2: Update download and upload methods
Use result.Switch() pattern for DownloadWorkOrdersTemplateAsync and UploadWorkOrdersAsync.
Step 3: Verify build and commit
git add src/JdeScoping.Client/Components/FilterPanels/WorkOrderFilterPanel.razor
git commit -m "feat(client): migrate WorkOrderFilterPanel to IFileApiClient"
Task 15: Migrate ComponentLotFilterPanel.razor
Files:
- Modify:
src/JdeScoping.Client/Components/FilterPanels/ComponentLotFilterPanel.razor
Step 1: Update inject statement
Change @inject IFileService FileService to @inject IFileApiClient FileApi
Step 2: Update download and upload methods
Use result.Switch() pattern for DownloadComponentLotsTemplateAsync and UploadComponentLotsAsync.
Step 3: Verify build and commit
git add src/JdeScoping.Client/Components/FilterPanels/ComponentLotFilterPanel.razor
git commit -m "feat(client): migrate ComponentLotFilterPanel to IFileApiClient"
Task 16: Migrate PartOperationFilterPanel.razor
Files:
- Modify:
src/JdeScoping.Client/Components/FilterPanels/PartOperationFilterPanel.razor
Step 1: Update inject statement
Change @inject IFileService FileService to @inject IFileApiClient FileApi
Step 2: Update download and upload methods
Use result.Switch() pattern for DownloadPartOperationsTemplateAsync and UploadPartOperationsAsync.
Step 3: Verify build and commit
git add src/JdeScoping.Client/Components/FilterPanels/PartOperationFilterPanel.razor
git commit -m "feat(client): migrate PartOperationFilterPanel to IFileApiClient"
Task 17: Update Program.cs - Remove Old Service Registrations
Files:
- Modify:
src/JdeScoping.Client/Program.cs
Step 1: Remove old service registrations
Remove these lines:
builder.Services.AddScoped<ISearchService, SearchService>();
builder.Services.AddScoped<ILookupService, LookupService>();
builder.Services.AddScoped<IFileService, FileService>();
Keep:
builder.Services.AddScoped<IAuthService, AuthService>(); // Keep temporarily - AuthService has crypto logic
Step 2: Verify build
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
Expected: Build succeeded with no references to old services
Step 3: Commit
git add src/JdeScoping.Client/Program.cs
git commit -m "chore(client): remove old service registrations"
Task 18: Delete Old Service Files
Files:
- Delete:
src/JdeScoping.Client/Services/ISearchService.cs - Delete:
src/JdeScoping.Client/Services/SearchService.cs - Delete:
src/JdeScoping.Client/Services/ILookupService.cs - Delete:
src/JdeScoping.Client/Services/LookupService.cs - Delete:
src/JdeScoping.Client/Services/IFileService.cs - Delete:
src/JdeScoping.Client/Services/FileService.cs
Step 1: Delete files
rm src/JdeScoping.Client/Services/ISearchService.cs
rm src/JdeScoping.Client/Services/SearchService.cs
rm src/JdeScoping.Client/Services/ILookupService.cs
rm src/JdeScoping.Client/Services/LookupService.cs
rm src/JdeScoping.Client/Services/IFileService.cs
rm src/JdeScoping.Client/Services/FileService.cs
Step 2: Verify build
Run: dotnet build src/JdeScoping.Client/JdeScoping.Client.csproj
Expected: Build succeeded
Step 3: Commit
git add -A
git commit -m "chore(client): delete old service files replaced by API clients"
Task 19: Final Build Verification
Files:
- All modified files
Step 1: Clean and rebuild entire solution
Run:
dotnet clean
dotnet build
Expected: Build succeeded with 0 errors
Step 2: Run any available tests
Run:
dotnet test
Expected: All tests pass
Step 3: Commit if any fixes needed
git add -A
git commit -m "fix(client): address any build issues from migration"
Task 20: Update _Imports.razor
Files:
- Modify:
src/JdeScoping.Client/_Imports.razor
Step 1: Add using for extensions namespace
Add:
@using JdeScoping.Client.Extensions
Step 2: Verify build and commit
git add src/JdeScoping.Client/_Imports.razor
git commit -m "chore(client): add Extensions namespace to global imports"
Summary
This plan migrates 12 Blazor components from old I*Service interfaces to new I*ApiClient interfaces:
- 5 pages (Searches, SearchQueue, SearchEdit, Login, MainLayout)
- 7 filter panels (ItemNumber, WorkCenter, ProfitCenter, Operator, WorkOrder, ComponentLot, PartOperation)
Key changes:
- Global 401 handling via AuthRedirectHandler
- View model mapping via extension methods
- ApiResult pattern with Switch() for error handling
- Delete 6 old service files after migration