Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
10 KiB
Domain Models Design
Overview
This document describes the design approach for implementing domain model entities in the JDE Scoping Tool .NET 10 migration.
Project Organization
All domain models reside in the JdeScoping.Core project:
JdeScoping.Core/
├── Models/
│ ├── Enums/
│ │ ├── SearchStatus.cs # Search processing states
│ │ └── UpdateTypes.cs # Data sync frequency types
│ │
│ ├── Search.cs # User search request entity
│ ├── SearchCriteria.cs # Filter parameters for queries
│ ├── SearchUpdate.cs # SignalR status update DTO
│ │
│ ├── WorkOrder.cs # JDE work order entity
│ ├── WorkOrderStep.cs # Work order operation step
│ ├── WorkOrderTime.cs # F31122 time transaction
│ ├── WorkOrderComponent.cs # Component usage
│ ├── WorkOrderRouting.cs # Step transaction
│ │
│ ├── Lot.cs # JDE lot entity
│ ├── LotUsage.cs # Cardex consumption record
│ ├── LotLocation.cs # Lot location tracking
│ │
│ ├── Item.cs # JDE item master
│ ├── WorkCenter.cs # JDE work center (IBusinessUnit)
│ ├── ProfitCenter.cs # JDE profit center (IBusinessUnit)
│ ├── Branch.cs # JDE branch entity
│ ├── JdeUser.cs # JDE operator entity
│ ├── StatusCode.cs # Work order status lookup
│ ├── FunctionCode.cs # Function code lookup
│ │
│ ├── DataUpdate.cs # Cache refresh tracking
│ ├── OrgHierarchy.cs # Profit center to work center mapping
│ ├── RouteMaster.cs # Item router master
│ ├── StatusUpdate.cs # Generic process status message
│ │
│ ├── MisData.cs # CMS MIS data entity
│ │
│ ├── UserInfo.cs # Authenticated user info
│ │
│ ├── POReceiver.cs # PO receiver record
│ ├── POInspect.cs # PO inspection record
│ ├── DcsLot.cs # DCS lot record
│ ├── CamstarMO.cs # Camstar manufacturing order
│ │
│ ├── QueryTypes.cs # Query type definitions
│ ├── TableSpec.cs # Dynamic SQL table spec
│ └── ColumnSpec.cs # Column specification
│
├── Interfaces/
│ └── IBusinessUnit.cs # WorkCenter/ProfitCenter interface
│
├── ViewModels/
│ ├── WorkOrderViewModel.cs # WorkOrder projection
│ ├── LotViewModel.cs # Lot projection
│ ├── ItemViewModel.cs # Item projection
│ ├── WorkCenterViewModel.cs # WorkCenter projection
│ ├── ProfitCenterViewModel.cs # ProfitCenter projection
│ ├── JdeUserViewModel.cs # JdeUser projection
│ └── PartOperationViewModel.cs # Item/operation/MIS combination
│
├── Extensions/
│ ├── WorkOrderExtensions.cs # WorkOrder.ToViewModel()
│ ├── LotExtensions.cs # Lot.ToViewModel()
│ ├── ItemExtensions.cs # Item.ToViewModel()
│ ├── WorkCenterExtensions.cs # WorkCenter.ToViewModel()
│ ├── ProfitCenterExtensions.cs # ProfitCenter.ToViewModel()
│ └── JdeUserExtensions.cs # JdeUser.ToViewModel()
│
└── Helpers/
└── JdeDateConverter.cs # JDE date/time conversion
Design Patterns
Nullable Reference Types
The project has <Nullable>enable</Nullable> in the .csproj. All properties follow these patterns:
| Pattern | Example | Usage |
|---|---|---|
| Required property | public string Name { get; set; } = string.Empty; |
Non-null, initialized |
| Optional property | public string? Description { get; set; } |
Explicitly nullable |
| Collection property | public List<string> Items { get; set; } = []; |
Never null, empty default |
| Nullable byte array | public byte[]? Results { get; set; } |
Null until populated |
System.Text.Json Serialization
All enums that may be serialized use the string converter:
using System.Text.Json.Serialization;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SearchStatus
{
New = 0,
Submitted = 1,
Started = 2,
Ended = 3,
Error = 4
}
This ensures JSON output is "Status": "Ended" instead of "Status": 3.
JDE Date Conversion Pattern
JDE stores dates as integers in CYYDDD format (century + year + day of year) and times as HHMMSS:
public static class JdeDateConverter
{
/// <summary>
/// Converts JDE date (CYYDDD) and time (HHMMSS) to DateTime.
/// Returns null for zero or invalid values (changed from legacy 1900-01-01).
/// </summary>
public static DateTime? ToDateTime(int jdeDate, int jdeTime = 0)
{
if (jdeDate <= 0)
return null;
try
{
// CYYDDD format: C = century (0=1900s, 1=2000s), YY = year, DDD = day of year
int century = jdeDate / 100000;
int yearInCentury = (jdeDate / 1000) % 100;
int dayOfYear = jdeDate % 1000;
int year = 1900 + (century * 100) + yearInCentury;
if (dayOfYear < 1 || dayOfYear > 366)
return null;
var date = new DateTime(year, 1, 1).AddDays(dayOfYear - 1);
// Add time component if provided
if (jdeTime > 0)
{
int hours = jdeTime / 10000;
int minutes = (jdeTime / 100) % 100;
int seconds = jdeTime % 100;
if (hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60)
{
date = date.AddHours(hours).AddMinutes(minutes).AddSeconds(seconds);
}
}
return date;
}
catch
{
return null;
}
}
}
Entity Computed Properties
Entities with JDE date fields use private backing fields and computed public properties:
public class WorkOrder
{
// Private backing fields (mapped from database via Dapper)
private int LastUpdateDate { get; set; }
private int LastUpdateTime { get; set; }
// Public computed property
public DateTime? LastUpdateDT => JdeDateConverter.ToDateTime(LastUpdateDate, LastUpdateTime);
// Other properties...
public long WorkOrderNumber { get; set; }
public string ItemNumber { get; set; } = string.Empty;
}
ToViewModel Extension Methods
Projections are implemented as extension methods for separation of concerns:
public static class WorkOrderExtensions
{
public static WorkOrderViewModel ToViewModel(this WorkOrder workOrder)
{
return new WorkOrderViewModel
{
WorkOrderNumber = workOrder.WorkOrderNumber,
ItemNumber = workOrder.ItemNumber
};
}
}
IBusinessUnit Interface
WorkCenter and ProfitCenter share a common interface:
public interface IBusinessUnit
{
string Code { get; }
string Description { get; }
DateTime? LastUpdateDT { get; }
}
public class WorkCenter : IBusinessUnit
{
public string Code { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
private int LastUpdateDate { get; set; }
private int LastUpdateTime { get; set; }
public DateTime? LastUpdateDT => JdeDateConverter.ToDateTime(LastUpdateDate, LastUpdateTime);
}
Classes vs Records
| Type | Pattern | Rationale |
|---|---|---|
| Database entities | class |
Mutable for Dapper mapping, private setters for computed fields |
| ViewModels | record or class |
Immutable DTOs, but class is fine for simple projections |
| SearchUpdate | class with init |
Constructor sets Timestamp to UtcNow |
| Enums | enum |
Standard enumeration with JsonStringEnumConverter |
Existing Code Reconciliation
The existing JdeScoping.Core/Models/ contains placeholder implementations that differ from the spec:
| Existing File | Action | Notes |
|---|---|---|
| Search.cs | Replace | Different property names, missing CriteriaJSON/Criteria pattern |
| WorkOrder.cs | Replace | Simplified placeholder, missing JDE-specific fields |
| Item.cs | Replace | Missing ShortItemNumber, nullable annotations |
| Lot.cs | Replace | Placeholder implementation |
| LotUsage.cs | Replace | Placeholder implementation |
| WorkCenter.cs | Replace | Missing IBusinessUnit interface |
| JdeUser.cs | Replace | Placeholder implementation |
Migration from Legacy
| Legacy Pattern | New Pattern | Rationale |
|---|---|---|
DataModel.Models namespace |
JdeScoping.Core.Models |
.NET naming conventions |
| Newtonsoft.Json attributes | System.Text.Json attributes | Built-in .NET serialization |
[JsonConverter(typeof(StringEnumConverter))] |
[JsonConverter(typeof(JsonStringEnumConverter))] |
System.Text.Json |
LDAPEntry class |
UserInfo class |
Modern auth pattern naming |
| Invalid JDE dates -> 1900-01-01 | Invalid JDE dates -> null | Explicit null handling |
| ToViewModel() on entity | Extension method | Separation of concerns |
File Structure Summary
After implementation, the Models folder structure:
JdeScoping.Core/Models/
├── Enums/ (2 files)
├── [Entity classes] (27 files)
├── Interfaces/ (1 file - moved from Models)
├── ViewModels/ (7 files)
├── Extensions/ (6 files)
└── Helpers/ (1 file)
Total: ~44 new/modified files in JdeScoping.Core