docs: add XML documentation and ConfigManager implementation plans

Add comprehensive XML documentation (param/returns tags) across 132 source
files to improve IntelliSense and API discoverability. Include ConfigManager
design documents and implementation plans for phases 1-9.
This commit is contained in:
Joseph Doherty
2026-01-20 02:26:26 -05:00
parent c044337539
commit d49330e697
136 changed files with 9181 additions and 4 deletions
+2
View File
@@ -12,6 +12,7 @@
<Project Path="src/JdeScoping.Infrastructure/JdeScoping.Infrastructure.csproj" />
</Folder>
<Folder Name="/utils/">
<Project Path="src/Utils/JdeScoping.ConfigManager/JdeScoping.ConfigManager.csproj" />
<Project Path="src/Utils/JdeScoping.SecureStoreManager/JdeScoping.SecureStoreManager.csproj" />
</Folder>
<Folder Name="/tests/">
@@ -26,6 +27,7 @@
<Project Path="tests/JdeScoping.ExcelIO.Tests/JdeScoping.ExcelIO.Tests.csproj" />
<Project Path="tests/JdeScoping.Host.Tests/JdeScoping.Host.Tests.csproj" />
<Project Path="tests/JdeScoping.Infrastructure.Tests/JdeScoping.Infrastructure.Tests.csproj" />
<Project Path="tests/JdeScoping.ConfigManager.Tests/JdeScoping.ConfigManager.Tests.csproj" />
<Project Path="tests/JdeScoping.SecureStoreManager.Tests/JdeScoping.SecureStoreManager.Tests.csproj" />
</Folder>
</Solution>
@@ -28,6 +28,12 @@ public class AuthController : ApiControllerBase
private readonly IRsaKeyService _rsaKeyService;
private readonly ILogger<AuthController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="AuthController"/> class.
/// </summary>
/// <param name="authService">The authentication service.</param>
/// <param name="rsaKeyService">The RSA key service for credential encryption.</param>
/// <param name="logger">Logger instance.</param>
public AuthController(
IAuthService authService,
IRsaKeyService rsaKeyService,
@@ -14,6 +14,8 @@ public partial class FileIOController
/// <summary>
/// Uploads an Excel file containing component lot/item pairs and returns the matched lots
/// </summary>
/// <param name="file">The Excel file containing component lot/item pairs.</param>
/// <param name="ct">The cancellation token.</param>
[HttpPost("componentlots/upload")]
[ProducesResponseType(typeof(FileUploadResult<LotViewModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(FileUploadResult<LotViewModel>), StatusCodes.Status400BadRequest)]
@@ -63,6 +65,7 @@ public partial class FileIOController
/// <summary>
/// Downloads an Excel template with current component lot data
/// </summary>
/// <param name="lotNumbers">The list of lot view models to include in the export.</param>
[HttpPost("componentlots/download")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public IActionResult DownloadComponentLots([FromBody] List<LotViewModel>? lotNumbers)
@@ -14,6 +14,8 @@ public partial class FileIOController
/// <summary>
/// Uploads an Excel file containing item numbers and returns the matched items
/// </summary>
/// <param name="file">The uploaded Excel file containing item numbers.</param>
/// <param name="ct">The cancellation token.</param>
[HttpPost("items/upload")]
[ProducesResponseType(typeof(FileUploadResult<ItemViewModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(FileUploadResult<ItemViewModel>), StatusCodes.Status400BadRequest)]
@@ -62,6 +64,7 @@ public partial class FileIOController
/// <summary>
/// Downloads an Excel template with current item data
/// </summary>
/// <param name="items">The list of items to include in the template.</param>
[HttpPost("items/download")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public IActionResult DownloadItems([FromBody] List<ItemViewModel>? items)
@@ -14,6 +14,8 @@ public partial class FileIOController
/// <summary>
/// Uploads an Excel file containing part operations and returns the parsed data
/// </summary>
/// <param name="file">The Excel file to upload.</param>
/// <returns>Result containing parsed part operations.</returns>
[HttpPost("partoperations/upload")]
[ProducesResponseType(typeof(FileUploadResult<PartOperationViewModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(FileUploadResult<PartOperationViewModel>), StatusCodes.Status400BadRequest)]
@@ -54,6 +56,8 @@ public partial class FileIOController
/// <summary>
/// Downloads an Excel template with current part operation data
/// </summary>
/// <param name="partOperations">Optional list of part operations to include in template.</param>
/// <returns>Excel file.</returns>
[HttpPost("partoperations/download")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public IActionResult DownloadPartOperations([FromBody] List<PartOperationViewModel>? partOperations)
@@ -14,6 +14,9 @@ public partial class FileIOController
/// <summary>
/// Uploads an Excel file containing work order numbers and returns the matched work orders
/// </summary>
/// <param name="file">The Excel file to upload.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Result containing parsed work orders.</returns>
[HttpPost("workorders/upload")]
[ProducesResponseType(typeof(FileUploadResult<WorkOrderViewModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(FileUploadResult<WorkOrderViewModel>), StatusCodes.Status400BadRequest)]
@@ -63,6 +66,8 @@ public partial class FileIOController
/// <summary>
/// Downloads an Excel template with current work order data
/// </summary>
/// <param name="workOrders">Optional list of work order numbers to include in template.</param>
/// <returns>Excel file.</returns>
[HttpPost("workorders/download")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
public IActionResult DownloadWorkOrders([FromBody] List<long>? workOrders)
@@ -21,6 +21,13 @@ public partial class FileIOController : ApiControllerBase
private readonly IExcelTemplateService _templateService;
private readonly ILogger<FileIOController> _logger;
/// <summary>
/// Initializes a new instance of the FileIOController class.
/// </summary>
/// <param name="repository">Repository for accessing lot finder data.</param>
/// <param name="parserService">Service for parsing Excel files.</param>
/// <param name="templateService">Service for generating Excel templates.</param>
/// <param name="logger">Logger for controller operations.</param>
public FileIOController(
ILotFinderRepository repository,
IExcelParserService parserService,
@@ -15,6 +15,10 @@ public class LookupController : ApiControllerBase
{
private readonly ILotFinderRepository _repository;
/// <summary>
/// Initializes a new instance of the <see cref="LookupController"/> class.
/// </summary>
/// <param name="repository">The LotFinder repository for data access.</param>
public LookupController(ILotFinderRepository repository)
{
_repository = repository;
@@ -20,6 +20,12 @@ public class PipelineController : ApiControllerBase
private readonly IDataUpdateRepository _dataUpdateRepository;
private readonly IPipelineMapper _mapper;
/// <summary>
/// Initializes a new instance of the <see cref="PipelineController"/> class.
/// </summary>
/// <param name="pipelineFactory">The ETL pipeline factory.</param>
/// <param name="dataUpdateRepository">The data update repository.</param>
/// <param name="mapper">The pipeline mapper.</param>
public PipelineController(
IEtlPipelineFactory pipelineFactory,
IDataUpdateRepository dataUpdateRepository,
@@ -45,6 +51,7 @@ public class PipelineController : ApiControllerBase
/// <summary>
/// Gets configuration for a specific pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
[HttpGet(ApiRoutes.Pipelines.ByName)]
public ActionResult<PipelineConfigDto> GetPipeline(string name)
{
@@ -60,6 +67,8 @@ public class PipelineController : ApiControllerBase
/// <summary>
/// Gets schedule status for a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="cancellationToken">The cancellation token.</param>
[HttpGet(ApiRoutes.Pipelines.Status)]
public async Task<ActionResult<PipelineStatusResponse>> GetStatus(
string name,
@@ -104,6 +113,9 @@ public class PipelineController : ApiControllerBase
/// <summary>
/// Gets recent execution history for a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="count">The maximum number of recent executions to retrieve.</param>
/// <param name="cancellationToken">The cancellation token.</param>
[HttpGet(ApiRoutes.Pipelines.Executions)]
public async Task<ActionResult<PipelineExecutionsResponse>> GetExecutions(
string name,
@@ -17,6 +17,10 @@ public class RefreshStatusController : ApiControllerBase
{
private readonly ILotFinderRepository _repository;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshStatusController"/> class.
/// </summary>
/// <param name="repository">The repository for accessing data update records.</param>
public RefreshStatusController(ILotFinderRepository repository)
{
_repository = repository;
@@ -26,6 +26,13 @@ public class SearchController : ApiControllerBase
private readonly ILogger<SearchController> _logger;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Initializes a new instance of the SearchController.
/// </summary>
/// <param name="repository">The lot finder repository for data access.</param>
/// <param name="hubContext">The SignalR hub context for sending search updates.</param>
/// <param name="logger">The logger for recording search operations.</param>
/// <param name="timeProvider">The time provider for generating timestamps.</param>
public SearchController(
ILotFinderRepository repository,
IHubContext<StatusHub> hubContext,
@@ -41,6 +48,7 @@ public class SearchController : ApiControllerBase
/// <summary>
/// Gets all searches for the current user
/// </summary>
/// <param name="ct">The cancellation token.</param>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<SearchViewModel>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -59,6 +67,7 @@ public class SearchController : ApiControllerBase
/// <summary>
/// Gets all queued searches
/// </summary>
/// <param name="ct">The cancellation token.</param>
[HttpGet("queue")]
[ProducesResponseType(typeof(IEnumerable<SearchViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<SearchViewModel>>> GetQueuedSearches(CancellationToken ct)
@@ -71,6 +80,8 @@ public class SearchController : ApiControllerBase
/// <summary>
/// Gets a single search by ID
/// </summary>
/// <param name="id">The search identifier.</param>
/// <param name="ct">The cancellation token.</param>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -87,6 +98,8 @@ public class SearchController : ApiControllerBase
/// <summary>
/// Copies an existing search for the current user (returns copy without persisting)
/// </summary>
/// <param name="id">The search identifier to copy.</param>
/// <param name="ct">The cancellation token.</param>
[HttpGet("{id:int}/copy")]
[ProducesResponseType(typeof(SearchViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -121,6 +134,8 @@ public class SearchController : ApiControllerBase
/// <summary>
/// Creates a new search
/// </summary>
/// <param name="viewModel">The search view model containing criteria and name.</param>
/// <param name="ct">The cancellation token.</param>
[HttpPost]
[ProducesResponseType(typeof(int), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -160,6 +175,8 @@ public class SearchController : ApiControllerBase
/// <summary>
/// Downloads search results as an Excel file
/// </summary>
/// <param name="id">The search identifier.</param>
/// <param name="ct">The cancellation token.</param>
[HttpGet("{id:int}/results")]
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
+6
View File
@@ -16,6 +16,12 @@ public class StatusHub : Hub
private readonly ILogger<StatusHub> _logger;
private readonly TimeProvider _timeProvider;
/// <summary>
/// Initializes a new instance of the <see cref="StatusHub"/> class.
/// </summary>
/// <param name="cache">The memory cache for caching status updates.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="timeProvider">The time provider for getting current UTC time.</param>
public StatusHub(IMemoryCache cache, ILogger<StatusHub> logger, TimeProvider timeProvider)
{
_cache = cache;
@@ -12,15 +12,23 @@ public interface IPipelineMapper
/// <summary>
/// Maps a pipeline configuration to its DTO representation.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="config">The pipeline configuration.</param>
/// <param name="defaults">The default schedule settings.</param>
PipelineConfigDto MapToDto(string name, PipelineConfig config, ScheduleDefaults defaults);
/// <summary>
/// Gets the effective interval for a schedule, applying defaults if not specified.
/// </summary>
/// <param name="config">The schedule configuration, or null to use defaults.</param>
/// <param name="defaults">The default schedule settings.</param>
/// <param name="updateType">The type of update to get the interval for.</param>
int GetEffectiveInterval(ScheduleConfig? config, ScheduleDefaults defaults, UpdateTypes updateType);
/// <summary>
/// Gets the schedule configuration for a specific update type.
/// </summary>
/// <param name="config">The pipeline configuration.</param>
/// <param name="updateType">The type of update to get the configuration for.</param>
ScheduleConfig? GetScheduleConfig(PipelineConfig config, UpdateTypes updateType);
}
@@ -18,6 +18,12 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider
private readonly ILogger<AuthStateProvider>? _logger;
private readonly ClaimsPrincipal _anonymous = new(new ClaimsIdentity());
/// <summary>
/// Initializes a new instance of the AuthStateProvider class.
/// </summary>
/// <param name="userStorage">Service for storing user authentication data.</param>
/// <param name="httpClient">The HTTP client for making API requests.</param>
/// <param name="logger">Optional logger for debugging authentication issues.</param>
public AuthStateProvider(
IUserStorageService userStorage,
HttpClient httpClient,
@@ -28,6 +34,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider
_logger = logger;
}
/// <inheritdoc />
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
// First check cached user info
@@ -93,6 +100,7 @@ public class AuthStateProvider : AuthenticationStateProvider, IAuthStateProvider
/// <summary>
/// Called after successful login to update auth state.
/// </summary>
/// <param name="user">The authenticated user information.</param>
public async Task MarkUserAsAuthenticated(UserInfoDto user)
{
await _userStorage.SetUserAsync(user);
@@ -17,6 +17,7 @@ public interface IUserStorageService
/// <summary>
/// Stores the user info.
/// </summary>
/// <param name="user">The user information to store.</param>
Task SetUserAsync(UserInfoDto user);
/// <summary>
@@ -14,11 +14,19 @@ public class UserStorageService : IUserStorageService
private const string UserKey = "jdescoping_user";
private readonly IJSRuntime _jsRuntime;
/// <summary>
/// Initializes a new instance of the <see cref="UserStorageService"/> class.
/// </summary>
/// <param name="jsRuntime">The JS interop runtime for accessing browser storage.</param>
public UserStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
/// <summary>
/// Retrieves the stored user information from browser session storage.
/// </summary>
/// <returns>A task that completes with the user information, or null if not found.</returns>
public async Task<UserInfoDto?> GetUserAsync()
{
try
@@ -40,12 +48,21 @@ public class UserStorageService : IUserStorageService
}
}
/// <summary>
/// Stores user information in browser session storage.
/// </summary>
/// <param name="user">The user information to store.</param>
/// <returns>A task that completes when the user information is stored.</returns>
public async Task SetUserAsync(UserInfoDto user)
{
var json = JsonSerializer.Serialize(user);
await _jsRuntime.InvokeVoidAsync("jdeScopingInterop.setSessionStorage", UserKey, json);
}
/// <summary>
/// Removes the stored user information from browser session storage.
/// </summary>
/// <returns>A task that completes when the user information is removed.</returns>
public async Task RemoveUserAsync()
{
await _jsRuntime.InvokeVoidAsync("jdeScopingInterop.removeSessionStorage", UserKey);
@@ -9,6 +9,9 @@ namespace JdeScoping.Client.Components.FilterPanels;
/// <typeparam name="TItem">The type of items displayed in the grid.</typeparam>
public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where TItem : class
{
/// <summary>
/// Gets or sets the Radzen dialog service for confirming user actions.
/// </summary>
[Inject]
protected DialogService DialogService { get; set; } = default!;
@@ -73,21 +76,29 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <summary>
/// Performs the search API call and returns matching items.
/// </summary>
/// <param name="filter">The search filter text.</param>
/// <returns>A task that returns the list of matching items.</returns>
protected abstract Task<List<TItem>> SearchApiAsync(string filter);
/// <summary>
/// Gets the unique key value for an item.
/// </summary>
/// <param name="item">The item to get the key for.</param>
/// <returns>The unique key value.</returns>
protected abstract object GetItemKey(TItem item);
/// <summary>
/// Gets the display text value for an item (used for matching in autocomplete).
/// </summary>
/// <param name="item">The item to get the display text for.</param>
/// <returns>The display text value.</returns>
protected abstract string GetDisplayText(TItem item);
/// <summary>
/// Handles the autocomplete search.
/// </summary>
/// <param name="args">The load data arguments containing the search filter.</param>
/// <returns>A task representing the asynchronous search operation.</returns>
protected async Task OnSearchAsync(LoadDataArgs args)
{
if (!string.IsNullOrEmpty(args.Filter) && args.Filter.Length >= 3)
@@ -103,6 +114,7 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <summary>
/// Handles selection from the autocomplete.
/// </summary>
/// <param name="value">The selected value from the autocomplete control.</param>
protected void OnItemSelected(object value)
{
if (value is string text && !string.IsNullOrEmpty(text))
@@ -138,6 +150,8 @@ public abstract class AutocompleteFilterPanelBase<TItem> : ComponentBase where T
/// <summary>
/// Removes an item from the list.
/// </summary>
/// <param name="item">The item to remove.</param>
/// <returns>A task representing the asynchronous operation.</returns>
protected async Task DeleteItem(TItem item)
{
Items.Remove(item);
@@ -11,12 +11,15 @@ namespace JdeScoping.Client.Components.FilterPanels;
/// <typeparam name="TItem">The type of items displayed in the grid.</typeparam>
public abstract class FileUploadFilterPanelBase<TItem> : ComponentBase where TItem : class
{
/// <summary>Service for managing dialogs.</summary>
[Inject]
protected DialogService DialogService { get; set; } = default!;
/// <summary>Service for displaying notifications.</summary>
[Inject]
protected NotificationService NotificationService { get; set; } = default!;
/// <summary>JavaScript interop runtime.</summary>
[Inject]
protected IJSRuntime JSRuntime { get; set; } = default!;
@@ -81,6 +84,8 @@ public abstract class FileUploadFilterPanelBase<TItem> : ComponentBase where TIt
/// <summary>
/// Uploads the file and returns the parsed items.
/// </summary>
/// <param name="stream">The file stream to upload.</param>
/// <param name="filename">The name of the file being uploaded.</param>
protected abstract Task<List<TItem>?> UploadFileApiAsync(Stream stream, string filename);
/// <summary>
@@ -106,6 +111,7 @@ public abstract class FileUploadFilterPanelBase<TItem> : ComponentBase where TIt
/// <summary>
/// Handles file selection and upload.
/// </summary>
/// <param name="e">The file change event arguments.</param>
protected async Task OnFileSelected(InputFileChangeEventArgs e)
{
if (e.File == null) return;
@@ -22,6 +22,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps Core SearchViewModel to Client SearchViewModel.
/// </summary>
/// <param name="vm">The Core SearchViewModel to map.</param>
public static SearchViewModel ToClient(this CoreSearch vm) => new()
{
Id = vm.Id,
@@ -37,6 +38,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps Client SearchViewModel to Core SearchViewModel.
/// </summary>
/// <param name="vm">The Client SearchViewModel to map.</param>
public static CoreSearch ToCore(this SearchViewModel vm)
{
ArgumentNullException.ThrowIfNull(vm);
@@ -58,6 +60,7 @@ public static class ViewModelMappingExtensions
/// Maps Core SearchCriteria to Client SearchCriteriaViewModel.
/// Core uses primitive lists; Client uses full view model objects.
/// </summary>
/// <param name="criteria">The Core SearchCriteria to map.</param>
public static SearchCriteriaViewModel ToClientCriteria(this SearchCriteria criteria)
{
var client = new SearchCriteriaViewModel
@@ -107,6 +110,7 @@ public static class ViewModelMappingExtensions
/// Maps Client SearchCriteriaViewModel to Core SearchCriteria.
/// Client uses full view model objects; Core uses primitive lists.
/// </summary>
/// <param name="criteria">The Client SearchCriteriaViewModel to map.</param>
public static SearchCriteria ToCoreCriteria(this SearchCriteriaViewModel criteria)
{
ArgumentNullException.ThrowIfNull(criteria);
@@ -131,6 +135,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps Core JdeUserViewModel to Client OperatorViewModel.
/// </summary>
/// <param name="vm">The Core JdeUserViewModel to map.</param>
public static OperatorViewModel ToClientOperator(this CoreJdeUser vm) => new()
{
AddressNumber = vm.AddressNumber,
@@ -141,6 +146,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps a collection of Core SearchViewModels to Client SearchViewModels.
/// </summary>
/// <param name="list">The collection of Core SearchViewModels to map.</param>
public static List<SearchViewModel> ToClientList(this IEnumerable<CoreSearch> list)
{
ArgumentNullException.ThrowIfNull(list);
@@ -150,6 +156,7 @@ public static class ViewModelMappingExtensions
/// <summary>
/// Maps a collection of Core JdeUserViewModels to Client OperatorViewModels.
/// </summary>
/// <param name="list">The collection of Core JdeUserViewModels to map.</param>
public static List<OperatorViewModel> ToClientOperatorList(this IEnumerable<CoreJdeUser> list)
{
ArgumentNullException.ThrowIfNull(list);
@@ -11,11 +11,16 @@ public class AuthRedirectHandler : DelegatingHandler
{
private readonly NavigationManager _navigationManager;
/// <summary>
/// Initializes a new instance of the AuthRedirectHandler class.
/// </summary>
/// <param name="navigationManager">The navigation manager for redirecting to login.</param>
public AuthRedirectHandler(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
@@ -7,16 +7,53 @@ namespace JdeScoping.Client.Models;
/// </summary>
public class SearchCriteriaViewModel
{
/// <summary>
/// Gets or sets the minimum date for search filtering.
/// </summary>
public DateTime? MinimumDt { get; set; }
/// <summary>
/// Gets or sets the maximum date for search filtering.
/// </summary>
public DateTime? MaximumDt { get; set; }
/// <summary>
/// Gets or sets the list of work orders to include in the search.
/// </summary>
public List<WorkOrderViewModel> WorkOrders { get; set; } = [];
/// <summary>
/// Gets or sets the list of items to include in the search.
/// </summary>
public List<ItemViewModel> Items { get; set; } = [];
/// <summary>
/// Gets or sets the list of profit centers to include in the search.
/// </summary>
public List<ProfitCenterViewModel> ProfitCenters { get; set; } = [];
/// <summary>
/// Gets or sets the list of work centers to include in the search.
/// </summary>
public List<WorkCenterViewModel> WorkCenters { get; set; } = [];
/// <summary>
/// Gets or sets the list of component lots to include in the search.
/// </summary>
public List<ComponentLotViewModel> ComponentLots { get; set; } = [];
/// <summary>
/// Gets or sets the list of operators to include in the search.
/// </summary>
public List<OperatorViewModel> Operators { get; set; } = [];
/// <summary>
/// Gets or sets the list of part operations to include in the search.
/// </summary>
public List<PartOperationViewModel> PartOperations { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether to extract MIS data in the search.
/// </summary>
public bool ExtractMisData { get; set; }
}
@@ -7,15 +7,45 @@ namespace JdeScoping.Client.Models;
/// </summary>
public class SearchViewModel
{
/// <summary>
/// Gets or sets the unique identifier for the search.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the name of the search.
/// </summary>
[Required(ErrorMessage = "Name is required.")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the username of the user who created the search.
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the current status of the search.
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the date and time when the search was submitted.
/// </summary>
public DateTime? SubmitDt { get; set; }
/// <summary>
/// Gets or sets the date and time when the search execution started.
/// </summary>
public DateTime? StartDt { get; set; }
/// <summary>
/// Gets or sets the date and time when the search execution ended.
/// </summary>
public DateTime? EndDt { get; set; }
/// <summary>
/// Gets or sets the search criteria for filtering.
/// </summary>
public SearchCriteriaViewModel Criteria { get; set; } = new();
/// <summary>
@@ -6,21 +6,74 @@ namespace JdeScoping.Client.Models;
/// </summary>
public class ValidCombination
{
/// <summary>
/// The unique identifier for this combination.
/// </summary>
public int Id { get; private init; }
/// <summary>
/// The display name for this combination.
/// </summary>
public string Name { get; private init; } = string.Empty;
/// <summary>
/// Whether the timespan filter is included in this combination.
/// </summary>
public bool Timespan { get; private init; }
/// <summary>
/// Whether the work order filter is included in this combination.
/// </summary>
public bool WorkOrder { get; private init; }
/// <summary>
/// Whether the item number filter is included in this combination.
/// </summary>
public bool ItemNumber { get; private init; }
/// <summary>
/// Whether the profit center filter is included in this combination.
/// </summary>
public bool ProfitCenter { get; private init; }
/// <summary>
/// Whether the work center filter is included in this combination.
/// </summary>
public bool WorkCenter { get; private init; }
/// <summary>
/// Whether the component lot filter is included in this combination.
/// </summary>
public bool ComponentLot { get; private init; }
/// <summary>
/// Whether the operator filter is included in this combination.
/// </summary>
public bool Operator { get; private init; }
/// <summary>
/// Whether the item operation MIS filter is included in this combination.
/// </summary>
public bool ItemOperationMis { get; private init; }
/// <summary>
/// Whether the extract MIS filter is included in this combination.
/// </summary>
public bool ExtractMis { get; private init; }
/// <summary>
/// Checks if the given filter flags match this combination.
/// </summary>
/// <param name="timespan">Whether timespan filter is enabled.</param>
/// <param name="workOrder">Whether work order filter is enabled.</param>
/// <param name="itemNumber">Whether item number filter is enabled.</param>
/// <param name="profitCenter">Whether profit center filter is enabled.</param>
/// <param name="workCenter">Whether work center filter is enabled.</param>
/// <param name="componentLot">Whether component lot filter is enabled.</param>
/// <param name="operator">Whether operator filter is enabled.</param>
/// <param name="itemOperationMis">Whether item operation MIS filter is enabled.</param>
/// <param name="extractMis">Whether extract MIS filter is enabled.</param>
/// <returns>True if all flags match this combination.</returns>
public bool Matches(
bool timespan,
bool workOrder,
@@ -17,26 +17,55 @@ public abstract class ApiClientBase
PropertyNameCaseInsensitive = true
};
/// <summary>
/// Initializes a new instance of the <see cref="ApiClientBase"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client to use for requests.</param>
protected ApiClientBase(HttpClient httpClient)
{
HttpClient = httpClient;
}
/// <summary>
/// Executes a GET request and deserializes the response to the specified type.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> GetAsync<T>(string route, CancellationToken ct = default)
{
return await ExecuteAsync<T>(() => HttpClient.GetAsync(route, ct));
}
/// <summary>
/// Executes a POST request with a JSON body.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <typeparam name="TBody">The request body type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="body">The request body.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> PostAsync<T, TBody>(string route, TBody body, CancellationToken ct = default)
{
return await ExecuteAsync<T>(() => HttpClient.PostAsJsonAsync(route, body, ct));
}
/// <summary>
/// Executes a POST request without a body.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> PostAsync<T>(string route, CancellationToken ct = default)
{
return await ExecuteAsync<T>(() => HttpClient.PostAsync(route, null, ct));
}
/// <summary>
/// Executes a GET request and returns the response as raw bytes.
/// </summary>
/// <param name="route">The API route.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<byte[]>> GetBytesAsync(string route, CancellationToken ct = default)
{
try
@@ -50,6 +79,13 @@ public abstract class ApiClientBase
}
}
/// <summary>
/// Executes a POST request with a JSON body and returns the response as raw bytes.
/// </summary>
/// <typeparam name="TBody">The request body type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="body">The request body.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<byte[]>> PostForBytesAsync<TBody>(string route, TBody body, CancellationToken ct = default)
{
try
@@ -63,6 +99,14 @@ public abstract class ApiClientBase
}
}
/// <summary>
/// Executes a multipart POST request with a file stream.
/// </summary>
/// <typeparam name="T">The response type.</typeparam>
/// <param name="route">The API route.</param>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The file name for the multipart form.</param>
/// <param name="ct">Cancellation token.</param>
protected async Task<ApiResult<T>> PostMultipartAsync<T>(
string route,
Stream fileStream,
@@ -161,6 +205,9 @@ public abstract class ApiClientBase
/// </summary>
private sealed class ValidationProblemDetails
{
/// <summary>
/// Gets or sets the validation errors by field name.
/// </summary>
public Dictionary<string, string[]>? Errors { get; set; }
}
}
@@ -9,17 +9,42 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class AuthApiClient : ApiClientBase, IAuthApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="AuthApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API requests.</param>
public AuthApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Retrieves the server's RSA public key for credential encryption.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The public key response.</returns>
public Task<ApiResult<PublicKeyResponse>> GetPublicKeyAsync(CancellationToken ct = default)
=> GetAsync<PublicKeyResponse>(ApiRoutes.Auth.PublicKey, ct);
/// <summary>
/// Authenticates a user with encrypted credentials.
/// </summary>
/// <param name="request">The encrypted login request.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The login result with user info on success.</returns>
public Task<ApiResult<LoginResultModel>> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default)
=> PostAsync<LoginResultModel, EncryptedLoginRequest>(ApiRoutes.Auth.Login, request, ct);
/// <summary>
/// Logs out the current user.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>Result of logout operation.</returns>
public Task<ApiResult<Unit>> LogoutAsync(CancellationToken ct = default)
=> PostAsync<Unit>(ApiRoutes.Auth.Logout, ct);
/// <summary>
/// Retrieves information about the current authenticated user.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>The current user's information.</returns>
public Task<ApiResult<UserInfoDto>> GetCurrentUserAsync(CancellationToken ct = default)
=> GetAsync<UserInfoDto>(ApiRoutes.Auth.Me, ct);
}
@@ -13,6 +13,12 @@ public class AuthService : IAuthService
private readonly ICryptoService _cryptoService;
private readonly IAuthStateProvider _authStateProvider;
/// <summary>
/// Initializes a new instance of the <see cref="AuthService"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API communication.</param>
/// <param name="cryptoService">The cryptography service.</param>
/// <param name="authStateProvider">The authentication state provider.</param>
public AuthService(
HttpClient httpClient,
ICryptoService cryptoService,
@@ -23,6 +29,11 @@ public class AuthService : IAuthService
_authStateProvider = authStateProvider;
}
/// <summary>
/// Authenticates a user with encrypted credentials.
/// </summary>
/// <param name="model">The login credentials.</param>
/// <returns>The login result with user information if successful.</returns>
public async Task<LoginResultModel> LoginAsync(LoginModel model)
{
try
@@ -54,6 +65,9 @@ public class AuthService : IAuthService
}
}
/// <summary>
/// Logs out the current user and clears authentication state.
/// </summary>
public async Task LogoutAsync()
{
try
@@ -17,6 +17,11 @@ public class CryptoService : ICryptoService, IAsyncDisposable
private readonly SemaphoreSlim _keyLock = new(1, 1);
private bool _disposed;
/// <summary>
/// Initializes a new instance of the CryptoService.
/// </summary>
/// <param name="httpClient">The HTTP client for fetching the public key from the server.</param>
/// <param name="jsRuntime">The JavaScript runtime for performing RSA encryption.</param>
public CryptoService(HttpClient httpClient, IJSRuntime jsRuntime)
{
_httpClient = httpClient;
@@ -9,33 +9,89 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class FileApiClient : ApiClientBase, IFileApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="FileApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API requests.</param>
public FileApiClient(HttpClient httpClient) : base(httpClient) { }
// Downloads
/// <summary>
/// Downloads an Excel template with optional existing work order data.
/// </summary>
/// <param name="existingData">Optional list of existing work orders to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadWorkOrdersTemplateAsync(IReadOnlyList<WorkOrderViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadWorkOrders, existingData, ct);
/// <summary>
/// Downloads an Excel template with optional existing item data.
/// </summary>
/// <param name="existingData">Optional list of existing items to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadItemsTemplateAsync(IReadOnlyList<ItemViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadItems, existingData, ct);
/// <summary>
/// Downloads an Excel template with optional existing component lot data.
/// </summary>
/// <param name="existingData">Optional list of existing lots to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadComponentLotsTemplateAsync(IReadOnlyList<LotViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadComponentLots, existingData, ct);
/// <summary>
/// Downloads an Excel template with optional existing part operation data.
/// </summary>
/// <param name="existingData">Optional list of existing part operations to include.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes.</returns>
public Task<ApiResult<byte[]>> DownloadPartOperationsTemplateAsync(IReadOnlyList<PartOperationViewModel>? existingData = null, CancellationToken ct = default)
=> PostForBytesAsync(ApiRoutes.FileIO.DownloadPartOperations, existingData, ct);
// Uploads
/// <summary>
/// Uploads and parses an Excel file containing work orders.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed work order view models.</returns>
public Task<ApiResult<IReadOnlyList<WorkOrderViewModel>>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<WorkOrderViewModel>>(ApiRoutes.FileIO.UploadWorkOrders, fileStream, fileName, ct);
/// <summary>
/// Uploads and parses an Excel file containing items.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed item view models.</returns>
public Task<ApiResult<IReadOnlyList<ItemViewModel>>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<ItemViewModel>>(ApiRoutes.FileIO.UploadItems, fileStream, fileName, ct);
/// <summary>
/// Uploads and parses an Excel file containing component lots.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed lot view models.</returns>
public Task<ApiResult<IReadOnlyList<LotViewModel>>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<LotViewModel>>(ApiRoutes.FileIO.UploadComponentLots, fileStream, fileName, ct);
/// <summary>
/// Uploads and parses an Excel file containing part operations.
/// </summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The original file name.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of parsed part operation view models.</returns>
public Task<ApiResult<IReadOnlyList<PartOperationViewModel>>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default)
=> PostMultipartAsync<IReadOnlyList<PartOperationViewModel>>(ApiRoutes.FileIO.UploadPartOperations, fileStream, fileName, ct);
}
@@ -13,16 +13,33 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
private readonly NavigationManager _navigationManager;
private HubConnection? _hubConnection;
/// <summary>
/// Raised when a search is updated.
/// </summary>
public event Action<SearchUpdateViewModel>? OnSearchUpdate;
/// <summary>
/// Raised when a status update is received.
/// </summary>
public event Action<StatusUpdateViewModel>? OnStatusUpdate;
/// <summary>
/// Gets a value indicating whether the SignalR connection is active.
/// </summary>
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
/// <summary>
/// Initializes a new instance of the <see cref="HubConnectionService"/> class.
/// </summary>
/// <param name="navigationManager">The navigation manager for resolving hub URL.</param>
public HubConnectionService(NavigationManager navigationManager)
{
_navigationManager = navigationManager;
}
/// <summary>
/// Establishes the SignalR connection and registers message handlers.
/// </summary>
public async Task StartAsync()
{
if (_hubConnection != null)
@@ -82,6 +99,9 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
}
}
/// <summary>
/// Stops the SignalR connection.
/// </summary>
public async Task StopAsync()
{
if (_hubConnection != null)
@@ -92,6 +112,10 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
}
}
/// <summary>
/// Retrieves the most recent status update from the server cache.
/// </summary>
/// <returns>The cached status update, or null if not available.</returns>
public async Task<StatusUpdateViewModel?> GetCachedStatusAsync()
{
if (_hubConnection == null || _hubConnection.State != HubConnectionState.Connected)
@@ -110,6 +134,9 @@ public class HubConnectionService : IHubConnectionService, IAsyncDisposable
}
}
/// <summary>
/// Releases the resources used by the service.
/// </summary>
public async ValueTask DisposeAsync()
{
await StopAsync();
@@ -10,6 +10,8 @@ public interface IAuthService
/// <summary>
/// Attempts to log in with the provided credentials (encrypted).
/// </summary>
/// <param name="model">The login credentials.</param>
/// <returns>The login result containing status and user information.</returns>
Task<LoginResultModel> LoginAsync(LoginModel model);
/// <summary>
@@ -10,5 +10,8 @@ public interface IRefreshStatusService
/// <summary>
/// Gets refresh status records within the specified date range.
/// </summary>
/// <param name="minDt">The minimum date/time (inclusive).</param>
/// <param name="maxDt">The maximum date/time (inclusive).</param>
/// <returns>A list of data update status records.</returns>
Task<List<DataUpdateDto>> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt);
}
@@ -38,10 +38,14 @@ public class SearchSubmissionResult
/// <summary>
/// Creates a successful result.
/// </summary>
/// <param name="searchId">The ID of the successfully submitted search.</param>
/// <returns>A successful search submission result.</returns>
public static SearchSubmissionResult Success(int searchId) => new() { SearchId = searchId };
/// <summary>
/// Creates a failure result.
/// </summary>
/// <param name="error">The error message describing the submission failure.</param>
/// <returns>A failed search submission result.</returns>
public static SearchSubmissionResult Failure(string error) => new() { ErrorMessage = error };
}
@@ -9,17 +9,45 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class LookupApiClient : ApiClientBase, ILookupApiClient
{
/// <summary>
/// Initializes a new instance of the LookupApiClient class.
/// </summary>
/// <param name="httpClient">The HTTP client for making API requests.</param>
public LookupApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Finds items matching the search query.
/// </summary>
/// <param name="query">The search query to match against item data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching items.</returns>
public Task<ApiResult<IReadOnlyList<ItemViewModel>>> FindItemsAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<ItemViewModel>>(ApiRoutes.Lookup.FindItems(query), ct);
/// <summary>
/// Finds profit centers matching the search query.
/// </summary>
/// <param name="query">The search query to match against profit center data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching profit centers.</returns>
public Task<ApiResult<IReadOnlyList<ProfitCenterViewModel>>> FindProfitCentersAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<ProfitCenterViewModel>>(ApiRoutes.Lookup.FindProfitCenters(query), ct);
/// <summary>
/// Finds work centers matching the search query.
/// </summary>
/// <param name="query">The search query to match against work center data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching work centers.</returns>
public Task<ApiResult<IReadOnlyList<WorkCenterViewModel>>> FindWorkCentersAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<WorkCenterViewModel>>(ApiRoutes.Lookup.FindWorkCenters(query), ct);
/// <summary>
/// Finds operators (JDE users) matching the search query.
/// </summary>
/// <param name="query">The search query to match against operator data.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>An API result containing matching operators.</returns>
public Task<ApiResult<IReadOnlyList<JdeUserViewModel>>> FindOperatorsAsync(string query, CancellationToken ct = default)
=> GetAsync<IReadOnlyList<JdeUserViewModel>>(ApiRoutes.Lookup.FindOperators(query), ct);
}
@@ -9,17 +9,45 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class PipelineApiClient : ApiClientBase, IPipelineApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="PipelineApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API communication.</param>
public PipelineApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Gets the list of available pipeline names from the API.
/// </summary>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline list.</returns>
public Task<ApiResult<PipelineListResponse>> GetPipelineNamesAsync(CancellationToken ct = default)
=> GetAsync<PipelineListResponse>(ApiRoutes.Pipelines.Base, ct);
/// <summary>
/// Gets the configuration for a specific pipeline by name.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline configuration.</returns>
public Task<ApiResult<PipelineConfigDto>> GetPipelineAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineConfigDto>(ApiRoutes.Pipelines.GetByName(name), ct);
/// <summary>
/// Gets the current execution status of a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline status.</returns>
public Task<ApiResult<PipelineStatusResponse>> GetStatusAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineStatusResponse>(ApiRoutes.Pipelines.GetStatus(name), ct);
/// <summary>
/// Gets the recent execution history for a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="count">The maximum number of execution records to retrieve.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the execution history.</returns>
public Task<ApiResult<PipelineExecutionsResponse>> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default)
=> GetAsync<PipelineExecutionsResponse>(ApiRoutes.Pipelines.GetExecutions(name, count), ct);
}
@@ -11,11 +11,21 @@ public class RefreshStatusService : IRefreshStatusService
{
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshStatusService"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API communication.</param>
public RefreshStatusService(HttpClient httpClient)
{
_httpClient = httpClient;
}
/// <summary>
/// Gets the data refresh status for a date range.
/// </summary>
/// <param name="minDt">The minimum date for the status query.</param>
/// <param name="maxDt">The maximum date for the status query.</param>
/// <returns>A list of data update information for the specified date range.</returns>
public async Task<List<DataUpdateDto>> GetRefreshStatusAsync(DateTime minDt, DateTime maxDt)
{
try
@@ -9,23 +9,61 @@ namespace JdeScoping.Client.Services;
/// </summary>
public class SearchApiClient : ApiClientBase, ISearchApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="SearchApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API requests.</param>
public SearchApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Retrieves the current user's searches.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of user searches.</returns>
public Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetUserSearchesAsync(CancellationToken ct = default)
=> GetAsync<IReadOnlyList<SearchViewModel>>(ApiRoutes.Search.Base, ct);
/// <summary>
/// Retrieves searches queued for processing.
/// </summary>
/// <param name="ct">Cancellation token.</param>
/// <returns>List of queued searches.</returns>
public Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetQueuedSearchesAsync(CancellationToken ct = default)
=> GetAsync<IReadOnlyList<SearchViewModel>>(ApiRoutes.Search.Queue, ct);
/// <summary>
/// Retrieves a specific search by ID.
/// </summary>
/// <param name="id">The search ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The search view model.</returns>
public Task<ApiResult<SearchViewModel>> GetSearchAsync(int id, CancellationToken ct = default)
=> GetAsync<SearchViewModel>(ApiRoutes.Search.GetById(id), ct);
/// <summary>
/// Creates a copy of an existing search.
/// </summary>
/// <param name="id">The ID of the search to copy.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The new copied search view model.</returns>
public Task<ApiResult<SearchViewModel>> CopySearchAsync(int id, CancellationToken ct = default)
=> GetAsync<SearchViewModel>(ApiRoutes.Search.GetCopy(id), ct);
/// <summary>
/// Creates a new search.
/// </summary>
/// <param name="search">The search view model to create.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The ID of the newly created search.</returns>
public Task<ApiResult<int>> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default)
=> PostAsync<int, SearchViewModel>(ApiRoutes.Search.Base, search, ct);
/// <summary>
/// Retrieves the results of a completed search.
/// </summary>
/// <param name="id">The search ID.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>The Excel file bytes containing results.</returns>
public Task<ApiResult<byte[]>> GetResultsAsync(int id, CancellationToken ct = default)
=> GetBytesAsync(ApiRoutes.Search.GetResults(id), ct);
}
@@ -11,6 +11,10 @@ public class SearchSubmissionService : ISearchSubmissionService
{
private readonly ISearchApiClient _searchApi;
/// <summary>
/// Initializes a new instance of the <see cref="SearchSubmissionService"/> class.
/// </summary>
/// <param name="searchApi">The search API client.</param>
public SearchSubmissionService(ISearchApiClient searchApi)
{
_searchApi = searchApi;
@@ -26,12 +26,18 @@ public static class ApiRoutes
public const string Results = "{id:int}/results";
/// <summary>Builds the route to get a specific search.</summary>
/// <param name="id">The search ID.</param>
/// <returns>The formatted route.</returns>
public static string GetById(int id) => $"api/search/{id}";
/// <summary>Builds the route to copy a search.</summary>
/// <param name="id">The search ID to copy.</param>
/// <returns>The formatted route.</returns>
public static string GetCopy(int id) => $"api/search/{id}/copy";
/// <summary>Builds the route to get search results.</summary>
/// <param name="id">The search ID.</param>
/// <returns>The formatted route.</returns>
public static string GetResults(int id) => $"api/search/{id}/results";
}
@@ -56,15 +62,23 @@ public static class ApiRoutes
public const string Operators = "api/lookup/operators";
/// <summary>Builds the route to find items with URL-encoded query.</summary>
/// <param name="query">The search query to URL-encode.</param>
/// <returns>The formatted route.</returns>
public static string FindItems(string query) => $"{Items}?q={Uri.EscapeDataString(query)}";
/// <summary>Builds the route to find profit centers with URL-encoded query.</summary>
/// <param name="query">The search query to URL-encode.</param>
/// <returns>The formatted route.</returns>
public static string FindProfitCenters(string query) => $"{ProfitCenters}?q={Uri.EscapeDataString(query)}";
/// <summary>Builds the route to find work centers with URL-encoded query.</summary>
/// <param name="query">The search query to URL-encode.</param>
/// <returns>The formatted route.</returns>
public static string FindWorkCenters(string query) => $"{WorkCenters}?q={Uri.EscapeDataString(query)}";
/// <summary>Builds the route to find operators with URL-encoded query.</summary>
/// <param name="query">The search query to URL-encode.</param>
/// <returns>The formatted route.</returns>
public static string FindOperators(string query) => $"{Operators}?q={Uri.EscapeDataString(query)}";
}
@@ -131,6 +145,9 @@ public static class ApiRoutes
public const string Base = "api/refresh-status";
/// <summary>Builds the route to get refresh status with date range.</summary>
/// <param name="minDt">The minimum date (inclusive).</param>
/// <param name="maxDt">The maximum date (inclusive).</param>
/// <returns>The formatted route.</returns>
public static string Get(DateTime minDt, DateTime maxDt) =>
$"api/refresh-status?minDT={minDt:yyyy-MM-dd}&maxDT={maxDt:yyyy-MM-dd}";
}
@@ -153,12 +170,19 @@ public static class ApiRoutes
public const string Executions = "{name}/executions";
/// <summary>Builds the route to get a specific pipeline config.</summary>
/// <param name="name">The pipeline name to URL-encode.</param>
/// <returns>The formatted route.</returns>
public static string GetByName(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}";
/// <summary>Builds the route to get pipeline status.</summary>
/// <param name="name">The pipeline name to URL-encode.</param>
/// <returns>The formatted route.</returns>
public static string GetStatus(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}/status";
/// <summary>Builds the route to get pipeline executions.</summary>
/// <param name="name">The pipeline name to URL-encode.</param>
/// <param name="count">The number of recent executions to retrieve.</param>
/// <returns>The formatted route.</returns>
public static string GetExecutions(string name, int count = 10) =>
$"api/pipelines/{Uri.EscapeDataString(name)}/executions?count={count}";
}
@@ -9,14 +9,19 @@ namespace JdeScoping.Core.ApiContracts;
public interface IAuthApiClient
{
/// <summary>Gets the server's RSA public key for encrypting login credentials.</summary>
/// <param name="ct">The cancellation token.</param>
Task<ApiResult<PublicKeyResponse>> GetPublicKeyAsync(CancellationToken ct = default);
/// <summary>Authenticates with encrypted credentials.</summary>
/// <param name="request">The encrypted login request containing credentials.</param>
/// <param name="ct">The cancellation token.</param>
Task<ApiResult<LoginResultModel>> LoginAsync(EncryptedLoginRequest request, CancellationToken ct = default);
/// <summary>Logs out the current user.</summary>
/// <param name="ct">The cancellation token.</param>
Task<ApiResult<Unit>> LogoutAsync(CancellationToken ct = default);
/// <summary>Gets the current authenticated user's information.</summary>
/// <param name="ct">The cancellation token.</param>
Task<ApiResult<UserInfoDto>> GetCurrentUserAsync(CancellationToken ct = default);
}
@@ -12,28 +12,48 @@ public interface IFileApiClient
// Downloads (POST with existing data, returns Excel bytes)
/// <summary>Downloads work orders template, optionally pre-filled with existing data.</summary>
/// <param name="existingData">Optional existing data to pre-fill the template.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<byte[]>> DownloadWorkOrdersTemplateAsync(IReadOnlyList<WorkOrderViewModel>? existingData = null, CancellationToken ct = default);
/// <summary>Downloads items template, optionally pre-filled with existing data.</summary>
/// <param name="existingData">Optional existing data to pre-fill the template.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<byte[]>> DownloadItemsTemplateAsync(IReadOnlyList<ItemViewModel>? existingData = null, CancellationToken ct = default);
/// <summary>Downloads component lots template, optionally pre-filled with existing data.</summary>
/// <param name="existingData">Optional existing data to pre-fill the template.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<byte[]>> DownloadComponentLotsTemplateAsync(IReadOnlyList<LotViewModel>? existingData = null, CancellationToken ct = default);
/// <summary>Downloads part operations template, optionally pre-filled with existing data.</summary>
/// <param name="existingData">Optional existing data to pre-fill the template.</param>
/// <param name="ct">Cancellation token.</param>
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>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The name of the file being uploaded.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<WorkOrderViewModel>>> UploadWorkOrdersAsync(Stream fileStream, string fileName, CancellationToken ct = default);
/// <summary>Uploads items Excel file and returns parsed data.</summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The name of the file being uploaded.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<ItemViewModel>>> UploadItemsAsync(Stream fileStream, string fileName, CancellationToken ct = default);
/// <summary>Uploads component lots Excel file and returns parsed data.</summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The name of the file being uploaded.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<LotViewModel>>> UploadComponentLotsAsync(Stream fileStream, string fileName, CancellationToken ct = default);
/// <summary>Uploads part operations Excel file and returns parsed data.</summary>
/// <param name="fileStream">The file stream to upload.</param>
/// <param name="fileName">The name of the file being uploaded.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<PartOperationViewModel>>> UploadPartOperationsAsync(Stream fileStream, string fileName, CancellationToken ct = default);
}
@@ -9,14 +9,22 @@ namespace JdeScoping.Core.ApiContracts;
public interface ILookupApiClient
{
/// <summary>Finds items matching the search query.</summary>
/// <param name="query">The search query to match against item data.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<ItemViewModel>>> FindItemsAsync(string query, CancellationToken ct = default);
/// <summary>Finds profit centers matching the search query.</summary>
/// <param name="query">The search query to match against profit center data.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<ProfitCenterViewModel>>> FindProfitCentersAsync(string query, CancellationToken ct = default);
/// <summary>Finds work centers matching the search query.</summary>
/// <param name="query">The search query to match against work center data.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<WorkCenterViewModel>>> FindWorkCentersAsync(string query, CancellationToken ct = default);
/// <summary>Finds operators (JDE users) matching the search query.</summary>
/// <param name="query">The search query to match against operator data.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<JdeUserViewModel>>> FindOperatorsAsync(string query, CancellationToken ct = default);
}
@@ -9,14 +9,22 @@ namespace JdeScoping.Core.ApiContracts;
public interface IPipelineApiClient
{
/// <summary>Gets list of all available pipeline names.</summary>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<PipelineListResponse>> GetPipelineNamesAsync(CancellationToken ct = default);
/// <summary>Gets configuration for a specific pipeline.</summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<PipelineConfigDto>> GetPipelineAsync(string name, CancellationToken ct = default);
/// <summary>Gets schedule status for a pipeline.</summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<PipelineStatusResponse>> GetStatusAsync(string name, CancellationToken ct = default);
/// <summary>Gets recent execution history for a pipeline.</summary>
/// <param name="name">The pipeline name.</param>
/// <param name="count">The maximum number of executions to return.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<PipelineExecutionsResponse>> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default);
}
@@ -9,20 +9,30 @@ namespace JdeScoping.Core.ApiContracts;
public interface ISearchApiClient
{
/// <summary>Gets all searches for the current user.</summary>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetUserSearchesAsync(CancellationToken ct = default);
/// <summary>Gets all queued searches.</summary>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<IReadOnlyList<SearchViewModel>>> GetQueuedSearchesAsync(CancellationToken ct = default);
/// <summary>Gets a specific search by ID.</summary>
/// <param name="id">The search identifier.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<SearchViewModel>> GetSearchAsync(int id, CancellationToken ct = default);
/// <summary>Copies an existing search to create a new one (returns copy without persisting).</summary>
/// <param name="id">The search identifier to copy.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<SearchViewModel>> CopySearchAsync(int id, CancellationToken ct = default);
/// <summary>Creates and submits a new search.</summary>
/// <param name="search">The search to create.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<int>> CreateSearchAsync(SearchViewModel search, CancellationToken ct = default);
/// <summary>Downloads the results for a completed search as Excel bytes.</summary>
/// <param name="id">The search identifier.</param>
/// <param name="ct">Cancellation token.</param>
Task<ApiResult<byte[]>> GetResultsAsync(int id, CancellationToken ct = default);
}
@@ -10,6 +10,8 @@ public readonly record struct ValidationError(IReadOnlyDictionary<string, string
/// <summary>
/// Creates a ValidationError from a dictionary of field errors.
/// </summary>
/// <param name="errors">Dictionary mapping field names to error message arrays.</param>
/// <returns>A ValidationError containing the field errors.</returns>
public static ValidationError FromDictionary(Dictionary<string, string[]> errors)
=> new(errors);
}
@@ -7,9 +7,15 @@ namespace JdeScoping.Core.Models.Auth;
/// </summary>
public class LoginModel
{
/// <summary>
/// Gets or sets the user's login username.
/// </summary>
[Required(ErrorMessage = "Username is required")]
public string Username { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the user's login password.
/// </summary>
[Required(ErrorMessage = "Password is required")]
public string Password { get; set; } = string.Empty;
}
@@ -6,20 +6,34 @@ namespace JdeScoping.Core.Models.Infrastructure;
/// </summary>
public class DataUpdateDto
{
/// <summary>The start time of the data update.</summary>
public DateTime StartDt { get; set; }
/// <summary>The end time of the data update.</summary>
public DateTime? EndDt { get; set; }
/// <summary>The number of branch records updated.</summary>
public int BranchRecords { get; set; }
/// <summary>The number of profit center records updated.</summary>
public int ProfitCenterRecords { get; set; }
/// <summary>The number of work center records updated.</summary>
public int WorkCenterRecords { get; set; }
/// <summary>The number of organizational hierarchy records updated.</summary>
public int OrgHierarchyRecords { get; set; }
/// <summary>The number of status code records updated.</summary>
public int StatusCodeRecords { get; set; }
/// <summary>The number of user records updated.</summary>
public int UserRecords { get; set; }
/// <summary>The number of item records updated.</summary>
public int ItemRecords { get; set; }
/// <summary>The number of lot records updated.</summary>
public int LotRecords { get; set; }
/// <summary>The number of work order records updated.</summary>
public int WorkOrderRecords { get; set; }
/// <summary>The number of work order step records updated.</summary>
public int WorkOrderStepRecords { get; set; }
/// <summary>The number of work order component records updated.</summary>
public int WorkOrderComponentRecords { get; set; }
/// <summary>Whether the data update was successful.</summary>
public bool WasSuccessful { get; set; }
}
@@ -5,16 +5,28 @@ namespace JdeScoping.Core.Models.SearchResults;
/// </summary>
public sealed class MisNonMatchSearchResult
{
/// <summary>The work center code.</summary>
public string WorkCenterCode { get; init; } = string.Empty;
/// <summary>The work order number.</summary>
public long WorkOrderNumber { get; init; }
/// <summary>The work order start date.</summary>
public DateTime WorkOrderStartDate { get; init; }
/// <summary>The job step number.</summary>
public decimal JobStepNumber { get; init; }
/// <summary>The job step description.</summary>
public string JobStepDescription { get; init; } = string.Empty;
/// <summary>The job step end date.</summary>
public DateTime? JobStepEndDate { get; init; }
/// <summary>The function code.</summary>
public string FunctionCode { get; init; } = string.Empty;
/// <summary>Indicates whether the job step was added.</summary>
public bool WasJobStepAdded { get; init; }
/// <summary>The matched job step number, if any.</summary>
public decimal? MatchedJobStepNumber { get; init; }
/// <summary>The item number.</summary>
public string ItemNumber { get; init; } = string.Empty;
/// <summary>The item description.</summary>
public string ItemDescription { get; init; } = string.Empty;
/// <summary>The routing type.</summary>
public string RoutingType { get; init; } = string.Empty;
}
@@ -5,23 +5,42 @@ namespace JdeScoping.Core.Models.SearchResults;
/// </summary>
public sealed class MisSearchResult
{
/// <summary>The item number.</summary>
public string ItemNumber { get; init; } = string.Empty;
/// <summary>The item description.</summary>
public string ItemDescription { get; init; } = string.Empty;
/// <summary>The sequence number.</summary>
public string SequenceNumber { get; init; } = string.Empty;
/// <summary>The MIS (Manufacturing Instruction Sheet) number.</summary>
public string MisNumber { get; init; } = string.Empty;
/// <summary>The revision ID.</summary>
public string RevId { get; init; } = string.Empty;
/// <summary>The status.</summary>
public string Status { get; init; } = string.Empty;
/// <summary>The release date.</summary>
public DateTime? ReleaseDate { get; init; }
/// <summary>The branch code.</summary>
public string BranchCode { get; init; } = string.Empty;
/// <summary>The job step sequence number.</summary>
public decimal JobStepSequenceNumber { get; init; }
/// <summary>The matched sequence number.</summary>
public decimal? MatchedSequenceNumber { get; init; }
/// <summary>Whether the routing matches.</summary>
public bool RoutingMatch { get; init; }
/// <summary>Whether the master matches.</summary>
public bool MasterMatch { get; init; }
/// <summary>The function operation description.</summary>
public string FunctionOperationDescription { get; init; } = string.Empty;
/// <summary>The character number.</summary>
public string CharNumber { get; init; } = string.Empty;
/// <summary>The test description.</summary>
public string TestDescription { get; init; } = string.Empty;
/// <summary>The sampling type.</summary>
public string SamplingType { get; init; } = string.Empty;
/// <summary>The sampling value.</summary>
public string SamplingValue { get; init; } = string.Empty;
/// <summary>The tools and gauges required.</summary>
public string ToolsGauges { get; init; } = string.Empty;
/// <summary>The work instructions.</summary>
public string WorkInstructions { get; init; } = string.Empty;
}
@@ -5,15 +5,25 @@ namespace JdeScoping.Core.Models.SearchResults;
/// </summary>
public class SearchModel
{
/// <summary>The unique identifier for the search.</summary>
public int Id { get; set; }
/// <summary>The username of the user who submitted the search.</summary>
public string UserName { get; set; } = string.Empty;
/// <summary>The name of the search.</summary>
public string Name { get; set; } = string.Empty;
/// <summary>The date and time the search was submitted.</summary>
public DateTime? SubmitDt { get; set; }
/// <summary>The date and time when search execution started.</summary>
public DateTime? StartDt { get; set; }
/// <summary>The date and time when search execution ended.</summary>
public DateTime? EndDt { get; set; }
/// <summary>Indicates whether MIS data extraction is enabled for this search.</summary>
public bool ExtractMisData { get; set; }
/// <summary>The main search results.</summary>
public List<SearchResult> Results { get; set; } = [];
/// <summary>The MIS-specific search results.</summary>
public List<MisSearchResult> MisResults { get; set; } = [];
/// <summary>The MIS non-match investigation results.</summary>
public List<MisNonMatchSearchResult> MisNonMatchResults { get; set; } = [];
}
@@ -5,30 +5,119 @@ namespace JdeScoping.Core.Models.SearchResults;
/// </summary>
public sealed class SearchResult
{
/// <summary>
/// Work order number.
/// </summary>
public long WorkOrderNumber { get; init; }
/// <summary>
/// Branch code for the work order.
/// </summary>
public string WorkOrderBranchCode { get; init; } = string.Empty;
/// <summary>
/// Lot number.
/// </summary>
public string LotNumber { get; init; } = string.Empty;
/// <summary>
/// Item number.
/// </summary>
public string ItemNumber { get; init; } = string.Empty;
/// <summary>
/// Planning family.
/// </summary>
public string PlanningFamily { get; init; } = string.Empty;
/// <summary>
/// Stocking type.
/// </summary>
public string StockingType { get; init; } = string.Empty;
/// <summary>
/// Order quantity.
/// </summary>
public decimal OrderQuantity { get; init; }
/// <summary>
/// Held quantity.
/// </summary>
public decimal HeldQuantity { get; init; }
/// <summary>
/// Scrapped quantity.
/// </summary>
public decimal ScrappedQuantity { get; init; }
/// <summary>
/// Shipped quantity.
/// </summary>
public decimal ShippedQuantity { get; init; }
/// <summary>
/// Branch code for the step.
/// </summary>
public string StepBranchCode { get; init; } = string.Empty;
/// <summary>
/// Step number.
/// </summary>
public decimal StepNumber { get; init; }
/// <summary>
/// Description of the step.
/// </summary>
public string StepDescription { get; init; } = string.Empty;
/// <summary>
/// Function operation description.
/// </summary>
public string FunctionOperationDescription { get; init; } = string.Empty;
/// <summary>
/// Step update date and time.
/// </summary>
public DateTime StepUpdateDt { get; init; }
/// <summary>
/// Status code.
/// </summary>
public string StatusCode { get; init; } = string.Empty;
/// <summary>
/// Status description.
/// </summary>
public string StatusDescription { get; init; } = string.Empty;
/// <summary>
/// Status update date and time.
/// </summary>
public DateTime? StatusUpdateDt { get; init; }
// Inclusion flags
/// <summary>
/// Indicates if manually specified in search criteria.
/// </summary>
public bool ManuallySpecified { get; init; }
/// <summary>
/// Indicates if part of a split order.
/// </summary>
public bool SplitOrder { get; init; }
/// <summary>
/// Indicates if flagged for cardex inclusion.
/// </summary>
public bool Cardex { get; init; }
/// <summary>
/// Indicates if included via parts list.
/// </summary>
public bool PartsList { get; init; }
/// <summary>
/// Indicates if flagged for inclusion.
/// </summary>
public bool Flagged { get; init; }
/// <summary>
@@ -8,9 +8,17 @@ namespace JdeScoping.Core.ViewModels;
/// </summary>
public class ComponentLotViewModel
{
/// <summary>
/// Gets or sets the lot number.
/// </summary>
public string LotNumber { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the item number.
/// </summary>
public string ItemNumber { get; set; } = string.Empty;
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (obj is ComponentLotViewModel other)
@@ -20,5 +28,6 @@ public class ComponentLotViewModel
return false;
}
/// <inheritdoc />
public override int GetHashCode() => HashCode.Combine(LotNumber, ItemNumber);
}
@@ -5,10 +5,22 @@ namespace JdeScoping.Core.ViewModels;
/// </summary>
public class OperatorViewModel
{
/// <summary>
/// Gets or sets the operator's address number.
/// </summary>
public long AddressNumber { get; set; }
/// <summary>
/// Gets or sets the operator's user ID.
/// </summary>
public string UserId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the operator's full name.
/// </summary>
public string FullName { get; set; } = string.Empty;
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (obj is OperatorViewModel other)
@@ -18,5 +30,6 @@ public class OperatorViewModel
return false;
}
/// <inheritdoc />
public override int GetHashCode() => UserId.GetHashCode();
}
@@ -13,6 +13,8 @@ public static class ViewModelExtensions
/// <summary>
/// Converts an Item to ItemViewModel
/// </summary>
/// <param name="item">The item to convert.</param>
/// <returns>An ItemViewModel containing the item data.</returns>
public static ItemViewModel ToViewModel(this Item item)
{
return new ItemViewModel
@@ -25,6 +27,8 @@ public static class ViewModelExtensions
/// <summary>
/// Converts a ProfitCenter to ProfitCenterViewModel
/// </summary>
/// <param name="profitCenter">The profit center to convert.</param>
/// <returns>A ProfitCenterViewModel containing the profit center data.</returns>
public static ProfitCenterViewModel ToViewModel(this ProfitCenter profitCenter)
{
return new ProfitCenterViewModel
@@ -37,6 +41,8 @@ public static class ViewModelExtensions
/// <summary>
/// Converts a WorkCenter to WorkCenterViewModel
/// </summary>
/// <param name="workCenter">The work center to convert.</param>
/// <returns>A WorkCenterViewModel containing the work center data.</returns>
public static WorkCenterViewModel ToViewModel(this WorkCenter workCenter)
{
return new WorkCenterViewModel
@@ -49,6 +55,8 @@ public static class ViewModelExtensions
/// <summary>
/// Converts a JdeUser to JdeUserViewModel
/// </summary>
/// <param name="user">The JDE user to convert.</param>
/// <returns>A JdeUserViewModel containing the user data.</returns>
public static JdeUserViewModel ToViewModel(this JdeUser user)
{
return new JdeUserViewModel
@@ -62,6 +70,8 @@ public static class ViewModelExtensions
/// <summary>
/// Converts a WorkOrder to WorkOrderViewModel
/// </summary>
/// <param name="workOrder">The work order to convert.</param>
/// <returns>A WorkOrderViewModel containing the work order data.</returns>
public static WorkOrderViewModel ToViewModel(this WorkOrder workOrder)
{
return new WorkOrderViewModel
@@ -74,6 +84,8 @@ public static class ViewModelExtensions
/// <summary>
/// Converts a Lot to LotViewModel
/// </summary>
/// <param name="lot">The lot to convert.</param>
/// <returns>A LotViewModel containing the lot data.</returns>
public static LotViewModel ToViewModel(this Lot lot)
{
return new LotViewModel
@@ -21,6 +21,9 @@ public partial class LotFinderRepository : ILotFinderRepository
/// <summary>
/// Initializes a new instance of the <see cref="LotFinderRepository"/> class.
/// </summary>
/// <param name="connectionFactory">The database connection factory.</param>
/// <param name="logger">The logger instance.</param>
/// <param name="options">The data access options.</param>
public LotFinderRepository(
IDbConnectionFactory connectionFactory,
ILogger<LotFinderRepository> logger,
@@ -27,6 +27,12 @@ public sealed class SearchProcessor : ISearchProcessor
/// <summary>
/// Initializes a new instance of SearchProcessor.
/// </summary>
/// <param name="connectionFactory">The database connection factory.</param>
/// <param name="queryBuilder">The search query builder.</param>
/// <param name="traversalService">The work order traversal service.</param>
/// <param name="misQueryBuilder">The MIS query builder.</param>
/// <param name="options">The search processing configuration options.</param>
/// <param name="logger">The logger instance.</param>
public SearchProcessor(
IDbConnectionFactory connectionFactory,
ISearchQueryBuilder queryBuilder,
@@ -7,6 +7,9 @@ public record DevPipelinesRoot(
DevPipelineSettings? Settings,
Dictionary<string, DevPipelineConfig> Pipelines)
{
/// <summary>
/// Gets the effective settings, using defaults if not specified.
/// </summary>
public DevPipelineSettings EffectiveSettings => Settings ?? new DevPipelineSettings();
}
@@ -15,6 +15,12 @@ public class DevEtlRegistry
private readonly string _cacheDirectory;
private readonly ILogger<DevEtlRegistry>? _logger;
/// <summary>
/// Initializes a new instance of the DevEtlRegistry.
/// </summary>
/// <param name="pipelineFactory">The factory for creating ETL pipelines.</param>
/// <param name="cacheDirectory">The directory containing cached protobuf files.</param>
/// <param name="logger">Optional logger for recording ETL operations.</param>
public DevEtlRegistry(
IDevEtlPipelineFactory pipelineFactory,
string cacheDirectory,
@@ -32,8 +38,16 @@ public class DevEtlRegistry
_logger = logger;
}
/// <summary>
/// Gets the list of available tables that can be loaded.
/// </summary>
public IEnumerable<string> GetAvailableTables() => _pipelineFactory.GetAvailableTables();
/// <summary>
/// Runs the ETL pipeline for a single table.
/// </summary>
/// <param name="tableName">The name of the table to load.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<PipelineResult> RunAsync(string tableName, CancellationToken cancellationToken = default)
{
_logger?.LogInformation("Running dev ETL for {TableName}", tableName);
@@ -51,6 +65,10 @@ public class DevEtlRegistry
return result;
}
/// <summary>
/// Runs the ETL pipeline for all available tables sequentially.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<IReadOnlyList<PipelineResult>> RunAllAsync(CancellationToken cancellationToken = default)
{
var results = new List<PipelineResult>();
@@ -31,6 +31,9 @@ public class DevEtlPipelineFactory : IDevEtlPipelineFactory
/// <summary>
/// Creates a new development pipeline factory.
/// </summary>
/// <param name="connectionFactory">Factory for creating database connections.</param>
/// <param name="options">Configuration options for the development pipeline.</param>
/// <param name="logger">Logger for pipeline operations.</param>
public DevEtlPipelineFactory(
IDbConnectionFactory connectionFactory,
IOptions<DevPipelineOptions> options,
@@ -49,6 +52,9 @@ public class DevEtlPipelineFactory : IDevEtlPipelineFactory
/// <summary>
/// Creates a new development pipeline factory with pre-loaded config (for testing).
/// </summary>
/// <param name="connectionFactory">Factory for creating database connections.</param>
/// <param name="config">Pre-loaded pipeline configuration.</param>
/// <param name="logger">Logger for pipeline operations.</param>
internal DevEtlPipelineFactory(
IDbConnectionFactory connectionFactory,
DevPipelinesRoot config,
@@ -26,8 +26,17 @@ public sealed class ProtobufZstdFileSource : IImportSource
private BufferedStream? _bufferedStream;
private IDataReader? _reader;
/// <summary>
/// The name of this source, including the file name.
/// </summary>
public string SourceName => $"Protobuf:{Path.GetFileName(_filePath)}";
/// <summary>
/// Initializes a new instance of the <see cref="ProtobufZstdFileSource"/> class.
/// </summary>
/// <param name="filePath">The path to the zstd-compressed protobuf file.</param>
/// <exception cref="ArgumentException">Thrown when filePath is null or empty.</exception>
/// <exception cref="FileNotFoundException">Thrown when the file does not exist.</exception>
public ProtobufZstdFileSource(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
@@ -39,6 +48,12 @@ public sealed class ProtobufZstdFileSource : IImportSource
_filePath = filePath;
}
/// <summary>
/// Reads and decompresses the protobuf file, returning an IDataReader for deserialization.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>An IDataReader for reading the decompressed protobuf data.</returns>
/// <exception cref="InvalidOperationException">Thrown if ReadDataAsync has already been called.</exception>
public async Task<IDataReader> ReadDataAsync(CancellationToken cancellationToken = default)
{
if (_fileStream != null)
@@ -85,6 +100,9 @@ public sealed class ProtobufZstdFileSource : IImportSource
_fileStream = null;
}
/// <summary>
/// Disposes of all unmanaged resources associated with this source.
/// </summary>
public async ValueTask DisposeAsync()
{
if (_reader != null)
@@ -5,7 +5,9 @@ public record PipelinesRoot(
ScheduleDefaults? ScheduleDefaults, // Optional - defaults applied if missing
Dictionary<string, PipelineConfig> Pipelines)
{
/// <summary>Gets the effective pipeline settings, using defaults if not specified.</summary>
public PipelineSettings EffectiveSettings => Settings ?? new PipelineSettings();
/// <summary>Gets the effective schedule defaults, using defaults if not specified.</summary>
public ScheduleDefaults EffectiveScheduleDefaults => ScheduleDefaults ?? new ScheduleDefaults();
}
@@ -33,6 +33,7 @@ public record ScheduleConfig
/// <summary>
/// Merges this config with defaults. Non-null/non-default values in this config override defaults.
/// </summary>
/// <param name="defaults">The default configuration to merge with.</param>
public ScheduleConfig MergeWith(ScheduleConfig defaults)
{
return new ScheduleConfig
@@ -92,7 +93,18 @@ public record ScheduleDefaults
/// </summary>
public record PipelineSchedules
{
/// <summary>
/// Gets or initializes the Mass schedule configuration override.
/// </summary>
public ScheduleConfig? Mass { get; init; }
/// <summary>
/// Gets or initializes the Daily schedule configuration override.
/// </summary>
public ScheduleConfig? Daily { get; init; }
/// <summary>
/// Gets or initializes the Hourly schedule configuration override.
/// </summary>
public ScheduleConfig? Hourly { get; init; }
}
@@ -11,6 +11,8 @@ public static class EtlServiceCollectionExtensions
/// <summary>
/// Adds ETL pipeline services to the service collection.
/// </summary>
/// <param name="services">The service collection to add ETL services to.</param>
/// <returns>The service collection for method chaining.</returns>
public static IServiceCollection AddEtlPipeline(this IServiceCollection services)
{
// Register the builder as transient so each request gets a fresh builder
@@ -15,8 +15,21 @@ public class EtlPipeline
private readonly IReadOnlyList<IScriptRunner> _postScripts;
private readonly ILogger<EtlPipeline> _logger;
/// <summary>
/// Gets the name of the pipeline.
/// </summary>
public string PipelineName { get; }
/// <summary>
/// Initializes a new instance of the EtlPipeline.
/// </summary>
/// <param name="name">The name of the pipeline.</param>
/// <param name="source">The data source for the pipeline.</param>
/// <param name="transformers">The data transformers to apply.</param>
/// <param name="destination">The destination for the transformed data.</param>
/// <param name="preScripts">Scripts to run before the pipeline executes.</param>
/// <param name="postScripts">Scripts to run after the pipeline executes.</param>
/// <param name="logger">The logger for recording pipeline execution.</param>
internal EtlPipeline(
string name,
IImportSource source,
@@ -35,6 +48,10 @@ public class EtlPipeline
_logger = logger;
}
/// <summary>
/// Executes the ETL pipeline, running all steps in sequence.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task<PipelineResult> ExecuteAsync(CancellationToken cancellationToken = default)
{
var steps = new List<StepResult>();
@@ -15,18 +15,33 @@ public class EtlPipelineBuilder
private ILogger<EtlPipeline>? _logger;
private int _defaultCommandTimeoutSeconds = 600;
/// <summary>
/// Sets the pipeline name.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithName(string name)
{
_name = name ?? throw new ArgumentNullException(nameof(name));
return this;
}
/// <summary>
/// Sets the data source for the pipeline.
/// </summary>
/// <param name="source">The import source.</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithSource(IImportSource source)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
return this;
}
/// <summary>
/// Adds a data transformer to the pipeline.
/// </summary>
/// <param name="transformer">The data transformer.</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithTransformer(IDataTransformer transformer)
{
ArgumentNullException.ThrowIfNull(transformer);
@@ -34,12 +49,22 @@ public class EtlPipelineBuilder
return this;
}
/// <summary>
/// Sets the destination for the transformed data.
/// </summary>
/// <param name="destination">The import destination.</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithDestination(IImportDestination destination)
{
_destination = destination ?? throw new ArgumentNullException(nameof(destination));
return this;
}
/// <summary>
/// Adds a pre-execution script to run before the pipeline starts.
/// </summary>
/// <param name="script">The script runner.</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithPreScript(IScriptRunner script)
{
ArgumentNullException.ThrowIfNull(script);
@@ -47,6 +72,11 @@ public class EtlPipelineBuilder
return this;
}
/// <summary>
/// Adds a post-execution script to run after the pipeline completes.
/// </summary>
/// <param name="script">The script runner.</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithPostScript(IScriptRunner script)
{
ArgumentNullException.ThrowIfNull(script);
@@ -54,6 +84,11 @@ public class EtlPipelineBuilder
return this;
}
/// <summary>
/// Sets the logger for the pipeline.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithLogger(ILogger<EtlPipeline> logger)
{
_logger = logger;
@@ -63,6 +98,11 @@ public class EtlPipelineBuilder
// TODO: Currently this timeout value is stored but not passed to destinations.
// In the future, the pipeline should pass this timeout to destinations that support it.
// For now, destinations use their own default timeout (600 seconds).
/// <summary>
/// Sets the command timeout for database operations.
/// </summary>
/// <param name="timeout">The timeout duration (must be between 0 and 24 hours).</param>
/// <returns>The builder for method chaining.</returns>
public EtlPipelineBuilder WithCommandTimeout(TimeSpan timeout)
{
if (timeout < TimeSpan.Zero || timeout > TimeSpan.FromHours(24))
@@ -72,6 +112,11 @@ public class EtlPipelineBuilder
return this;
}
/// <summary>
/// Builds and returns the configured ETL pipeline.
/// </summary>
/// <returns>The constructed ETL pipeline.</returns>
/// <exception cref="InvalidOperationException">Thrown when source or destination is not configured.</exception>
public EtlPipeline Build()
{
if (_source == null)
@@ -9,6 +9,7 @@ public static class CommonScripts
/// Parses a table name, extracting schema if present.
/// Supports: "Table", "dbo.Table", "[dbo].[Table]"
/// </summary>
/// <param name="tableName">The table name to parse.</param>
public static (string Schema, string Table) ParseTableName(string tableName)
{
var cleaned = tableName.Replace("[", "").Replace("]", "");
@@ -21,12 +22,19 @@ public static class CommonScripts
/// <summary>
/// Formats a table name as a properly quoted [schema].[table] identifier.
/// </summary>
/// <param name="tableName">The table name to format.</param>
public static string FormatQualifiedTableName(string tableName)
{
var (schema, table) = ParseTableName(tableName);
return $"[{schema}].[{table}]";
}
/// <summary>
/// Creates a script runner that disables all indexes on a table.
/// </summary>
/// <param name="factory">The database connection factory.</param>
/// <param name="tableName">The table name to disable indexes for.</param>
/// <param name="timeoutSeconds">The execution timeout in seconds (default 300).</param>
public static IScriptRunner DisableIndexes(
IDbConnectionFactory factory,
string tableName,
@@ -54,6 +62,12 @@ IF LEN(@sql) > 0 EXEC sp_executesql @sql;";
timeoutSeconds: timeoutSeconds);
}
/// <summary>
/// Creates a script runner that rebuilds all indexes on a table.
/// </summary>
/// <param name="factory">The database connection factory.</param>
/// <param name="tableName">The table name to rebuild indexes for.</param>
/// <param name="timeoutSeconds">The execution timeout in seconds (default 3600).</param>
public static IScriptRunner RebuildIndexes(
IDbConnectionFactory factory,
string tableName,
@@ -70,6 +84,12 @@ EXEC sp_executesql @sql;";
timeoutSeconds: timeoutSeconds);
}
/// <summary>
/// Creates a script runner that updates statistics on a table.
/// </summary>
/// <param name="factory">The database connection factory.</param>
/// <param name="tableName">The table name to update statistics for.</param>
/// <param name="timeoutSeconds">The execution timeout in seconds (default 600).</param>
public static IScriptRunner UpdateStatistics(
IDbConnectionFactory factory,
string tableName,
@@ -86,6 +106,14 @@ EXEC sp_executesql @sql;";
timeoutSeconds: timeoutSeconds);
}
/// <summary>
/// Creates a script runner for executing custom SQL.
/// </summary>
/// <param name="factory">The database connection factory.</param>
/// <param name="sql">The SQL statement to execute.</param>
/// <param name="name">The name/description of the script.</param>
/// <param name="parameters">Optional parameters for the SQL statement.</param>
/// <param name="timeoutSeconds">The execution timeout in seconds (default 30).</param>
public static IScriptRunner CustomSql(
IDbConnectionFactory factory,
string sql,
@@ -4,6 +4,9 @@ using JdeScoping.DataSync.Etl.Contracts;
namespace JdeScoping.DataSync.Etl.Scripts;
/// <summary>
/// SQL script runner that executes SQL commands against the database.
/// </summary>
public class SqlScriptRunner : IScriptRunner
{
private readonly IDbConnectionFactory _connectionFactory;
@@ -11,8 +14,19 @@ public class SqlScriptRunner : IScriptRunner
private readonly object? _parameters;
private readonly int _timeoutSeconds;
/// <summary>
/// The name of this script.
/// </summary>
public string ScriptName { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SqlScriptRunner"/> class.
/// </summary>
/// <param name="connectionFactory">The database connection factory.</param>
/// <param name="sql">The SQL command to execute.</param>
/// <param name="name">The optional name of the script.</param>
/// <param name="parameters">The optional parameters for the SQL command.</param>
/// <param name="timeoutSeconds">The command timeout in seconds.</param>
public SqlScriptRunner(
IDbConnectionFactory connectionFactory,
string sql,
@@ -30,6 +44,10 @@ public class SqlScriptRunner : IScriptRunner
ScriptName = name ?? "SqlScript";
}
/// <summary>
/// Executes the SQL script asynchronously.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(cancellationToken);
@@ -31,36 +31,53 @@ public abstract class DataTransformerBase : IDataTransformer
/// Gets the field count from the source reader.
/// Override to add or remove fields.
/// </summary>
/// <param name="source">The source data reader.</param>
/// <returns>The field count.</returns>
public virtual int GetFieldCount(IDataReader source) => source.FieldCount;
/// <summary>
/// Gets the name of a field at the specified ordinal.
/// Override to rename fields.
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="source">The source data reader.</param>
/// <returns>The field name.</returns>
public virtual string GetName(int ordinal, IDataReader source) => source.GetName(ordinal);
/// <summary>
/// Gets the type of a field at the specified ordinal.
/// Override to change field types.
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="source">The source data reader.</param>
/// <returns>The field type.</returns>
public virtual Type GetFieldType(int ordinal, IDataReader source) => source.GetFieldType(ordinal);
/// <summary>
/// Gets the value of a field at the specified ordinal.
/// Override to transform values.
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="source">The source data reader.</param>
/// <returns>The field value.</returns>
public virtual object GetValue(int ordinal, IDataReader source) => source.GetValue(ordinal);
/// <summary>
/// Gets the ordinal of a field by name.
/// Override to support renamed fields.
/// </summary>
/// <param name="name">The field name.</param>
/// <param name="source">The source data reader.</param>
/// <returns>The field ordinal.</returns>
public virtual int GetOrdinal(string name, IDataReader source) => source.GetOrdinal(name);
/// <summary>
/// Checks if a field value is DBNull.
/// Override to handle null transformations.
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="source">The source data reader.</param>
/// <returns>True if the field value is DBNull; otherwise, false.</returns>
public virtual bool IsDBNull(int ordinal, IDataReader source) => source.IsDBNull(ordinal);
/// <summary>
@@ -76,6 +93,13 @@ public abstract class DataTransformerBase : IDataTransformer
/// Gets bytes from a field at the specified ordinal.
/// Throws NotSupportedException for computed columns (where MapOrdinal returns -1).
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="fieldOffset">The offset in the field data.</param>
/// <param name="buffer">The buffer to copy bytes into.</param>
/// <param name="bufferOffset">The offset in the buffer where copying begins.</param>
/// <param name="length">The maximum number of bytes to copy.</param>
/// <param name="source">The source data reader.</param>
/// <returns>The number of bytes actually copied.</returns>
public virtual long GetBytes(int ordinal, long fieldOffset, byte[]? buffer,
int bufferOffset, int length, IDataReader source)
{
@@ -90,6 +114,13 @@ public abstract class DataTransformerBase : IDataTransformer
/// Gets characters from a field at the specified ordinal.
/// Throws NotSupportedException for computed columns (where MapOrdinal returns -1).
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="fieldOffset">The offset in the field data.</param>
/// <param name="buffer">The buffer to copy characters into.</param>
/// <param name="bufferOffset">The offset in the buffer where copying begins.</param>
/// <param name="length">The maximum number of characters to copy.</param>
/// <param name="source">The source data reader.</param>
/// <returns>The number of characters actually copied.</returns>
public virtual long GetChars(int ordinal, long fieldOffset, char[]? buffer,
int bufferOffset, int length, IDataReader source)
{
@@ -104,6 +135,9 @@ public abstract class DataTransformerBase : IDataTransformer
/// Gets nested data reader for a field at the specified ordinal.
/// Throws NotSupportedException for computed columns (where MapOrdinal returns -1).
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="source">The source data reader.</param>
/// <returns>A nested data reader for the field.</returns>
public virtual IDataReader GetData(int ordinal, IDataReader source)
{
var sourceOrdinal = MapOrdinal(ordinal, source);
@@ -117,6 +151,9 @@ public abstract class DataTransformerBase : IDataTransformer
/// Gets the data type name for a field at the specified ordinal.
/// Throws NotSupportedException for computed columns (where MapOrdinal returns -1).
/// </summary>
/// <param name="ordinal">The field ordinal.</param>
/// <param name="source">The source data reader.</param>
/// <returns>The data type name.</returns>
public virtual string GetDataTypeName(int ordinal, IDataReader source)
{
var sourceOrdinal = MapOrdinal(ordinal, source);
@@ -11,6 +11,11 @@ internal sealed class TransformingDataReader : IDataReader
private readonly IDataReader _source;
private readonly DataTransformerBase _transformer;
/// <summary>
/// Initializes a new instance of the <see cref="TransformingDataReader"/> class.
/// </summary>
/// <param name="source">The underlying data reader to wrap.</param>
/// <param name="transformer">The transformer to apply to field values.</param>
public TransformingDataReader(IDataReader source, DataTransformerBase transformer)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
@@ -18,41 +23,196 @@ internal sealed class TransformingDataReader : IDataReader
}
// Properties and methods delegated to transformer
/// <summary>
/// Gets the number of columns in the current row.
/// </summary>
public int FieldCount => _transformer.GetFieldCount(_source);
/// <summary>
/// Gets the name of the specified column.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The name of the specified column.</returns>
public string GetName(int i) => _transformer.GetName(i, _source);
/// <summary>
/// Gets the data type of the specified column.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The data type of the specified column.</returns>
public Type GetFieldType(int i) => _transformer.GetFieldType(i, _source);
/// <summary>
/// Gets the column ordinal given the name of the column.
/// </summary>
/// <param name="name">The name of the column.</param>
/// <returns>The zero-based column ordinal.</returns>
public int GetOrdinal(string name) => _transformer.GetOrdinal(name, _source);
/// <summary>
/// Gets the value of the specified column as an object, applying transformations.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The transformed value of the specified column.</returns>
public object GetValue(int i) => _transformer.GetValue(i, _source);
/// <summary>
/// Gets whether the value of the specified column is null.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>True if the value is null; otherwise, false.</returns>
public bool IsDBNull(int i) => _transformer.IsDBNull(i, _source);
/// <summary>
/// Gets the value of the column at the specified ordinal, applying transformations.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The transformed value of the column.</returns>
public object this[int i] => GetValue(i);
/// <summary>
/// Gets the value of the column with the specified name, applying transformations.
/// </summary>
/// <param name="name">The name of the column.</param>
/// <returns>The transformed value of the column.</returns>
public object this[string name] => GetValue(GetOrdinal(name));
// Row navigation - delegated directly to source
/// <summary>
/// Advances the reader to the next record.
/// </summary>
/// <returns>True if there are more rows; otherwise, false.</returns>
public bool Read() => _source.Read();
/// <summary>
/// Advances the reader to the next result set.
/// </summary>
/// <returns>True if there are more result sets; otherwise, false.</returns>
public bool NextResult() => _source.NextResult();
/// <summary>
/// Gets the nesting depth of the current row.
/// </summary>
public int Depth => _source.Depth;
/// <summary>
/// Gets whether the reader is closed.
/// </summary>
public bool IsClosed => _source.IsClosed;
/// <summary>
/// Gets the number of rows affected by the last operation.
/// </summary>
public int RecordsAffected => _source.RecordsAffected;
/// <summary>
/// Closes the reader.
/// </summary>
public void Close() => _source.Close();
/// <summary>
/// Releases all resources used by the reader.
/// </summary>
public void Dispose() => _source.Dispose();
// Typed accessors - use GetValue for transformation support
/// <summary>
/// Gets the value of the specified column as a boolean.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The boolean value of the column.</returns>
public bool GetBoolean(int i) => (bool)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a byte.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The byte value of the column.</returns>
public byte GetByte(int i) => (byte)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a character.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The character value of the column.</returns>
public char GetChar(int i) => (char)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a DateTime.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The DateTime value of the column.</returns>
public DateTime GetDateTime(int i) => (DateTime)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a decimal.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The decimal value of the column.</returns>
public decimal GetDecimal(int i) => (decimal)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a double.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The double value of the column.</returns>
public double GetDouble(int i) => (double)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a float.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The float value of the column.</returns>
public float GetFloat(int i) => (float)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a GUID.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The GUID value of the column.</returns>
public Guid GetGuid(int i) => (Guid)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a 16-bit signed integer.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The 16-bit signed integer value of the column.</returns>
public short GetInt16(int i) => (short)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a 32-bit signed integer.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The 32-bit signed integer value of the column.</returns>
public int GetInt32(int i) => (int)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a 64-bit signed integer.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The 64-bit signed integer value of the column.</returns>
public long GetInt64(int i) => (long)GetValue(i);
/// <summary>
/// Gets the value of the specified column as a string.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The string value of the column.</returns>
public string GetString(int i) => (string)GetValue(i);
// Schema and bulk data access - delegated to transformer
/// <summary>
/// Gets the name of the type for the specified column.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>The name of the data type of the specified column.</returns>
public string GetDataTypeName(int i) => _transformer.GetDataTypeName(i, _source);
/// <summary>
/// Populates an object array with the values of the current row.
/// </summary>
/// <param name="values">An array of objects to populate with column values.</param>
/// <returns>The number of values populated.</returns>
public int GetValues(object[] values)
{
var count = Math.Min(values.Length, FieldCount);
@@ -61,13 +221,40 @@ internal sealed class TransformingDataReader : IDataReader
return count;
}
/// <summary>
/// Gets the byte values of the specified column starting at the given offset.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <param name="fieldOffset">The offset within the field.</param>
/// <param name="buffer">The buffer to read bytes into.</param>
/// <param name="bufferoffset">The offset in the buffer to start writing.</param>
/// <param name="length">The maximum number of bytes to read.</param>
/// <returns>The number of bytes read.</returns>
public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length)
=> _transformer.GetBytes(i, fieldOffset, buffer, bufferoffset, length, _source);
/// <summary>
/// Gets the character values of the specified column starting at the given offset.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <param name="fieldoffset">The offset within the field.</param>
/// <param name="buffer">The buffer to read characters into.</param>
/// <param name="bufferoffset">The offset in the buffer to start writing.</param>
/// <param name="length">The maximum number of characters to read.</param>
/// <returns>The number of characters read.</returns>
public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length)
=> _transformer.GetChars(i, fieldoffset, buffer, bufferoffset, length, _source);
/// <summary>
/// Gets a nested data reader for the specified column.
/// </summary>
/// <param name="i">The zero-based column ordinal.</param>
/// <returns>A nested data reader for the specified column.</returns>
public IDataReader GetData(int i) => _transformer.GetData(i, _source);
/// <summary>
/// Gets a DataTable that describes the schema of the result set.
/// </summary>
/// <returns>A DataTable describing the result set schema, or null if no schema is available.</returns>
public DataTable? GetSchemaTable() => _source.GetSchemaTable();
}
@@ -13,6 +13,7 @@ public class DataSyncHealthCheck : IHealthCheck
/// <summary>
/// Initializes a new instance of the <see cref="DataSyncHealthCheck"/> class.
/// </summary>
/// <param name="repository">The data update repository.</param>
public DataSyncHealthCheck(IDataUpdateRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
@@ -3,5 +3,9 @@ namespace JdeScoping.DataSync.Options;
public class PipelineOptions
{
public const string SectionName = "Pipelines";
/// <summary>
/// Gets or sets the path to the pipeline configuration file.
/// </summary>
public string ConfigPath { get; set; } = "Pipelines/pipelines.json";
}
@@ -22,6 +22,8 @@ public class DataUpdateRepository : IDataUpdateRepository
/// <summary>
/// Initializes a new instance of the <see cref="DataUpdateRepository"/> class.
/// </summary>
/// <param name="connectionFactory">The database connection factory.</param>
/// <param name="logger">The logger instance.</param>
public DataUpdateRepository(
IDbConnectionFactory connectionFactory,
ILogger<DataUpdateRepository> logger)
@@ -53,6 +53,9 @@ public class EtlPipelineFactory : IEtlPipelineFactory
/// <summary>
/// Creates a new pipeline factory with a pre-loaded configuration (for testing).
/// </summary>
/// <param name="connectionFactory">Factory for creating database connections.</param>
/// <param name="config">Pre-loaded pipeline configuration.</param>
/// <param name="logger">Logger for pipeline execution.</param>
internal EtlPipelineFactory(
IDbConnectionFactory connectionFactory,
PipelinesRoot config,
@@ -167,6 +170,15 @@ public class EtlPipelineFactory : IEtlPipelineFactory
private UpdateTypes _updateType = UpdateTypes.Hourly;
private DateTime? _minDtOverride;
/// <summary>
/// Initializes a new instance of the PipelineBuilder class.
/// </summary>
/// <param name="connectionFactory">Factory for creating database connections.</param>
/// <param name="tableName">The name of the table for this pipeline.</param>
/// <param name="config">The pipeline configuration.</param>
/// <param name="settings">Global pipeline settings.</param>
/// <param name="scheduleDefaults">Default schedule configuration.</param>
/// <param name="logger">Logger for pipeline execution.</param>
public PipelineBuilder(
IDbConnectionFactory connectionFactory,
string tableName,
@@ -183,18 +195,32 @@ public class EtlPipelineFactory : IEtlPipelineFactory
_logger = logger;
}
/// <summary>
/// Specifies the update type for this pipeline.
/// </summary>
/// <param name="updateType">The type of update (Mass, Daily, or Hourly).</param>
/// <returns>The builder for fluent configuration.</returns>
public IEtlPipelineBuilder WithUpdateType(UpdateTypes updateType)
{
_updateType = updateType;
return this;
}
/// <summary>
/// Specifies the minimum date for incremental data extraction.
/// </summary>
/// <param name="minDt">The minimum date, or null for no filter.</param>
/// <returns>The builder for fluent configuration.</returns>
public IEtlPipelineBuilder WithMinimumDate(DateTime? minDt)
{
_minDtOverride = minDt;
return this;
}
/// <summary>
/// Builds and returns the configured ETL pipeline.
/// </summary>
/// <returns>A configured ETL pipeline ready for execution.</returns>
public EtlPipeline Build()
{
return BuildWithSchedules();
@@ -21,6 +21,9 @@ public class ScheduleChecker : IScheduleChecker
/// <summary>
/// Initializes a new instance of the <see cref="ScheduleChecker"/> class.
/// </summary>
/// <param name="repository">Repository for data update records.</param>
/// <param name="options">Data sync configuration options.</param>
/// <param name="logger">Logger instance.</param>
public ScheduleChecker(
IDataUpdateRepository repository,
IOptions<DataSyncOptions> options,
@@ -21,6 +21,15 @@ public class SearchExecutionService : ISearchExecutionService
private readonly WorkProcessorOptions _options;
private readonly ILogger<SearchExecutionService> _logger;
/// <summary>
/// Initializes a new instance of the SearchExecutionService.
/// </summary>
/// <param name="searchRepository">The repository for search data access.</param>
/// <param name="searchProcessor">The processor for executing search queries.</param>
/// <param name="excelExportService">The service for generating Excel export files.</param>
/// <param name="notificationService">The service for sending search status notifications.</param>
/// <param name="options">The work processor configuration options.</param>
/// <param name="logger">The logger for recording search execution events.</param>
public SearchExecutionService(
ISearchRepository searchRepository,
ISearchProcessor searchProcessor,
@@ -21,6 +21,11 @@ public class SyncOrchestrator : ISyncOrchestrator
/// <summary>
/// Initializes a new instance of the <see cref="SyncOrchestrator"/> class.
/// </summary>
/// <param name="scopeFactory">Factory for creating service scopes.</param>
/// <param name="scheduleChecker">Checker for determining pending sync tasks.</param>
/// <param name="options">Data sync configuration options.</param>
/// <param name="logger">Logger instance.</param>
/// <param name="metrics">Metrics collector for sync operations.</param>
public SyncOrchestrator(
IServiceScopeFactory scopeFactory,
IScheduleChecker scheduleChecker,
@@ -24,6 +24,11 @@ public class TableSyncOperation : ITableSyncOperation
/// <summary>
/// Initializes a new instance of the <see cref="TableSyncOperation"/> class.
/// </summary>
/// <param name="pipelineFactory">Factory for creating ETL pipelines.</param>
/// <param name="updateRepository">Repository for managing data update records.</param>
/// <param name="options">Data sync configuration options.</param>
/// <param name="logger">Logger for operation events.</param>
/// <param name="metrics">Metrics collector for operation tracking.</param>
public TableSyncOperation(
IEtlPipelineFactory pipelineFactory,
IDataUpdateRepository updateRepository,
@@ -18,6 +18,7 @@ public class DataSyncMetrics
/// <summary>
/// Initializes a new instance of the <see cref="DataSyncMetrics"/> class.
/// </summary>
/// <param name="meterFactory">The meter factory for creating metrics.</param>
public DataSyncMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("JdeScoping.DataSync");
@@ -61,6 +62,8 @@ public class DataSyncMetrics
/// <summary>
/// Records that a sync operation has started.
/// </summary>
/// <param name="tableName">The name of the table being synced.</param>
/// <param name="updateType">The type of update (e.g., Mass, Daily, Hourly).</param>
public void RecordOperationStarted(string tableName, string updateType)
{
_operationsStarted.Add(1,
@@ -71,6 +74,10 @@ public class DataSyncMetrics
/// <summary>
/// Records that a sync operation completed successfully.
/// </summary>
/// <param name="tableName">The name of the table being synced.</param>
/// <param name="updateType">The type of update (e.g., Mass, Daily, Hourly).</param>
/// <param name="recordCount">The number of records processed.</param>
/// <param name="durationSeconds">The operation duration in seconds.</param>
public void RecordOperationCompleted(string tableName, string updateType, long recordCount, double durationSeconds)
{
var tags = new KeyValuePair<string, object?>[]
@@ -87,6 +94,8 @@ public class DataSyncMetrics
/// <summary>
/// Records that a sync operation failed.
/// </summary>
/// <param name="tableName">The name of the table being synced.</param>
/// <param name="updateType">The type of update (e.g., Mass, Daily, Hourly).</param>
public void RecordOperationFailed(string tableName, string updateType)
{
_operationsFailed.Add(1,
@@ -105,6 +114,9 @@ public class DataSyncMetrics
/// <summary>
/// Records completion of a sync cycle.
/// </summary>
/// <param name="successCount">Number of successful operations in the cycle.</param>
/// <param name="failedCount">Number of failed operations in the cycle.</param>
/// <param name="durationSeconds">The cycle duration in seconds.</param>
public void RecordCycleCompleted(int successCount, int failedCount, double durationSeconds)
{
_cyclesCompleted.Add(1,
@@ -26,6 +26,10 @@ public class WorkProcessor : BackgroundService
/// <summary>
/// Initializes a new instance of the <see cref="WorkProcessor"/> class.
/// </summary>
/// <param name="scopeFactory">Factory for creating service scopes.</param>
/// <param name="options">Configuration options for work processing.</param>
/// <param name="logger">Logger for recording work processor events.</param>
/// <param name="metrics">Metrics collector for tracking work cycles.</param>
public WorkProcessor(
IServiceScopeFactory scopeFactory,
IOptions<WorkProcessorOptions> options,
@@ -12,6 +12,10 @@ public sealed class FluentTableWriter
{
private readonly ExcelMapRegistry _registry;
/// <summary>
/// Initializes a new instance of the <see cref="FluentTableWriter"/> class.
/// </summary>
/// <param name="registry">The registry containing Excel column maps for types.</param>
public FluentTableWriter(ExcelMapRegistry registry)
{
_registry = registry;
@@ -20,6 +24,15 @@ public sealed class FluentTableWriter
/// <summary>
/// Writes a table to the worksheet using the registered map for type T.
/// </summary>
/// <typeparam name="T">The type of objects to write to the table.</typeparam>
/// <param name="worksheet">The Excel worksheet to write to.</param>
/// <param name="startRow">The starting row number (1-based).</param>
/// <param name="startCol">The starting column number (1-based).</param>
/// <param name="data">The data to write to the table.</param>
/// <param name="tableNameOverride">Optional override for the table name; uses map name if null.</param>
/// <param name="showHeader">Whether to show a merged header above the table.</param>
/// <param name="headerText">Optional text to display in the merged header.</param>
/// <returns>The created Excel table, or null if no columns are configured.</returns>
public IXLTable? WriteTable<T>(
IXLWorksheet worksheet,
int startRow,
@@ -9,12 +9,17 @@ public sealed class ColumnBuilder<T, TProperty>
{
private readonly ColumnDefinition _definition;
/// <summary>
/// Initializes a new instance of the <see cref="ColumnBuilder{T, TProperty}"/> class.
/// </summary>
/// <param name="definition">The column definition to configure.</param>
internal ColumnBuilder(ColumnDefinition definition)
{
_definition = definition;
}
/// <summary>Sets the column display order.</summary>
/// <param name="order">The display order position.</param>
public ColumnBuilder<T, TProperty> Order(int order)
{
_definition.Order = order;
@@ -22,6 +27,7 @@ public sealed class ColumnBuilder<T, TProperty>
}
/// <summary>Sets the column header text.</summary>
/// <param name="text">The header text to display.</param>
public ColumnBuilder<T, TProperty> Header(string text)
{
_definition.HeaderText = text;
@@ -29,6 +35,7 @@ public sealed class ColumnBuilder<T, TProperty>
}
/// <summary>Sets the Excel number format.</summary>
/// <param name="format">The Excel number format string.</param>
public ColumnBuilder<T, TProperty> Format(string format)
{
_definition.Format = format;
@@ -36,6 +43,7 @@ public sealed class ColumnBuilder<T, TProperty>
}
/// <summary>Sets a fixed column width (disables auto-width).</summary>
/// <param name="width">The fixed column width.</param>
public ColumnBuilder<T, TProperty> Width(double width)
{
_definition.AutoWidth = false;
@@ -44,6 +52,7 @@ public sealed class ColumnBuilder<T, TProperty>
}
/// <summary>Enables text wrapping for the column.</summary>
/// <param name="wrap">Whether to enable text wrapping.</param>
public ColumnBuilder<T, TProperty> WrapText(bool wrap = true)
{
_definition.WrapText = wrap;
@@ -44,6 +44,8 @@ public abstract class ExcelClassMap<T> : IExcelClassMap
/// <summary>
/// Configures the table and tab names for this model.
/// </summary>
/// <param name="tableName">The Excel table name (for named ranges).</param>
/// <param name="tabName">The worksheet tab name.</param>
protected void Table(string tableName, string tabName)
{
TableName = tableName;
@@ -53,6 +55,9 @@ public abstract class ExcelClassMap<T> : IExcelClassMap
/// <summary>
/// Maps a property to an Excel column.
/// </summary>
/// <typeparam name="TProperty">The type of the property being mapped.</typeparam>
/// <param name="property">Expression specifying the property to map.</param>
/// <returns>A column builder for further configuration of the mapped property.</returns>
protected ColumnBuilder<T, TProperty> Map<TProperty>(Expression<Func<T, TProperty>> property)
{
var memberExpr = property.Body as MemberExpression
@@ -10,6 +10,8 @@ public sealed class ExcelMapRegistry
/// <summary>
/// Registers a map for a type.
/// </summary>
/// <typeparam name="T">The type to register a map for.</typeparam>
/// <param name="map">The Excel class map.</param>
public void Register<T>(ExcelClassMap<T> map)
{
_maps[typeof(T)] = map;
@@ -18,11 +20,15 @@ public sealed class ExcelMapRegistry
/// <summary>
/// Gets the map for a type.
/// </summary>
/// <typeparam name="T">The type to get the map for.</typeparam>
/// <returns>The registered Excel class map.</returns>
public IExcelClassMap GetMap<T>() => GetMap(typeof(T));
/// <summary>
/// Gets the map for a type.
/// </summary>
/// <param name="type">The type to get the map for.</param>
/// <returns>The registered Excel class map.</returns>
public IExcelClassMap GetMap(Type type)
{
if (_maps.TryGetValue(type, out var map))
@@ -36,10 +42,14 @@ public sealed class ExcelMapRegistry
/// <summary>
/// Checks if a map exists for a type.
/// </summary>
/// <typeparam name="T">The type to check for.</typeparam>
/// <returns>True if a map exists for the type; otherwise, false.</returns>
public bool HasMap<T>() => HasMap(typeof(T));
/// <summary>
/// Checks if a map exists for a type.
/// </summary>
/// <param name="type">The type to check for.</param>
/// <returns>True if a map exists for the type; otherwise, false.</returns>
public bool HasMap(Type type) => _maps.ContainsKey(type);
}
@@ -8,6 +8,9 @@ namespace JdeScoping.ExcelIO.Mapping.Maps;
/// </summary>
public sealed class TimespanFilterMap : ExcelClassMap<TimespanFilter>
{
/// <summary>
/// Initializes a new instance of the <see cref="TimespanFilterMap"/> class.
/// </summary>
public TimespanFilterMap()
{
Table("Timespan_Filter", "Timespan Filter");
@@ -22,6 +25,9 @@ public sealed class TimespanFilterMap : ExcelClassMap<TimespanFilter>
/// </summary>
public sealed class WorkOrderFilterEntryMap : ExcelClassMap<WorkOrderFilterEntry>
{
/// <summary>
/// Initializes a new instance of the <see cref="WorkOrderFilterEntryMap"/> class.
/// </summary>
public WorkOrderFilterEntryMap()
{
Table("Work_Order_Filter", "Work Order Filter");
@@ -36,6 +42,9 @@ public sealed class WorkOrderFilterEntryMap : ExcelClassMap<WorkOrderFilterEntry
/// </summary>
public sealed class ItemNumberFilterEntryMap : ExcelClassMap<ItemNumberFilterEntry>
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemNumberFilterEntryMap"/> class.
/// </summary>
public ItemNumberFilterEntryMap()
{
Table("Item_Number_Filter", "Item Number Filter");
@@ -50,6 +59,9 @@ public sealed class ItemNumberFilterEntryMap : ExcelClassMap<ItemNumberFilterEnt
/// </summary>
public sealed class ProfitCenterFilterEntryMap : ExcelClassMap<ProfitCenterFilterEntry>
{
/// <summary>
/// Initializes a new instance of the <see cref="ProfitCenterFilterEntryMap"/> class.
/// </summary>
public ProfitCenterFilterEntryMap()
{
Table("Profit_Center_Filter", "Profit Center Filter");
@@ -64,6 +76,9 @@ public sealed class ProfitCenterFilterEntryMap : ExcelClassMap<ProfitCenterFilte
/// </summary>
public sealed class WorkCenterFilterEntryMap : ExcelClassMap<WorkCenterFilterEntry>
{
/// <summary>
/// Initializes a new instance of the <see cref="WorkCenterFilterEntryMap"/> class.
/// </summary>
public WorkCenterFilterEntryMap()
{
Table("Work_Center_Filter", "Work Center Filter");
@@ -78,6 +93,9 @@ public sealed class WorkCenterFilterEntryMap : ExcelClassMap<WorkCenterFilterEnt
/// </summary>
public sealed class OperatorFilterEntryMap : ExcelClassMap<OperatorFilterEntry>
{
/// <summary>
/// Initializes a new instance of the <see cref="OperatorFilterEntryMap"/> class.
/// </summary>
public OperatorFilterEntryMap()
{
Table("Operator_Filter", "Operator Filter");
@@ -93,6 +111,9 @@ public sealed class OperatorFilterEntryMap : ExcelClassMap<OperatorFilterEntry>
/// </summary>
public sealed class ComponentLotFilterEntryMap : ExcelClassMap<ComponentLotFilterEntry>
{
/// <summary>
/// Initializes a new instance of the <see cref="ComponentLotFilterEntryMap"/> class.
/// </summary>
public ComponentLotFilterEntryMap()
{
Table("Component_Lot_Filter", "Component Lot Filter");
@@ -107,6 +128,9 @@ public sealed class ComponentLotFilterEntryMap : ExcelClassMap<ComponentLotFilte
/// </summary>
public sealed class ItemOperationMisFilterEntryMap : ExcelClassMap<ItemOperationMisFilterEntry>
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemOperationMisFilterEntryMap"/> class.
/// </summary>
public ItemOperationMisFilterEntryMap()
{
Table("Item_Operation_MIS_Filter", "Item/Operation/MIS Filter");
@@ -8,6 +8,10 @@ namespace JdeScoping.ExcelIO.Mapping.Maps;
/// </summary>
public sealed class MisNonMatchSearchResultMap : ExcelClassMap<MisNonMatchSearchResult>
{
/// <summary>
/// Initializes a new instance of the <see cref="MisNonMatchSearchResultMap"/> class.
/// Defines the column mappings and headers for exporting MisNonMatchSearchResult to Excel.
/// </summary>
public MisNonMatchSearchResultMap()
{
Table("Investigation", "Investigation");
@@ -8,6 +8,9 @@ namespace JdeScoping.ExcelIO.Mapping.Maps;
/// </summary>
public sealed class MisSearchResultMap : ExcelClassMap<MisSearchResult>
{
/// <summary>
/// Initializes a new instance of the <see cref="MisSearchResultMap"/> class.
/// </summary>
public MisSearchResultMap()
{
Table("MIS_Info", "MIS Info");
@@ -8,6 +8,9 @@ namespace JdeScoping.ExcelIO.Mapping.Maps;
/// </summary>
public sealed class SearchResultMap : ExcelClassMap<SearchResult>
{
/// <summary>
/// Initializes a new instance of the SearchResultMap class.
/// </summary>
public SearchResultMap()
{
Table("Search_Results", "Search Results");
@@ -12,6 +12,10 @@ public class ExcelParserService : IExcelParserService
{
private readonly ILogger<ExcelParserService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ExcelParserService"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
public ExcelParserService(ILogger<ExcelParserService> logger)
{
_logger = logger;
@@ -38,6 +38,11 @@ public sealed class LdapAuthService : IAuthenticationService
private readonly LdapOptions _options;
private readonly ILogger<LdapAuthService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="LdapAuthService"/> class.
/// </summary>
/// <param name="options">The LDAP configuration options.</param>
/// <param name="logger">Logger for recording LDAP authentication events.</param>
public LdapAuthService(
IOptions<LdapOptions> options,
ILogger<LdapAuthService> logger)
@@ -56,6 +56,9 @@ public class RsaKeyService : IRsaKeyService, IDisposable
public byte[] Decrypt(byte[] ciphertext)
=> _rsa.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256);
/// <summary>
/// Releases the RSA key resources.
/// </summary>
public void Dispose()
{
_rsa.Dispose();
@@ -22,6 +22,13 @@ public class SecretsMigrator
public const string ExcelCriteriaPasswordKey = "ExcelExport:CriteriaSheetPassword";
public const string ExcelDataPasswordKey = "ExcelExport:DataSheetPassword";
/// <summary>
/// Initializes a new instance of the <see cref="SecretsMigrator"/> class.
/// </summary>
/// <param name="secureStore">Service for secure secret storage.</param>
/// <param name="configuration">Application configuration containing existing secrets.</param>
/// <param name="options">Options controlling secret migration behavior.</param>
/// <param name="logger">Logger for recording migration operations.</param>
public SecretsMigrator(
ISecureStoreService secureStore,
IConfiguration configuration,
@@ -22,6 +22,8 @@ public class SecureStoreRsaKeyService : IRsaKeyService, IDisposable
/// <summary>
/// Creates a new SecureStore-backed RSA key service.
/// </summary>
/// <param name="secureStore">Service for storing keys securely.</param>
/// <param name="logger">Logger for key service operations.</param>
public SecureStoreRsaKeyService(
ISecureStoreService secureStore,
ILogger<SecureStoreRsaKeyService> logger)
@@ -61,6 +63,9 @@ public class SecureStoreRsaKeyService : IRsaKeyService, IDisposable
return _rsa.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256);
}
/// <summary>
/// Releases resources used by the RSA key service.
/// </summary>
public void Dispose()
{
if (_disposed) return;
@@ -22,6 +22,8 @@ public class SecureStoreService : ISecureStoreService
/// <summary>
/// Creates a new SecureStoreService instance.
/// </summary>
/// <param name="options">The SecureStore configuration options.</param>
/// <param name="logger">The logger instance.</param>
public SecureStoreService(IOptions<SecureStoreOptions> options, ILogger<SecureStoreService> logger)
{
_logger = logger;
@@ -160,6 +162,9 @@ public class SecureStoreService : ISecureStoreService
/// <inheritdoc />
public IEnumerable<string> Keys => _keys.ToList().AsReadOnly();
/// <summary>
/// Disposes the secure store and saves any pending changes.
/// </summary>
public void Dispose()
{
if (_disposed) return;
@@ -7,69 +7,226 @@ namespace JdeScoping.ConfigManager.Models;
/// </summary>
public class ConfigModel
{
/// <summary>
/// Gets or sets the data synchronization configuration.
/// </summary>
public DataSyncSection DataSync { get; set; } = new();
/// <summary>
/// Gets or sets the data access configuration.
/// </summary>
public DataAccessSection DataAccess { get; set; } = new();
/// <summary>
/// Gets or sets the authentication configuration.
/// </summary>
public AuthSection Auth { get; set; } = new();
/// <summary>
/// Gets or sets the LDAP directory configuration.
/// </summary>
public LdapSection Ldap { get; set; } = new();
/// <summary>
/// Gets or sets the search processing configuration.
/// </summary>
public SearchSection Search { get; set; } = new();
/// <summary>
/// Gets or sets the Excel export configuration.
/// </summary>
public ExcelExportSection ExcelExport { get; set; } = new();
/// <summary>
/// Gets or sets the connection strings for external data sources.
/// </summary>
public Dictionary<string, string> ConnectionStrings { get; set; } = new();
}
public class DataSyncSection
{
/// <summary>
/// Gets or sets the interval between successive data sync checks.
/// </summary>
public TimeSpan CheckInterval { get; set; } = TimeSpan.FromMinutes(1);
/// <summary>
/// Gets or sets the maximum degree of parallelism for sync operations.
/// </summary>
public int MaxDegreeOfParallelism { get; set; } = 4;
/// <summary>
/// Gets or sets the batch size for data sync operations.
/// </summary>
public int BatchSize { get; set; } = 50000;
/// <summary>
/// Gets or sets the batch size for bulk copy operations.
/// </summary>
public int BulkCopyBatchSize { get; set; } = 5000;
/// <summary>
/// Gets or sets the lookback multiplier for data sync delta calculations.
/// </summary>
public double LookbackMultiplier { get; set; } = 1.5;
/// <summary>
/// Gets or sets the number of days to retain synced data before purging.
/// </summary>
public int PurgeRetentionDays { get; set; } = 90;
/// <summary>
/// Gets or sets the timeout in seconds for sync operations.
/// </summary>
public int SyncTimeoutSeconds { get; set; } = 3600;
/// <summary>
/// Gets or sets a value indicating whether data synchronization is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
}
public class DataAccessSection
{
/// <summary>
/// Gets or sets the default timeout in seconds for database queries.
/// </summary>
public int DefaultTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Gets or sets the timeout in seconds for lot usage queries.
/// </summary>
public int LotUsageTimeoutSeconds { get; set; } = 120;
/// <summary>
/// Gets or sets the timeout in seconds for MIS data queries.
/// </summary>
public int MisDataTimeoutSeconds { get; set; } = 300;
/// <summary>
/// Gets or sets the schema name for production data.
/// </summary>
public string ProductionSchema { get; set; } = "prod";
/// <summary>
/// Gets or sets the schema name for archive data.
/// </summary>
public string ArchiveSchema { get; set; } = "archive";
/// <summary>
/// Gets or sets the schema name for staging data.
/// </summary>
public string StageSchema { get; set; } = "stage";
/// <summary>
/// Gets or sets a value indicating whether detailed query logging is enabled.
/// </summary>
public bool EnableDetailedLogging { get; set; } = false;
}
public class AuthSection
{
/// <summary>
/// Gets or sets the name of the authentication cookie.
/// </summary>
public string CookieName { get; set; } = ".JdeScoping.Auth";
/// <summary>
/// Gets or sets the cookie expiration time in minutes.
/// </summary>
public int CookieExpirationMinutes { get; set; } = 480;
}
public class LdapSection
{
/// <summary>
/// Gets or sets the LDAP server URLs to connect to.
/// </summary>
public string[] ServerUrls { get; set; } = [];
/// <summary>
/// Gets or sets the distinguished name of the LDAP group for authorization.
/// </summary>
public string GroupDn { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the base distinguished name for LDAP searches.
/// </summary>
public string SearchBase { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the connection timeout in seconds for LDAP operations.
/// </summary>
public int ConnectionTimeoutSeconds { get; set; } = 30;
/// <summary>
/// Gets or sets a value indicating whether to use fake authentication instead of LDAP.
/// </summary>
public bool UseFakeAuth { get; set; } = false;
/// <summary>
/// Gets or sets an array of user names that bypass group membership validation.
/// </summary>
public string[] AdminBypassUsers { get; set; } = [];
}
public class SearchSection
{
/// <summary>
/// Gets or sets the maximum number of result rows returned by a search.
/// </summary>
public int MaxResultRows { get; set; } = 100000;
/// <summary>
/// Gets or sets the timeout in seconds for search operations.
/// </summary>
public int TimeoutSeconds { get; set; } = 300;
/// <summary>
/// Gets or sets the maximum number of concurrent search operations allowed.
/// </summary>
public int MaxConcurrentSearches { get; set; } = 5;
}
public class ExcelExportSection
{
/// <summary>
/// Gets or sets the password for protecting the criteria worksheet.
/// </summary>
public string CriteriaSheetPassword { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the password for protecting the data worksheet.
/// </summary>
public string DataSheetPassword { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the maximum number of rows per Excel worksheet.
/// </summary>
public int MaxRowsPerSheet { get; set; } = 1000000;
/// <summary>
/// Gets or sets the default date format for Excel exports.
/// </summary>
public string DefaultDateFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
/// <summary>
/// Gets or sets a value indicating whether to write debug output to files.
/// </summary>
public bool DebugWriteToFile { get; set; } = false;
/// <summary>
/// Gets or sets the directory path for debug output files.
/// </summary>
public string DebugOutputDirectory { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the time zone identifier for date/time conversions.
/// </summary>
public string TimezoneId { get; set; } = "America/Chicago";
/// <summary>
/// Gets or sets the time zone abbreviation for display purposes.
/// </summary>
public string TimezoneAbbreviation { get; set; } = "CT";
}
@@ -5,56 +5,144 @@ namespace JdeScoping.ConfigManager.Models;
/// </summary>
public class PipelinesConfigModel
{
/// <summary>
/// Gets or sets the pipeline settings.
/// </summary>
public PipelineSettings Settings { get; set; } = new();
/// <summary>
/// Gets or sets the default schedules for all pipelines.
/// </summary>
public ScheduleDefaults ScheduleDefaults { get; set; } = new();
/// <summary>
/// Gets or sets the collection of named pipelines.
/// </summary>
public Dictionary<string, PipelineModel> Pipelines { get; set; } = new();
}
public class PipelineSettings
{
/// <summary>
/// Gets or sets the timezone for scheduling operations.
/// </summary>
public string Timezone { get; set; } = "UTC";
}
public class ScheduleDefaults
{
/// <summary>
/// Gets or sets the default mass data refresh schedule.
/// </summary>
public ScheduleModel Mass { get; set; } = new() { Enabled = true, IntervalMinutes = 10080, PrePurge = true, ReIndex = true };
/// <summary>
/// Gets or sets the default daily data refresh schedule.
/// </summary>
public ScheduleModel Daily { get; set; } = new() { Enabled = true, IntervalMinutes = 1440 };
/// <summary>
/// Gets or sets the default hourly data refresh schedule.
/// </summary>
public ScheduleModel Hourly { get; set; } = new() { Enabled = true, IntervalMinutes = 60 };
}
public class PipelineModel
{
/// <summary>
/// Gets or sets the source configuration for data extraction.
/// </summary>
public PipelineSource Source { get; set; } = new();
/// <summary>
/// Gets or sets the schedule configurations for this pipeline.
/// </summary>
public PipelineSchedules Schedules { get; set; } = new();
/// <summary>
/// Gets or sets the destination configuration for data loading.
/// </summary>
public PipelineDestination Destination { get; set; } = new();
/// <summary>
/// Gets or sets optional scripts to execute after pipeline completion.
/// </summary>
public string[]? PostScripts { get; set; }
}
public class PipelineSource
{
/// <summary>
/// Gets or sets the source database connection name.
/// </summary>
public string Connection { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the query to extract data from the source.
/// </summary>
public string Query { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the optional mass query for full data extraction.
/// </summary>
public string? MassQuery { get; set; }
/// <summary>
/// Gets or sets the query parameters and their definitions.
/// </summary>
public Dictionary<string, ParameterDefinition> Parameters { get; set; } = new();
}
public class ParameterDefinition
{
/// <summary>
/// Gets or sets the parameter name.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the optional parameter format string.
/// </summary>
public string? Format { get; set; }
/// <summary>
/// Gets or sets the optional parameter source or derivation logic.
/// </summary>
public string? Source { get; set; }
}
public class PipelineSchedules
{
/// <summary>
/// Gets or sets the mass refresh schedule for this pipeline.
/// </summary>
public ScheduleModel? Mass { get; set; }
/// <summary>
/// Gets or sets the daily refresh schedule for this pipeline.
/// </summary>
public ScheduleModel? Daily { get; set; }
/// <summary>
/// Gets or sets the hourly refresh schedule for this pipeline.
/// </summary>
public ScheduleModel? Hourly { get; set; }
}
public class PipelineDestination
{
/// <summary>
/// Gets or sets the destination table name.
/// </summary>
public string Table { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the columns used to match existing records for updates.
/// </summary>
public string[] MatchColumns { get; set; } = [];
/// <summary>
/// Gets or sets the columns to exclude from update operations.
/// </summary>
public string[] ExcludeFromUpdate { get; set; } = [];
}
@@ -5,8 +5,23 @@ namespace JdeScoping.ConfigManager.Models;
/// </summary>
public class ScheduleModel
{
/// <summary>
/// Gets or sets a value indicating whether the scheduled task is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Gets or sets the interval in minutes between scheduled task executions.
/// </summary>
public int IntervalMinutes { get; set; } = 60;
/// <summary>
/// Gets or sets a value indicating whether to purge data before task execution.
/// </summary>
public bool PrePurge { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether to reindex after task execution.
/// </summary>
public bool ReIndex { get; set; } = false;
}
@@ -4,10 +4,18 @@ namespace JdeScoping.ConfigManager;
class Program
{
/// <summary>
/// The entry point of the application.
/// </summary>
/// <param name="args">Command-line arguments.</param>
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
/// <summary>
/// Builds the Avalonia application builder with platform and font configuration.
/// </summary>
/// <returns>A configured AppBuilder ready for application startup.</returns>
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
@@ -17,12 +17,22 @@ public class AutoDiscoveryService : IAutoDiscoveryService
private const string EnvVarName = "JDESCOPING_CONFIG_PATH";
private const string AppSettingsFileName = "appsettings.json";
/// <summary>
/// Initializes a new instance of the <see cref="AutoDiscoveryService"/> class.
/// </summary>
/// <param name="fileSystem">The file system abstraction to use for directory and file checks.</param>
/// <param name="logger">Optional logger for recording discovery process information.</param>
public AutoDiscoveryService(IFileSystem fileSystem, ILogger<AutoDiscoveryService>? logger = null)
{
_fileSystem = fileSystem;
_logger = logger;
}
/// <summary>
/// Finds the configuration folder using a prioritized search strategy.
/// </summary>
/// <param name="ct">Cancellation token for the async operation.</param>
/// <returns>The path to a valid configuration folder, or null if none is found.</returns>
public Task<string?> FindConfigFolderAsync(CancellationToken ct = default)
{
// 1. Check environment variable
@@ -12,12 +12,24 @@ public class BackupService : IBackupService
private readonly ILogger<BackupService>? _logger;
private const string TimestampFormat = "yyyy-MM-dd_HHmmss";
/// <summary>
/// Initializes a new instance of the <see cref="BackupService"/> class.
/// </summary>
/// <param name="fileSystem">The file system abstraction for file operations.</param>
/// <param name="logger">Optional logger for diagnostic messages.</param>
public BackupService(IFileSystem fileSystem, ILogger<BackupService>? logger = null)
{
_fileSystem = fileSystem;
_logger = logger;
}
/// <summary>
/// Creates a backup copy of the specified file with a timestamp suffix.
/// </summary>
/// <param name="filePath">The path of the file to backup.</param>
/// <param name="ct">Cancellation token for the operation.</param>
/// <returns>The path of the created backup file.</returns>
/// <exception cref="FileNotFoundException">Thrown when the source file does not exist.</exception>
public async Task<string> CreateBackupAsync(string filePath, CancellationToken ct = default)
{
if (!_fileSystem.FileExists(filePath))
@@ -34,6 +46,12 @@ public class BackupService : IBackupService
return backupPath;
}
/// <summary>
/// Retrieves a list of existing backups for the specified file, sorted by timestamp descending.
/// </summary>
/// <param name="filePath">The original file path to find backups for.</param>
/// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A read-only list of backup information sorted by timestamp.</returns>
public async Task<IReadOnlyList<BackupInfo>> GetBackupsAsync(string filePath, CancellationToken ct = default)
{
var directory = _fileSystem.GetDirectoryName(filePath);
@@ -59,12 +77,24 @@ public class BackupService : IBackupService
return backups.OrderByDescending(b => b.Timestamp).ToList();
}
/// <summary>
/// Restores a backup file by copying it to the target location.
/// </summary>
/// <param name="backupPath">The path of the backup file to restore from.</param>
/// <param name="targetPath">The target path where the backup should be restored.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task RestoreBackupAsync(string backupPath, string targetPath, CancellationToken ct = default)
{
_logger?.LogInformation("Restoring backup from {BackupPath} to {TargetPath}", backupPath, targetPath);
await _fileSystem.CopyFileAsync(backupPath, targetPath, ct);
}
/// <summary>
/// Removes old backup files, keeping only the most recent backups.
/// </summary>
/// <param name="filePath">The original file path to find backups for.</param>
/// <param name="keepCount">The number of most recent backups to retain (default: 10).</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task CleanupOldBackupsAsync(string filePath, int keepCount = 10, CancellationToken ct = default)
{
var backups = await GetBackupsAsync(filePath, ct);
@@ -21,12 +21,24 @@ public class ConfigFileService : IConfigFileService
PropertyNameCaseInsensitive = true
};
/// <summary>
/// Initializes a new instance of the <see cref="ConfigFileService"/> class.
/// </summary>
/// <param name="fileSystem">The file system abstraction for file operations.</param>
/// <param name="logger">Optional logger for diagnostic messages.</param>
public ConfigFileService(IFileSystem fileSystem, ILogger<ConfigFileService>? logger = null)
{
_fileSystem = fileSystem;
_logger = logger;
}
/// <summary>
/// Loads the application settings configuration from the specified file path.
/// </summary>
/// <param name="path">The file path to load appsettings from.</param>
/// <param name="ct">Cancellation token for the operation.</param>
/// <returns>The loaded configuration model or a new empty model if deserialization fails.</returns>
/// <exception cref="ConfigLoadException">Thrown when the JSON cannot be parsed.</exception>
public async Task<ConfigModel> LoadAppSettingsAsync(string path, CancellationToken ct = default)
{
_logger?.LogInformation("Loading appsettings from {Path}", path);
@@ -43,6 +55,13 @@ public class ConfigFileService : IConfigFileService
}
}
/// <summary>
/// Loads the pipelines configuration from the specified file path.
/// </summary>
/// <param name="path">The file path to load pipelines from.</param>
/// <param name="ct">Cancellation token for the operation.</param>
/// <returns>The loaded pipelines configuration model or a new empty model if deserialization fails.</returns>
/// <exception cref="ConfigLoadException">Thrown when the JSON cannot be parsed.</exception>
public async Task<PipelinesConfigModel> LoadPipelinesAsync(string path, CancellationToken ct = default)
{
_logger?.LogInformation("Loading pipelines from {Path}", path);
@@ -59,6 +78,12 @@ public class ConfigFileService : IConfigFileService
}
}
/// <summary>
/// Saves the application settings configuration to the specified file path.
/// </summary>
/// <param name="path">The file path to save appsettings to.</param>
/// <param name="config">The configuration model to save.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task SaveAppSettingsAsync(string path, ConfigModel config, CancellationToken ct = default)
{
_logger?.LogInformation("Saving appsettings to {Path}", path);
@@ -66,6 +91,12 @@ public class ConfigFileService : IConfigFileService
await _fileSystem.WriteAllTextAsync(path, json, ct);
}
/// <summary>
/// Saves the pipelines configuration to the specified file path.
/// </summary>
/// <param name="path">The file path to save pipelines to.</param>
/// <param name="config">The pipelines configuration model to save.</param>
/// <param name="ct">Cancellation token for the operation.</param>
public async Task SavePipelinesAsync(string path, PipelinesConfigModel config, CancellationToken ct = default)
{
_logger?.LogInformation("Saving pipelines to {Path}", path);
@@ -5,8 +5,17 @@ namespace JdeScoping.ConfigManager.Services;
/// </summary>
public class ConfigLoadException : Exception
{
/// <summary>
/// Gets the path to the configuration file that failed to load.
/// </summary>
public string FilePath { get; }
/// <summary>
/// Initializes a new instance of the ConfigLoadException class.
/// </summary>
/// <param name="filePath">The path to the configuration file that failed to load.</param>
/// <param name="message">The error message describing the failure.</param>
/// <param name="inner">The inner exception that caused this exception, if any.</param>
public ConfigLoadException(string filePath, string message, Exception? inner = null)
: base(message, inner)
{

Some files were not shown because too many files have changed in this diff Show More