diff --git a/ECMJobRunner.Application/DependencyInjection.cs b/ECMJobRunner.Application/DependencyInjection.cs new file mode 100644 index 0000000..060e1b9 --- /dev/null +++ b/ECMJobRunner.Application/DependencyInjection.cs @@ -0,0 +1,39 @@ +using ECMJobRunner.Application.Behaviors; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace ECMJobRunner.Application +{ + /// + /// Extension methods for configuring Application layer services + /// + public static class DependencyInjection + { + /// + /// Adds Application layer services to the dependency injection container + /// Registers MediatR, pipeline behaviors, and AutoMapper + /// + /// The service collection + /// The service collection for chaining + public static IServiceCollection AddApplication(this IServiceCollection services) + { + var assembly = Assembly.GetExecutingAssembly(); + + // Register MediatR with all handlers from this assembly +#if NET48 + services.AddMediatR(assembly); +#else + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly)); +#endif + + // Register pipeline behaviors in execution order + // Order matters: MainQuery -> CheckQuery -> ReCRequest + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(MainQueryExecutionBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CheckQueryExecutionBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ReCRequestExecutionBehavior<,>)); + + return services; + } + } +} diff --git a/ECMJobRunner.Application/ECMJobRunner.Application.csproj b/ECMJobRunner.Application/ECMJobRunner.Application.csproj index 557368e..544e80d 100644 --- a/ECMJobRunner.Application/ECMJobRunner.Application.csproj +++ b/ECMJobRunner.Application/ECMJobRunner.Application.csproj @@ -30,6 +30,7 @@ + diff --git a/ECMJobRunner.Application/README.md b/ECMJobRunner.Application/README.md new file mode 100644 index 0000000..352b82c --- /dev/null +++ b/ECMJobRunner.Application/README.md @@ -0,0 +1,373 @@ +# 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