# 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 GetByIdAsync(int id) { using var connection = new SqlConnection(_connectionString); return await connection.QuerySingleOrDefaultAsync( "SELECT * FROM Search WHERE Id = @Id", new { Id = id }); } public async Task CreateAsync(Search search) { using var connection = new SqlConnection(_connectionString); return await connection.ExecuteScalarAsync( @"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> GetWorkOrdersAsync(DateTime since); Task> GetLotsAsync(DateTime since); Task> GetItemsAsync(); // ... other data retrieval methods } public interface ICmsDataSource { Task> 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> GetWorkOrdersAsync(DateTime since) { using var connection = new OracleConnection(_connectionString); return await connection.QueryAsync( "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> GetWorkOrdersAsync(DateTime since) { var filePath = Path.Combine(_dataDirectory, "workorders.json"); var json = await File.ReadAllTextAsync(filePath); var allOrders = JsonSerializer.Deserialize>(json); return allOrders.Where(wo => wo.UpdateDate >= since); } } ``` ### Registration by Environment ```csharp // In Program.cs if (builder.Environment.IsDevelopment()) { builder.Services.AddScoped(); builder.Services.AddScoped(); } else { builder.Services.AddScoped(); builder.Services.AddScoped(); } ``` 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 AuthenticateAsync(string username, string password); Task 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 options) { _options = options.Value; } public async Task 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 options) { _options = options.Value; } public Task 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 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(); if (authOptions?.UseFakeAuth == true) { builder.Services.AddScoped(); } else { builder.Services.AddScoped(); } ``` ## 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)