1618b6664d
Remove legacy JDE and CMS direct-access code that is no longer used: - Delete ICmsDataSource, IJdeDataSource interfaces - Delete ISearchProcessor, IUpdateProcessor interfaces - Delete IJdeRepository and ICmsRepository (all partials) - Delete JdeRepository and CmsRepository implementations - Delete JdeQueries and CmsQueries - Delete JdeFileDataSource, JdeOracleDataSource - Delete CmsFileDataSource, CmsOracleDataSource - Remove unused methods from LotFinderRepository interfaces - Delete associated unit tests (CmsRepositoryTests, JdeRepositoryTests) All data sync now uses ETL pipelines via DataSync project.
1433 lines
41 KiB
Markdown
1433 lines
41 KiB
Markdown
# 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;
|
|
|
|
/// <summary>
|
|
/// Defines how a property maps to an Excel column.
|
|
/// </summary>
|
|
public sealed class ColumnDefinition
|
|
{
|
|
/// <summary>Property name for debugging/error messages.</summary>
|
|
public required string PropertyName { get; init; }
|
|
|
|
/// <summary>Compiled delegate to get property value from object.</summary>
|
|
public required Func<object, object?> ValueGetter { get; init; }
|
|
|
|
/// <summary>Property type for formatting decisions.</summary>
|
|
public required Type PropertyType { get; init; }
|
|
|
|
/// <summary>Column display order (lower = leftmost).</summary>
|
|
public int Order { get; set; }
|
|
|
|
/// <summary>Column header text.</summary>
|
|
public string HeaderText { get; set; } = string.Empty;
|
|
|
|
/// <summary>Excel number format string.</summary>
|
|
public string Format { get; set; } = "@";
|
|
|
|
/// <summary>Auto-size column width.</summary>
|
|
public bool AutoWidth { get; set; } = true;
|
|
|
|
/// <summary>Manual column width (when AutoWidth = false).</summary>
|
|
public double Width { get; set; }
|
|
|
|
/// <summary>Enable text wrapping.</summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Fluent builder for configuring Excel column properties.
|
|
/// </summary>
|
|
/// <typeparam name="T">The model type being mapped.</typeparam>
|
|
/// <typeparam name="TProperty">The property type.</typeparam>
|
|
public sealed class ColumnBuilder<T, TProperty>
|
|
{
|
|
private readonly ColumnDefinition _definition;
|
|
|
|
internal ColumnBuilder(ColumnDefinition definition)
|
|
{
|
|
_definition = definition;
|
|
}
|
|
|
|
/// <summary>Sets the column display order.</summary>
|
|
public ColumnBuilder<T, TProperty> Order(int order)
|
|
{
|
|
_definition.Order = order;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>Sets the column header text.</summary>
|
|
public ColumnBuilder<T, TProperty> Header(string text)
|
|
{
|
|
_definition.HeaderText = text;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>Sets the Excel number format.</summary>
|
|
public ColumnBuilder<T, TProperty> Format(string format)
|
|
{
|
|
_definition.Format = format;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>Sets a fixed column width (disables auto-width).</summary>
|
|
public ColumnBuilder<T, TProperty> Width(double width)
|
|
{
|
|
_definition.AutoWidth = false;
|
|
_definition.Width = width;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>Enables text wrapping for the column.</summary>
|
|
public ColumnBuilder<T, TProperty> 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;
|
|
|
|
/// <summary>
|
|
/// Interface for Excel class maps (non-generic access).
|
|
/// </summary>
|
|
public interface IExcelClassMap
|
|
{
|
|
/// <summary>The type this map applies to.</summary>
|
|
Type MappedType { get; }
|
|
|
|
/// <summary>Excel table name (for named ranges).</summary>
|
|
string? TableName { get; }
|
|
|
|
/// <summary>Worksheet tab name.</summary>
|
|
string? TabName { get; }
|
|
|
|
/// <summary>Ordered column definitions.</summary>
|
|
IReadOnlyList<ColumnDefinition> Columns { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Base class for defining Excel column mappings via fluent API.
|
|
/// </summary>
|
|
/// <typeparam name="T">The model type to map.</typeparam>
|
|
public abstract class ExcelClassMap<T> : IExcelClassMap
|
|
{
|
|
private readonly List<ColumnDefinition> _columns = [];
|
|
|
|
/// <inheritdoc />
|
|
public Type MappedType => typeof(T);
|
|
|
|
/// <inheritdoc />
|
|
public string? TableName { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public string? TabName { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<ColumnDefinition> Columns =>
|
|
_columns.OrderBy(c => c.Order).ThenBy(c => c.PropertyName).ToList();
|
|
|
|
/// <summary>
|
|
/// Configures the table and tab names for this model.
|
|
/// </summary>
|
|
protected void Table(string tableName, string tabName)
|
|
{
|
|
TableName = tableName;
|
|
TabName = tabName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a property to an Excel column.
|
|
/// </summary>
|
|
protected ColumnBuilder<T, TProperty> Map<TProperty>(Expression<Func<T, TProperty>> 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<T, TProperty>(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;
|
|
|
|
/// <summary>
|
|
/// Registry for Excel class maps, used for DI and lookup.
|
|
/// </summary>
|
|
public sealed class ExcelMapRegistry
|
|
{
|
|
private readonly Dictionary<Type, IExcelClassMap> _maps = new();
|
|
|
|
/// <summary>
|
|
/// Registers a map for a type.
|
|
/// </summary>
|
|
public void Register<T>(ExcelClassMap<T> map)
|
|
{
|
|
_maps[typeof(T)] = map;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the map for a type.
|
|
/// </summary>
|
|
public IExcelClassMap GetMap<T>() => GetMap(typeof(T));
|
|
|
|
/// <summary>
|
|
/// Gets the map for a type.
|
|
/// </summary>
|
|
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().");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a map exists for a type.
|
|
/// </summary>
|
|
public bool HasMap<T>() => HasMap(typeof(T));
|
|
|
|
/// <summary>
|
|
/// Checks if a map exists for a type.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Standard Excel format strings for column configuration.
|
|
/// </summary>
|
|
public static class ExcelFormats
|
|
{
|
|
/// <summary>Text format (default).</summary>
|
|
public const string Text = "@";
|
|
|
|
/// <summary>Date format: MM/dd/yyyy</summary>
|
|
public const string Date = "[$-409]MM/dd/yyyy;@";
|
|
|
|
/// <summary>Timestamp format: m/d/yy h:mm AM/PM</summary>
|
|
public const string Timestamp = "[$-409]m/d/yy h:mm AM/PM;@";
|
|
|
|
/// <summary>Default width for wrapped text columns.</summary>
|
|
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<TestModel>
|
|
{
|
|
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<DummyModel>
|
|
{
|
|
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<DummyModel>();
|
|
|
|
retrieved.ShouldBe(map);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetMap_UnregisteredType_ThrowsInvalidOperationException()
|
|
{
|
|
var registry = new ExcelMapRegistry();
|
|
|
|
Should.Throw<InvalidOperationException>(() => registry.GetMap<DummyModel>());
|
|
}
|
|
|
|
[Fact]
|
|
public void HasMap_RegisteredType_ReturnsTrue()
|
|
{
|
|
var registry = new ExcelMapRegistry();
|
|
registry.Register(new DummyMap());
|
|
|
|
registry.HasMap<DummyModel>().ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void HasMap_UnregisteredType_ReturnsFalse()
|
|
{
|
|
var registry = new ExcelMapRegistry();
|
|
|
|
registry.HasMap<DummyModel>().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;
|
|
|
|
/// <summary>
|
|
/// JDE search result - work order with current status.
|
|
/// </summary>
|
|
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; }
|
|
|
|
/// <summary>
|
|
/// Computed reason why this work order was included in results.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// MIS (Manufacturing Instruction Sheet) data result.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// MIS non-match investigation result.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Aggregates search metadata and results for export.
|
|
/// </summary>
|
|
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<SearchResult> Results { get; set; } = [];
|
|
public List<MisSearchResult> MisResults { get; set; } = [];
|
|
public List<MisNonMatchSearchResult> 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;
|
|
|
|
/// <summary>
|
|
/// Excel column mapping for SearchResult.
|
|
/// </summary>
|
|
public sealed class SearchResultMap : ExcelClassMap<SearchResult>
|
|
{
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Excel column mapping for MisSearchResult.
|
|
/// </summary>
|
|
public sealed class MisSearchResultMap : ExcelClassMap<MisSearchResult>
|
|
{
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Excel column mapping for MisNonMatchSearchResult.
|
|
/// </summary>
|
|
public sealed class MisNonMatchSearchResultMap : ExcelClassMap<MisNonMatchSearchResult>
|
|
{
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Writes Excel tables using fluent mapping configuration.
|
|
/// </summary>
|
|
public sealed class FluentTableWriter
|
|
{
|
|
private readonly ExcelMapRegistry _registry;
|
|
|
|
public FluentTableWriter(ExcelMapRegistry registry)
|
|
{
|
|
_registry = registry;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Writes a table to the worksheet using the registered map for type T.
|
|
/// </summary>
|
|
public IXLTable? WriteTable<T>(
|
|
IXLWorksheet worksheet,
|
|
int startRow,
|
|
int startCol,
|
|
IEnumerable<T> data,
|
|
string? tableNameOverride = null,
|
|
bool showHeader = false,
|
|
string? headerText = null)
|
|
{
|
|
var map = _registry.GetMap<T>();
|
|
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
|
|
/// <summary>Multiplier for auto-width columns.</summary>
|
|
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<FluentTableWriter>();
|
|
```
|
|
|
|
**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<ExcelExportService> logger,
|
|
IOptions<ExcelExportOptions> options,
|
|
CriteriaSheetGenerator criteriaGenerator,
|
|
FluentTableWriter tableWriter)
|
|
```
|
|
|
|
**Step 3: Update GenerateResultsSheet to use map for tab name**
|
|
|
|
```csharp
|
|
private void GenerateResultsSheet(XLWorkbook workbook, List<SearchResult> results)
|
|
{
|
|
var map = _registry.GetMap<SearchResult>();
|
|
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<OutputColumnCache>();
|
|
services.AddSingleton<AttributeTableWriter>();
|
|
```
|
|
|
|
**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<T>, ColumnBuilder<T,P> |
|
|
| OutputColumnCache (reflection) | ExcelMapRegistry (explicit registration) |
|
|
| AttributeTableWriter | FluentTableWriter |
|
|
|
|
**Files Created:** 12
|
|
**Files Deleted:** ~15
|
|
**Net reduction:** ~3 files, significantly cleaner architecture
|