# 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 GenerateAsync( SearchModel search, CancellationToken cancellationToken = default); } ``` #### Business Rules - The `GenerateAsync` method MUST return `Task` - 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` 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 { ["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 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` - 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( configuration.GetSection(ExcelExportOptions.SectionName)); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); 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> _cache = new(); public IReadOnlyList GetColumns() => _cache.GetOrAdd(typeof(T), BuildColumns); private IReadOnlyList BuildColumns(Type type) { return type.GetProperties() .Where(p => p.GetCustomAttribute() != null) .Select(p => new OutputColumn( p.Name, p, p.GetCustomAttribute()!)) .OrderBy(c => c.Attribute.Order) .ThenBy(c => c.Name) .ToList(); } } ``` #### Scenario: Cache column metadata on first access - **WHEN** `GetColumns()` 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 pattern | | Microsoft.Extensions.Logging.Abstractions | 9.0.* | ILogger interface | | Microsoft.Extensions.Configuration.Abstractions | 9.0.* | IConfiguration interface |