d4135e8ad3
The WHERE clause was comparing Code to itself instead of the aliased table reference, which would always be true.
1291 lines
40 KiB
Markdown
1291 lines
40 KiB
Markdown
# API Client Contracts Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Implement shared API contracts in Core that ensure compile-time safety for URLs, parameters, and return types between API and Client projects.
|
|
|
|
**Architecture:** Shared `ApiRoutes` constants define URLs usable in controller attributes and client code. Client interfaces (`ISearchApiClient`, etc.) return `ApiResult<T>` using OneOf for type-safe error handling. Controllers continue returning `ActionResult<T>` with proper HTTP semantics.
|
|
|
|
**Tech Stack:** .NET 10, OneOf library, Blazor WebAssembly, ASP.NET Core Web API
|
|
|
|
---
|
|
|
|
## Task 1: Add OneOf Package to Core
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
|
|
**Step 1: Add OneOf package reference**
|
|
|
|
Add to the `<ItemGroup>` containing PackageReferences in `JdeScoping.Core.csproj`:
|
|
|
|
```xml
|
|
<PackageReference Include="OneOf" Version="3.0.271" />
|
|
```
|
|
|
|
**Step 2: Verify package restores**
|
|
|
|
Run: `dotnet restore NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Restore completed successfully
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/JdeScoping.Core.csproj
|
|
git commit -m "chore: add OneOf package to Core for API result types"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Create Result Types - Error Markers
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/NotFound.cs`
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/Unauthorized.cs`
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/Forbidden.cs`
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/Unit.cs`
|
|
|
|
**Step 1: Create NotFound.cs**
|
|
|
|
```csharp
|
|
namespace JdeScoping.Core.ApiContracts.Results;
|
|
|
|
/// <summary>
|
|
/// Resource not found (HTTP 404).
|
|
/// </summary>
|
|
public readonly record struct NotFound;
|
|
```
|
|
|
|
**Step 2: Create Unauthorized.cs**
|
|
|
|
```csharp
|
|
namespace JdeScoping.Core.ApiContracts.Results;
|
|
|
|
/// <summary>
|
|
/// Authentication required (HTTP 401).
|
|
/// </summary>
|
|
public readonly record struct Unauthorized;
|
|
```
|
|
|
|
**Step 3: Create Forbidden.cs**
|
|
|
|
```csharp
|
|
namespace JdeScoping.Core.ApiContracts.Results;
|
|
|
|
/// <summary>
|
|
/// Access denied (HTTP 403).
|
|
/// </summary>
|
|
public readonly record struct Forbidden;
|
|
```
|
|
|
|
**Step 4: Create Unit.cs**
|
|
|
|
```csharp
|
|
namespace JdeScoping.Core.ApiContracts.Results;
|
|
|
|
/// <summary>
|
|
/// Empty success type for void operations.
|
|
/// </summary>
|
|
public readonly record struct Unit;
|
|
```
|
|
|
|
**Step 5: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/Results/
|
|
git commit -m "feat: add empty marker result types (NotFound, Unauthorized, Forbidden, Unit)"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Create Result Types - Error Types with Data
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/ValidationError.cs`
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/ApiError.cs`
|
|
|
|
**Step 1: Create ValidationError.cs**
|
|
|
|
```csharp
|
|
namespace JdeScoping.Core.ApiContracts.Results;
|
|
|
|
/// <summary>
|
|
/// Validation failed (HTTP 400) with field-level errors.
|
|
/// Maps to ASP.NET Core ValidationProblemDetails format.
|
|
/// </summary>
|
|
/// <param name="FieldErrors">Dictionary mapping field names to error messages.</param>
|
|
public readonly record struct ValidationError(IReadOnlyDictionary<string, string[]> FieldErrors)
|
|
{
|
|
/// <summary>
|
|
/// Creates a ValidationError from a dictionary of field errors.
|
|
/// </summary>
|
|
public static ValidationError FromDictionary(Dictionary<string, string[]> errors)
|
|
=> new(errors);
|
|
}
|
|
```
|
|
|
|
**Step 2: Create ApiError.cs**
|
|
|
|
```csharp
|
|
namespace JdeScoping.Core.ApiContracts.Results;
|
|
|
|
/// <summary>
|
|
/// General API error with message and optional status code.
|
|
/// </summary>
|
|
/// <param name="Message">Error message describing what went wrong.</param>
|
|
/// <param name="StatusCode">Optional HTTP status code.</param>
|
|
public readonly record struct ApiError(string Message, int? StatusCode = null);
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/Results/
|
|
git commit -m "feat: add ValidationError and ApiError result types"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Create ApiResult<T> Type
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/ApiResult.cs`
|
|
|
|
**Step 1: Create ApiResult.cs**
|
|
|
|
```csharp
|
|
using OneOf;
|
|
|
|
namespace JdeScoping.Core.ApiContracts.Results;
|
|
|
|
/// <summary>
|
|
/// Standard API result type for client-side operations.
|
|
/// Represents either success with value T, or one of several error types.
|
|
/// </summary>
|
|
/// <typeparam name="T">The success value type.</typeparam>
|
|
[GenerateOneOf]
|
|
public partial class ApiResult<T> : OneOfBase<T, NotFound, ValidationError, Unauthorized, Forbidden, ApiError>
|
|
{
|
|
/// <summary>Returns true if the result is a success value.</summary>
|
|
public bool IsSuccess => IsT0;
|
|
|
|
/// <summary>Returns true if the result is NotFound.</summary>
|
|
public bool IsNotFound => IsT1;
|
|
|
|
/// <summary>Returns true if the result is a ValidationError.</summary>
|
|
public bool IsValidationError => IsT2;
|
|
|
|
/// <summary>Returns true if the result is Unauthorized.</summary>
|
|
public bool IsUnauthorized => IsT3;
|
|
|
|
/// <summary>Returns true if the result is Forbidden.</summary>
|
|
public bool IsForbidden => IsT4;
|
|
|
|
/// <summary>Returns true if the result is an ApiError.</summary>
|
|
public bool IsError => IsT5;
|
|
|
|
/// <summary>Gets the success value. Throws if not a success.</summary>
|
|
public T Value => AsT0;
|
|
|
|
/// <summary>Gets the validation error. Throws if not a validation error.</summary>
|
|
public ValidationError ValidationError => AsT2;
|
|
|
|
/// <summary>Gets the API error. Throws if not an API error.</summary>
|
|
public ApiError Error => AsT5;
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/Results/ApiResult.cs
|
|
git commit -m "feat: add ApiResult<T> discriminated union type using OneOf"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Create ApiRoutes - Search Routes
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs`
|
|
|
|
**Step 1: Create ApiRoutes.cs with Search routes**
|
|
|
|
```csharp
|
|
namespace JdeScoping.Core.ApiContracts;
|
|
|
|
/// <summary>
|
|
/// Shared API route constants. Use in controller attributes and client implementations.
|
|
/// </summary>
|
|
public static class ApiRoutes
|
|
{
|
|
/// <summary>
|
|
/// Routes for search API endpoints.
|
|
/// </summary>
|
|
public static class Search
|
|
{
|
|
/// <summary>Base route for search endpoints.</summary>
|
|
public const string Base = "api/search";
|
|
|
|
/// <summary>Route for queued searches.</summary>
|
|
public const string Queue = "api/search/queue";
|
|
|
|
/// <summary>Route template for getting a search by ID (use in controller attributes).</summary>
|
|
public const string ById = "{id:int}";
|
|
|
|
/// <summary>Route template for copying a search (use in controller attributes).</summary>
|
|
public const string Copy = "{id:int}/copy";
|
|
|
|
/// <summary>Route template for getting search results (use in controller attributes).</summary>
|
|
public const string Results = "{id:int}/results";
|
|
|
|
/// <summary>Builds the route to get a specific search.</summary>
|
|
public static string GetById(int id) => $"api/search/{id}";
|
|
|
|
/// <summary>Builds the route to copy a search.</summary>
|
|
public static string GetCopy(int id) => $"api/search/{id}/copy";
|
|
|
|
/// <summary>Builds the route to get search results.</summary>
|
|
public static string GetResults(int id) => $"api/search/{id}/results";
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs
|
|
git commit -m "feat: add ApiRoutes.Search constants and builder methods"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Add ApiRoutes - Lookup Routes
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs`
|
|
|
|
**Step 1: Add Lookup routes to ApiRoutes.cs**
|
|
|
|
Add the following class inside the `ApiRoutes` class, after the `Search` class:
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// Routes for lookup/autocomplete API endpoints.
|
|
/// </summary>
|
|
public static class Lookup
|
|
{
|
|
/// <summary>Base route for lookup endpoints.</summary>
|
|
public const string Base = "api/lookup";
|
|
|
|
/// <summary>Route for item lookup.</summary>
|
|
public const string Items = "api/lookup/items";
|
|
|
|
/// <summary>Route for profit center lookup.</summary>
|
|
public const string ProfitCenters = "api/lookup/profit-centers";
|
|
|
|
/// <summary>Route for work center lookup.</summary>
|
|
public const string WorkCenters = "api/lookup/work-centers";
|
|
|
|
/// <summary>Route for operator lookup.</summary>
|
|
public const string Operators = "api/lookup/operators";
|
|
|
|
/// <summary>Builds the route to find items with URL-encoded query.</summary>
|
|
public static string FindItems(string query) => $"{Items}?q={Uri.EscapeDataString(query)}";
|
|
|
|
/// <summary>Builds the route to find profit centers with URL-encoded query.</summary>
|
|
public static string FindProfitCenters(string query) => $"{ProfitCenters}?q={Uri.EscapeDataString(query)}";
|
|
|
|
/// <summary>Builds the route to find work centers with URL-encoded query.</summary>
|
|
public static string FindWorkCenters(string query) => $"{WorkCenters}?q={Uri.EscapeDataString(query)}";
|
|
|
|
/// <summary>Builds the route to find operators with URL-encoded query.</summary>
|
|
public static string FindOperators(string query) => $"{Operators}?q={Uri.EscapeDataString(query)}";
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs
|
|
git commit -m "feat: add ApiRoutes.Lookup constants with URL encoding"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Add ApiRoutes - Auth Routes
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs`
|
|
|
|
**Step 1: Add Auth routes to ApiRoutes.cs**
|
|
|
|
Add the following class inside the `ApiRoutes` class:
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// Routes for authentication API endpoints.
|
|
/// </summary>
|
|
public static class Auth
|
|
{
|
|
/// <summary>Base route for auth endpoints.</summary>
|
|
public const string Base = "api/auth";
|
|
|
|
/// <summary>Route to get the public key for credential encryption.</summary>
|
|
public const string PublicKey = "api/auth/public-key";
|
|
|
|
/// <summary>Route for login.</summary>
|
|
public const string Login = "api/auth/login";
|
|
|
|
/// <summary>Route for logout.</summary>
|
|
public const string Logout = "api/auth/logout";
|
|
|
|
/// <summary>Route to get current user info.</summary>
|
|
public const string Me = "api/auth/me";
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs
|
|
git commit -m "feat: add ApiRoutes.Auth constants"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Add ApiRoutes - FileIO Routes
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs`
|
|
|
|
**Step 1: Add FileIO routes to ApiRoutes.cs**
|
|
|
|
Add the following class inside the `ApiRoutes` class:
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// Routes for file upload/download API endpoints.
|
|
/// </summary>
|
|
public static class FileIO
|
|
{
|
|
/// <summary>Base route for file IO endpoints.</summary>
|
|
public const string Base = "api/fileio";
|
|
|
|
/// <summary>Route to download work orders template.</summary>
|
|
public const string DownloadWorkOrders = "api/fileio/workorders/download";
|
|
|
|
/// <summary>Route to download items template.</summary>
|
|
public const string DownloadItems = "api/fileio/items/download";
|
|
|
|
/// <summary>Route to download component lots template.</summary>
|
|
public const string DownloadComponentLots = "api/fileio/componentlots/download";
|
|
|
|
/// <summary>Route to download part operations template.</summary>
|
|
public const string DownloadPartOperations = "api/fileio/partoperations/download";
|
|
|
|
/// <summary>Route to upload work orders.</summary>
|
|
public const string UploadWorkOrders = "api/fileio/workorders/upload";
|
|
|
|
/// <summary>Route to upload items.</summary>
|
|
public const string UploadItems = "api/fileio/items/upload";
|
|
|
|
/// <summary>Route to upload component lots.</summary>
|
|
public const string UploadComponentLots = "api/fileio/componentlots/upload";
|
|
|
|
/// <summary>Route to upload part operations.</summary>
|
|
public const string UploadPartOperations = "api/fileio/partoperations/upload";
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/ApiRoutes.cs
|
|
git commit -m "feat: add ApiRoutes.FileIO constants"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9: Create ISearchApiClient Interface
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/ISearchApiClient.cs`
|
|
|
|
**Step 1: Create ISearchApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.ViewModels;
|
|
|
|
namespace JdeScoping.Core.ApiContracts;
|
|
|
|
/// <summary>
|
|
/// Client contract for search API operations.
|
|
/// </summary>
|
|
public interface ISearchApiClient
|
|
{
|
|
/// <summary>Gets all searches for the current user.</summary>
|
|
Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetUserSearchesAsync(CancellationToken ct = default);
|
|
|
|
/// <summary>Gets all queued searches.</summary>
|
|
Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetQueuedSearchesAsync(CancellationToken ct = default);
|
|
|
|
/// <summary>Gets a specific search by ID.</summary>
|
|
Task<ApiResult<SearchViewModel>> GetSearchAsync(int id, CancellationToken ct = default);
|
|
|
|
/// <summary>Copies an existing search to create a new one (returns copy without persisting).</summary>
|
|
Task<ApiResult<SearchViewModel>> CopySearchAsync(int id, CancellationToken ct = default);
|
|
|
|
/// <summary>Creates and submits a new search.</summary>
|
|
Task<ApiResult<int>> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default);
|
|
|
|
/// <summary>Downloads the results for a completed search as Excel bytes.</summary>
|
|
Task<ApiResult<byte[]>> GetResultsAsync(int id, CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/ISearchApiClient.cs
|
|
git commit -m "feat: add ISearchApiClient interface"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 10: Create ILookupApiClient Interface
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/ILookupApiClient.cs`
|
|
|
|
**Step 1: Create ILookupApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.ViewModels;
|
|
|
|
namespace JdeScoping.Core.ApiContracts;
|
|
|
|
/// <summary>
|
|
/// Client contract for lookup/autocomplete API operations.
|
|
/// </summary>
|
|
public interface ILookupApiClient
|
|
{
|
|
/// <summary>Finds items matching the search query.</summary>
|
|
Task<ApiResult<IReadOnlyList<ItemViewModel>>> FindItemsAsync(string query, CancellationToken ct = default);
|
|
|
|
/// <summary>Finds profit centers matching the search query.</summary>
|
|
Task<ApiResult<IReadOnlyList<ProfitCenterViewModel>>> FindProfitCentersAsync(string query, CancellationToken ct = default);
|
|
|
|
/// <summary>Finds work centers matching the search query.</summary>
|
|
Task<ApiResult<IReadOnlyList<WorkCenterViewModel>>> FindWorkCentersAsync(string query, CancellationToken ct = default);
|
|
|
|
/// <summary>Finds operators (JDE users) matching the search query.</summary>
|
|
Task<ApiResult<IReadOnlyList<JdeUserViewModel>>> FindOperatorsAsync(string query, CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/ILookupApiClient.cs
|
|
git commit -m "feat: add ILookupApiClient interface"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 11: Create IAuthApiClient Interface
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/IAuthApiClient.cs`
|
|
|
|
**Step 1: Create IAuthApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.Models;
|
|
using JdeScoping.Core.Models.Auth;
|
|
|
|
namespace JdeScoping.Core.ApiContracts;
|
|
|
|
/// <summary>
|
|
/// Client contract for authentication API operations.
|
|
/// </summary>
|
|
public interface IAuthApiClient
|
|
{
|
|
/// <summary>Gets the server's RSA public key for encrypting login credentials.</summary>
|
|
Task<ApiResult<PublicKeyResponse>> GetPublicKeyAsync(CancellationToken ct = default);
|
|
|
|
/// <summary>Authenticates with encrypted credentials.</summary>
|
|
Task<ApiResult<LoginResultModel>> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default);
|
|
|
|
/// <summary>Logs out the current user.</summary>
|
|
Task<ApiResult<Unit>> LogoutAsync(CancellationToken ct = default);
|
|
|
|
/// <summary>Gets the current authenticated user's information.</summary>
|
|
Task<ApiResult<UserInfo>> GetCurrentUserAsync(CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/IAuthApiClient.cs
|
|
git commit -m "feat: add IAuthApiClient interface"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 12: Create IFileApiClient Interface
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Core/ApiContracts/IFileApiClient.cs`
|
|
|
|
**Step 1: Create IFileApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.ViewModels;
|
|
|
|
namespace JdeScoping.Core.ApiContracts;
|
|
|
|
/// <summary>
|
|
/// Client contract for file upload/download API operations.
|
|
/// Note: Uses Stream for client-side; controllers use IFormFile.
|
|
/// </summary>
|
|
public interface IFileApiClient
|
|
{
|
|
// Downloads (POST with existing data, returns Excel bytes)
|
|
|
|
/// <summary>Downloads work orders template, optionally pre-filled with existing data.</summary>
|
|
Task<ApiResult<byte[]>> DownloadWorkOrdersTemplateAsync(IReadOnlyList<WorkOrderViewModel>? existingData = null, CancellationToken ct = default);
|
|
|
|
/// <summary>Downloads items template, optionally pre-filled with existing data.</summary>
|
|
Task<ApiResult<byte[]>> DownloadItemsTemplateAsync(IReadOnlyList<ItemViewModel>? existingData = null, CancellationToken ct = default);
|
|
|
|
/// <summary>Downloads component lots template, optionally pre-filled with existing data.</summary>
|
|
Task<ApiResult<byte[]>> DownloadComponentLotsTemplateAsync(IReadOnlyList<LotViewModel>? existingData = null, CancellationToken ct = default);
|
|
|
|
/// <summary>Downloads part operations template, optionally pre-filled with existing data.</summary>
|
|
Task<ApiResult<byte[]>> DownloadPartOperationsTemplateAsync(IReadOnlyList<PartOperationViewModel>? existingData = null, CancellationToken ct = default);
|
|
|
|
// Uploads (multipart form, returns parsed data)
|
|
|
|
/// <summary>Uploads work orders Excel file and returns parsed data.</summary>
|
|
Task<ApiResult<IReadOnlyList<WorkOrderViewModel>>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default);
|
|
|
|
/// <summary>Uploads items Excel file and returns parsed data.</summary>
|
|
Task<ApiResult<IReadOnlyList<ItemViewModel>>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default);
|
|
|
|
/// <summary>Uploads component lots Excel file and returns parsed data.</summary>
|
|
Task<ApiResult<IReadOnlyList<LotViewModel>>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default);
|
|
|
|
/// <summary>Uploads part operations Excel file and returns parsed data.</summary>
|
|
Task<ApiResult<IReadOnlyList<PartOperationViewModel>>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Core/JdeScoping.Core.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Core/ApiContracts/IFileApiClient.cs
|
|
git commit -m "feat: add IFileApiClient interface"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 13: Create ApiClientBase in Client Project
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Client/Services/ApiClientBase.cs`
|
|
|
|
**Step 1: Create ApiClientBase.cs**
|
|
|
|
```csharp
|
|
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
|
|
namespace JdeScoping.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Base class for API clients with shared HTTP execution logic.
|
|
/// </summary>
|
|
public abstract class ApiClientBase
|
|
{
|
|
protected readonly HttpClient HttpClient;
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
protected ApiClientBase(HttpClient httpClient)
|
|
{
|
|
HttpClient = httpClient;
|
|
}
|
|
|
|
protected async Task<ApiResult<T>> GetAsync<T>(string route, CancellationToken ct = default)
|
|
{
|
|
return await ExecuteAsync<T>(() => HttpClient.GetAsync(route, ct));
|
|
}
|
|
|
|
protected async Task<ApiResult<T>> PostAsync<T, TBody>(string route, TBody body, CancellationToken ct = default)
|
|
{
|
|
return await ExecuteAsync<T>(() => HttpClient.PostAsJsonAsync(route, body, ct));
|
|
}
|
|
|
|
protected async Task<ApiResult<T>> PostAsync<T>(string route, CancellationToken ct = default)
|
|
{
|
|
return await ExecuteAsync<T>(() => HttpClient.PostAsync(route, null, ct));
|
|
}
|
|
|
|
protected async Task<ApiResult<byte[]>> GetBytesAsync(string route, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await HttpClient.GetAsync(route, ct);
|
|
return await MapResponseToBytesAsync(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ApiError(ex.Message);
|
|
}
|
|
}
|
|
|
|
protected async Task<ApiResult<byte[]>> PostForBytesAsync<TBody>(string route, TBody body, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await HttpClient.PostAsJsonAsync(route, body, ct);
|
|
return await MapResponseToBytesAsync(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ApiError(ex.Message);
|
|
}
|
|
}
|
|
|
|
protected async Task<ApiResult<T>> PostMultipartAsync<T>(
|
|
string route,
|
|
Stream fileStream,
|
|
string fileName,
|
|
CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
using var content = new MultipartFormDataContent();
|
|
using var streamContent = new StreamContent(fileStream);
|
|
content.Add(streamContent, "file", fileName);
|
|
|
|
var response = await HttpClient.PostAsync(route, content, ct);
|
|
return await MapResponseAsync<T>(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ApiError(ex.Message);
|
|
}
|
|
}
|
|
|
|
private async Task<ApiResult<T>> ExecuteAsync<T>(Func<Task<HttpResponseMessage>> request)
|
|
{
|
|
try
|
|
{
|
|
var response = await request();
|
|
return await MapResponseAsync<T>(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new ApiError(ex.Message);
|
|
}
|
|
}
|
|
|
|
private async Task<ApiResult<T>> MapResponseAsync<T>(HttpResponseMessage response)
|
|
{
|
|
return response.StatusCode switch
|
|
{
|
|
HttpStatusCode.OK or HttpStatusCode.Created =>
|
|
await response.Content.ReadFromJsonAsync<T>(JsonOptions)
|
|
is T value ? value : new ApiError("Invalid response format"),
|
|
|
|
HttpStatusCode.NoContent =>
|
|
typeof(T) == typeof(Unit) ? (ApiResult<T>)(object)new Unit() : new ApiError("Unexpected empty response"),
|
|
|
|
HttpStatusCode.NotFound => new NotFound(),
|
|
HttpStatusCode.Unauthorized => new Unauthorized(),
|
|
HttpStatusCode.Forbidden => new Forbidden(),
|
|
|
|
HttpStatusCode.BadRequest => await ParseValidationErrorAsync<T>(response),
|
|
|
|
_ => new ApiError(
|
|
await response.Content.ReadAsStringAsync(),
|
|
(int)response.StatusCode)
|
|
};
|
|
}
|
|
|
|
private async Task<ApiResult<byte[]>> MapResponseToBytesAsync(HttpResponseMessage response)
|
|
{
|
|
return response.StatusCode switch
|
|
{
|
|
HttpStatusCode.OK => await response.Content.ReadAsByteArrayAsync(),
|
|
HttpStatusCode.NotFound => new NotFound(),
|
|
HttpStatusCode.Unauthorized => new Unauthorized(),
|
|
HttpStatusCode.Forbidden => new Forbidden(),
|
|
_ => new ApiError(
|
|
await response.Content.ReadAsStringAsync(),
|
|
(int)response.StatusCode)
|
|
};
|
|
}
|
|
|
|
private static async Task<ApiResult<T>> ParseValidationErrorAsync<T>(HttpResponseMessage response)
|
|
{
|
|
try
|
|
{
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var problemDetails = JsonSerializer.Deserialize<ValidationProblemDetails>(content, JsonOptions);
|
|
|
|
if (problemDetails?.Errors is { } errors)
|
|
{
|
|
return new ValidationError(errors);
|
|
}
|
|
|
|
return new ApiError(content, (int)response.StatusCode);
|
|
}
|
|
catch
|
|
{
|
|
return new ApiError("Validation error", (int)response.StatusCode);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Matches ASP.NET Core ValidationProblemDetails structure.
|
|
/// </summary>
|
|
private sealed class ValidationProblemDetails
|
|
{
|
|
public Dictionary<string, string[]>? Errors { get; set; }
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Client/Services/ApiClientBase.cs
|
|
git commit -m "feat: add ApiClientBase with shared HTTP execution logic"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 14: Create SearchApiClient Implementation
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Client/Services/SearchApiClient.cs`
|
|
|
|
**Step 1: Create SearchApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.ViewModels;
|
|
|
|
namespace JdeScoping.Client.Services;
|
|
|
|
/// <summary>
|
|
/// HTTP client implementation of ISearchApiClient.
|
|
/// </summary>
|
|
public class SearchApiClient : ApiClientBase, ISearchApiClient
|
|
{
|
|
public SearchApiClient(HttpClient httpClient) : base(httpClient) { }
|
|
|
|
public Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetUserSearchesAsync(CancellationToken ct = default)
|
|
=> GetAsync<IReadOnlyList<SearchViewModel>>(ApiRoutes.Search.Base, ct);
|
|
|
|
public Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetQueuedSearchesAsync(CancellationToken ct = default)
|
|
=> GetAsync<IReadOnlyList<SearchViewModel>>(ApiRoutes.Search.Queue, ct);
|
|
|
|
public Task<ApiResult<SearchViewModel>> GetSearchAsync(int id, CancellationToken ct = default)
|
|
=> GetAsync<SearchViewModel>(ApiRoutes.Search.GetById(id), ct);
|
|
|
|
public Task<ApiResult<SearchViewModel>> CopySearchAsync(int id, CancellationToken ct = default)
|
|
=> GetAsync<SearchViewModel>(ApiRoutes.Search.GetCopy(id), ct);
|
|
|
|
public Task<ApiResult<int>> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default)
|
|
=> PostAsync<int, SearchViewModel>(ApiRoutes.Search.Base, search, ct);
|
|
|
|
public Task<ApiResult<byte[]>> GetResultsAsync(int id, CancellationToken ct = default)
|
|
=> GetBytesAsync(ApiRoutes.Search.GetResults(id), ct);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Client/Services/SearchApiClient.cs
|
|
git commit -m "feat: add SearchApiClient implementation"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 15: Create LookupApiClient Implementation
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Client/Services/LookupApiClient.cs`
|
|
|
|
**Step 1: Create LookupApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.ViewModels;
|
|
|
|
namespace JdeScoping.Client.Services;
|
|
|
|
/// <summary>
|
|
/// HTTP client implementation of ILookupApiClient.
|
|
/// </summary>
|
|
public class LookupApiClient : ApiClientBase, ILookupApiClient
|
|
{
|
|
public LookupApiClient(HttpClient httpClient) : base(httpClient) { }
|
|
|
|
public Task<ApiResult<IReadOnlyList<ItemViewModel>>> FindItemsAsync(string query, CancellationToken ct = default)
|
|
=> GetAsync<IReadOnlyList<ItemViewModel>>(ApiRoutes.Lookup.FindItems(query), ct);
|
|
|
|
public Task<ApiResult<IReadOnlyList<ProfitCenterViewModel>>> FindProfitCentersAsync(string query, CancellationToken ct = default)
|
|
=> GetAsync<IReadOnlyList<ProfitCenterViewModel>>(ApiRoutes.Lookup.FindProfitCenters(query), ct);
|
|
|
|
public Task<ApiResult<IReadOnlyList<WorkCenterViewModel>>> FindWorkCentersAsync(string query, CancellationToken ct = default)
|
|
=> GetAsync<IReadOnlyList<WorkCenterViewModel>>(ApiRoutes.Lookup.FindWorkCenters(query), ct);
|
|
|
|
public Task<ApiResult<IReadOnlyList<JdeUserViewModel>>> FindOperatorsAsync(string query, CancellationToken ct = default)
|
|
=> GetAsync<IReadOnlyList<JdeUserViewModel>>(ApiRoutes.Lookup.FindOperators(query), ct);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Client/Services/LookupApiClient.cs
|
|
git commit -m "feat: add LookupApiClient implementation"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 16: Create AuthApiClient Implementation
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Client/Services/AuthApiClient.cs`
|
|
|
|
**Step 1: Create AuthApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.Models;
|
|
using JdeScoping.Core.Models.Auth;
|
|
|
|
namespace JdeScoping.Client.Services;
|
|
|
|
/// <summary>
|
|
/// HTTP client implementation of IAuthApiClient.
|
|
/// </summary>
|
|
public class AuthApiClient : ApiClientBase, IAuthApiClient
|
|
{
|
|
public AuthApiClient(HttpClient httpClient) : base(httpClient) { }
|
|
|
|
public Task<ApiResult<PublicKeyResponse>> GetPublicKeyAsync(CancellationToken ct = default)
|
|
=> GetAsync<PublicKeyResponse>(ApiRoutes.Auth.PublicKey, ct);
|
|
|
|
public Task<ApiResult<LoginResultModel>> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default)
|
|
=> PostAsync<LoginResultModel, EncryptedLoginRequest>(ApiRoutes.Auth.Login, request, ct);
|
|
|
|
public Task<ApiResult<Unit>> LogoutAsync(CancellationToken ct = default)
|
|
=> PostAsync<Unit>(ApiRoutes.Auth.Logout, ct);
|
|
|
|
public Task<ApiResult<UserInfo>> GetCurrentUserAsync(CancellationToken ct = default)
|
|
=> GetAsync<UserInfo>(ApiRoutes.Auth.Me, ct);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Client/Services/AuthApiClient.cs
|
|
git commit -m "feat: add AuthApiClient implementation"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 17: Create FileApiClient Implementation
|
|
|
|
**Files:**
|
|
- Create: `NEW/src/JdeScoping.Client/Services/FileApiClient.cs`
|
|
|
|
**Step 1: Create FileApiClient.cs**
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
using JdeScoping.Core.ApiContracts.Results;
|
|
using JdeScoping.Core.ViewModels;
|
|
|
|
namespace JdeScoping.Client.Services;
|
|
|
|
/// <summary>
|
|
/// HTTP client implementation of IFileApiClient.
|
|
/// </summary>
|
|
public class FileApiClient : ApiClientBase, IFileApiClient
|
|
{
|
|
public FileApiClient(HttpClient httpClient) : base(httpClient) { }
|
|
|
|
// Downloads
|
|
|
|
public Task<ApiResult<byte[]>> DownloadWorkOrdersTemplateAsync(IReadOnlyList<WorkOrderViewModel>? existingData = null, CancellationToken ct = default)
|
|
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadWorkOrders, existingData, ct);
|
|
|
|
public Task<ApiResult<byte[]>> DownloadItemsTemplateAsync(IReadOnlyList<ItemViewModel>? existingData = null, CancellationToken ct = default)
|
|
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadItems, existingData, ct);
|
|
|
|
public Task<ApiResult<byte[]>> DownloadComponentLotsTemplateAsync(IReadOnlyList<LotViewModel>? existingData = null, CancellationToken ct = default)
|
|
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadComponentLots, existingData, ct);
|
|
|
|
public Task<ApiResult<byte[]>> DownloadPartOperationsTemplateAsync(IReadOnlyList<PartOperationViewModel>? existingData = null, CancellationToken ct = default)
|
|
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadPartOperations, existingData, ct);
|
|
|
|
// Uploads
|
|
|
|
public Task<ApiResult<IReadOnlyList<WorkOrderViewModel>>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default)
|
|
=> PostMultipartAsync<IReadOnlyList<WorkOrderViewModel>>(ApiRoutes.FileIO.UploadWorkOrders, fileStream, fileName, ct);
|
|
|
|
public Task<ApiResult<IReadOnlyList<ItemViewModel>>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
|
|
=> PostMultipartAsync<IReadOnlyList<ItemViewModel>>(ApiRoutes.FileIO.UploadItems, fileStream, fileName, ct);
|
|
|
|
public Task<ApiResult<IReadOnlyList<LotViewModel>>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
|
|
=> PostMultipartAsync<IReadOnlyList<LotViewModel>>(ApiRoutes.FileIO.UploadComponentLots, fileStream, fileName, ct);
|
|
|
|
public Task<ApiResult<IReadOnlyList<PartOperationViewModel>>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
|
|
=> PostMultipartAsync<IReadOnlyList<PartOperationViewModel>>(ApiRoutes.FileIO.UploadPartOperations, fileStream, fileName, ct);
|
|
}
|
|
```
|
|
|
|
**Step 2: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Client/Services/FileApiClient.cs
|
|
git commit -m "feat: add FileApiClient implementation"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 18: Update SearchController to Use ApiRoutes
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Api/Controllers/SearchController.cs`
|
|
|
|
**Step 1: Add using statement**
|
|
|
|
Add at the top of the file:
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
```
|
|
|
|
**Step 2: Update Route attribute**
|
|
|
|
Change the class-level `[Route]` attribute from:
|
|
|
|
```csharp
|
|
[Route("api/search")]
|
|
```
|
|
|
|
To:
|
|
|
|
```csharp
|
|
[Route(ApiRoutes.Search.Base)]
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Api/JdeScoping.Api.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Api/Controllers/SearchController.cs
|
|
git commit -m "refactor: update SearchController to use ApiRoutes constants"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 19: Update LookupController to Use ApiRoutes
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Api/Controllers/LookupController.cs`
|
|
|
|
**Step 1: Add using statement**
|
|
|
|
Add at the top of the file:
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
```
|
|
|
|
**Step 2: Update Route attribute**
|
|
|
|
Change the class-level `[Route]` attribute from:
|
|
|
|
```csharp
|
|
[Route("api/lookup")]
|
|
```
|
|
|
|
To:
|
|
|
|
```csharp
|
|
[Route(ApiRoutes.Lookup.Base)]
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Api/JdeScoping.Api.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Api/Controllers/LookupController.cs
|
|
git commit -m "refactor: update LookupController to use ApiRoutes constants"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 20: Update AuthController to Use ApiRoutes
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Api/Controllers/AuthController.cs`
|
|
|
|
**Step 1: Add using statement**
|
|
|
|
Add at the top of the file:
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
```
|
|
|
|
**Step 2: Update Route attribute**
|
|
|
|
Change the class-level `[Route]` attribute from:
|
|
|
|
```csharp
|
|
[Route("api/auth")]
|
|
```
|
|
|
|
To:
|
|
|
|
```csharp
|
|
[Route(ApiRoutes.Auth.Base)]
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Api/JdeScoping.Api.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Api/Controllers/AuthController.cs
|
|
git commit -m "refactor: update AuthController to use ApiRoutes constants"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 21: Update FileIOController to Use ApiRoutes
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Api/Controllers/FileController.cs`
|
|
|
|
**Step 1: Add using statement**
|
|
|
|
Add at the top of the file:
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
```
|
|
|
|
**Step 2: Update Route attribute**
|
|
|
|
Change the class-level `[Route]` attribute from:
|
|
|
|
```csharp
|
|
[Route("api/fileio")]
|
|
```
|
|
|
|
To:
|
|
|
|
```csharp
|
|
[Route(ApiRoutes.FileIO.Base)]
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Api/JdeScoping.Api.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Api/Controllers/FileController.cs
|
|
git commit -m "refactor: update FileIOController to use ApiRoutes constants"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 22: Register New API Clients in Client DI
|
|
|
|
**Files:**
|
|
- Modify: `NEW/src/JdeScoping.Client/Program.cs`
|
|
|
|
**Step 1: Add using statement**
|
|
|
|
Add at the top of the file:
|
|
|
|
```csharp
|
|
using JdeScoping.Core.ApiContracts;
|
|
```
|
|
|
|
**Step 2: Add new client registrations**
|
|
|
|
Find where services are registered and add:
|
|
|
|
```csharp
|
|
builder.Services.AddScoped<ISearchApiClient, SearchApiClient>();
|
|
builder.Services.AddScoped<ILookupApiClient, LookupApiClient>();
|
|
builder.Services.AddScoped<IAuthApiClient, AuthApiClient>();
|
|
builder.Services.AddScoped<IFileApiClient, FileApiClient>();
|
|
```
|
|
|
|
**Step 3: Verify build**
|
|
|
|
Run: `dotnet build NEW/src/JdeScoping.Client/JdeScoping.Client.csproj`
|
|
Expected: Build succeeded
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add NEW/src/JdeScoping.Client/Program.cs
|
|
git commit -m "feat: register new API clients in Client DI container"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 23: Full Solution Build Verification
|
|
|
|
**Files:**
|
|
- None (verification only)
|
|
|
|
**Step 1: Build entire solution**
|
|
|
|
Run: `dotnet build NEW/JdeScopingTool.sln`
|
|
Expected: Build succeeded with no errors
|
|
|
|
**Step 2: Run existing tests**
|
|
|
|
Run: `dotnet test NEW/JdeScopingTool.sln --no-build`
|
|
Expected: All tests pass
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
This implementation plan adds:
|
|
|
|
1. **Core/ApiContracts/Results/** - Result types (ApiResult, NotFound, ValidationError, etc.)
|
|
2. **Core/ApiContracts/ApiRoutes.cs** - Shared route constants
|
|
3. **Core/ApiContracts/I*ApiClient.cs** - Client interface contracts
|
|
4. **Client/Services/ApiClientBase.cs** - Shared HTTP execution logic
|
|
5. **Client/Services/*ApiClient.cs** - Client implementations
|
|
6. **Controller updates** - Use ApiRoutes constants
|
|
|
|
The old services (`SearchService`, `LookupService`, `AuthService`, `FileService`) remain functional. Migration of Blazor components to use the new clients is a separate follow-up task.
|