refactor(application): redesign exception hierarchy with flexible base class

- Replace DEXJobException with JobException base class
  - Flexible params-based detail collection
  - Automatic message formatting with visual separators
  - Optional null-handling for contextual details
- Add JobHttpException for HTTP client failures
  - Properties: ClientLibrary, ClientMethod
  - Use case: ReC API, REST requests
- Add JobSqlException for SQL query failures
  - Property: Query (virtual for customization)
  - Use case: Main/Check query execution
- Comprehensive XML documentation for all exception classes
This commit is contained in:
2026-07-11 16:44:10 +02:00
parent 26ea1db09f
commit 222a5e24bf
4 changed files with 183 additions and 68 deletions

View File

@@ -1,68 +0,0 @@
using System;
namespace ECMJobRunner.Application.Common.Exceptions
{
/// <summary>
/// Exception thrown when a DEX job operation fails
/// </summary>
public class DEXJobException : Exception
{
/// <summary>
/// Initializes a new instance with an inner exception
/// </summary>
/// <param name="queryName">Name of the query that failed</param>
/// <param name="batchId">Batch ID associated with the operation</param>
/// <param name="sqlQuery">SQL query that was executed (nullable)</param>
/// <param name="innerException">The inner exception that caused the failure</param>
public DEXJobException(string queryName, string batchId, string? sqlQuery, Exception innerException)
: base(
$"[SQL Execution Failure] Query '{queryName}' could not be completed for Batch '{batchId}'." + Environment.NewLine +
"─────────────────────────────────────────" + Environment.NewLine +
" Query:" + (sqlQuery is null ? string.Empty : Environment.NewLine + $" {sqlQuery}") + Environment.NewLine + Environment.NewLine +
" Root Cause:" + Environment.NewLine +
$" {innerException?.Message}" + Environment.NewLine +
"─────────────────────────────────────────",
innerException)
{
SqlQuery = sqlQuery;
BatchId = batchId;
QueryName = queryName;
}
/// <summary>
/// Initializes a new instance with a reason message
/// </summary>
/// <param name="queryName">Name of the query that failed</param>
/// <param name="batchId">Batch ID associated with the operation</param>
/// <param name="sqlQuery">SQL query that was executed (nullable)</param>
/// <param name="reason">Reason for the failure</param>
public DEXJobException(string queryName, string batchId, string? sqlQuery, string reason)
: base(
$"[SQL Execution Failure] Query '{queryName}' could not be completed for Batch '{batchId}'." + Environment.NewLine +
"─────────────────────────────────────────" + Environment.NewLine +
" Query:" + (sqlQuery is null ? string.Empty : Environment.NewLine + $" {sqlQuery}") + Environment.NewLine + Environment.NewLine +
" Reason:" + Environment.NewLine +
$" {reason}" + Environment.NewLine +
"─────────────────────────────────────────")
{
SqlQuery = sqlQuery;
BatchId = batchId;
QueryName = queryName;
}
/// <summary>
/// Gets the SQL query that failed (if available)
/// </summary>
public string? SqlQuery { get; }
/// <summary>
/// Gets the batch ID associated with the operation
/// </summary>
public string BatchId { get; }
/// <summary>
/// Gets the name of the query that failed
/// </summary>
public string QueryName { get; }
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
namespace ECMJobRunner.Application.Common.Exceptions
{
/// <summary>
/// Base exception class for job execution failures
/// Provides a flexible structure for capturing job context and detailed error information
/// </summary>
public class JobException : Exception
{
/// <summary>
/// Initializes a new instance of JobException with detailed context information
/// </summary>
/// <param name="jobName">Name of the job that failed (e.g., "SQL Main Query", "ReC Request")</param>
/// <param name="processName">Name of the process/stage being executed (e.g., "MainQueryExecution", "CheckQueryValidation")</param>
/// <param name="batchId">Unique batch identifier for tracking the execution</param>
/// <param name="reason">Human-readable reason for the failure (nullable)</param>
/// <param name="innerException">The underlying exception that caused the failure (nullable)</param>
/// <param name="details">Additional contextual details as name-value pairs with optional null-handling</param>
/// <remarks>
/// The details parameter accepts tuples with:
/// - Name: Display name of the detail
/// - Value: String value of the detail (nullable)
/// - IgnoreIfNull: If true, the detail is omitted from the message when value is null
/// </remarks>
public JobException(string jobName, string processName, string batchId, string? reason, Exception? innerException, params (string Name, string? Value, bool IgnoreIfNull)[] details)
: base(
Message(jobName,
[
("Process Name", processName, false),
("Batch Id", batchId, false),
("Reason", reason, true),
..details
]),
innerException)
{
JobName = jobName;
ProcessName = processName;
BatchId = batchId;
}
/// <summary>
/// Gets the name of the job that failed
/// </summary>
public string JobName { get; }
/// <summary>
/// Gets the name of the process/stage that was being executed when the failure occurred
/// </summary>
public string ProcessName { get; }
/// <summary>
/// Gets the unique batch identifier for tracking the execution
/// </summary>
public string BatchId { get; }
/// <summary>
/// Generates a formatted error message with job context and details
/// </summary>
/// <param name="jobName">Name of the job that failed</param>
/// <param name="details">Collection of name-value pairs with optional null-handling</param>
/// <returns>Formatted multi-line error message with visual separators</returns>
/// <remarks>
/// Message format:
/// <code>
/// {jobName} could not be completed.
/// ─────────────────────────────────────────
/// Process Name: {processName}
/// Batch Id: {batchId}
/// {additional details...}
/// ─────────────────────────────────────────
/// </code>
/// Details with IgnoreIfNull=true are omitted when their value is null.
/// </remarks>
internal static string Message(string jobName, IEnumerable<(string Name, string? Value, bool IgnoreIfNull)> details)
{
var message = new System.Text.StringBuilder();
message.AppendLine($"{jobName} could not be completed.");
message.AppendLine("─────────────────────────────────────────");
foreach (var (name, value, ignoreNullValue) in details)
{
if (ignoreNullValue && value is null)
continue;
message.AppendLine($" {name}: {value}");
}
message.AppendLine("─────────────────────────────────────────");
return message.ToString();
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
namespace ECMJobRunner.Application.Common.Exceptions
{
/// <summary>
/// Exception for HTTP client-related job failures (e.g., ReC API calls, REST requests)
/// Extends JobException with client library and method context
/// </summary>
public class JobHttpException : JobException
{
/// <summary>
/// Initializes a new instance of JobHttpException with HTTP client context
/// </summary>
/// <param name="jobName">Name of the job that failed (e.g., "ReC Request", "API Call")</param>
/// <param name="processName">Name of the process/stage being executed</param>
/// <param name="batchId">Unique batch identifier for tracking</param>
/// <param name="reason">Human-readable reason for the failure (nullable)</param>
/// <param name="clientLibrary">Name of the HTTP client library used (e.g., "ReC.Client", "HttpClient") (nullable)</param>
/// <param name="clientMethod">Name of the client method that failed (e.g., "ExecuteAsync", "PostAsync") (nullable)</param>
/// <param name="innerException">The underlying exception that caused the failure (nullable)</param>
/// <remarks>
/// Use this exception for HTTP-related failures such as:
/// - ReC API request failures
/// - REST API communication errors
/// - HTTP client timeout/network issues
/// - Authentication/authorization failures
/// </remarks>
public JobHttpException(string jobName, string processName, string batchId, string? reason, string? clientLibrary, string? clientMethod, Exception? innerException) : base(jobName, processName, batchId, reason, innerException,
("Client Library", clientLibrary, true),
("Client Method", clientMethod, true))
{
ClientLibrary = clientLibrary;
ClientMethod = clientMethod;
}
/// <summary>
/// Gets the name of the HTTP client library that was used (e.g., "ReC.Client", "HttpClient")
/// </summary>
public string? ClientLibrary { get; }
/// <summary>
/// Gets the name of the client method that failed (e.g., "ExecuteAsync", "PostAsync")
/// </summary>
public string? ClientMethod { get; }
}
}

View File

@@ -0,0 +1,46 @@
using System;
namespace ECMJobRunner.Application.Common.Exceptions
{
/// <summary>
/// Exception for SQL query execution failures
/// Extends JobException with SQL query context for debugging
/// </summary>
public class JobSqlException : JobException
{
/// <summary>
/// Initializes a new instance of JobSqlException with SQL query context
/// </summary>
/// <param name="jobName">Name of the job that failed (e.g., "Main Query Execution", "Check Query")</param>
/// <param name="processName">Name of the process/stage being executed</param>
/// <param name="batchId">Unique batch identifier for tracking</param>
/// <param name="reason">Human-readable reason for the failure (nullable)</param>
/// <param name="query">The SQL query that failed (nullable, for debugging purposes)</param>
/// <param name="innerException">The underlying SQL exception (nullable)</param>
/// <remarks>
/// Use this exception for SQL-related failures such as:
/// - Main query execution errors
/// - Check query validation failures
/// - Database connection issues
/// - SQL syntax errors
/// - Query timeout exceptions
///
/// The Query property can be logged for debugging but should be handled carefully
/// to avoid exposing sensitive data in production logs.
/// </remarks>
public JobSqlException(string jobName, string processName, string batchId, string? reason, string? query, Exception? innerException) : base(jobName, processName, batchId, reason, innerException,
("Query", query, true))
{
Query = query;
}
/// <summary>
/// Gets the SQL query that failed (nullable)
/// </summary>
/// <remarks>
/// This property is marked as virtual to allow derived classes to customize query handling
/// (e.g., sanitizing sensitive data, truncating long queries)
/// </remarks>
public virtual string? Query { get; }
}
}