# 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` using OneOf for type-safe error handling. Controllers continue returning `ActionResult` 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 `` containing PackageReferences in `JdeScoping.Core.csproj`: ```xml ``` **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; /// /// Resource not found (HTTP 404). /// public readonly record struct NotFound; ``` **Step 2: Create Unauthorized.cs** ```csharp namespace JdeScoping.Core.ApiContracts.Results; /// /// Authentication required (HTTP 401). /// public readonly record struct Unauthorized; ``` **Step 3: Create Forbidden.cs** ```csharp namespace JdeScoping.Core.ApiContracts.Results; /// /// Access denied (HTTP 403). /// public readonly record struct Forbidden; ``` **Step 4: Create Unit.cs** ```csharp namespace JdeScoping.Core.ApiContracts.Results; /// /// Empty success type for void operations. /// 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; /// /// Validation failed (HTTP 400) with field-level errors. /// Maps to ASP.NET Core ValidationProblemDetails format. /// /// Dictionary mapping field names to error messages. public readonly record struct ValidationError(IReadOnlyDictionary FieldErrors) { /// /// Creates a ValidationError from a dictionary of field errors. /// public static ValidationError FromDictionary(Dictionary errors) => new(errors); } ``` **Step 2: Create ApiError.cs** ```csharp namespace JdeScoping.Core.ApiContracts.Results; /// /// General API error with message and optional status code. /// /// Error message describing what went wrong. /// Optional HTTP status code. 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 Type **Files:** - Create: `NEW/src/JdeScoping.Core/ApiContracts/Results/ApiResult.cs` **Step 1: Create ApiResult.cs** ```csharp using OneOf; namespace JdeScoping.Core.ApiContracts.Results; /// /// Standard API result type for client-side operations. /// Represents either success with value T, or one of several error types. /// /// The success value type. [GenerateOneOf] public partial class ApiResult : OneOfBase { /// Returns true if the result is a success value. public bool IsSuccess => IsT0; /// Returns true if the result is NotFound. public bool IsNotFound => IsT1; /// Returns true if the result is a ValidationError. public bool IsValidationError => IsT2; /// Returns true if the result is Unauthorized. public bool IsUnauthorized => IsT3; /// Returns true if the result is Forbidden. public bool IsForbidden => IsT4; /// Returns true if the result is an ApiError. public bool IsError => IsT5; /// Gets the success value. Throws if not a success. public T Value => AsT0; /// Gets the validation error. Throws if not a validation error. public ValidationError ValidationError => AsT2; /// Gets the API error. Throws if not an API error. 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 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; /// /// Shared API route constants. Use in controller attributes and client implementations. /// public static class ApiRoutes { /// /// Routes for search API endpoints. /// public static class Search { /// Base route for search endpoints. public const string Base = "api/search"; /// Route for queued searches. public const string Queue = "api/search/queue"; /// Route template for getting a search by ID (use in controller attributes). public const string ById = "{id:int}"; /// Route template for copying a search (use in controller attributes). public const string Copy = "{id:int}/copy"; /// Route template for getting search results (use in controller attributes). public const string Results = "{id:int}/results"; /// Builds the route to get a specific search. public static string GetById(int id) => $"api/search/{id}"; /// Builds the route to copy a search. public static string GetCopy(int id) => $"api/search/{id}/copy"; /// Builds the route to get search results. 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 /// /// Routes for lookup/autocomplete API endpoints. /// public static class Lookup { /// Base route for lookup endpoints. public const string Base = "api/lookup"; /// Route for item lookup. public const string Items = "api/lookup/items"; /// Route for profit center lookup. public const string ProfitCenters = "api/lookup/profit-centers"; /// Route for work center lookup. public const string WorkCenters = "api/lookup/work-centers"; /// Route for operator lookup. public const string Operators = "api/lookup/operators"; /// Builds the route to find items with URL-encoded query. public static string FindItems(string query) => $"{Items}?q={Uri.EscapeDataString(query)}"; /// Builds the route to find profit centers with URL-encoded query. public static string FindProfitCenters(string query) => $"{ProfitCenters}?q={Uri.EscapeDataString(query)}"; /// Builds the route to find work centers with URL-encoded query. public static string FindWorkCenters(string query) => $"{WorkCenters}?q={Uri.EscapeDataString(query)}"; /// Builds the route to find operators with URL-encoded query. 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 /// /// Routes for authentication API endpoints. /// public static class Auth { /// Base route for auth endpoints. public const string Base = "api/auth"; /// Route to get the public key for credential encryption. public const string PublicKey = "api/auth/public-key"; /// Route for login. public const string Login = "api/auth/login"; /// Route for logout. public const string Logout = "api/auth/logout"; /// Route to get current user info. 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 /// /// Routes for file upload/download API endpoints. /// public static class FileIO { /// Base route for file IO endpoints. public const string Base = "api/fileio"; /// Route to download work orders template. public const string DownloadWorkOrders = "api/fileio/workorders/download"; /// Route to download items template. public const string DownloadItems = "api/fileio/items/download"; /// Route to download component lots template. public const string DownloadComponentLots = "api/fileio/componentlots/download"; /// Route to download part operations template. public const string DownloadPartOperations = "api/fileio/partoperations/download"; /// Route to upload work orders. public const string UploadWorkOrders = "api/fileio/workorders/upload"; /// Route to upload items. public const string UploadItems = "api/fileio/items/upload"; /// Route to upload component lots. public const string UploadComponentLots = "api/fileio/componentlots/upload"; /// Route to upload part operations. 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; /// /// Client contract for search API operations. /// public interface ISearchApiClient { /// Gets all searches for the current user. Task>> GetUserSearchesAsync(CancellationToken ct = default); /// Gets all queued searches. Task>> GetQueuedSearchesAsync(CancellationToken ct = default); /// Gets a specific search by ID. Task> GetSearchAsync(int id, CancellationToken ct = default); /// Copies an existing search to create a new one (returns copy without persisting). Task> CopySearchAsync(int id, CancellationToken ct = default); /// Creates and submits a new search. Task> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default); /// Downloads the results for a completed search as Excel bytes. Task> 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; /// /// Client contract for lookup/autocomplete API operations. /// public interface ILookupApiClient { /// Finds items matching the search query. Task>> FindItemsAsync(string query, CancellationToken ct = default); /// Finds profit centers matching the search query. Task>> FindProfitCentersAsync(string query, CancellationToken ct = default); /// Finds work centers matching the search query. Task>> FindWorkCentersAsync(string query, CancellationToken ct = default); /// Finds operators (JDE users) matching the search query. Task>> 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; /// /// Client contract for authentication API operations. /// public interface IAuthApiClient { /// Gets the server's RSA public key for encrypting login credentials. Task> GetPublicKeyAsync(CancellationToken ct = default); /// Authenticates with encrypted credentials. Task> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default); /// Logs out the current user. Task> LogoutAsync(CancellationToken ct = default); /// Gets the current authenticated user's information. Task> 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; /// /// Client contract for file upload/download API operations. /// Note: Uses Stream for client-side; controllers use IFormFile. /// public interface IFileApiClient { // Downloads (POST with existing data, returns Excel bytes) /// Downloads work orders template, optionally pre-filled with existing data. Task> DownloadWorkOrdersTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); /// Downloads items template, optionally pre-filled with existing data. Task> DownloadItemsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); /// Downloads component lots template, optionally pre-filled with existing data. Task> DownloadComponentLotsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); /// Downloads part operations template, optionally pre-filled with existing data. Task> DownloadPartOperationsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default); // Uploads (multipart form, returns parsed data) /// Uploads work orders Excel file and returns parsed data. Task>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default); /// Uploads items Excel file and returns parsed data. Task>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default); /// Uploads component lots Excel file and returns parsed data. Task>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default); /// Uploads part operations Excel file and returns parsed data. Task>> 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; /// /// Base class for API clients with shared HTTP execution logic. /// 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> GetAsync(string route, CancellationToken ct = default) { return await ExecuteAsync(() => HttpClient.GetAsync(route, ct)); } protected async Task> PostAsync(string route, TBody body, CancellationToken ct = default) { return await ExecuteAsync(() => HttpClient.PostAsJsonAsync(route, body, ct)); } protected async Task> PostAsync(string route, CancellationToken ct = default) { return await ExecuteAsync(() => HttpClient.PostAsync(route, null, ct)); } protected async Task> 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> PostForBytesAsync(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> PostMultipartAsync( 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(response); } catch (Exception ex) { return new ApiError(ex.Message); } } private async Task> ExecuteAsync(Func> request) { try { var response = await request(); return await MapResponseAsync(response); } catch (Exception ex) { return new ApiError(ex.Message); } } private async Task> MapResponseAsync(HttpResponseMessage response) { return response.StatusCode switch { HttpStatusCode.OK or HttpStatusCode.Created => await response.Content.ReadFromJsonAsync(JsonOptions) is T value ? value : new ApiError("Invalid response format"), HttpStatusCode.NoContent => typeof(T) == typeof(Unit) ? (ApiResult)(object)new Unit() : new ApiError("Unexpected empty response"), HttpStatusCode.NotFound => new NotFound(), HttpStatusCode.Unauthorized => new Unauthorized(), HttpStatusCode.Forbidden => new Forbidden(), HttpStatusCode.BadRequest => await ParseValidationErrorAsync(response), _ => new ApiError( await response.Content.ReadAsStringAsync(), (int)response.StatusCode) }; } private async Task> 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> ParseValidationErrorAsync(HttpResponseMessage response) { try { var content = await response.Content.ReadAsStringAsync(); var problemDetails = JsonSerializer.Deserialize(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); } } /// /// Matches ASP.NET Core ValidationProblemDetails structure. /// private sealed class ValidationProblemDetails { public Dictionary? 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; /// /// HTTP client implementation of ISearchApiClient. /// public class SearchApiClient : ApiClientBase, ISearchApiClient { public SearchApiClient(HttpClient httpClient) : base(httpClient) { } public Task>> GetUserSearchesAsync(CancellationToken ct = default) => GetAsync>(ApiRoutes.Search.Base, ct); public Task>> GetQueuedSearchesAsync(CancellationToken ct = default) => GetAsync>(ApiRoutes.Search.Queue, ct); public Task> GetSearchAsync(int id, CancellationToken ct = default) => GetAsync(ApiRoutes.Search.GetById(id), ct); public Task> CopySearchAsync(int id, CancellationToken ct = default) => GetAsync(ApiRoutes.Search.GetCopy(id), ct); public Task> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default) => PostAsync(ApiRoutes.Search.Base, search, ct); public Task> 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; /// /// HTTP client implementation of ILookupApiClient. /// public class LookupApiClient : ApiClientBase, ILookupApiClient { public LookupApiClient(HttpClient httpClient) : base(httpClient) { } public Task>> FindItemsAsync(string query, CancellationToken ct = default) => GetAsync>(ApiRoutes.Lookup.FindItems(query), ct); public Task>> FindProfitCentersAsync(string query, CancellationToken ct = default) => GetAsync>(ApiRoutes.Lookup.FindProfitCenters(query), ct); public Task>> FindWorkCentersAsync(string query, CancellationToken ct = default) => GetAsync>(ApiRoutes.Lookup.FindWorkCenters(query), ct); public Task>> FindOperatorsAsync(string query, CancellationToken ct = default) => GetAsync>(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; /// /// HTTP client implementation of IAuthApiClient. /// public class AuthApiClient : ApiClientBase, IAuthApiClient { public AuthApiClient(HttpClient httpClient) : base(httpClient) { } public Task> GetPublicKeyAsync(CancellationToken ct = default) => GetAsync(ApiRoutes.Auth.PublicKey, ct); public Task> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default) => PostAsync(ApiRoutes.Auth.Login, request, ct); public Task> LogoutAsync(CancellationToken ct = default) => PostAsync(ApiRoutes.Auth.Logout, ct); public Task> GetCurrentUserAsync(CancellationToken ct = default) => GetAsync(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; /// /// HTTP client implementation of IFileApiClient. /// public class FileApiClient : ApiClientBase, IFileApiClient { public FileApiClient(HttpClient httpClient) : base(httpClient) { } // Downloads public Task> DownloadWorkOrdersTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadWorkOrders, existingData, ct); public Task> DownloadItemsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadItems, existingData, ct); public Task> DownloadComponentLotsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadComponentLots, existingData, ct); public Task> DownloadPartOperationsTemplateAsync(IReadOnlyList? existingData = null, CancellationToken ct = default) => PostForBytesAsync(ApiRoutes.FileIO.DownloadPartOperations, existingData, ct); // Uploads public Task>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(ApiRoutes.FileIO.UploadWorkOrders, fileStream, fileName, ct); public Task>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(ApiRoutes.FileIO.UploadItems, fileStream, fileName, ct); public Task>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(ApiRoutes.FileIO.UploadComponentLots, fileStream, fileName, ct); public Task>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default) => PostMultipartAsync>(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(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); ``` **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.