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:
39
ECMJobRunner.Application/DependencyInjection.cs
Normal file
39
ECMJobRunner.Application/DependencyInjection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'">
|
||||
|
||||
373
ECMJobRunner.Application/README.md
Normal file
373
ECMJobRunner.Application/README.md
Normal 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
|
||||
Reference in New Issue
Block a user