Files
ECMJobRunner/ECMJobRunner.Application
TekH dd9e6a710b feat(history): add ProfileHistory creation with job execution results
TriggeringProfileJobCommand:
- Add RecActionResult property to store ReC action execution results
- Convert handler to primary constructor with ISender injection
- Create ProfileHistory after successful job execution
- Include detailed execution metrics in history (TotalActionCount, ActionExceptionCount, BatchId)
- Add required using statements for ProfileHistories and ValueObjects

ProfileWorkerOptionsValidator:
- Fix XML documentation reference to ValidateOptionsResult.Fail(string)
2026-08-04 16:34:30 +02:00
..

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 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:

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

{
  "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:

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:

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:

  1. ISQLExecutor: SQL query execution and DTO mapping

    public interface ISQLExecutor
    {
        Task<TResult?> ExecuteQueryAsync<TResult>(string sql, CancellationToken cancellationToken = default);
    }
    
  2. IProfileSqlJobRepository: Repository for ProfileSqlJob entities

    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:

    new DEXJobException("SQL Main Query", batchId, sqlQuery, innerException)
    
  2. 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 execution
  • IOptions<DexJobOptions> - for configuration
  • ReCClient - for HTTP requests
  • IProfileSqlJobRepository - 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