# Fluent Excel Mapping Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace attribute-based Excel column configuration with fluent mapping classes, moving result models to Core as pure POCOs. **Architecture:** External metadata configuration pattern (like Entity Framework Fluent API). Core models remain pure domain objects without presentation concerns. ExcelIO owns all Excel-specific configuration via fluent map classes. **Tech Stack:** C# records, expression trees for property access, ClosedXML for Excel generation. --- ## Phase 1: Create Mapping Infrastructure in ExcelIO ### Task 1: Create ColumnDefinition class **Files:** - Create: `src/JdeScoping.ExcelIO/Mapping/ColumnDefinition.cs` **Step 1: Create the Mapping directory** ```bash mkdir -p src/JdeScoping.ExcelIO/Mapping ``` **Step 2: Write ColumnDefinition class** ```csharp namespace JdeScoping.ExcelIO.Mapping; /// /// Defines how a property maps to an Excel column. /// public sealed class ColumnDefinition { /// Property name for debugging/error messages. public required string PropertyName { get; init; } /// Compiled delegate to get property value from object. public required Func ValueGetter { get; init; } /// Property type for formatting decisions. public required Type PropertyType { get; init; } /// Column display order (lower = leftmost). public int Order { get; set; } /// Column header text. public string HeaderText { get; set; } = string.Empty; /// Excel number format string. public string Format { get; set; } = "@"; /// Auto-size column width. public bool AutoWidth { get; set; } = true; /// Manual column width (when AutoWidth = false). public double Width { get; set; } /// Enable text wrapping. public bool WrapText { get; set; } } ``` **Step 3: Verify file compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 4: Commit** ```bash git add src/JdeScoping.ExcelIO/Mapping/ColumnDefinition.cs git commit -m "feat(ExcelIO): add ColumnDefinition for fluent mapping" ``` --- ### Task 2: Create ColumnBuilder fluent API **Files:** - Create: `src/JdeScoping.ExcelIO/Mapping/ColumnBuilder.cs` **Step 1: Write ColumnBuilder class** ```csharp namespace JdeScoping.ExcelIO.Mapping; /// /// Fluent builder for configuring Excel column properties. /// /// The model type being mapped. /// The property type. public sealed class ColumnBuilder { private readonly ColumnDefinition _definition; internal ColumnBuilder(ColumnDefinition definition) { _definition = definition; } /// Sets the column display order. public ColumnBuilder Order(int order) { _definition.Order = order; return this; } /// Sets the column header text. public ColumnBuilder Header(string text) { _definition.HeaderText = text; return this; } /// Sets the Excel number format. public ColumnBuilder Format(string format) { _definition.Format = format; return this; } /// Sets a fixed column width (disables auto-width). public ColumnBuilder Width(double width) { _definition.AutoWidth = false; _definition.Width = width; return this; } /// Enables text wrapping for the column. public ColumnBuilder WrapText(bool wrap = true) { _definition.WrapText = wrap; return this; } } ``` **Step 2: Verify file compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 3: Commit** ```bash git add src/JdeScoping.ExcelIO/Mapping/ColumnBuilder.cs git commit -m "feat(ExcelIO): add ColumnBuilder fluent API" ``` --- ### Task 3: Create ExcelClassMap base class **Files:** - Create: `src/JdeScoping.ExcelIO/Mapping/ExcelClassMap.cs` **Step 1: Write ExcelClassMap and IExcelClassMap** ```csharp using System.Linq.Expressions; namespace JdeScoping.ExcelIO.Mapping; /// /// Interface for Excel class maps (non-generic access). /// public interface IExcelClassMap { /// The type this map applies to. Type MappedType { get; } /// Excel table name (for named ranges). string? TableName { get; } /// Worksheet tab name. string? TabName { get; } /// Ordered column definitions. IReadOnlyList Columns { get; } } /// /// Base class for defining Excel column mappings via fluent API. /// /// The model type to map. public abstract class ExcelClassMap : IExcelClassMap { private readonly List _columns = []; /// public Type MappedType => typeof(T); /// public string? TableName { get; private set; } /// public string? TabName { get; private set; } /// public IReadOnlyList Columns => _columns.OrderBy(c => c.Order).ThenBy(c => c.PropertyName).ToList(); /// /// Configures the table and tab names for this model. /// protected void Table(string tableName, string tabName) { TableName = tableName; TabName = tabName; } /// /// Maps a property to an Excel column. /// protected ColumnBuilder Map(Expression> property) { var memberExpr = property.Body as MemberExpression ?? throw new ArgumentException("Expression must be a property access", nameof(property)); var propertyName = memberExpr.Member.Name; var compiled = property.Compile(); var definition = new ColumnDefinition { PropertyName = propertyName, PropertyType = typeof(TProperty), ValueGetter = obj => compiled((T)obj), HeaderText = propertyName // Default to property name }; _columns.Add(definition); return new ColumnBuilder(definition); } } ``` **Step 2: Verify file compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 3: Commit** ```bash git add src/JdeScoping.ExcelIO/Mapping/ExcelClassMap.cs git commit -m "feat(ExcelIO): add ExcelClassMap base class with fluent Map method" ``` --- ### Task 4: Create ExcelMapRegistry **Files:** - Create: `src/JdeScoping.ExcelIO/Mapping/ExcelMapRegistry.cs` **Step 1: Write ExcelMapRegistry class** ```csharp namespace JdeScoping.ExcelIO.Mapping; /// /// Registry for Excel class maps, used for DI and lookup. /// public sealed class ExcelMapRegistry { private readonly Dictionary _maps = new(); /// /// Registers a map for a type. /// public void Register(ExcelClassMap map) { _maps[typeof(T)] = map; } /// /// Gets the map for a type. /// public IExcelClassMap GetMap() => GetMap(typeof(T)); /// /// Gets the map for a type. /// public IExcelClassMap GetMap(Type type) { if (_maps.TryGetValue(type, out var map)) return map; throw new InvalidOperationException( $"No Excel map registered for type {type.Name}. " + $"Register a map using ExcelMapRegistry.Register()."); } /// /// Checks if a map exists for a type. /// public bool HasMap() => HasMap(typeof(T)); /// /// Checks if a map exists for a type. /// public bool HasMap(Type type) => _maps.ContainsKey(type); } ``` **Step 2: Verify file compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 3: Commit** ```bash git add src/JdeScoping.ExcelIO/Mapping/ExcelMapRegistry.cs git commit -m "feat(ExcelIO): add ExcelMapRegistry for DI integration" ``` --- ### Task 5: Create ExcelFormats constants **Files:** - Create: `src/JdeScoping.ExcelIO/Mapping/ExcelFormats.cs` **Step 1: Write ExcelFormats class** ```csharp namespace JdeScoping.ExcelIO.Mapping; /// /// Standard Excel format strings for column configuration. /// public static class ExcelFormats { /// Text format (default). public const string Text = "@"; /// Date format: MM/dd/yyyy public const string Date = "[$-409]MM/dd/yyyy;@"; /// Timestamp format: m/d/yy h:mm AM/PM public const string Timestamp = "[$-409]m/d/yy h:mm AM/PM;@"; /// Default width for wrapped text columns. public const double WrappedColumnWidth = 65; } ``` **Step 2: Verify file compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 3: Commit** ```bash git add src/JdeScoping.ExcelIO/Mapping/ExcelFormats.cs git commit -m "feat(ExcelIO): add ExcelFormats constants" ``` --- ### Task 6: Write unit tests for mapping infrastructure **Files:** - Create: `tests/JdeScoping.ExcelIO.Tests/Mapping/ExcelClassMapTests.cs` **Step 1: Write test class** ```csharp using JdeScoping.ExcelIO.Mapping; using Shouldly; using Xunit; namespace JdeScoping.ExcelIO.Tests.Mapping; public class ExcelClassMapTests { // Test model private sealed class TestModel { public int Id { get; init; } public string Name { get; init; } = string.Empty; public DateTime CreatedAt { get; init; } } // Test map private sealed class TestModelMap : ExcelClassMap { public TestModelMap() { Table("Test_Table", "Test Tab"); Map(x => x.Id).Order(10).Header("ID Number"); Map(x => x.Name).Order(20).Header("Full Name"); Map(x => x.CreatedAt).Order(30).Header("Created").Format(ExcelFormats.Timestamp); } } [Fact] public void MappedType_ReturnsCorrectType() { var map = new TestModelMap(); map.MappedType.ShouldBe(typeof(TestModel)); } [Fact] public void Table_SetsTableAndTabName() { var map = new TestModelMap(); map.TableName.ShouldBe("Test_Table"); map.TabName.ShouldBe("Test Tab"); } [Fact] public void Columns_ReturnsOrderedColumns() { var map = new TestModelMap(); var columns = map.Columns; columns.Count.ShouldBe(3); columns[0].PropertyName.ShouldBe("Id"); columns[1].PropertyName.ShouldBe("Name"); columns[2].PropertyName.ShouldBe("CreatedAt"); } [Fact] public void Map_SetsHeaderText() { var map = new TestModelMap(); var columns = map.Columns; columns[0].HeaderText.ShouldBe("ID Number"); columns[1].HeaderText.ShouldBe("Full Name"); columns[2].HeaderText.ShouldBe("Created"); } [Fact] public void Map_SetsFormat() { var map = new TestModelMap(); var columns = map.Columns; columns[2].Format.ShouldBe(ExcelFormats.Timestamp); } [Fact] public void ValueGetter_ExtractsPropertyValue() { var map = new TestModelMap(); var columns = map.Columns; var testObj = new TestModel { Id = 42, Name = "Test", CreatedAt = new DateTime(2024, 1, 15) }; columns[0].ValueGetter(testObj).ShouldBe(42); columns[1].ValueGetter(testObj).ShouldBe("Test"); columns[2].ValueGetter(testObj).ShouldBe(new DateTime(2024, 1, 15)); } } public class ExcelMapRegistryTests { private sealed class DummyModel { public int Id { get; init; } } private sealed class DummyMap : ExcelClassMap { public DummyMap() { Map(x => x.Id).Order(1).Header("ID"); } } [Fact] public void Register_AndGetMap_ReturnsMap() { var registry = new ExcelMapRegistry(); var map = new DummyMap(); registry.Register(map); var retrieved = registry.GetMap(); retrieved.ShouldBe(map); } [Fact] public void GetMap_UnregisteredType_ThrowsInvalidOperationException() { var registry = new ExcelMapRegistry(); Should.Throw(() => registry.GetMap()); } [Fact] public void HasMap_RegisteredType_ReturnsTrue() { var registry = new ExcelMapRegistry(); registry.Register(new DummyMap()); registry.HasMap().ShouldBeTrue(); } [Fact] public void HasMap_UnregisteredType_ReturnsFalse() { var registry = new ExcelMapRegistry(); registry.HasMap().ShouldBeFalse(); } } ``` **Step 2: Run tests** Run: `dotnet test tests/JdeScoping.ExcelIO.Tests --filter "FullyQualifiedName~ExcelClassMapTests|FullyQualifiedName~ExcelMapRegistryTests"` Expected: All tests pass **Step 3: Commit** ```bash git add tests/JdeScoping.ExcelIO.Tests/Mapping/ExcelClassMapTests.cs git commit -m "test(ExcelIO): add unit tests for fluent mapping infrastructure" ``` --- ## Phase 2: Move Models to Core ### Task 7: Create SearchResults models in Core **Files:** - Create: `src/JdeScoping.Core/Models/SearchResults/SearchResult.cs` - Create: `src/JdeScoping.Core/Models/SearchResults/MisSearchResult.cs` - Create: `src/JdeScoping.Core/Models/SearchResults/MisNonMatchSearchResult.cs` - Create: `src/JdeScoping.Core/Models/SearchResults/SearchModel.cs` **Step 1: Create directory** ```bash mkdir -p src/JdeScoping.Core/Models/SearchResults ``` **Step 2: Write SearchResult.cs (pure POCO, no attributes)** ```csharp namespace JdeScoping.Core.Models.SearchResults; /// /// JDE search result - work order with current status. /// public sealed class SearchResult { public long WorkOrderNumber { get; init; } public string WorkOrderBranchCode { get; init; } = string.Empty; public string LotNumber { get; init; } = string.Empty; public string ItemNumber { get; init; } = string.Empty; public string PlanningFamily { get; init; } = string.Empty; public string StockingType { get; init; } = string.Empty; public decimal OrderQuantity { get; init; } public decimal HeldQuantity { get; init; } public decimal ScrappedQuantity { get; init; } public decimal ShippedQuantity { get; init; } public string StepBranchCode { get; init; } = string.Empty; public decimal StepNumber { get; init; } public string StepDescription { get; init; } = string.Empty; public string FunctionOperationDescription { get; init; } = string.Empty; public DateTime StepUpdateDt { get; init; } public string StatusCode { get; init; } = string.Empty; public string StatusDescription { get; init; } = string.Empty; public DateTime? StatusUpdateDt { get; init; } // Inclusion flags public bool ManuallySpecified { get; init; } public bool SplitOrder { get; init; } public bool Cardex { get; init; } public bool PartsList { get; init; } public bool Flagged { get; init; } /// /// Computed reason why this work order was included in results. /// public string InclusionReason => (ManuallySpecified, Flagged, Cardex, PartsList, SplitOrder) switch { (true, _, _, _, _) => "ManuallySpecified", (_, true, _, _, _) => "Flagged", (_, _, true, true, _) => "ComponentUsage (CARDEX + Parts List)", (_, _, true, false, _) => "ComponentUsage (CARDEX)", (_, _, false, true, _) => "ComponentUsage (Parts List)", (_, _, _, _, true) => "Split order", _ => "UNKNOWN" }; } ``` **Step 3: Write MisSearchResult.cs** ```csharp namespace JdeScoping.Core.Models.SearchResults; /// /// MIS (Manufacturing Instruction Sheet) data result. /// public sealed class MisSearchResult { public string ItemNumber { get; init; } = string.Empty; public string ItemDescription { get; init; } = string.Empty; public string SequenceNumber { get; init; } = string.Empty; public string MisNumber { get; init; } = string.Empty; public string RevId { get; init; } = string.Empty; public string Status { get; init; } = string.Empty; public DateTime? ReleaseDate { get; init; } public string BranchCode { get; init; } = string.Empty; public decimal JobStepSequenceNumber { get; init; } public decimal? MatchedSequenceNumber { get; init; } public bool RoutingMatch { get; init; } public bool MasterMatch { get; init; } public string FunctionOperationDescription { get; init; } = string.Empty; public string CharNumber { get; init; } = string.Empty; public string TestDescription { get; init; } = string.Empty; public string SamplingType { get; init; } = string.Empty; public string SamplingValue { get; init; } = string.Empty; public string ToolsGauges { get; init; } = string.Empty; public string WorkInstructions { get; init; } = string.Empty; } ``` **Step 4: Write MisNonMatchSearchResult.cs** ```csharp namespace JdeScoping.Core.Models.SearchResults; /// /// MIS non-match investigation result. /// public sealed class MisNonMatchSearchResult { public string WorkCenterCode { get; init; } = string.Empty; public long WorkOrderNumber { get; init; } public DateTime WorkOrderStartDate { get; init; } public decimal JobStepNumber { get; init; } public string JobStepDescription { get; init; } = string.Empty; public DateTime? JobStepEndDate { get; init; } public string FunctionCode { get; init; } = string.Empty; public bool WasJobStepAdded { get; init; } public decimal? MatchedJobStepNumber { get; init; } public string ItemNumber { get; init; } = string.Empty; public string ItemDescription { get; init; } = string.Empty; public string RoutingType { get; init; } = string.Empty; } ``` **Step 5: Write SearchModel.cs** ```csharp namespace JdeScoping.Core.Models.SearchResults; /// /// Aggregates search metadata and results for export. /// public class SearchModel { public int Id { get; set; } public string UserName { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public DateTime? SubmitDt { get; set; } public DateTime? StartDt { get; set; } public DateTime? EndDt { get; set; } public bool ExtractMisData { get; set; } public List Results { get; set; } = []; public List MisResults { get; set; } = []; public List MisNonMatchResults { get; set; } = []; } ``` **Step 6: Verify Core compiles** Run: `dotnet build src/JdeScoping.Core` Expected: Success **Step 7: Commit** ```bash git add src/JdeScoping.Core/Models/SearchResults/ git commit -m "feat(Core): add SearchResults models as pure POCOs" ``` --- ### Task 8: Create fluent maps in ExcelIO **Files:** - Create: `src/JdeScoping.ExcelIO/Mapping/Maps/SearchResultMap.cs` - Create: `src/JdeScoping.ExcelIO/Mapping/Maps/MisSearchResultMap.cs` - Create: `src/JdeScoping.ExcelIO/Mapping/Maps/MisNonMatchSearchResultMap.cs` **Step 1: Create Maps directory** ```bash mkdir -p src/JdeScoping.ExcelIO/Mapping/Maps ``` **Step 2: Write SearchResultMap.cs** ```csharp using JdeScoping.Core.Models.SearchResults; namespace JdeScoping.ExcelIO.Mapping.Maps; /// /// Excel column mapping for SearchResult. /// public sealed class SearchResultMap : ExcelClassMap { public SearchResultMap() { Table("Search_Results", "Search Results"); Map(x => x.WorkOrderNumber).Order(10).Header("Work Order Number"); Map(x => x.WorkOrderBranchCode).Order(20).Header("Work Order Branch Code"); Map(x => x.LotNumber).Order(30).Header("Lot Number"); Map(x => x.ItemNumber).Order(40).Header("Item Number"); Map(x => x.PlanningFamily).Order(50).Header("Planning Family"); Map(x => x.StockingType).Order(55).Header("Stocking Type"); Map(x => x.OrderQuantity).Order(60).Header("Order Quantity"); Map(x => x.HeldQuantity).Order(70).Header("Held Quantity"); Map(x => x.ScrappedQuantity).Order(80).Header("Scrapped Quantity"); Map(x => x.ShippedQuantity).Order(90).Header("Shipped Quantity"); Map(x => x.StepBranchCode).Order(100).Header("Operation Step Branch Code"); Map(x => x.StepNumber).Order(110).Header("Operation Step"); Map(x => x.StepDescription).Order(120).Header("Operation Step Description"); Map(x => x.FunctionOperationDescription).Order(130).Header("Function Operation Description"); Map(x => x.StepUpdateDt).Order(140).Header("Operation Step Update Timestamp").Format(ExcelFormats.Timestamp); Map(x => x.StatusCode).Order(150).Header("Status Code"); Map(x => x.StatusDescription).Order(160).Header("Status Description"); Map(x => x.StatusUpdateDt).Order(170).Header("Status Update Timestamp").Format(ExcelFormats.Date); Map(x => x.InclusionReason).Order(180).Header("Inclusion Reason"); } } ``` **Step 3: Write MisSearchResultMap.cs** ```csharp using JdeScoping.Core.Models.SearchResults; namespace JdeScoping.ExcelIO.Mapping.Maps; /// /// Excel column mapping for MisSearchResult. /// public sealed class MisSearchResultMap : ExcelClassMap { public MisSearchResultMap() { Table("MIS_Info", "MIS Info"); Map(x => x.ItemNumber).Order(10).Header("Item Number"); Map(x => x.SequenceNumber).Order(20).Header("MIS Job Step Sequence Number"); Map(x => x.MisNumber).Order(30).Header("MIS Number"); Map(x => x.RevId).Order(40).Header("MIS Revision"); Map(x => x.ItemDescription).Order(50).Header("Item Description"); Map(x => x.Status).Order(60).Header("MIS Release Status"); Map(x => x.ReleaseDate).Order(70).Header("MIS Release Date").Format(ExcelFormats.Timestamp); Map(x => x.BranchCode).Order(80).Header("Branch Code"); Map(x => x.JobStepSequenceNumber).Order(90).Header("Job Step Sequence Number"); Map(x => x.MatchedSequenceNumber).Order(100).Header("Matched Sequence Number"); Map(x => x.RoutingMatch).Order(110).Header("Matched to F3112Z1?"); Map(x => x.MasterMatch).Order(120).Header("Matched to F3003?"); Map(x => x.FunctionOperationDescription).Order(130).Header("Function Operation Description"); Map(x => x.CharNumber).Order(140).Header("Char Number"); Map(x => x.TestDescription).Order(150).Header("Test Description").Width(ExcelFormats.WrappedColumnWidth).WrapText(); Map(x => x.SamplingType).Order(160).Header("Sampling Type"); Map(x => x.SamplingValue).Order(170).Header("Sampling Value"); Map(x => x.ToolsGauges).Order(180).Header("Tools & Gauges").Width(ExcelFormats.WrappedColumnWidth).WrapText(); Map(x => x.WorkInstructions).Order(190).Header("Work Instructions").Width(ExcelFormats.WrappedColumnWidth).WrapText(); } } ``` **Step 4: Write MisNonMatchSearchResultMap.cs** ```csharp using JdeScoping.Core.Models.SearchResults; namespace JdeScoping.ExcelIO.Mapping.Maps; /// /// Excel column mapping for MisNonMatchSearchResult. /// public sealed class MisNonMatchSearchResultMap : ExcelClassMap { public MisNonMatchSearchResultMap() { Table("Investigation", "Investigation"); Map(x => x.WorkCenterCode).Order(10).Header("Work Center Code"); Map(x => x.WorkOrderNumber).Order(20).Header("Work Order Number"); Map(x => x.WorkOrderStartDate).Order(30).Header("Work Order Start Date").Format(ExcelFormats.Date); Map(x => x.JobStepNumber).Order(40).Header("Job Step Number"); Map(x => x.JobStepDescription).Order(50).Header("Function Operation Description"); Map(x => x.JobStepEndDate).Order(60).Header("Job Step End Date").Format(ExcelFormats.Date); Map(x => x.FunctionCode).Order(70).Header("Function Code"); Map(x => x.WasJobStepAdded).Order(75).Header("Was Job Step Added?"); Map(x => x.MatchedJobStepNumber).Order(76).Header("Matched Job Step Number"); Map(x => x.ItemNumber).Order(80).Header("Item Number"); Map(x => x.ItemDescription).Order(90).Header("Item Description"); Map(x => x.RoutingType).Order(100).Header("Routing Type"); } } ``` **Step 5: Verify ExcelIO compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 6: Commit** ```bash git add src/JdeScoping.ExcelIO/Mapping/Maps/ git commit -m "feat(ExcelIO): add fluent maps for SearchResult models" ``` --- ## Phase 3: Create FluentTableWriter ### Task 9: Create FluentTableWriter **Files:** - Create: `src/JdeScoping.ExcelIO/Generators/FluentTableWriter.cs` **Step 1: Write FluentTableWriter class** ```csharp using ClosedXML.Excel; using JdeScoping.ExcelIO.Formatting; using JdeScoping.ExcelIO.Mapping; namespace JdeScoping.ExcelIO.Generators; /// /// Writes Excel tables using fluent mapping configuration. /// public sealed class FluentTableWriter { private readonly ExcelMapRegistry _registry; public FluentTableWriter(ExcelMapRegistry registry) { _registry = registry; } /// /// Writes a table to the worksheet using the registered map for type T. /// public IXLTable? WriteTable( IXLWorksheet worksheet, int startRow, int startCol, IEnumerable data, string? tableNameOverride = null, bool showHeader = false, string? headerText = null) { var map = _registry.GetMap(); var columns = map.Columns; var tableName = tableNameOverride ?? map.TableName ?? typeof(T).Name; var header = headerText ?? map.TabName ?? string.Empty; if (columns.Count == 0) return null; var dataList = data.ToList(); var baseRow = startRow; // Write merged header if requested if (showHeader && !string.IsNullOrEmpty(header)) { var mergedHeaderRange = worksheet.Range(baseRow, startCol, baseRow, startCol + columns.Count - 1); HeaderFormatter.ApplyHeaderFormat(mergedHeaderRange, header, merge: true); baseRow++; } // Write column headers var col = startCol; foreach (var column in columns) { var cell = worksheet.Cell(baseRow, col); HeaderFormatter.ApplyHeaderFormat(cell, column.HeaderText); // Pre-set column formatting worksheet.Column(col).Style.Alignment.WrapText = column.WrapText; if (!column.AutoWidth) { worksheet.Column(col).Width = column.Width; } col++; } // Write data rows var row = baseRow + 1; foreach (var item in dataList) { col = startCol; foreach (var column in columns) { var value = column.ValueGetter(item!); worksheet.Cell(row, col).Value = ConvertToXlValue(value); col++; } row++; } // Handle empty data case if (dataList.Count == 0) { row = baseRow + 1; } // Create table range var dataRange = worksheet.Range( baseRow, startCol, baseRow + dataList.Count, startCol + columns.Count - 1); // Create table var table = dataRange.CreateTable(tableName); table.Theme = XLTableTheme.TableStyleLight18; table.ShowTotalsRow = false; // Apply column formatting col = startCol; var tableStartRow = table.RangeAddress.FirstAddress.RowNumber; var tableEndRow = table.RangeAddress.LastAddress.RowNumber; foreach (var column in columns) { // Apply number format worksheet.Range(tableStartRow, col, tableEndRow, col) .Style.NumberFormat.Format = column.Format; // Apply column width if (column.WrapText && !column.AutoWidth) { worksheet.Column(col).Width = column.Width; } else if (column.AutoWidth) { worksheet.Column(col).AdjustToContents(); worksheet.Column(col).Width *= ExcelFormats.DataPaddingFactor; } else { worksheet.Column(col).Width = column.Width; } col++; } return table; } private static XLCellValue ConvertToXlValue(object? value) { return value switch { null => Blank.Value, string s => s, int i => i, long l => l, decimal d => d, double dbl => dbl, float f => f, DateTime dt => dt, bool b => b, _ => value.ToString() ?? string.Empty }; } } ``` **Step 2: Add DataPaddingFactor to ExcelFormats if missing** Check if `ExcelFormats.DataPaddingFactor` exists in `Formatting/ExcelFormats.cs`. If not, add it to `Mapping/ExcelFormats.cs`: ```csharp /// Multiplier for auto-width columns. public const double DataPaddingFactor = 1.1; ``` **Step 3: Verify ExcelIO compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 4: Commit** ```bash git add src/JdeScoping.ExcelIO/Generators/FluentTableWriter.cs git add src/JdeScoping.ExcelIO/Mapping/ExcelFormats.cs git commit -m "feat(ExcelIO): add FluentTableWriter using map registry" ``` --- ### Task 10: Register maps in DependencyInjection **Files:** - Modify: `src/JdeScoping.ExcelIO/DependencyInjection.cs` **Step 1: Update DependencyInjection.cs** Add imports and registry registration: ```csharp using JdeScoping.ExcelIO.Mapping; using JdeScoping.ExcelIO.Mapping.Maps; ``` Add to `AddExcelIO` method: ```csharp // Register Excel map registry with all maps services.AddSingleton(sp => { var registry = new ExcelMapRegistry(); registry.Register(new SearchResultMap()); registry.Register(new MisSearchResultMap()); registry.Register(new MisNonMatchSearchResultMap()); return registry; }); // Register fluent table writer services.AddSingleton(); ``` **Step 2: Verify ExcelIO compiles** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 3: Commit** ```bash git add src/JdeScoping.ExcelIO/DependencyInjection.cs git commit -m "feat(ExcelIO): register ExcelMapRegistry and FluentTableWriter in DI" ``` --- ## Phase 4: Migrate ExcelExportService and DataAccess ### Task 11: Update ExcelExportService to use Core models and FluentTableWriter **Files:** - Modify: `src/JdeScoping.ExcelIO/ExcelExportService.cs` **Step 1: Update imports** Replace: ```csharp using JdeScoping.ExcelIO.Models.Reporting; ``` With: ```csharp using JdeScoping.Core.Models.SearchResults; ``` **Step 2: Replace AttributeTableWriter with FluentTableWriter** Change constructor and field: ```csharp private readonly FluentTableWriter _tableWriter; public ExcelExportService( ILogger logger, IOptions options, CriteriaSheetGenerator criteriaGenerator, FluentTableWriter tableWriter) ``` **Step 3: Update GenerateResultsSheet to use map for tab name** ```csharp private void GenerateResultsSheet(XLWorkbook workbook, List results) { var map = _registry.GetMap(); var tabName = map.TabName ?? "Search Results"; var worksheet = workbook.Worksheets.Add(tabName); var table = _tableWriter.WriteTable(worksheet, 1, 1, results); // ... rest unchanged } ``` Inject `ExcelMapRegistry _registry` in constructor and update all sheet generation methods similarly. **Step 4: Verify build** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 5: Commit** ```bash git add src/JdeScoping.ExcelIO/ExcelExportService.cs git commit -m "refactor(ExcelIO): migrate ExcelExportService to Core models and FluentTableWriter" ``` --- ### Task 12: Update DataAccess to use Core models **Files:** - Modify: `src/JdeScoping.DataAccess/Services/SearchProcessor.cs` **Step 1: Update imports** Replace: ```csharp using JdeScoping.DataAccess.Models; using JdeScoping.DataAccess.Models.Results; ``` With: ```csharp using JdeScoping.Core.Models.SearchResults; using JdeScoping.DataAccess.Models; // Keep for SearchQueryResult ``` **Step 2: Verify build** Run: `dotnet build src/JdeScoping.DataAccess` Expected: Success **Step 3: Commit** ```bash git add src/JdeScoping.DataAccess/Services/SearchProcessor.cs git commit -m "refactor(DataAccess): use Core.Models.SearchResults" ``` --- ### Task 13: Update CriteriaSheetGenerator to use Core models **Files:** - Modify: `src/JdeScoping.ExcelIO/Generators/CriteriaSheetGenerator.cs` **Step 1: Update imports** Replace: ```csharp using JdeScoping.ExcelIO.Models.Reporting; ``` With: ```csharp using JdeScoping.Core.Models.SearchResults; ``` **Step 2: Verify build** Run: `dotnet build src/JdeScoping.ExcelIO` Expected: Success **Step 3: Commit** ```bash git add src/JdeScoping.ExcelIO/Generators/CriteriaSheetGenerator.cs git commit -m "refactor(ExcelIO): update CriteriaSheetGenerator to use Core models" ``` --- ## Phase 5: Delete Duplicate Code ### Task 14: Delete DataAccess Models and Attributes **Files:** - Delete: `src/JdeScoping.DataAccess/Models/Results/` (entire directory) - Delete: `src/JdeScoping.DataAccess/Models/SearchModel.cs` - Delete: `src/JdeScoping.DataAccess/Attributes/` (entire directory) **Step 1: Verify no remaining usages** Run: ```bash grep -r "JdeScoping.DataAccess.Models.Results" src/ grep -r "JdeScoping.DataAccess.Attributes" src/ ``` Expected: No matches (or only in files being deleted) **Step 2: Delete files** ```bash rm -rf src/JdeScoping.DataAccess/Models/Results/ rm src/JdeScoping.DataAccess/Models/SearchModel.cs rm -rf src/JdeScoping.DataAccess/Attributes/ ``` **Step 3: Verify build** Run: `dotnet build` Expected: Success **Step 4: Commit** ```bash git add -A git commit -m "refactor(DataAccess): remove duplicate Models and Attributes (now in Core/ExcelIO)" ``` --- ### Task 15: Delete ExcelIO duplicate Models/Reporting **Files:** - Delete: `src/JdeScoping.ExcelIO/Models/Reporting/SearchResult.cs` - Delete: `src/JdeScoping.ExcelIO/Models/Reporting/MisSearchResult.cs` - Delete: `src/JdeScoping.ExcelIO/Models/Reporting/MisNonMatchSearchResult.cs` - Delete: `src/JdeScoping.ExcelIO/Models/Reporting/SearchModel.cs` Note: Keep filter entry models (`*FilterEntry.cs`, `TimespanFilter.cs`) as they're specific to ExcelIO criteria sheet. **Step 1: Verify no remaining usages of old models** Run: ```bash grep -r "JdeScoping.ExcelIO.Models.Reporting.SearchResult" src/ grep -r "JdeScoping.ExcelIO.Models.Reporting.MisSearchResult" src/ grep -r "JdeScoping.ExcelIO.Models.Reporting.SearchModel" src/ ``` Expected: No matches **Step 2: Delete files** ```bash rm src/JdeScoping.ExcelIO/Models/Reporting/SearchResult.cs rm src/JdeScoping.ExcelIO/Models/Reporting/MisSearchResult.cs rm src/JdeScoping.ExcelIO/Models/Reporting/MisNonMatchSearchResult.cs rm src/JdeScoping.ExcelIO/Models/Reporting/SearchModel.cs ``` **Step 3: Verify build** Run: `dotnet build` Expected: Success **Step 4: Commit** ```bash git add -A git commit -m "refactor(ExcelIO): remove duplicate result models (now in Core)" ``` --- ### Task 16: Delete old attribute-based infrastructure from ExcelIO **Files:** - Delete: `src/JdeScoping.ExcelIO/Attributes/OutputColumnAttribute.cs` - Delete: `src/JdeScoping.ExcelIO/Attributes/OutputTableAttribute.cs` - Delete: `src/JdeScoping.ExcelIO/Helpers/OutputColumnCache.cs` - Delete: `src/JdeScoping.ExcelIO/Models/OutputColumn.cs` - Delete: `src/JdeScoping.ExcelIO/Generators/AttributeTableWriter.cs` **Step 1: Verify no remaining usages** Run: ```bash grep -r "OutputColumnAttribute" src/ grep -r "OutputTableAttribute" src/ grep -r "OutputColumnCache" src/ grep -r "AttributeTableWriter" src/ ``` Expected: No matches in source files (tests may still reference) **Step 2: Delete files** ```bash rm src/JdeScoping.ExcelIO/Attributes/OutputColumnAttribute.cs rm src/JdeScoping.ExcelIO/Attributes/OutputTableAttribute.cs rm src/JdeScoping.ExcelIO/Helpers/OutputColumnCache.cs rm src/JdeScoping.ExcelIO/Models/OutputColumn.cs rm src/JdeScoping.ExcelIO/Generators/AttributeTableWriter.cs rmdir src/JdeScoping.ExcelIO/Attributes 2>/dev/null || true rmdir src/JdeScoping.ExcelIO/Helpers 2>/dev/null || true ``` **Step 3: Update DependencyInjection.cs to remove old registrations** Remove these lines: ```csharp services.AddSingleton(); services.AddSingleton(); ``` **Step 4: Verify build** Run: `dotnet build` Expected: Success **Step 5: Commit** ```bash git add -A git commit -m "refactor(ExcelIO): remove attribute-based table writer infrastructure" ``` --- ## Phase 6: Update Tests ### Task 17: Update ExcelIO tests **Files:** - Modify/Delete tests that reference old attribute-based infrastructure - Update tests to use Core models **Step 1: Run existing tests to identify failures** Run: `dotnet test tests/JdeScoping.ExcelIO.Tests` Expected: Some failures from removed types **Step 2: Update test files to use Core models** Update imports in test files from: ```csharp using JdeScoping.ExcelIO.Models.Reporting; ``` To: ```csharp using JdeScoping.Core.Models.SearchResults; ``` **Step 3: Remove or update tests for deleted infrastructure** - Delete `AttributeTableWriterTests.cs` if it exists - Delete `OutputColumnCacheTests.cs` if it exists - Update any integration tests to use new FluentTableWriter **Step 4: Run all tests** Run: `dotnet test` Expected: All tests pass **Step 5: Commit** ```bash git add -A git commit -m "test: update ExcelIO tests for fluent mapping" ``` --- ### Task 18: Update DataAccess tests **Files:** - Update tests that reference old DataAccess models **Step 1: Update imports in test files** Update from: ```csharp using JdeScoping.DataAccess.Models.Results; ``` To: ```csharp using JdeScoping.Core.Models.SearchResults; ``` **Step 2: Run all tests** Run: `dotnet test` Expected: All tests pass **Step 3: Commit** ```bash git add -A git commit -m "test: update DataAccess tests for Core models" ``` --- ## Phase 7: Final Verification ### Task 19: Full build and test **Step 1: Clean build** Run: `dotnet clean && dotnet build` Expected: Success with 0 errors, 0 warnings **Step 2: Run all tests** Run: `dotnet test` Expected: All tests pass **Step 3: Verify no duplicate types** Run: ```bash grep -r "class SearchResult" src/ --include="*.cs" | grep -v "ExcelClassMap" grep -r "class MisSearchResult" src/ --include="*.cs" | grep -v "ExcelClassMap" ``` Expected: Only one definition of each in Core **Step 4: Final commit** ```bash git add -A git commit -m "refactor: complete fluent Excel mapping migration" ``` --- ## Summary | Before | After | |--------|-------| | Models duplicated in DataAccess + ExcelIO | Single source of truth in Core | | Attributes on models (presentation in domain) | Fluent maps in ExcelIO (separation of concerns) | | OutputColumnAttribute, OutputTableAttribute | ExcelClassMap, ColumnBuilder | | OutputColumnCache (reflection) | ExcelMapRegistry (explicit registration) | | AttributeTableWriter | FluentTableWriter | **Files Created:** 12 **Files Deleted:** ~15 **Net reduction:** ~3 files, significantly cleaner architecture