Initial commit: JDE Scoping Tool migration project

Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
This commit is contained in:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,353 @@
# Excel Export Specification Delta
This document describes ADDED and MODIFIED requirements for the Excel export subsystem migration from .NET Framework 4.8 to .NET 10.
## ADDED Requirements
### Requirement: ClosedXML Library Usage
The system SHALL use ClosedXML library for all Excel generation operations.
#### Business Rules
- The implementation MUST use `XLWorkbook` class from ClosedXML (not EPPlus)
- Colors MUST use `XLColor` type (e.g., `XLColor.Gainsboro`)
- Table styles MUST use `XLTableTheme` enumeration
- Worksheet protection MUST use `IXLWorksheet.Protect()` method
- Cell access MUST use `worksheet.Cell(row, col)` syntax
- Table creation MUST use `range.CreateTable()` or `range.AsTable()` methods
#### Rationale
EPPlus v7+ requires a commercial license (Polyform Noncommercial). ClosedXML is MIT-licensed and provides comparable functionality for .NET 10.
#### Scenario: Create workbook with ClosedXML
- **WHEN** generating an Excel export
- **THEN** the system uses `new XLWorkbook()` for workbook creation
- **AND** uses `worksheet.Cell(row, col)` for cell access
- **AND** uses `XLColor.Gainsboro` for header backgrounds
---
### Requirement: Async Generation Pattern
The system SHALL provide async-first API for Excel generation.
#### Interface Definition
```csharp
public interface IExcelExportService
{
Task<byte[]> GenerateAsync(
SearchModel search,
CancellationToken cancellationToken = default);
}
```
#### Business Rules
- The `GenerateAsync` method MUST return `Task<byte[]>`
- The method MUST accept `CancellationToken` for cancellation support
- Since ClosedXML's `SaveAs` to `MemoryStream` is synchronous, the implementation MUST wrap CPU-bound work in `Task.Run()`
- The method MUST check cancellation token before generating each sheet
- The method MUST throw `OperationCanceledException` when cancelled
#### Scenario: Support cancellation during export
- **WHEN** `GenerateAsync` is called with a `CancellationToken` that is cancelled
- **THEN** the operation throws `OperationCanceledException`
- **AND** partial workbook resources are disposed
---
### Requirement: Scoped Structured Logging
The system SHALL use scoped structured logging for export operations.
#### Business Rules
- The service MUST use `ILogger<ExcelExportService>` for structured logging
- Log entries MUST include search context via `ILogger.BeginScope()`
- Scope MUST include `SearchId` and `SearchName` keys
- Export start, sheet generation, and completion MUST be logged at Information level
- Errors MUST be logged at Error level with exception details
#### Implementation Pattern
```csharp
using var scope = _logger.BeginScope(new Dictionary<string, object>
{
["SearchId"] = search.Id,
["SearchName"] = search.Name
});
_logger.LogInformation("Starting Excel export generation");
```
#### Scenario: Log export operations with context
- **WHEN** generating an export for a search
- **THEN** log entries include SearchId and SearchName via scope
- **AND** structured logging captures operation progress
---
### Requirement: Configuration via IOptions Pattern
The system SHALL use IOptions<T> pattern for configuration.
#### Configuration Class
```csharp
public class ExcelExportOptions
{
public const string SectionName = "ExcelExport";
public string CriteriaSheetPassword { get; set; } = "JDE_SCOPING_TOOL_PASS";
public string DataSheetPassword { get; set; } = "JDESCOPINGTOOL";
public bool DebugWriteToFile { get; set; } = false;
public string DebugOutputDirectory { get; set; } = "";
}
```
#### Business Rules
- Protection passwords MUST be loaded from `IOptions<ExcelExportOptions>`
- Default password values MUST match legacy values for backward compatibility
- Configuration section MUST be named "ExcelExport" in appsettings.json
- Debug file writing MUST be optional and disabled by default
#### Scenario: Configure via appsettings.json
- **WHEN** appsettings.json contains ExcelExport section
- **THEN** ExcelExportOptions binds to configured values
- **AND** passwords from configuration are used for worksheet protection
---
### Requirement: Service Registration Extension Method
The system SHALL provide a DI extension method for service registration.
#### Implementation
```csharp
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddExcelExport(
this IServiceCollection services,
IConfiguration configuration)
{
services.Configure<ExcelExportOptions>(
configuration.GetSection(ExcelExportOptions.SectionName));
services.AddScoped<IExcelExportService, ExcelExportService>();
services.AddSingleton<OutputColumnCache>();
services.AddSingleton<AttributeTableWriter>();
services.AddSingleton<DataEntryTemplateGenerator>();
return services;
}
}
```
#### Business Rules
- `IExcelExportService` MUST be registered as scoped (per-request lifetime)
- Helper classes (caches, writers) MUST be registered as singleton (stateless)
- Options MUST be bound from IConfiguration
- Extension method MUST return IServiceCollection for chaining
#### Scenario: Register service in DI container
- **WHEN** the application starts and calls `AddExcelExport()`
- **THEN** `IExcelExportService` is registered with `ExcelExportService` implementation
- **AND** helper services are registered as singletons
---
### Requirement: Native Reflection for Property Access
The system SHALL use native .NET reflection for attribute-driven column configuration.
#### Business Rules
- Property access MUST use native `PropertyInfo.GetValue()` (not Fasterflect)
- Column metadata MUST be cached using `ConcurrentDictionary` for performance
- Cache key MUST be the model Type
- Cached data MUST include PropertyInfo, OutputColumnAttribute, and computed values
#### Implementation Pattern
```csharp
public class OutputColumnCache
{
private readonly ConcurrentDictionary<Type, IReadOnlyList<OutputColumn>> _cache = new();
public IReadOnlyList<OutputColumn> GetColumns<T>() =>
_cache.GetOrAdd(typeof(T), BuildColumns);
private IReadOnlyList<OutputColumn> BuildColumns(Type type)
{
return type.GetProperties()
.Where(p => p.GetCustomAttribute<OutputColumnAttribute>() != null)
.Select(p => new OutputColumn(
p.Name,
p,
p.GetCustomAttribute<OutputColumnAttribute>()!))
.OrderBy(c => c.Attribute.Order)
.ThenBy(c => c.Name)
.ToList();
}
}
```
#### Scenario: Cache column metadata on first access
- **WHEN** `GetColumns<SearchResult>()` is called multiple times
- **THEN** reflection is performed only once
- **AND** subsequent calls return cached metadata
---
### Requirement: Temp File Cleanup
The system SHALL clean up any debug temp files based on configuration.
#### Business Rules
- Debug file writing MUST only occur when `DebugWriteToFile` is true
- Debug files MUST be written to `DebugOutputDirectory` path
- File naming MUST follow pattern: `Search_{SearchId}_{timestamp}.xlsx`
- Cleanup of old debug files is NOT automatic (manual cleanup responsibility)
#### Scenario: Write debug file when enabled
- **WHEN** `DebugWriteToFile` is true in configuration
- **THEN** a copy of the export is written to the debug directory
- **AND** the byte[] result is still returned normally
---
## MODIFIED Requirements
### Requirement: Table Style Application
The system SHALL use ClosedXML table theme enumeration.
#### Migration
| Legacy (EPPlus) | New (ClosedXML) |
|-----------------|-----------------|
| `TableStyles.Light18` | `XLTableTheme.TableStyleLight18` |
| `TableStyles.Medium1` | `XLTableTheme.TableStyleMedium1` |
#### Business Rules
- Filter tables in criteria sheet MUST use `XLTableTheme.TableStyleLight18`
- Data tables (Results, MIS Info, Investigation) MUST use `XLTableTheme.TableStyleLight18`
- Table totals row MUST be disabled (`table.ShowTotalsRow = false`)
#### Scenario: Apply table theme via ClosedXML
- **WHEN** creating a data table
- **THEN** the system uses `XLTableTheme.TableStyleLight18`
- **AND** disables the totals row via `table.ShowTotalsRow = false`
---
### Requirement: Color Type Usage
The system SHALL use ClosedXML color types.
#### Migration
| Legacy (EPPlus/System.Drawing) | New (ClosedXML) |
|--------------------------------|-----------------|
| `Color.Gainsboro` | `XLColor.Gainsboro` |
| `Color.FromArgb(...)` | `XLColor.FromArgb(...)` |
#### Business Rules
- Header background color MUST use `XLColor.Gainsboro`
- All color assignments MUST use `XLColor` type
#### Scenario: Apply Gainsboro background via ClosedXML
- **WHEN** formatting a header cell
- **THEN** the system uses `cell.Style.Fill.BackgroundColor = XLColor.Gainsboro`
---
### Requirement: Column Auto-Fit Method
The system SHALL use ClosedXML auto-fit methods.
#### Migration
| Legacy (EPPlus) | New (ClosedXML) |
|-----------------|-----------------|
| `column.AutoFit()` | `column.AdjustToContents()` |
| `column.Width = x` | `column.Width = x` (same) |
#### Business Rules
- Auto-fit MUST use `AdjustToContents()` method
- Padding factor MUST be applied after auto-fit: `column.Width *= paddingFactor`
- Criteria sheet padding: 1.15 (15%)
- Data sheet padding: 1.30 (30%)
- Wrapped columns MUST NOT call `AdjustToContents()` (use fixed width)
#### Scenario: Auto-fit column with padding
- **WHEN** auto-fitting a data column
- **THEN** the system calls `column.AdjustToContents()`
- **AND** applies 30% padding via `column.Width *= 1.30`
---
### Requirement: Worksheet Protection API
The system SHALL use ClosedXML protection API.
#### Migration
| Legacy (EPPlus) | New (ClosedXML) |
|-----------------|-----------------|
| `worksheet.Protection.SetPassword(pwd)` | `worksheet.Protect(pwd)` |
| `worksheet.Protection.AllowAutoFilter = true` | `protection.AllowElement(XLSheetProtectionElements.AutoFilter)` |
| `worksheet.ProtectedRanges.Add(...)` | `range.Style.Protection.Locked = false` |
#### Business Rules
- Protection MUST return `IXLSheetProtection` object
- Allowed operations MUST use `AllowElement()` method
- Cells beyond data range MUST have `Locked = false` for user editing
- Extension area: 1000 rows and columns beyond data
#### Scenario: Apply worksheet protection via ClosedXML
- **WHEN** protecting a data worksheet
- **THEN** the system calls `worksheet.Protect(password)`
- **AND** enables filtering via `protection.AllowElement(XLSheetProtectionElements.AutoFilter)`
---
## NuGet Package Changes
### REMOVED Dependencies
| Package | Reason |
|---------|--------|
| EPPlus (4.x LGPL) | Replaced by ClosedXML |
| Fasterflect | Replaced by native reflection |
### ADDED Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| ClosedXML | 0.104.* | Excel generation (MIT license) |
| Microsoft.Extensions.Options | 9.0.* | IOptions<T> pattern |
| Microsoft.Extensions.Logging.Abstractions | 9.0.* | ILogger<T> interface |
| Microsoft.Extensions.Configuration.Abstractions | 9.0.* | IConfiguration interface |