Files
jdescopingtool/DOCUMENTATION/Architecture/CoreProject.md
T
Joseph Doherty 26ff8d9b4f Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
2026-01-02 07:43:29 -05:00

278 lines
8.5 KiB
Markdown

# Core Project
The `JdeScoping.Core` project contains business logic and data access. It has no ASP.NET dependencies and is fully testable.
## Project Structure
```
JdeScoping.Core/
├── Models/
│ ├── Search.cs # Search request + status
│ ├── SearchCriteria.cs # Search parameters
│ ├── SearchStatus.cs # Status enum
│ ├── WorkOrder.cs # Work order entity
│ ├── Lot.cs # Lot entity
│ ├── Item.cs # Item entity
│ └── DataUpdate.cs # Sync tracking
├── Interfaces/
│ ├── ISearchRepository.cs # Local SQL Server cache
│ ├── IJdeDataSource.cs # JDE data access (interface)
│ ├── ICmsDataSource.cs # CMS data access (interface)
│ ├── ISearchService.cs # Search execution
│ └── IExcelExportService.cs # Excel generation
├── Repositories/
│ ├── SearchRepository.cs # Dapper against SQL Server
│ ├── Jde/
│ │ ├── JdeOracleDataSource.cs # Production: Oracle connection
│ │ └── JdeFileDataSource.cs # Development: File-based data
│ └── Cms/
│ ├── CmsOracleDataSource.cs # Production: Oracle connection
│ └── CmsFileDataSource.cs # Development: File-based data
├── Services/
│ ├── SearchService.cs # Search execution logic
│ ├── ExcelExportService.cs # ClosedXML generation
│ └── DataSyncOrchestrator.cs # Sync orchestration logic
└── Auth/
├── IAuthService.cs # Authentication interface
├── LdapAuthService.cs # Production: Real LDAP server
└── FakeAuthService.cs # Development: Accepts any credentials
```
## Repository Pattern
Repositories use Dapper for data access. Connections are created per-query and disposed after use.
```csharp
public class SearchRepository : ISearchRepository
{
private readonly string _connectionString;
public SearchRepository(IConfiguration config)
{
_connectionString = config.GetConnectionString("SqlServer");
}
public async Task<Search> GetByIdAsync(int id)
{
using var connection = new SqlConnection(_connectionString);
return await connection.QuerySingleOrDefaultAsync<Search>(
"SELECT * FROM Search WHERE Id = @Id", new { Id = id });
}
public async Task<int> CreateAsync(Search search)
{
using var connection = new SqlConnection(_connectionString);
return await connection.ExecuteScalarAsync<int>(
@"INSERT INTO Search (UserId, Criteria, Status, CreatedAt)
VALUES (@UserId, @Criteria, @Status, @CreatedAt);
SELECT SCOPE_IDENTITY();", search);
}
}
```
## Data Source Pattern (JDE/CMS)
JDE and CMS data access uses an interface with two implementations: production (Oracle) and development (file-based). This allows development without Oracle connectivity.
### Interface Definition
```csharp
public interface IJdeDataSource
{
Task<IEnumerable<WorkOrder>> GetWorkOrdersAsync(DateTime since);
Task<IEnumerable<Lot>> GetLotsAsync(DateTime since);
Task<IEnumerable<Item>> GetItemsAsync();
// ... other data retrieval methods
}
public interface ICmsDataSource
{
Task<IEnumerable<LotUsage>> GetLotUsageAsync(DateTime since);
// ... other CMS data methods
}
```
### Production Implementation (Oracle)
```csharp
public class JdeOracleDataSource : IJdeDataSource
{
private readonly string _connectionString;
public JdeOracleDataSource(IConfiguration config)
{
_connectionString = config.GetConnectionString("JdeOracle");
}
public async Task<IEnumerable<WorkOrder>> GetWorkOrdersAsync(DateTime since)
{
using var connection = new OracleConnection(_connectionString);
return await connection.QueryAsync<WorkOrder>(
"SELECT * FROM F4801 WHERE UPMJ >= :Since", new { Since = since });
}
}
```
### Development Implementation (File-based)
```csharp
public class JdeFileDataSource : IJdeDataSource
{
private readonly string _dataDirectory;
public JdeFileDataSource(IConfiguration config)
{
_dataDirectory = config["DataSource:FileDirectory"] ?? "DevData";
}
public async Task<IEnumerable<WorkOrder>> GetWorkOrdersAsync(DateTime since)
{
var filePath = Path.Combine(_dataDirectory, "workorders.json");
var json = await File.ReadAllTextAsync(filePath);
var allOrders = JsonSerializer.Deserialize<List<WorkOrder>>(json);
return allOrders.Where(wo => wo.UpdateDate >= since);
}
}
```
### Registration by Environment
```csharp
// In Program.cs
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IJdeDataSource, JdeFileDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsFileDataSource>();
}
else
{
builder.Services.AddScoped<IJdeDataSource, JdeOracleDataSource>();
builder.Services.AddScoped<ICmsDataSource, CmsOracleDataSource>();
}
```
This pattern enables:
- Development without Oracle database access
- Testing with predictable data sets
- Easy switching between implementations via configuration
## Authentication Pattern
Authentication uses the same interface pattern with production and development implementations.
### Interface Definition
```csharp
public interface IAuthService
{
Task<AuthResult> AuthenticateAsync(string username, string password);
Task<bool> IsInGroupAsync(string username, string groupName);
}
public class AuthResult
{
public bool Success { get; set; }
public string DisplayName { get; set; }
public string Email { get; set; }
public string ErrorMessage { get; set; }
}
```
### Production Implementation (LDAP)
```csharp
public class LdapAuthService : IAuthService
{
private readonly LdapOptions _options;
public LdapAuthService(IOptions<LdapOptions> options)
{
_options = options.Value;
}
public async Task<AuthResult> AuthenticateAsync(string username, string password)
{
using var connection = new LdapConnection(_options.Url);
try
{
connection.Bind(new NetworkCredential(username, password));
// Retrieve user details from directory
return new AuthResult { Success = true, DisplayName = "..." };
}
catch (LdapException)
{
return new AuthResult { Success = false, ErrorMessage = "Invalid credentials" };
}
}
}
```
### Development Implementation (Fake)
```csharp
public class FakeAuthService : IAuthService
{
private readonly AuthOptions _options;
public FakeAuthService(IOptions<AuthOptions> options)
{
_options = options.Value;
}
public Task<AuthResult> AuthenticateAsync(string username, string password)
{
// Accept any non-empty credentials in development
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
return Task.FromResult(new AuthResult
{
Success = false,
ErrorMessage = "Username and password required"
});
}
return Task.FromResult(new AuthResult
{
Success = true,
DisplayName = username,
Email = $"{username}@dev.local"
});
}
public Task<bool> IsInGroupAsync(string username, string groupName)
{
// Always return true in development
return Task.FromResult(true);
}
}
```
### Registration by Configuration
```csharp
// In Program.cs
var authOptions = builder.Configuration.GetSection("Auth").Get<AuthOptions>();
if (authOptions?.UseFakeAuth == true)
{
builder.Services.AddScoped<IAuthService, FakeAuthService>();
}
else
{
builder.Services.AddScoped<IAuthService, LdapAuthService>();
}
```
## Porting Strategy
The legacy Dapper queries in `LotFinderDB*.cs`, `JDE*.cs`, and `CMS*.cs` port with minimal changes:
- Update namespaces (`System.Data.SqlClient` to `Microsoft.Data.SqlClient`)
- Adapt CMS queries from Sybase to Oracle syntax
- Use async/await consistently
## Related Documentation
- [Solution Structure](./SolutionStructure.md)
- [Data Flow](./DataFlow.md)
- [Testing](./Testing.md)