Files
T
Joseph Doherty 26ff8d9b4f 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.
2026-01-02 07:43:29 -05:00

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 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

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

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

  • 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

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 pattern
Microsoft.Extensions.Logging.Abstractions 9.0.* ILogger interface
Microsoft.Extensions.Configuration.Abstractions 9.0.* IConfiguration interface