feat(application): add dependency injection configuration & documentation

- 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
This commit is contained in:
2026-07-11 16:44:39 +02:00
parent 63a16410ea
commit 5d2f128cc7
3 changed files with 413 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
using ECMJobRunner.Application.Behaviors;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
namespace ECMJobRunner.Application
{
/// <summary>
/// Extension methods for configuring Application layer services
/// </summary>
public static class DependencyInjection
{
/// <summary>
/// Adds Application layer services to the dependency injection container
/// Registers MediatR, pipeline behaviors, and AutoMapper
/// </summary>
/// <param name="services">The service collection</param>
/// <returns>The service collection for chaining</returns>
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;
}
}
}

View File

@@ -30,6 +30,7 @@
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.1" />
<!-- MediatR for .NET Framework 4.8 -->
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">

View File

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