# ECMJobRunner.Application - DEX Job Integration ## Overview This document describes the **DEX Job triggering system** implemented in `ECMJobRunner.Application` using the **CQRS pattern with MediatR** and **Pipeline Behaviors**. ## Architecture ### Commands #### `TriggeringDEXJobBatchCommand` Orchestrates batch job execution for a profile. **Properties:** - `ProfileId` (int): Profile ID to trigger all associated SQL jobs **Handler:** - Creates a unique 20-character timestamp-based batch ID - Retrieves all SQL jobs for the profile - Executes each job sequentially using `TriggeringDEXJobCommand` **Usage:** ```csharp var command = new TriggeringDEXJobBatchCommand { ProfileId = 123 }; await mediator.Send(command); ``` #### `TriggeringDEXJobCommand` Executes a single DEX job with three-stage pipeline. **Properties:** - `Job` (ProfileSqlJob): The SQL job to execute - `BatchId` (string): Unique batch identifier **Execution Pipeline:** 1. **MainQueryExecutionBehavior**: Executes main SQL query with batch ID placeholder replacement 2. **CheckQueryExecutionBehavior**: Validates execution with return value check (> 0) 3. **ReCRequestExecutionBehavior**: Invokes ReC API with batch ID reference **Handler:** - Empty handler - all logic delegated to pipeline behaviors **Usage:** ```csharp var command = new TriggeringDEXJobCommand { Job = profileSqlJob, BatchId = "20260711143025123456" }; await mediator.Send(command); ``` ### Pipeline Behaviors #### Execution Order 1. `MainQueryExecutionBehavior<,>` - SQL main query execution 2. `CheckQueryExecutionBehavior<,>` - SQL check query validation 3. `ReCRequestExecutionBehavior<,>` - ReC HTTP request Each behavior: - Checks if request is `TriggeringDEXJobCommand` - Executes its stage logic - Calls `next()` to continue pipeline ## Configuration ### appsettings.json ```json { "DexJob": { "Error": { "MainQuery": { "OnExecution": "Stop", "IfNullOrWhiteSpace": "Ignore", "OnUnexpectedResult": "Stop" }, "CheckQuery": { "OnExecution": "Stop", "IfNullOrWhiteSpace": "Ignore", "OnUnexpectedResult": "Stop" }, "ReCRequest": { "OnSending": "Stop" } }, "Placeholders": { "BatchId": { "Pattern": "#INT#BATCH_ID", "RegexOptions": "IgnoreCase" } } } } ``` ### Configuration Options #### `DexJobOptions` Root configuration object for DEX job execution. **Properties:** - `Error` (DexJobErrorHandlingOptions): Error handling configuration - `Placeholders` (PlaceHolderOptions): Placeholder replacement configuration #### `DexJobErrorHandlingOptions` Hierarchical error handling per stage. **Properties:** - `MainQuery` (SqlQueryErrorHandlingOptions): Main query error handling - `CheckQuery` (SqlQueryErrorHandlingOptions): Check query error handling - `ReCRequest` (HttpRequestErrorHandlingOptions): ReC request error handling #### `SqlQueryErrorHandlingOptions` Error handling for SQL query execution. **Properties:** - `OnExecution` (ErrorAction): Action when query execution fails (default: `Stop`) - `IfNullOrWhiteSpace` (ErrorAction): Action when query is null/empty (default: `Ignore`) - `OnUnexpectedResult` (ErrorAction): Action when result is unexpected (default: `Stop`) **Error Actions:** - `Ignore`: Continue execution - `Stop`: Throw `DEXJobException` #### `HttpRequestErrorHandlingOptions` Error handling for HTTP requests. **Properties:** - `OnSending` (ErrorAction): Action when HTTP request fails (default: `Stop`) #### `PlaceHolderOptions` Configuration for dynamic placeholder replacement. **Properties:** - `BatchId` (PlaceHolder): Batch ID placeholder configuration - `Pattern` (string): Regex pattern (default: `#INT#BATCH_ID`) - `RegexOptions` (RegexOptions): Regex options (default: `IgnoreCase`) ### Expected Query Results #### Main Query (`SqlMainQuery`) **Expected Result:** - `ReturnValue` should be **null** - If not null: throws `DEXJobException` (when `OnUnexpectedResult` = `Stop`) **Example SQL:** ```sql INSERT INTO TBJR_OUT_PROFILE_HISTORY (BatchId, ProfileId, CreatedAt) VALUES ('#INT#BATCH_ID', 123, GETDATE()) ``` #### Check Query (`SqlCheckQuery`) **Expected Result:** - `ReturnValue` should be **> 0** - If ≤ 0: throws `DEXJobException` (when `OnUnexpectedResult` = `Stop`) **Example SQL:** ```sql SELECT COUNT(*) AS [Return Value] FROM TBJR_OUT_PROFILE_HISTORY WHERE BatchId = '#INT#BATCH_ID' ``` ### Placeholder Replacement The `#INT#BATCH_ID` placeholder in SQL queries is replaced with the actual batch ID using regex: **Before:** ```sql INSERT INTO Table (BatchId) VALUES ('#INT#BATCH_ID') ``` **After:** ```sql INSERT INTO Table (BatchId) VALUES ('20260711143025123456') ``` ## Dependency Injection ### Setup in Startup.cs / Program.cs ```csharp using ECMJobRunner.Application; using ECMJobRunner.Application.Common.Options; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; // Add Application layer services services.AddApplication(); // Configure DexJobOptions from appsettings.json services.Configure(configuration.GetSection("DexJob")); // Register dependencies services.AddSingleton(); // Your implementation services.AddSingleton(); // ReC client configuration ``` ### Required Dependencies The following interfaces must be implemented in your Infrastructure layer: 1. **`ISQLExecutor`**: SQL query execution and DTO mapping ```csharp public interface ISQLExecutor { Task ExecuteQueryAsync(string sql, CancellationToken cancellationToken = default); } ``` 2. **`IProfileSqlJobRepository`**: Repository for ProfileSqlJob entities ```csharp public interface IProfileSqlJobRepository : IRepository { Task> FindAsync(Expression> predicate, CancellationToken cancellationToken); } ``` 3. **`ReCClient`**: ReC HTTP client (from `ReC.Client` NuGet package) ## Exception Handling ### `DEXJobException` Custom exception thrown when DEX job operations fail. **Properties:** - `QueryName` (string): Name of the query that failed - `BatchId` (string): Batch ID associated with the operation - `SqlQuery` (string?): SQL query that was executed (nullable) - `Message` (string): Formatted error message with query details **Constructors:** 1. With inner exception: ```csharp new DEXJobException("SQL Main Query", batchId, sqlQuery, innerException) ``` 2. With reason message: ```csharp new DEXJobException("SQL Check Query", batchId, sqlQuery, "Check Query returned nothing.") ``` **Example Error Message:** ``` [SQL Execution Failure] Query 'SQL Main Query' could not be completed for Batch '20260711143025123456'. ───────────────────────────────────────── Query: INSERT INTO Table (BatchId) VALUES ('#INT#BATCH_ID') Root Cause: Timeout expired. The timeout period elapsed prior to completion of the operation. ───────────────────────────────────────── ``` ## Multi-Targeting Support The project targets: - **.NET Framework 4.8** (`net480`) - MediatR 9.0.0 - **.NET 8.0** (`net8.0`) - MediatR 12.4.1 Pipeline behaviors use conditional compilation for MediatR signature differences: ```csharp #if NET48 public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) #else public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) #endif ``` ## Testing Unit tests should mock: - `ISQLExecutor` - for SQL query execution - `IOptions` - for configuration - `ReCClient` - for HTTP requests - `IProfileSqlJobRepository` - for data access **Example test structure:** ```csharp [Fact] public async Task Handle_WithValidBatchCommand_ExecutesAllJobs() { // Arrange var mockRepo = new Mock(); var mockSender = new Mock(); mockRepo.Setup(r => r.FindAsync(It.IsAny>>(), It.IsAny())) .ReturnsAsync(new List { job1, job2 }); var handler = new TriggeringDEXJobBatchCommandHandler(mockRepo.Object, mockSender.Object); var command = new TriggeringDEXJobBatchCommand { ProfileId = 123 }; // Act await handler.Handle(command, CancellationToken.None); // Assert mockSender.Verify(s => s.Send(It.IsAny(), It.IsAny()), Times.Exactly(2)); } ``` ## Batch ID Format Batch IDs are 20-character timestamp strings: **Format:** `yyyyMMddHHmmssfffffff` (truncated to 20 chars) **Example:** `20260711143025123456` **Components:** - `yyyy`: Year (4 digits) - `MM`: Month (2 digits) - `dd`: Day (2 digits) - `HH`: Hour - 24-hour format (2 digits) - `mm`: Minute (2 digits) - `ss`: Second (2 digits) - `fffffff`: Fractions of a second / 100-nanoseconds (7 digits) - truncated to 6 digits ## Troubleshooting ### Common Issues #### 1. "Main Query returned nothing" **Cause:** Main query execution did not return any result. **Solution:** Check if SQL query is valid and returns expected structure with `[Return Value]` column. #### 2. "The query unexpectedly returned the value X. The expected value was null." **Cause:** Main query returned non-null value indicating an error. **Solution:** Check SQL query logic and ensure it returns NULL on success. #### 3. "Check Query returned nothing" **Cause:** Check query did not return any result. **Solution:** Verify check query syntax and ensure it returns a `[Return Value]` column. #### 4. "The query unexpectedly returned the value X. The expected value was any value greater than 0." **Cause:** Check query returned ≤ 0 indicating validation failure. **Solution:** Verify main query executed successfully and check query logic is correct. #### 5. "SQL Main Query is null or empty" **Cause:** `SqlMainQuery` property is null or whitespace. **Solution:** Set `Error.MainQuery.IfNullOrWhiteSpace = Ignore` in configuration if optional. ### Debug Configuration For detailed error information, set all error actions to `Stop`: ```json { "DexJob": { "Error": { "MainQuery": { "OnExecution": "Stop", "IfNullOrWhiteSpace": "Stop", "OnUnexpectedResult": "Stop" }, "CheckQuery": { "OnExecution": "Stop", "IfNullOrWhiteSpace": "Stop", "OnUnexpectedResult": "Stop" }, "ReCRequest": { "OnSending": "Stop" } } } } ``` ## Company Information **Author:** Digital Data GmbH **Copyright:** 2026 **Repository:** http://git.dd:3000/AppStd/ECMJobRunner.git