- Implement DependencyInjection.cs with AddApplication() extension
- Register MediatR with assembly scanning
- Register pipeline behaviors in execution order:
1. CheckQueryExecutionBehavior
2. MainQueryExecutionBehavior
3. ReCRequestExecutionBehavior
- Conditional package versions (MediatR 9.0.0/12.4.1)
- Add comprehensive README.md
- Architecture overview & CQRS pipeline flow
- Configuration guide (DexJobOptions, ErrorAction)
- DI setup examples (AddApplication, AddInfrastructure)
- Exception handling patterns
- Multi-targeting notes & troubleshooting
- Update project file with MediatR.Extensions.DependencyInjection
374 lines
11 KiB
Markdown
374 lines
11 KiB
Markdown
# 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<DexJobOptions>(configuration.GetSection("DexJob"));
|
|
|
|
// Register dependencies
|
|
services.AddSingleton<ISQLExecutor, SqlExecutor>(); // Your implementation
|
|
services.AddSingleton<ReCClient>(); // 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<TResult?> ExecuteQueryAsync<TResult>(string sql, CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
2. **`IProfileSqlJobRepository`**: Repository for ProfileSqlJob entities
|
|
```csharp
|
|
public interface IProfileSqlJobRepository : IRepository
|
|
{
|
|
Task<IEnumerable<ProfileSqlJob>> FindAsync(Expression<Func<ProfileSqlJob, bool>> 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<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
|
|
#else
|
|
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
|
#endif
|
|
```
|
|
|
|
## Testing
|
|
|
|
Unit tests should mock:
|
|
- `ISQLExecutor` - for SQL query execution
|
|
- `IOptions<DexJobOptions>` - 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<IProfileSqlJobRepository>();
|
|
var mockSender = new Mock<ISender>();
|
|
mockRepo.Setup(r => r.FindAsync(It.IsAny<Expression<Func<ProfileSqlJob, bool>>>(), It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(new List<ProfileSqlJob> { 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<TriggeringDEXJobCommand>(), It.IsAny<CancellationToken>()), 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
|