# Data Access Design ## Overview This document describes the architecture and implementation approach for the data access layer, including repository interfaces, connection factory, exception handling, and service registration. ## Architecture ### Repository Pattern ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Service Layer │ │ (SearchProcessor, DataSyncService, etc.) │ └─────────────────────────────────────────────────────────────────────┘ │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ILotFinder │ │IJde │ │ICms │ │Repository │ │Repository │ │Repository │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ ┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐ │LotFinder │ │Jde │ │Cms │ │Repository │ │Repository │ │Repository │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ └────────────────┼────────────────┘ ▼ ┌─────────────────────────┐ │ IDbConnectionFactory │ └─────────────┬───────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ SqlConnection │ │OracleConnection│ │OracleConnection│ │ (LotFinderDB) │ │ (JDE/Stage) │ │ (CMS) │ └───────────────┘ └───────────────┘ └───────────────┘ ``` ### Project Structure ``` NEW/src/JdeScoping.DataAccess/ ├── Exceptions/ │ ├── DataAccessException.cs │ ├── ConnectionException.cs │ ├── QueryException.cs │ └── DataAccessTimeoutException.cs ├── Interfaces/ │ ├── IDbConnectionFactory.cs │ ├── ILotFinderRepository.cs │ ├── IJdeRepository.cs │ └── ICmsRepository.cs ├── Repositories/ │ ├── LotFinderRepository.cs │ ├── JdeRepository.cs │ └── CmsRepository.cs ├── Queries/ │ ├── LotFinderQueries.cs (const string SQL statements) │ ├── JdeQueries.cs (const string SQL statements) │ └── CmsQueries.cs (const string SQL statements) ├── Configuration/ │ └── DataAccessOptions.cs ├── DbConnectionFactory.cs ├── ServiceCollectionExtensions.cs └── JdeScoping.DataAccess.csproj ``` ## Connection Factory ### IDbConnectionFactory Interface ```csharp public interface IDbConnectionFactory { Task CreateLotFinderConnectionAsync(CancellationToken ct = default); Task CreateJdeConnectionAsync(CancellationToken ct = default); Task CreateJdeStageConnectionAsync(CancellationToken ct = default); Task CreateCmsConnectionAsync(CancellationToken ct = default); } ``` ### Implementation Details - Registered as **singleton** (stateless, creates new connections) - Connection strings read from `IConfiguration["ConnectionStrings:*"]` - Secrets retrieved from .NET Secret Manager (local) or Azure Key Vault (production) - Connections opened asynchronously before returning - Caller responsible for disposing returned connections ### Connection String Keys | Key | Database | Driver | |-----|----------|--------| | `ConnectionStrings:LotFinderDB` | SQL Server cache | Microsoft.Data.SqlClient | | `ConnectionStrings:JDE` | JDE Oracle (PRODDTA) | Oracle.ManagedDataAccess.Core | | `ConnectionStrings:JDEStage` | JDE Oracle (JDESTAGE) | Oracle.ManagedDataAccess.Core | | `ConnectionStrings:CMS` | CMS Oracle (INFODBA) | Oracle.ManagedDataAccess.Core | ## Repository Interfaces ### Registration Lifetimes | Interface | Lifetime | Rationale | |-----------|----------|-----------| | `IDbConnectionFactory` | Singleton | Stateless, creates new connections | | `ILotFinderRepository` | Scoped | Per-request, uses scoped DbContext pattern | | `IJdeRepository` | Scoped | Per-request, creates connections as needed | | `ICmsRepository` | Scoped | Per-request, creates connections as needed | ### Constructor Dependencies All repository implementations receive: - `IDbConnectionFactory` - For database connections - `ILogger` - For structured logging - `IOptions` - For configurable timeouts and schemas ## Async Streaming Pattern ### IAsyncEnumerable for Large Datasets JDE and CMS repositories return `IAsyncEnumerable` for all collection queries: ```csharp public async IAsyncEnumerable GetWorkOrdersAsync( DateTime? lastUpdateDT = null, [EnumeratorCancellation] CancellationToken ct = default) { await using var connection = await _connectionFactory.CreateJdeConnectionAsync(ct); var sql = lastUpdateDT.HasValue ? JdeQueries.SQL_GET_WORKORDERS_FILTERED : JdeQueries.SQL_GET_WORKORDERS; var parameters = BuildWorkOrderParameters(lastUpdateDT); await foreach (var workOrder in connection.QueryUnbufferedAsync( sql, parameters, commandTimeout: _options.Value.DefaultTimeoutSeconds) .WithCancellation(ct)) { yield return workOrder; } } ``` ### Benefits - Memory efficient: rows streamed one at a time - Cancellation support: stops iteration on cancellation - Backpressure: consumer controls iteration speed - Compatible with `await foreach` syntax ## Query Management ### SQL Query Storage SQL queries stored as compile-time constants in static classes: ```csharp public static class JdeQueries { public const string SQL_GET_WORKORDERS = @" SELECT wo.WADOCO AS WorkOrderNumber, TRIM(wo.WAMMCU) AS BranchCode, -- ... rest of query FROM {ProductionSchema}.F4801 wo"; public const string SQL_GET_WORKORDERS_FILTERED = SQL_GET_WORKORDERS + @" WHERE (wo.WAUPMJ > :dateUpdated OR (wo.WAUPMJ = :dateUpdated AND wo.WATDAY >= :timeUpdated))"; } ``` ### Schema Placeholder Replacement Schema names replaced at runtime from `DataAccessOptions`: ```csharp private string ApplySchemaPlaceholders(string sql) { return sql .Replace("{ProductionSchema}", _options.Value.ProductionSchema) .Replace("{ArchiveSchema}", _options.Value.ArchiveSchema) .Replace("{StageSchema}", _options.Value.StageSchema); } ``` ## Exception Handling ### Exception Hierarchy ``` Exception └── DataAccessException (base for all data access errors) ├── ConnectionException (connection failures) ├── QueryException (query execution failures) └── DataAccessTimeoutException (timeout errors) ``` ### Exception Properties ```csharp public class DataAccessException : Exception { public string? Operation { get; } // Method name (e.g., "GetWorkOrdersAsync") public string? Repository { get; } // Repository name (e.g., "JdeRepository") } public class ConnectionException : DataAccessException { public string? DataSource { get; } // Database identifier (e.g., "JDE", "CMS") } public class QueryException : DataAccessException { public string? QueryName { get; } // Query identifier (e.g., "SQL_GET_WORKORDERS") } public class DataAccessTimeoutException : DataAccessException { public int TimeoutSeconds { get; } // Configured timeout value } ``` ### Logging Pattern All exceptions logged at throw site with scope context: ```csharp try { // Execute query } catch (OracleException ex) when (ex.Number == 1017) // Invalid credentials { using (_logger.BeginScope(new Dictionary { ["DataSource"] = "JDE", ["Operation"] = "GetWorkOrdersAsync" })) { _logger.LogError(ex, "Failed to connect to JDE Oracle database"); } throw new ConnectionException("JDE: Failed to connect to database", "JDE", ex); } ``` ## Configuration Options ### DataAccessOptions Class ```csharp public class DataAccessOptions { public const string SectionName = "DataAccess"; public int DefaultTimeoutSeconds { get; set; } = 600; public int LotUsageTimeoutSeconds { get; set; } = 999999; public int MisDataTimeoutSeconds { get; set; } = 60000; public int RebuildIndexTimeoutSeconds { get; set; } = 600; public string ProductionSchema { get; set; } = "PRODDTA"; public string ArchiveSchema { get; set; } = "ARCDTAPD"; public string StageSchema { get; set; } = "JDESTAGE"; } ``` ### Configuration Binding ```json { "DataAccess": { "DefaultTimeoutSeconds": 600, "LotUsageTimeoutSeconds": 999999, "MisDataTimeoutSeconds": 60000, "RebuildIndexTimeoutSeconds": 600, "ProductionSchema": "PRODDTA", "ArchiveSchema": "ARCDTAPD", "StageSchema": "JDESTAGE" } } ``` ## Service Registration ### AddDataAccess Extension Method ```csharp public static class ServiceCollectionExtensions { public static IServiceCollection AddDataAccess( this IServiceCollection services, IConfiguration configuration) { // Bind options services.Configure( configuration.GetSection(DataAccessOptions.SectionName)); // Register connection factory (singleton) services.AddSingleton(); // Register repositories (scoped) services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; } } ``` ## SQL Injection Prevention ### RebuildIndicesAsync Whitelist Table names validated against explicit whitelist: ```csharp private static readonly HashSet ValidTableNames = new(StringComparer.OrdinalIgnoreCase) { "Branch", "DataUpdate", "FunctionCode", "Item", "JdeUser", "Lot", "LotLocation", "LotUsage_Curr", "LotUsage_Hist", "MisData", "OrgHierarchy", "ProfitCenter", "RouteMaster", "Search", "StatusCode", "WorkCenter", "WorkOrder_Curr", "WorkOrder_Hist", "WorkOrderComponent_Curr", "WorkOrderComponent_Hist", "WorkOrderRouting", "WorkOrderStep_Curr", "WorkOrderStep_Hist", "WorkOrderTime_Curr", "WorkOrderTime_Hist" }; public async Task RebuildIndicesAsync(string tableName, CancellationToken ct = default) { if (!ValidTableNames.Contains(tableName)) { throw new ArgumentException($"Invalid table name: {tableName}", nameof(tableName)); } await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); var sql = $"ALTER INDEX ALL ON [{tableName}] REBUILD WITH (FILLFACTOR = 95)"; await connection.ExecuteAsync(sql, commandTimeout: _options.Value.RebuildIndexTimeoutSeconds); } ``` ## NuGet Dependencies ### Required Packages ```xml ``` ## Testing Strategy ### Unit Tests - Mock `IDbConnectionFactory` to return mock connections - Use in-memory test data for query result mapping - Verify exception handling scenarios - Test cancellation token propagation ### Integration Tests - Use Docker containers for SQL Server and Oracle - Test actual query execution - Verify streaming behavior for large datasets - Test connection pooling under load