Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
11 KiB
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
XLWorkbookclass from ClosedXML (not EPPlus) - Colors MUST use
XLColortype (e.g.,XLColor.Gainsboro) - Table styles MUST use
XLTableThemeenumeration - Worksheet protection MUST use
IXLWorksheet.Protect()method - Cell access MUST use
worksheet.Cell(row, col)syntax - Table creation MUST use
range.CreateTable()orrange.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.Gainsborofor header backgrounds
Requirement: Async Generation Pattern
The system SHALL provide async-first API for Excel generation.
Interface Definition
public interface IExcelExportService
{
Task<byte[]> GenerateAsync(
SearchModel search,
CancellationToken cancellationToken = default);
}
Business Rules
- The
GenerateAsyncmethod MUST returnTask<byte[]> - The method MUST accept
CancellationTokenfor cancellation support - Since ClosedXML's
SaveAstoMemoryStreamis synchronous, the implementation MUST wrap CPU-bound work inTask.Run() - The method MUST check cancellation token before generating each sheet
- The method MUST throw
OperationCanceledExceptionwhen cancelled
Scenario: Support cancellation during export
- WHEN
GenerateAsyncis called with aCancellationTokenthat 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
SearchIdandSearchNamekeys - Export start, sheet generation, and completion MUST be logged at Information level
- Errors MUST be logged at Error level with exception details
Implementation Pattern
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 pattern for configuration.
Configuration Class
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
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
IExcelExportServiceMUST 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
IExcelExportServiceis registered withExcelExportServiceimplementation - 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
ConcurrentDictionaryfor performance - Cache key MUST be the model Type
- Cached data MUST include PropertyInfo, OutputColumnAttribute, and computed values
Implementation Pattern
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
DebugWriteToFileis true - Debug files MUST be written to
DebugOutputDirectorypath - 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
DebugWriteToFileis 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
XLColortype
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
IXLSheetProtectionobject - Allowed operations MUST use
AllowElement()method - Cells beyond data range MUST have
Locked = falsefor 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 |