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:
+353
@@ -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 |
|
||||
Reference in New Issue
Block a user