- Add SectionName constant to DexJobOptions
- Update placeholder pattern to {#INT#BATCH_ID}
- Add DexJob configuration section to appsettings.json
- Add IConfiguration parameter to DependencyInjection for options binding
- Add Microsoft.Extensions.Options.ConfigurationExtensions package for .NET Framework 4.8
- Improve code documentation and move class into namespace
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:
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 executeBatchId(string): Unique batch identifier
Execution Pipeline:
- MainQueryExecutionBehavior: Executes main SQL query with batch ID placeholder replacement
- CheckQueryExecutionBehavior: Validates execution with return value check (> 0)
- ReCRequestExecutionBehavior: Invokes ReC API with batch ID reference
Handler:
- Empty handler - all logic delegated to pipeline behaviors
Usage:
var command = new TriggeringDEXJobCommand
{
Job = profileSqlJob,
BatchId = "20260711143025123456"
};
await mediator.Send(command);
Pipeline Behaviors
Execution Order
MainQueryExecutionBehavior<,>- SQL main query executionCheckQueryExecutionBehavior<,>- SQL check query validationReCRequestExecutionBehavior<,>- ReC HTTP request
Each behavior:
- Checks if request is
TriggeringDEXJobCommand - Executes its stage logic
- Calls
next()to continue pipeline
Configuration
appsettings.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 configurationPlaceholders(PlaceHolderOptions): Placeholder replacement configuration
DexJobErrorHandlingOptions
Hierarchical error handling per stage.
Properties:
MainQuery(SqlQueryErrorHandlingOptions): Main query error handlingCheckQuery(SqlQueryErrorHandlingOptions): Check query error handlingReCRequest(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 executionStop: ThrowDEXJobException
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 configurationPattern(string): Regex pattern (default:#INT#BATCH_ID)RegexOptions(RegexOptions): Regex options (default:IgnoreCase)
Expected Query Results
Main Query (SqlMainQuery)
Expected Result:
ReturnValueshould be null- If not null: throws
DEXJobException(whenOnUnexpectedResult=Stop)
Example SQL:
INSERT INTO TBJR_OUT_PROFILE_HISTORY (BatchId, ProfileId, CreatedAt)
VALUES ('#INT#BATCH_ID', 123, GETDATE())
Check Query (SqlCheckQuery)
Expected Result:
ReturnValueshould be > 0- If ≤ 0: throws
DEXJobException(whenOnUnexpectedResult=Stop)
Example 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:
INSERT INTO Table (BatchId) VALUES ('#INT#BATCH_ID')
After:
INSERT INTO Table (BatchId) VALUES ('20260711143025123456')
Dependency Injection
Setup in Startup.cs / Program.cs
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:
-
ISQLExecutor: SQL query execution and DTO mappingpublic interface ISQLExecutor { Task<TResult?> ExecuteQueryAsync<TResult>(string sql, CancellationToken cancellationToken = default); } -
IProfileSqlJobRepository: Repository for ProfileSqlJob entitiespublic interface IProfileSqlJobRepository : IRepository { Task<IEnumerable<ProfileSqlJob>> FindAsync(Expression<Func<ProfileSqlJob, bool>> predicate, CancellationToken cancellationToken); } -
ReCClient: ReC HTTP client (fromReC.ClientNuGet package)
Exception Handling
DEXJobException
Custom exception thrown when DEX job operations fail.
Properties:
QueryName(string): Name of the query that failedBatchId(string): Batch ID associated with the operationSqlQuery(string?): SQL query that was executed (nullable)Message(string): Formatted error message with query details
Constructors:
-
With inner exception:
new DEXJobException("SQL Main Query", batchId, sqlQuery, innerException) -
With reason message:
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:
#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 executionIOptions<DexJobOptions>- for configurationReCClient- for HTTP requestsIProfileSqlJobRepository- for data access
Example test structure:
[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:
{
"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