refactor(data-access): remove TVP code and simplify SearchModel

- Remove all List<*FilterEntry> properties and *FilterEnabled computed properties from SearchModel
- Delete TableValuedParameterExtensions.cs
- Delete entire FilterEntries folder and all filter entry model classes
- Delete FilterHandlers folder and all filter handler classes
- Delete IFilterHandler interface and FilterResult model
- Update MisQueryBuilder to use SQL extraction functions instead of model properties
- Update SearchProcessor to get ExtractMisData from database using fn_GetSearchExtractMisData
- Update DependencyInjection to remove filter handler registrations
- Delete obsolete tests for TVP extensions and filter handlers

Filter criteria are now stored as JSON in Search.Criteria column and extracted using SQL functions (fn_GetSearch*) during query execution.
This commit is contained in:
Joseph Doherty
2026-01-06 14:32:03 -05:00
parent a2a8bb3e9f
commit 691a6d1ffd
26 changed files with 37 additions and 1910 deletions
@@ -1,480 +0,0 @@
using System.Data;
using System.Reflection;
using Dapper;
using JdeScoping.DataAccess.Extensions;
using JdeScoping.DataAccess.Models;
using JdeScoping.DataAccess.Models.FilterEntries;
using Shouldly;
using Xunit;
namespace JdeScoping.DataAccess.Tests.Extensions;
/// <summary>
/// Unit tests for TableValuedParameterExtensions.
/// </summary>
public sealed class TableValuedParameterExtensionsTests
{
#region CreateWorkOrderFilterParameter Tests
[Fact]
public void CreateWorkOrderFilterParameter_ProducesCorrectSchema()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter =
[
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC" }
]
};
// Act
var param = model.CreateWorkOrderFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.ShouldNotBeNull();
dataTable.Columns.Count.ShouldBe(1);
dataTable.Columns.Contains("WorkOrderNumber").ShouldBeTrue();
dataTable.Columns["WorkOrderNumber"]!.DataType.ShouldBe(typeof(long));
}
[Fact]
public void CreateWorkOrderFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter = []
};
// Act
var param = model.CreateWorkOrderFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.ShouldNotBeNull();
dataTable.Rows.Count.ShouldBe(0);
}
[Fact]
public void CreateWorkOrderFilterParameter_PopulatesCorrectData()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter =
[
new WorkOrderFilterEntry { WorkOrderNumber = 12345 },
new WorkOrderFilterEntry { WorkOrderNumber = 67890 }
]
};
// Act
var param = model.CreateWorkOrderFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(2);
dataTable.Rows[0]["WorkOrderNumber"].ShouldBe(12345L);
dataTable.Rows[1]["WorkOrderNumber"].ShouldBe(67890L);
}
#endregion
#region CreateItemNumberFilterParameter Tests
[Fact]
public void CreateItemNumberFilterParameter_ProducesCorrectSchema()
{
// Arrange
var model = new SearchModel
{
ItemNumberFilter =
[
new ItemNumberFilterEntry { ItemNumber = "ABC123" }
]
};
// Act
var param = model.CreateItemNumberFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.ShouldNotBeNull();
dataTable.Columns.Count.ShouldBe(1);
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
}
[Fact]
public void CreateItemNumberFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
{
// Arrange
var model = new SearchModel
{
ItemNumberFilter = []
};
// Act
var param = model.CreateItemNumberFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(0);
}
#endregion
#region CreateProfitCenterFilterParameter Tests
[Fact]
public void CreateProfitCenterFilterParameter_ProducesCorrectSchema()
{
// Arrange
var model = new SearchModel
{
ProfitCenterFilter =
[
new ProfitCenterFilterEntry { Code = "PC001" }
]
};
// Act
var param = model.CreateProfitCenterFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Columns.Count.ShouldBe(1);
dataTable.Columns.Contains("Code").ShouldBeTrue();
dataTable.Columns["Code"]!.DataType.ShouldBe(typeof(string));
}
[Fact]
public void CreateProfitCenterFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
{
// Arrange
var model = new SearchModel
{
ProfitCenterFilter = []
};
// Act
var param = model.CreateProfitCenterFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(0);
}
#endregion
#region CreateWorkCenterFilterParameter Tests
[Fact]
public void CreateWorkCenterFilterParameter_ProducesCorrectSchema()
{
// Arrange
var model = new SearchModel
{
WorkCenterFilter =
[
new WorkCenterFilterEntry { Code = "WC001" }
]
};
// Act
var param = model.CreateWorkCenterFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Columns.Count.ShouldBe(1);
dataTable.Columns.Contains("Code").ShouldBeTrue();
dataTable.Columns["Code"]!.DataType.ShouldBe(typeof(string));
}
[Fact]
public void CreateWorkCenterFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
{
// Arrange
var model = new SearchModel
{
WorkCenterFilter = []
};
// Act
var param = model.CreateWorkCenterFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(0);
}
#endregion
#region CreateComponentLotFilterParameter Tests
[Fact]
public void CreateComponentLotFilterParameter_ProducesCorrectSchema()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var param = model.CreateComponentLotFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Columns.Count.ShouldBe(2);
dataTable.Columns.Contains("ComponentLotNumber").ShouldBeTrue();
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
dataTable.Columns["ComponentLotNumber"]!.DataType.ShouldBe(typeof(string));
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
}
[Fact]
public void CreateComponentLotFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter = []
};
// Act
var param = model.CreateComponentLotFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(0);
}
[Fact]
public void CreateComponentLotFilterParameter_PopulatesCorrectData()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" },
new ComponentLotFilterEntry { LotNumber = "LOT002", ItemNumber = "ITEM002" }
]
};
// Act
var param = model.CreateComponentLotFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(2);
dataTable.Rows[0]["ComponentLotNumber"].ShouldBe("LOT001");
dataTable.Rows[0]["ItemNumber"].ShouldBe("ITEM001");
}
#endregion
#region CreateOperatorFilterParameter Tests
[Fact]
public void CreateOperatorFilterParameter_ProducesCorrectSchema()
{
// Arrange
var model = new SearchModel
{
OperatorFilter =
[
new OperatorFilterEntry { UserId = "USER01", AddressNumber = 123 }
]
};
// Act
var param = model.CreateOperatorFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Columns.Count.ShouldBe(1);
dataTable.Columns.Contains("UserName").ShouldBeTrue();
dataTable.Columns["UserName"]!.DataType.ShouldBe(typeof(string));
}
[Fact]
public void CreateOperatorFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
{
// Arrange
var model = new SearchModel
{
OperatorFilter = []
};
// Act
var param = model.CreateOperatorFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(0);
}
#endregion
#region CreateItemOperationMisFilterParameter Tests
[Fact]
public void CreateItemOperationMisFilterParameter_ProducesCorrectSchema()
{
// Arrange
var model = new SearchModel
{
ItemOperationMisFilter =
[
new ItemOperationMisFilterEntry
{
ItemNumber = "ITEM001",
OperationNumber = "010",
MisNumber = "MIS001",
MisRevision = "A"
}
]
};
// Act
var param = model.CreateItemOperationMisFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Columns.Count.ShouldBe(4);
dataTable.Columns.Contains("ItemNumber").ShouldBeTrue();
dataTable.Columns.Contains("OperationNumber").ShouldBeTrue();
dataTable.Columns.Contains("MisNumber").ShouldBeTrue();
dataTable.Columns.Contains("MisRevision").ShouldBeTrue();
dataTable.Columns["ItemNumber"]!.DataType.ShouldBe(typeof(string));
dataTable.Columns["OperationNumber"]!.DataType.ShouldBe(typeof(string));
dataTable.Columns["MisNumber"]!.DataType.ShouldBe(typeof(string));
dataTable.Columns["MisRevision"]!.DataType.ShouldBe(typeof(string));
}
[Fact]
public void CreateItemOperationMisFilterParameter_WithEmptyCollection_ProducesEmptyDataTable()
{
// Arrange
var model = new SearchModel
{
ItemOperationMisFilter = []
};
// Act
var param = model.CreateItemOperationMisFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(0);
}
[Fact]
public void CreateItemOperationMisFilterParameter_PopulatesCorrectData()
{
// Arrange
var model = new SearchModel
{
ItemOperationMisFilter =
[
new ItemOperationMisFilterEntry
{
ItemNumber = "ITEM001",
OperationNumber = "010",
MisNumber = "MIS001",
MisRevision = "A"
}
]
};
// Act
var param = model.CreateItemOperationMisFilterParameter();
var dataTable = ExtractDataTable(param);
// Assert
dataTable.Rows.Count.ShouldBe(1);
dataTable.Rows[0]["ItemNumber"].ShouldBe("ITEM001");
dataTable.Rows[0]["OperationNumber"].ShouldBe("010");
dataTable.Rows[0]["MisNumber"].ShouldBe("MIS001");
dataTable.Rows[0]["MisRevision"].ShouldBe("A");
}
#endregion
#region Helper Methods
/// <summary>
/// Extracts the underlying DataTable from a Dapper table-valued parameter.
/// Uses reflection to access internal fields across different Dapper versions.
/// </summary>
private static DataTable ExtractDataTable(SqlMapper.ICustomQueryParameter param)
{
// The TableValuedParameter wraps a DataTable - try multiple field/property names
// across different Dapper versions
var type = param.GetType();
var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
// Try field names used in different Dapper versions
var fieldNames = new[] { "_table", "table", "Table", "_dataTable", "dataTable" };
foreach (var fieldName in fieldNames)
{
var field = type.GetField(fieldName, bindingFlags);
if (field != null && field.FieldType == typeof(DataTable))
{
var value = field.GetValue(param);
if (value is DataTable dt)
return dt;
}
}
// Try property names
var propertyNames = new[] { "Table", "DataTable", "table", "_table" };
foreach (var propName in propertyNames)
{
var prop = type.GetProperty(propName, bindingFlags);
if (prop != null && prop.PropertyType == typeof(DataTable))
{
var value = prop.GetValue(param);
if (value is DataTable dt)
return dt;
}
}
// Last resort: scan all fields
foreach (var field in type.GetFields(bindingFlags))
{
if (field.FieldType == typeof(DataTable))
{
var value = field.GetValue(param);
if (value is DataTable dt)
return dt;
}
}
// Scan all properties
foreach (var prop in type.GetProperties(bindingFlags))
{
if (prop.PropertyType == typeof(DataTable))
{
var value = prop.GetValue(param);
if (value is DataTable dt)
return dt;
}
}
throw new InvalidOperationException(
$"Could not extract DataTable from {type.FullName}. " +
$"Fields: {string.Join(", ", type.GetFields(bindingFlags).Select(f => f.Name))}. " +
$"Properties: {string.Join(", ", type.GetProperties(bindingFlags).Select(p => p.Name))}");
}
#endregion
}
@@ -1,196 +0,0 @@
using JdeScoping.DataAccess.FilterHandlers;
using JdeScoping.DataAccess.Models;
using JdeScoping.DataAccess.Models.FilterEntries;
using Shouldly;
using SqlKata.Compilers;
using Xunit;
namespace JdeScoping.DataAccess.Tests.FilterHandlers;
/// <summary>
/// Unit tests for ComponentLotFilterHandler.
/// </summary>
public sealed class ComponentLotFilterHandlerTests
{
private readonly SqlServerCompiler _compiler = new();
private readonly ComponentLotFilterHandler _handler = new();
[Fact]
public void IsEnabled_WithComponentLotFilters_ReturnsTrue()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var result = _handler.IsEnabled(model);
// Assert
result.ShouldBeTrue();
}
[Fact]
public void IsEnabled_WithEmptyComponentLotFilters_ReturnsFalse()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter = []
};
// Act
var result = _handler.IsEnabled(model);
// Assert
result.ShouldBeFalse();
}
[Fact]
public void IsEnabled_WithNullComponentLotFilters_ReturnsFalse()
{
// Arrange
var model = new SearchModel();
// Act
var result = _handler.IsEnabled(model);
// Assert
result.ShouldBeFalse();
}
[Fact]
public void Apply_GeneratedSql_ContainsWorkOrderComponentJoin()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
result.ShouldNotBeNull();
result.SetupSql.ShouldNotBeEmpty();
var allSql = string.Join("\n", result.SetupSql);
allSql.ShouldContain("dbo.WorkOrderComponent AS woc");
}
[Fact]
public void Apply_GeneratedSql_ContainsLotUsageJoin()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
var allSql = string.Join("\n", result.SetupSql);
allSql.ShouldContain("dbo.LotUsage AS lu");
}
[Fact]
public void Apply_GeneratedSql_SetsCARDEXFlag()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
var allSql = string.Join("\n", result.SetupSql);
// CARDEX flag is set (not PartsList) per the ComponentLotFilterHandler implementation
allSql.ShouldContain("TARGET.CARDEX = 1");
}
[Fact]
public void Apply_GeneratedSql_DoesNotSetPartsListFlag()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
var allSql = string.Join("\n", result.SetupSql);
// ComponentLotFilterHandler sets CARDEX, not PartsList
allSql.ShouldNotContain("PartsList = 1");
}
[Fact]
public void Apply_GeneratedSql_ContainsSplitOrderLogic()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
var allSql = string.Join("\n", result.SetupSql);
allSql.ShouldContain("SplitOrder");
}
[Fact]
public void Apply_Parameters_ContainsComponentLotFilterParameter()
{
// Arrange
var model = new SearchModel
{
ComponentLotFilter =
[
new ComponentLotFilterEntry { LotNumber = "LOT001", ItemNumber = "ITEM001" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
result.Parameters.ShouldContainKey("p_ComponentLotFilter");
}
[Fact]
public void Priority_ReturnsExpectedValue()
{
// Assert
_handler.Priority.ShouldBe(30);
}
}
@@ -1,155 +0,0 @@
using JdeScoping.DataAccess.FilterHandlers;
using JdeScoping.DataAccess.Models;
using JdeScoping.DataAccess.Models.FilterEntries;
using Shouldly;
using SqlKata.Compilers;
using Xunit;
namespace JdeScoping.DataAccess.Tests.FilterHandlers;
/// <summary>
/// Unit tests for WorkOrderFilterHandler.
/// </summary>
public sealed class WorkOrderFilterHandlerTests
{
private readonly SqlServerCompiler _compiler = new();
private readonly WorkOrderFilterHandler _handler = new();
[Fact]
public void IsEnabled_WithWorkOrderFilters_ReturnsTrue()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter =
[
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
]
};
// Act
var result = _handler.IsEnabled(model);
// Assert
result.ShouldBeTrue();
}
[Fact]
public void IsEnabled_WithEmptyWorkOrderFilters_ReturnsFalse()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter = []
};
// Act
var result = _handler.IsEnabled(model);
// Assert
result.ShouldBeFalse();
}
[Fact]
public void IsEnabled_WithNullWorkOrderFilters_ReturnsFalse()
{
// Arrange
var model = new SearchModel();
// Act
var result = _handler.IsEnabled(model);
// Assert
result.ShouldBeFalse();
}
[Fact]
public void Apply_GeneratedSql_ContainsMerge()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter =
[
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
result.ShouldNotBeNull();
result.SetupSql.ShouldNotBeEmpty();
var allSql = string.Join("\n", result.SetupSql);
allSql.ShouldContain("MERGE #Temp_WO AS TARGET");
}
[Fact]
public void Apply_GeneratedSql_ContainsManuallySpecified()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter =
[
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
var allSql = string.Join("\n", result.SetupSql);
allSql.ShouldContain("ManuallySpecified = 1");
}
[Fact]
public void Apply_GeneratedSql_ContainsSplitOrderLogic()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter =
[
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
var allSql = string.Join("\n", result.SetupSql);
allSql.ShouldContain("SplitOrder");
allSql.ShouldContain("ParentWorkOrderNumber");
}
[Fact]
public void Apply_Parameters_ContainsWorkOrderFilterParameter()
{
// Arrange
var model = new SearchModel
{
WorkOrderFilter =
[
new WorkOrderFilterEntry { WorkOrderNumber = 12345, ItemNumber = "ABC123" }
]
};
// Act
var result = _handler.Apply(model, _compiler);
// Assert
result.Parameters.ShouldContainKey("p_WorkOrderFilter");
}
[Fact]
public void Priority_ReturnsExpectedValue()
{
// Assert
_handler.Priority.ShouldBe(10);
}
}