refactor(ExcelIO): delete old attribute-based infrastructure

- Remove OutputColumnAttribute, OutputTableAttribute, OutputColumnCache
- Remove AttributeTableWriter and ColumnFormatter
- Remove duplicate ExcelFormats from Mapping (use Formatting version)
- Remove OutputColumn model
- Add FilterEntryMaps for criteria sheet filter models
- Update CriteriaSheetGenerator to use FluentTableWriter
- Remove attributes from filter entry models (now use fluent maps)
- Update DI to register filter entry maps and remove old services
- Update tests to use new fluent infrastructure
- Delete obsolete test files for removed infrastructure

Task 16 of fluent-excel-mapping-implementation plan.
This commit is contained in:
Joseph Doherty
2026-01-06 23:56:02 -05:00
parent e98ce636e2
commit 621dd41a97
30 changed files with 287 additions and 891 deletions
@@ -1,187 +0,0 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Attributes;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Helpers;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class AttributeTableWriterTests
{
private readonly OutputColumnCache _cache = new();
private readonly AttributeTableWriter _writer;
public AttributeTableWriterTests()
{
_writer = new AttributeTableWriter(_cache);
}
[OutputTable(TabName = "Test Items", TableName = "Test_Items", ShowHeader = false)]
private class TestItem
{
[OutputColumn(Order = 10, HeaderText = "ID")]
public int Id { get; set; }
[OutputColumn(Order = 20, HeaderText = "Name")]
public string Name { get; set; } = string.Empty;
[OutputColumn(Order = 30, HeaderText = "Value")]
public decimal Value { get; set; }
}
[OutputTable(TabName = "Wrapped Table", TableName = "Wrapped_Table")]
private class WrappedItem
{
[OutputColumn(Order = 10, HeaderText = "Description", WrapText = true, AutoWidth = false, Width = 65)]
public string Description { get; set; } = string.Empty;
}
private class NoAttributeItem
{
public string Data { get; set; } = string.Empty;
}
[Fact]
public void WriteTable_CreatesTableWithCorrectColumns()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem>
{
new() { Id = 1, Name = "Item 1", Value = 10.5m },
new() { Id = 2, Name = "Item 2", Value = 20.5m }
};
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldNotBeNull();
table.ColumnCount().ShouldBe(3);
table.RowCount().ShouldBe(3); // Header + 2 data rows
}
[Fact]
public void WriteTable_UsesLight18TableStyle()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldNotBeNull();
table.Theme.ShouldBe(XLTableTheme.TableStyleLight18);
}
[Fact]
public void WriteTable_SetsColumnHeaders()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
_writer.WriteTable(worksheet, 1, 1, data);
worksheet.Cell(1, 1).Value.GetText().ShouldBe("ID");
worksheet.Cell(1, 2).Value.GetText().ShouldBe("Name");
worksheet.Cell(1, 3).Value.GetText().ShouldBe("Value");
}
[Fact]
public void WriteTable_WritesDataRows()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem>
{
new() { Id = 1, Name = "Item 1", Value = 10.5m },
new() { Id = 2, Name = "Item 2", Value = 20.5m }
};
_writer.WriteTable(worksheet, 1, 1, data);
worksheet.Cell(2, 1).Value.GetNumber().ShouldBe(1);
worksheet.Cell(2, 2).Value.GetText().ShouldBe("Item 1");
worksheet.Cell(2, 3).Value.GetNumber().ShouldBe(10.5);
worksheet.Cell(3, 1).Value.GetNumber().ShouldBe(2);
}
[Fact]
public void WriteTable_WithShowHeader_CreatesMergedHeader()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
_writer.WriteTable(worksheet, 1, 1, data, showHeader: true, headerText: "Test Header");
// First row should be merged header
var headerRange = worksheet.Range(1, 1, 1, 3);
headerRange.IsMerged().ShouldBeTrue();
worksheet.Cell(1, 1).Value.GetText().ShouldBe("Test Header");
// Column headers should be on row 2
worksheet.Cell(2, 1).Value.GetText().ShouldBe("ID");
}
[Fact]
public void WriteTable_EmptyData_CreatesTableWithHeaderOnly()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem>();
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldNotBeNull();
// Table should exist with headers
table.ColumnCount().ShouldBe(3);
}
[Fact]
public void WriteTable_NoAttributes_ReturnsNull()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<NoAttributeItem> { new() { Data = "Test" } };
var table = _writer.WriteTable(worksheet, 1, 1, data);
table.ShouldBeNull();
}
[Fact]
public void WriteTable_WrappedColumn_SetsFixedWidth()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<WrappedItem> { new() { Description = "Long description text" } };
_writer.WriteTable(worksheet, 1, 1, data);
worksheet.Column(1).Width.ShouldBe(65);
worksheet.Column(1).Style.Alignment.WrapText.ShouldBeTrue();
}
[Fact]
public void WriteTable_TableNameOverride_UsesProvidedName()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var data = new List<TestItem> { new() { Id = 1, Name = "Test", Value = 100m } };
var table = _writer.WriteTable(worksheet, 1, 1, data, tableNameOverride: "Custom_Table");
table.ShouldNotBeNull();
table.Name.ShouldBe("Custom_Table");
}
}
@@ -1,116 +0,0 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Attributes;
using JdeScoping.ExcelIO.Formatting;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class ColumnFormatterTests
{
[Fact]
public void ApplyColumnFormat_AutoWidth_AdjustsToContents()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
worksheet.Cell(1, 1).Value = "Some Text Value";
var attr = new OutputColumnAttribute
{
AutoWidth = true,
Format = OutputColumnAttribute.StdFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
// Width should be greater than default after adjustment
worksheet.Column(1).Width.ShouldBeGreaterThan(0);
}
[Fact]
public void ApplyColumnFormat_FixedWidth_SetsExactWidth()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
AutoWidth = false,
Width = 50.0,
Format = OutputColumnAttribute.StdFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Width.ShouldBe(50.0);
}
[Fact]
public void ApplyColumnFormat_WrapText_EnablesWrapping()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
WrapText = true,
AutoWidth = false,
Width = 65.0,
Format = OutputColumnAttribute.StdFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Style.Alignment.WrapText.ShouldBeTrue();
worksheet.Column(1).Width.ShouldBe(65.0);
}
[Fact]
public void ApplyColumnFormat_DateFormat_AppliesCorrectFormat()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
AutoWidth = false,
Width = 20.0,
Format = OutputColumnAttribute.DateFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe(OutputColumnAttribute.DateFormat);
}
[Fact]
public void ApplyColumnFormat_TimestampFormat_AppliesCorrectFormat()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
var attr = new OutputColumnAttribute
{
AutoWidth = false,
Width = 25.0,
Format = OutputColumnAttribute.TimestampFormat
};
ColumnFormatter.ApplyColumnFormat(worksheet.Column(1), attr);
worksheet.Column(1).Style.NumberFormat.Format.ShouldBe(OutputColumnAttribute.TimestampFormat);
}
[Fact]
public void AutoFitWithPadding_AppliesPaddingFactor()
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Test");
worksheet.Cell(1, 1).Value = "Some Text";
ColumnFormatter.AutoFitWithPadding(worksheet.Column(1), 1.30);
// Width should be greater than 0 and include padding
worksheet.Column(1).Width.ShouldBeGreaterThan(0);
}
}
@@ -1,7 +1,8 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Helpers;
using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Models.Reporting;
using Microsoft.Extensions.Options;
using Shouldly;
@@ -21,11 +22,25 @@ public class CriteriaSheetGeneratorTests
CriteriaSheetPassword = "TestPassword"
});
var cache = new OutputColumnCache();
var tableWriter = new AttributeTableWriter(cache);
var registry = CreateTestRegistry();
var tableWriter = new FluentTableWriter(registry);
_generator = new CriteriaSheetGenerator(_options, tableWriter);
}
private static ExcelMapRegistry CreateTestRegistry()
{
var registry = new ExcelMapRegistry();
registry.Register(new TimespanFilterMap());
registry.Register(new WorkOrderFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap());
registry.Register(new ProfitCenterFilterEntryMap());
registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new OperatorFilterEntryMap());
registry.Register(new ComponentLotFilterEntryMap());
registry.Register(new ItemOperationMisFilterEntryMap());
return registry;
}
[Fact]
public void Generate_CreatesSearchCriteriaSheet()
{
@@ -1,7 +1,8 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Helpers;
using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Models.Reporting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -9,6 +10,10 @@ using NSubstitute;
using Shouldly;
using Xunit;
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;
using MisSearchResult = JdeScoping.Core.Models.SearchResults.MisSearchResult;
using MisNonMatchSearchResult = JdeScoping.Core.Models.SearchResults.MisNonMatchSearchResult;
namespace JdeScoping.ExcelIO.Tests;
/// <summary>
@@ -29,11 +34,33 @@ public class ExcelExportIntegrationTests
DataSheetPassword = "TestDataPass"
});
var cache = new OutputColumnCache();
var tableWriter = new AttributeTableWriter(cache);
var registry = CreateTestRegistry();
var tableWriter = new FluentTableWriter(registry);
var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter);
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter);
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter, registry);
}
private static ExcelMapRegistry CreateTestRegistry()
{
var registry = new ExcelMapRegistry();
// Search result maps
registry.Register(new SearchResultMap());
registry.Register(new MisSearchResultMap());
registry.Register(new MisNonMatchSearchResultMap());
// Filter entry maps
registry.Register(new TimespanFilterMap());
registry.Register(new WorkOrderFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap());
registry.Register(new ProfitCenterFilterEntryMap());
registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new OperatorFilterEntryMap());
registry.Register(new ComponentLotFilterEntryMap());
registry.Register(new ItemOperationMisFilterEntryMap());
return registry;
}
#region Sheet Count Tests
@@ -387,10 +414,13 @@ public class ExcelExportIntegrationTests
public async Task GenerateAsync_SearchResults_ContainsCorrectData()
{
var search = CreateMinimalSearchModel();
var searchResult = CreateSampleSearchResult();
searchResult.WorkOrderNumber = 99999;
searchResult.ItemNumber = "TEST-ITEM-001";
searchResult.LotNumber = "LOT-999";
var searchResult = new SearchResult
{
WorkOrderNumber = 99999,
ItemNumber = "TEST-ITEM-001",
LotNumber = "LOT-999",
Flagged = true
};
search.Results.Add(searchResult);
var result = await _service.GenerateAsync(search);
@@ -408,9 +438,16 @@ public class ExcelExportIntegrationTests
[Fact]
public async Task GenerateAsync_MisInfo_ContainsCorrectData()
{
var search = CreateSearchModelWithMisData();
search.MisResults![0].ItemNumber = "MIS-ITEM-001";
search.MisResults[0].MisNumber = "MIS-12345";
var search = CreateMinimalSearchModel();
search.ExtractMisData = true;
search.MisResults = [
new MisSearchResult
{
ItemNumber = "MIS-ITEM-001",
MisNumber = "MIS-12345"
}
];
search.MisNonMatchResults = [];
var result = await _service.GenerateAsync(search);
@@ -426,9 +463,16 @@ public class ExcelExportIntegrationTests
[Fact]
public async Task GenerateAsync_Investigation_ContainsCorrectData()
{
var search = CreateSearchModelWithMisData();
search.MisNonMatchResults![0].WorkOrderNumber = 77777;
search.MisNonMatchResults[0].ItemNumber = "INV-ITEM-001";
var search = CreateMinimalSearchModel();
search.ExtractMisData = true;
search.MisResults = [];
search.MisNonMatchResults = [
new MisNonMatchSearchResult
{
WorkOrderNumber = 77777,
ItemNumber = "INV-ITEM-001"
}
];
var result = await _service.GenerateAsync(search);
@@ -1,7 +1,8 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Helpers;
using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Models.Reporting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -9,6 +10,10 @@ using NSubstitute;
using Shouldly;
using Xunit;
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;
using MisSearchResult = JdeScoping.Core.Models.SearchResults.MisSearchResult;
using MisNonMatchSearchResult = JdeScoping.Core.Models.SearchResults.MisNonMatchSearchResult;
namespace JdeScoping.ExcelIO.Tests;
public class ExcelExportServiceTests
@@ -26,11 +31,33 @@ public class ExcelExportServiceTests
DataSheetPassword = "TestDataPass"
});
var cache = new OutputColumnCache();
var tableWriter = new AttributeTableWriter(cache);
var registry = CreateTestRegistry();
var tableWriter = new FluentTableWriter(registry);
var criteriaGenerator = new CriteriaSheetGenerator(_options, tableWriter);
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter);
_service = new ExcelExportService(_logger, _options, criteriaGenerator, tableWriter, registry);
}
private static ExcelMapRegistry CreateTestRegistry()
{
var registry = new ExcelMapRegistry();
// Search result maps
registry.Register(new SearchResultMap());
registry.Register(new MisSearchResultMap());
registry.Register(new MisNonMatchSearchResultMap());
// Filter entry maps
registry.Register(new TimespanFilterMap());
registry.Register(new WorkOrderFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap());
registry.Register(new ProfitCenterFilterEntryMap());
registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new OperatorFilterEntryMap());
registry.Register(new ComponentLotFilterEntryMap());
registry.Register(new ItemOperationMisFilterEntryMap());
return registry;
}
[Fact]
@@ -1,4 +1,4 @@
using JdeScoping.ExcelIO.Models.Reporting;
using JdeScoping.Core.Models.SearchResults;
using Shouldly;
using Xunit;
@@ -1,8 +1,8 @@
using ClosedXML.Excel;
using JdeScoping.ExcelIO.Options;
using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Generators;
using JdeScoping.ExcelIO.Helpers;
using JdeScoping.ExcelIO.Mapping;
using JdeScoping.ExcelIO.Mapping.Maps;
using JdeScoping.ExcelIO.Models.Reporting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -10,6 +10,11 @@ using NSubstitute;
using Shouldly;
using Xunit;
using ExcelFormats = JdeScoping.ExcelIO.Formatting.ExcelFormats;
using SearchResult = JdeScoping.Core.Models.SearchResults.SearchResult;
using MisSearchResult = JdeScoping.Core.Models.SearchResults.MisSearchResult;
using MisNonMatchSearchResult = JdeScoping.Core.Models.SearchResults.MisNonMatchSearchResult;
namespace JdeScoping.ExcelIO.Tests;
/// <summary>
@@ -30,11 +35,33 @@ public class LegacyComparisonTests
DataSheetPassword = "JDESCOPINGTOOL"
});
var cache = new OutputColumnCache();
var tableWriter = new AttributeTableWriter(cache);
var registry = CreateTestRegistry();
var tableWriter = new FluentTableWriter(registry);
var criteriaGenerator = new CriteriaSheetGenerator(options, tableWriter);
_service = new ExcelExportService(logger, options, criteriaGenerator, tableWriter);
_service = new ExcelExportService(logger, options, criteriaGenerator, tableWriter, registry);
}
private static ExcelMapRegistry CreateTestRegistry()
{
var registry = new ExcelMapRegistry();
// Search result maps
registry.Register(new SearchResultMap());
registry.Register(new MisSearchResultMap());
registry.Register(new MisNonMatchSearchResultMap());
// Filter entry maps
registry.Register(new TimespanFilterMap());
registry.Register(new WorkOrderFilterEntryMap());
registry.Register(new ItemNumberFilterEntryMap());
registry.Register(new ProfitCenterFilterEntryMap());
registry.Register(new WorkCenterFilterEntryMap());
registry.Register(new OperatorFilterEntryMap());
registry.Register(new ComponentLotFilterEntryMap());
registry.Register(new ItemOperationMisFilterEntryMap());
return registry;
}
#region Search Results Column Order Tests
@@ -1,3 +1,4 @@
using JdeScoping.ExcelIO.Formatting;
using JdeScoping.ExcelIO.Mapping;
using Shouldly;
using Xunit;
@@ -23,7 +24,7 @@ public class ExcelClassMapTests
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);
Map(x => x.CreatedAt).Order(30).Header("Created").Format(ExcelFormats.TimestampFormat);
}
}
@@ -71,7 +72,7 @@ public class ExcelClassMapTests
var map = new TestModelMap();
var columns = map.Columns;
columns[2].Format.ShouldBe(ExcelFormats.Timestamp);
columns[2].Format.ShouldBe(ExcelFormats.TimestampFormat);
}
[Fact]
@@ -1,100 +0,0 @@
using JdeScoping.ExcelIO.Attributes;
using JdeScoping.ExcelIO.Helpers;
using Shouldly;
using Xunit;
namespace JdeScoping.ExcelIO.Tests;
public class OutputColumnCacheTests
{
private readonly OutputColumnCache _cache = new();
[OutputTable(TabName = "Test Table", TableName = "Test_Table")]
private class TestModel
{
[OutputColumn(Order = 30, HeaderText = "Column C")]
public string ColumnC { get; set; } = string.Empty;
[OutputColumn(Order = 10, HeaderText = "Column A")]
public string ColumnA { get; set; } = string.Empty;
[OutputColumn(Order = 20, HeaderText = "Column B")]
public string ColumnB { get; set; } = string.Empty;
public string NonOutputColumn { get; set; } = string.Empty;
}
private class TieBreakModel
{
[OutputColumn(Order = 10, HeaderText = "Zebra")]
public string Zebra { get; set; } = string.Empty;
[OutputColumn(Order = 10, HeaderText = "Apple")]
public string Apple { get; set; } = string.Empty;
[OutputColumn(Order = 10, HeaderText = "Mango")]
public string Mango { get; set; } = string.Empty;
}
private class EmptyModel
{
public string NoAttributes { get; set; } = string.Empty;
}
[Fact]
public void GetColumns_ReturnsColumnsOrderedByOrderProperty()
{
var columns = _cache.GetColumns<TestModel>();
columns.Count.ShouldBe(3);
columns[0].Attribute.HeaderText.ShouldBe("Column A");
columns[1].Attribute.HeaderText.ShouldBe("Column B");
columns[2].Attribute.HeaderText.ShouldBe("Column C");
}
[Fact]
public void GetColumns_TieBreaksAlphabeticallyByPropertyName()
{
var columns = _cache.GetColumns<TieBreakModel>();
columns.Count.ShouldBe(3);
// All have Order=10, so should be sorted by property name
columns[0].Name.ShouldBe("Apple");
columns[1].Name.ShouldBe("Mango");
columns[2].Name.ShouldBe("Zebra");
}
[Fact]
public void GetColumns_ExcludesPropertiesWithoutAttribute()
{
var columns = _cache.GetColumns<TestModel>();
columns.Count.ShouldBe(3);
columns.ShouldNotContain(c => c.Name == "NonOutputColumn");
}
[Fact]
public void GetColumns_ReturnsEmptyForEmptyModel()
{
var columns = _cache.GetColumns<EmptyModel>();
columns.Count.ShouldBe(0);
}
[Fact]
public void GetColumns_CachesResults()
{
var columns1 = _cache.GetColumns<TestModel>();
var columns2 = _cache.GetColumns<TestModel>();
ReferenceEquals(columns1, columns2).ShouldBeTrue();
}
[Fact]
public void GetColumns_ByType_ReturnsCorrectColumns()
{
var columns = _cache.GetColumns(typeof(TestModel));
columns.Count.ShouldBe(3);
}
}