Compare commits
22 Commits
8327c0fd0b
...
feat/dex-j
| Author | SHA1 | Date | |
|---|---|---|---|
| 612022862a | |||
| 94c2495515 | |||
| d7c8607fbf | |||
| 8d4c08fbec | |||
| bc9f234810 | |||
| 545648fe87 | |||
| dd9e6a710b | |||
| 5b67035a07 | |||
| 5b865c0442 | |||
| 111281ac08 | |||
| 2067ffdf2e | |||
| f75524f85d | |||
| 73db8fbd27 | |||
| 90916f6d03 | |||
| eb060aa54e | |||
| 45a7086c9a | |||
| 1110728741 | |||
| a698f8daae | |||
| afdeb8839c | |||
| dc2aaa245f | |||
| 2d7c54ceae | |||
| af3c8be133 |
@@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECMJobRunner.Tests", "ECMJo
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECMJobRunner.WebCron", "ECMJobRunner.WebCron\ECMJobRunner.WebCron.csproj", "{C60BC965-D293-EA64-B153-1941F0648DF4}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7704FD14-0546-4ABA-AA37-5EFA6BD7908D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -43,6 +47,13 @@ Global
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{CB94ADEF-59FE-4D7A-83EF-2D57CD325B8F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{0DC84EFF-0002-4A40-ADDE-D3FE3778D6AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{D97CD489-10D7-432C-9921-196F2E0505FF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{F64352B6-32BB-4BDE-90FD-FB77482D44E0} = {7704FD14-0546-4ABA-AA37-5EFA6BD7908D}
|
||||
{C60BC965-D293-EA64-B153-1941F0648DF4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {F4408662-65F4-4D6A-8E12-770DED292847}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -6,42 +6,31 @@ namespace ECMJobRunner.Application.Common.Exceptions
|
||||
/// Exception thrown when attempting to execute a job for an inactive profile
|
||||
/// Extends JobException with profile-specific context
|
||||
/// </summary>
|
||||
public class InactiveProfileException : JobException
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of InactiveProfileException
|
||||
/// </remarks>
|
||||
/// <param name="profileId">ID of the inactive profile</param>
|
||||
/// <param name="profileName">Name of the inactive profile (nullable)</param>
|
||||
/// <param name="batchId">Unique batch identifier for tracking</param>
|
||||
/// <remarks>
|
||||
/// Use this exception when:
|
||||
/// - Attempting to trigger DEX job batch for an inactive profile
|
||||
/// - Attempting to execute individual jobs from an inactive profile
|
||||
/// - Profile is deactivated during execution
|
||||
/// </remarks>
|
||||
public class InactiveProfileException(long profileId, string? profileName, string batchId) : JobException(
|
||||
profileId,
|
||||
jobName: "Profile Execution",
|
||||
processName: "Profile Active Status Validation",
|
||||
batchId: batchId,
|
||||
reason: "The profile is marked as inactive and cannot be executed",
|
||||
innerException: null,
|
||||
("Profile ID", profileId.ToString(), false),
|
||||
("Profile Name", profileName, true))
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of InactiveProfileException
|
||||
/// </summary>
|
||||
/// <param name="profileId">ID of the inactive profile</param>
|
||||
/// <param name="profileName">Name of the inactive profile (nullable)</param>
|
||||
/// <param name="batchId">Unique batch identifier for tracking</param>
|
||||
/// <remarks>
|
||||
/// Use this exception when:
|
||||
/// - Attempting to trigger DEX job batch for an inactive profile
|
||||
/// - Attempting to execute individual jobs from an inactive profile
|
||||
/// - Profile is deactivated during execution
|
||||
/// </remarks>
|
||||
public InactiveProfileException(long profileId, string? profileName, string batchId)
|
||||
: base(
|
||||
jobName: "Profile Execution",
|
||||
processName: "Profile Active Status Validation",
|
||||
batchId: batchId,
|
||||
reason: "The profile is marked as inactive and cannot be executed",
|
||||
innerException: null,
|
||||
("Profile ID", profileId.ToString(), false),
|
||||
("Profile Name", profileName, true))
|
||||
{
|
||||
ProfileId = profileId;
|
||||
ProfileName = profileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the inactive profile
|
||||
/// </summary>
|
||||
public long ProfileId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the inactive profile (nullable)
|
||||
/// </summary>
|
||||
public string? ProfileName { get; }
|
||||
public string? ProfileName { get; } = profileName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,53 +7,54 @@ namespace ECMJobRunner.Application.Common.Exceptions
|
||||
/// 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,
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of JobException with detailed context information
|
||||
/// </remarks>
|
||||
/// <param name="profileId">Identifier of the profile associated with the job</param>
|
||||
/// <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 class JobException(long profileId, string jobName, string processName, string batchId, string? reason, Exception? innerException, params (string Name, string? Value, bool IgnoreIfNull)[] details)
|
||||
: Exception(
|
||||
CreateMessage(jobName,
|
||||
[
|
||||
("Profile Id", profileId.ToString(), false),
|
||||
("Job Name", jobName, false),
|
||||
("Process Name", processName, false),
|
||||
("Batch Id", batchId, false),
|
||||
("Reason", reason, true),
|
||||
..details
|
||||
]),
|
||||
innerException)
|
||||
{
|
||||
JobName = jobName;
|
||||
ProcessName = processName;
|
||||
BatchId = batchId;
|
||||
}
|
||||
innerException)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the profile identifier associated with the job execution
|
||||
/// </summary>
|
||||
public long ProfileId { get; } = profileId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the job that failed
|
||||
/// </summary>
|
||||
public string JobName { get; }
|
||||
public string JobName { get; } = jobName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the process/stage that was being executed when the failure occurred
|
||||
/// </summary>
|
||||
public string ProcessName { get; }
|
||||
public string ProcessName { get; } = processName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the unique batch identifier for tracking the execution
|
||||
/// </summary>
|
||||
public string BatchId { get; }
|
||||
public string BatchId { get; } = batchId;
|
||||
|
||||
/// <summary>
|
||||
/// Generates a formatted error message with job context and details
|
||||
@@ -65,26 +66,22 @@ namespace ECMJobRunner.Application.Common.Exceptions
|
||||
/// 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)
|
||||
internal static string CreateMessage(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,41 +6,36 @@ namespace ECMJobRunner.Application.Common.Exceptions
|
||||
/// 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
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of JobHttpException with HTTP client context
|
||||
/// </remarks>
|
||||
/// <param name="profileId">Identifier of the profile associated with the job</param>
|
||||
/// <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 class JobHttpException(long profileId, string jobName, string processName, string batchId, string? reason, string? clientLibrary, string? clientMethod, Exception? innerException)
|
||||
: JobException(profileId, jobName, processName, batchId, reason, innerException, ("Client Library", clientLibrary, true), ("Client Method", clientMethod, true))
|
||||
{
|
||||
/// <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; }
|
||||
public string? ClientLibrary { get; } = clientLibrary;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the client method that failed (e.g., "ExecuteAsync", "PostAsync")
|
||||
/// </summary>
|
||||
public string? ClientMethod { get; }
|
||||
public string? ClientMethod { get; } = clientMethod;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,33 +6,30 @@ namespace ECMJobRunner.Application.Common.Exceptions
|
||||
/// Exception for SQL query execution failures
|
||||
/// Extends JobException with SQL query context for debugging
|
||||
/// </summary>
|
||||
public class JobSqlException : JobException
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of JobSqlException with SQL query context
|
||||
/// </remarks>
|
||||
/// <param name="profileId">Identifier of the profile associated with the job</param>
|
||||
/// <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 class JobSqlException(long profileId, string jobName, string processName, string batchId, string? reason, string? query, Exception? innerException)
|
||||
: JobException(profileId, jobName, processName, batchId, reason, innerException, ("Query", query, true))
|
||||
{
|
||||
/// <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)
|
||||
@@ -41,6 +38,6 @@ namespace ECMJobRunner.Application.Common.Exceptions
|
||||
/// 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; }
|
||||
public virtual string? Query { get; } = query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace ECMJobRunner.Application.Common.Mapping
|
||||
/// </summary>
|
||||
public class ProfileMappingProfile : Profile
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures AutoMapper mappings for <see cref="ECMJobRunner.Domain.Entities.CfgProfile"/> and <see cref="ECMJobRunner.Domain.Entities.ProfileSqlJob"/> entities
|
||||
/// </summary>
|
||||
public ProfileMappingProfile()
|
||||
{
|
||||
// CfgProfile -> CfgProfileDto
|
||||
|
||||
@@ -1,105 +1,109 @@
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ECMJobRunner.Application.Common.Options
|
||||
namespace ECMJobRunner.Application.Common.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for DEX job execution
|
||||
/// </summary>
|
||||
public class DexJobOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration options for DEX job execution
|
||||
/// The configuration section name used to bind this options class from application settings
|
||||
/// </summary>
|
||||
public class DexJobOptions
|
||||
public const string SectionName = "DexJob";
|
||||
|
||||
/// <summary>
|
||||
/// Error handling options for DEX job operations
|
||||
/// </summary>
|
||||
public record DexJobErrorHandlingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Error handling options for DEX job operations
|
||||
/// Error handling options for SQL query execution
|
||||
/// </summary>
|
||||
public record DexJobErrorHandlingOptions
|
||||
public record SqlQueryErrorHandlingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Error handling options for SQL query execution
|
||||
/// Action to take when query execution fails
|
||||
/// </summary>
|
||||
public record SqlQueryErrorHandlingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Action to take when query execution fails
|
||||
/// </summary>
|
||||
public ErrorAction OnExecution { get; set; } = ErrorAction.Stop;
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when query is null or whitespace
|
||||
/// </summary>
|
||||
public ErrorAction IfNullOrWhiteSpace { get; set; } = ErrorAction.Ignore;
|
||||
|
||||
/// <summary>
|
||||
/// Action to take when query returns unexpected result
|
||||
/// </summary>
|
||||
public ErrorAction OnUnexpectedResult { get; set; } = ErrorAction.Stop;
|
||||
}
|
||||
public ErrorAction OnExecution { get; set; } = ErrorAction.Stop;
|
||||
|
||||
/// <summary>
|
||||
/// Error handling options for HTTP requests
|
||||
/// Action to take when query is null or whitespace
|
||||
/// </summary>
|
||||
public record HttpRequestErrorHandlingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Action to take when HTTP request fails
|
||||
/// </summary>
|
||||
public ErrorAction OnSending { get; set; } = ErrorAction.Stop;
|
||||
}
|
||||
public ErrorAction IfNullOrWhiteSpace { get; set; } = ErrorAction.Ignore;
|
||||
|
||||
/// <summary>
|
||||
/// Error handling for main SQL query
|
||||
/// Action to take when query returns unexpected result
|
||||
/// </summary>
|
||||
public SqlQueryErrorHandlingOptions MainQuery { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Error handling for check SQL query
|
||||
/// </summary>
|
||||
public SqlQueryErrorHandlingOptions CheckQuery { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Error handling for ReC HTTP request
|
||||
/// </summary>
|
||||
public HttpRequestErrorHandlingOptions ReCRequest { get; set; } = new();
|
||||
public ErrorAction OnUnexpectedResult { get; set; } = ErrorAction.Stop;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error handling configuration
|
||||
/// Error handling options for HTTP requests
|
||||
/// </summary>
|
||||
public DexJobErrorHandlingOptions Error { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder configuration for dynamic value replacement
|
||||
/// </summary>
|
||||
public record PlaceHolderOptions
|
||||
public record HttpRequestErrorHandlingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for a single placeholder
|
||||
/// Action to take when HTTP request fails
|
||||
/// </summary>
|
||||
public record PlaceHolder
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex pattern to match the placeholder
|
||||
/// </summary>
|
||||
public string Pattern { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Regex options for pattern matching
|
||||
/// </summary>
|
||||
public RegexOptions RegexOptions { get; set; } = RegexOptions.IgnoreCase;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BatchId placeholder configuration
|
||||
/// </summary>
|
||||
public PlaceHolder BatchId { get; set; } = new()
|
||||
{
|
||||
Pattern = "#INT#BATCH_ID",
|
||||
RegexOptions = RegexOptions.IgnoreCase
|
||||
};
|
||||
public ErrorAction OnSending { get; set; } = ErrorAction.Stop;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder configuration
|
||||
/// Error handling for main SQL query
|
||||
/// </summary>
|
||||
public PlaceHolderOptions Placeholders { get; set; } = new();
|
||||
public SqlQueryErrorHandlingOptions MainQuery { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Error handling for check SQL query
|
||||
/// </summary>
|
||||
public SqlQueryErrorHandlingOptions CheckQuery { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Error handling for ReC HTTP request
|
||||
/// </summary>
|
||||
public HttpRequestErrorHandlingOptions ReCRequest { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error handling configuration
|
||||
/// </summary>
|
||||
public DexJobErrorHandlingOptions Error { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder configuration for dynamic value replacement
|
||||
/// </summary>
|
||||
public record PlaceHolderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for a single placeholder
|
||||
/// </summary>
|
||||
public record PlaceHolder
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex pattern to match the placeholder
|
||||
/// </summary>
|
||||
public string Pattern { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Regex options for pattern matching
|
||||
/// </summary>
|
||||
public RegexOptions RegexOptions { get; set; } = RegexOptions.IgnoreCase;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BatchId placeholder configuration
|
||||
/// </summary>
|
||||
public PlaceHolder BatchId { get; set; } = new()
|
||||
{
|
||||
Pattern = "{#INT#BATCH_ID}",
|
||||
RegexOptions = RegexOptions.IgnoreCase
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder configuration
|
||||
/// </summary>
|
||||
public PlaceHolderOptions Placeholders { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using ECMJobRunner.Application.Behaviors;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.Profiles.Commands.Behaviors;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ReC.Client;
|
||||
using System.Reflection;
|
||||
@@ -16,8 +18,10 @@ namespace ECMJobRunner.Application
|
||||
/// Registers MediatR, pipeline behaviors, and AutoMapper
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection</param>
|
||||
/// <param name="recClientApiUrl">The base URL for the ReC client API</param>
|
||||
/// <param name="configuration">The application configuration</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddJobRunnerServices(this IServiceCollection services, string recClientApiUrl)
|
||||
public static IServiceCollection AddJobRunnerServices(this IServiceCollection services, string recClientApiUrl, IConfiguration configuration)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
@@ -27,13 +31,20 @@ namespace ECMJobRunner.Application
|
||||
#else
|
||||
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(assembly));
|
||||
#endif
|
||||
|
||||
// Register AutoMapper with all profiles from this assembly
|
||||
services.AddAutoMapper(assembly);
|
||||
services.AddRecClient(recClientApiUrl, opt =>
|
||||
{
|
||||
opt.LogSuccessfulRequests = true;
|
||||
});
|
||||
|
||||
// Configure DexJobOptions from appsettings.json
|
||||
services.Configure<DexJobOptions>(configuration.GetSection(DexJobOptions.SectionName));
|
||||
|
||||
// Register pipeline behaviors in execution order
|
||||
// Order matters: MainQuery -> CheckQuery -> ReCRequest
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(JobExceptionHandlingBehavior<,>));
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(MainQueryExecutionBehavior<,>));
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CheckQueryExecutionBehavior<,>));
|
||||
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ReCRequestExecutionBehavior<,>));
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ReC.Client" Version="2.0.0-beta" />
|
||||
<PackageReference Include="ReC.Client" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -31,6 +31,8 @@
|
||||
<!-- MediatR for .NET Framework 4.8 -->
|
||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||
<!-- Options configuration binding for .NET Framework 4.8 -->
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Domain.ValueObjects;
|
||||
using MediatR;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.ProfileHistories.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Command to create a new profile execution history record
|
||||
/// </summary>
|
||||
public class CreateProfileHistoryCommand : IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Foreign key to the related profile
|
||||
/// </summary>
|
||||
public long ProfileId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Execution result type
|
||||
/// </summary>
|
||||
public ResultType Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Result text/message
|
||||
/// </summary>
|
||||
#if NET
|
||||
public required string ResultText { get; set; }
|
||||
#else
|
||||
|
||||
public string ResultText { get; set; } = null!;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Created by (max 50 chars)
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string AddedWho { get; set; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for <see cref="CreateProfileHistoryCommand"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Constructor
|
||||
/// </remarks>
|
||||
public class CreateProfileHistoryCommandHandler(IProfileHistoryRepository Repository) : IRequestHandler<CreateProfileHistoryCommand, Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the command by persisting a new <see cref="ProfileHistory"/> record
|
||||
/// </summary>
|
||||
public async Task<Unit> Handle(CreateProfileHistoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await Repository.AddAsync(request, cancellationToken);
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
ECMJobRunner.Application/ProfileHistories/MappingProfiles.cs
Normal file
28
ECMJobRunner.Application/ProfileHistories/MappingProfiles.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using ECMJobRunner.Application.ProfileHistories.Commands;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.ProfileHistories;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class MappingProfiles : AutoMapper.Profile
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public MappingProfiles()
|
||||
{
|
||||
CreateMap<CreateProfileHistoryCommand, ProfileHistory>()
|
||||
.ForMember(dest => dest.Id, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.AddedWhen, opt => opt.MapFrom(_ => DateTime.Now))
|
||||
.ForMember(dest => dest.ChangedWho, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.ChangedWhen, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.CfgProfile, opt => opt.Ignore());
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Application.Profiles.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
@@ -11,7 +11,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.Behaviors
|
||||
namespace ECMJobRunner.Application.Profiles.Commands.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Pipeline behavior that executes the check SQL query for DEX jobs
|
||||
@@ -46,7 +46,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
#endif
|
||||
{
|
||||
if (request is TriggeringDEXJobCommand command)
|
||||
if (request is TriggeringProfileJobCommand command)
|
||||
{
|
||||
await ExecuteCheckQueryAsync(command, cancellationToken);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
return await next();
|
||||
}
|
||||
|
||||
private async Task ExecuteCheckQueryAsync(TriggeringDEXJobCommand command, CancellationToken cancel)
|
||||
private async Task ExecuteCheckQueryAsync(TriggeringProfileJobCommand command, CancellationToken cancel)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(command.Job.SqlCheckQuery))
|
||||
{
|
||||
@@ -71,6 +71,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
{
|
||||
if (result is null)
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "Check Query",
|
||||
batchId: command.BatchId,
|
||||
@@ -79,6 +80,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
innerException: null);
|
||||
else if (result.ReturnValue <= 0)
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "Check Query",
|
||||
batchId: command.BatchId,
|
||||
@@ -91,6 +93,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
{
|
||||
if (_options.Error.CheckQuery.OnExecution == ErrorAction.Stop)
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "Check Query",
|
||||
batchId: command.BatchId,
|
||||
@@ -103,6 +106,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
else if (_options.Error.CheckQuery.IfNullOrWhiteSpace == ErrorAction.Stop)
|
||||
{
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName:"Triggering DEX",
|
||||
processName:"Check Query",
|
||||
batchId:command.BatchId,
|
||||
@@ -0,0 +1,51 @@
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Application.ProfileHistories.Commands;
|
||||
using ECMJobRunner.Domain.ValueObjects;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.Profiles.Commands.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Pipeline behavior that catches <see cref="ECMJobRunner.Application.Common.Exceptions.JobException"/> exceptions,
|
||||
/// persists a profile history error record and re-throws the exception
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">The type of the MediatR request</typeparam>
|
||||
/// <typeparam name="TResponse">The type of the MediatR response</typeparam>
|
||||
/// <param name="Sender">MediatR sender used to dispatch the <see cref="ECMJobRunner.Application.ProfileHistories.Commands.CreateProfileHistoryCommand"/></param>
|
||||
/// <param name="Logger">Logger for diagnostic output.</param>
|
||||
public class JobExceptionHandlingBehavior<TRequest, TResponse>(ISender Sender, ILogger<JobExceptionHandlingBehavior<TRequest, TResponse>> Logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : notnull
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the pipeline behavior
|
||||
/// Executes main query if request is TriggeringDEXJobCommand
|
||||
/// </summary>
|
||||
#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
|
||||
{
|
||||
try
|
||||
{
|
||||
return await next();
|
||||
}
|
||||
catch (JobException ex)
|
||||
{
|
||||
var cmd = new CreateProfileHistoryCommand()
|
||||
{
|
||||
ProfileId = ex.ProfileId,
|
||||
Result = ResultType.Ok,
|
||||
ResultText = ex.ToString(),
|
||||
AddedWho = "ECMJobRunner"
|
||||
};
|
||||
await Sender.Send(cmd, cancellationToken);
|
||||
|
||||
Logger.LogWarning(ex, "JobException caught in JobExceptionHandlingBehavior for ProfileId {ProfileId}, JobName {JobName}, ProcessName {ProcessName}, BatchId {BatchId}", ex.ProfileId, ex.JobName, ex.ProcessName, ex.BatchId);
|
||||
|
||||
return default!;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Application.Profiles.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
@@ -11,7 +11,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.Behaviors
|
||||
namespace ECMJobRunner.Application.Profiles.Commands.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Pipeline behavior that executes the main SQL query for DEX jobs
|
||||
@@ -46,7 +46,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
#endif
|
||||
{
|
||||
if (request is TriggeringDEXJobCommand command)
|
||||
if (request is TriggeringProfileJobCommand command)
|
||||
{
|
||||
await ExecuteMainQueryAsync(command, cancellationToken);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
return await next();
|
||||
}
|
||||
|
||||
private async Task ExecuteMainQueryAsync(TriggeringDEXJobCommand command, CancellationToken cancel)
|
||||
private async Task ExecuteMainQueryAsync(TriggeringProfileJobCommand command, CancellationToken cancel)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(command.Job.SqlMainQuery))
|
||||
{
|
||||
@@ -71,18 +71,20 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
{
|
||||
if (result is null)
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "Main Query",
|
||||
batchId: command.BatchId,
|
||||
reason: "Main Query returned nothing.",
|
||||
query: sqlMainQuery,
|
||||
innerException: null);
|
||||
else if (result.ReturnValue is not null)
|
||||
else if (result.ReturnValue is not null && result.ReturnValue != 0)
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "Main Query",
|
||||
batchId: command.BatchId,
|
||||
reason: $"The query unexpectedly returned the value {result.ReturnValue}. The expected value was null.",
|
||||
reason: $"The query unexpectedly returned the value {result.ReturnValue}. The expected value was null or 0.",
|
||||
query: sqlMainQuery, innerException: null);
|
||||
}
|
||||
}
|
||||
@@ -90,6 +92,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
{
|
||||
if (_options.Error.MainQuery.OnExecution == ErrorAction.Stop)
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "Main Query",
|
||||
batchId: command.BatchId,
|
||||
@@ -101,6 +104,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
else if (_options.Error.MainQuery.IfNullOrWhiteSpace == ErrorAction.Stop)
|
||||
{
|
||||
throw new JobSqlException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "Main Query",
|
||||
batchId: command.BatchId,
|
||||
@@ -1,8 +1,9 @@
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Application.Profiles.Commands;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ReC.Client;
|
||||
using ReC.Client.Api;
|
||||
@@ -10,7 +11,7 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.Behaviors
|
||||
namespace ECMJobRunner.Application.Profiles.Commands.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Pipeline behavior that sends ReC HTTP request for DEX jobs
|
||||
@@ -18,22 +19,15 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">The request type</typeparam>
|
||||
/// <typeparam name="TResponse">The response type</typeparam>
|
||||
public class ReCRequestExecutionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of ReCRequestExecutionBehavior
|
||||
/// </remarks>
|
||||
/// <param name="ReCClient">ReC client for HTTP requests</param>
|
||||
/// <param name="options">DEX job configuration options</param>
|
||||
public class ReCRequestExecutionBehavior<TRequest, TResponse>(ReCClient ReCClient, IOptions<DexJobOptions> options, ILogger<ReCRequestExecutionBehavior<TRequest, TResponse>> Logger) : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
private readonly ReCClient _reCClient;
|
||||
private readonly DexJobOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of ReCRequestExecutionBehavior
|
||||
/// </summary>
|
||||
/// <param name="reCClient">ReC client for HTTP requests</param>
|
||||
/// <param name="options">DEX job configuration options</param>
|
||||
public ReCRequestExecutionBehavior(ReCClient reCClient, IOptions<DexJobOptions> options)
|
||||
{
|
||||
_reCClient = reCClient;
|
||||
_options = options.Value;
|
||||
}
|
||||
private readonly DexJobOptions Options = options.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the pipeline behavior
|
||||
@@ -45,7 +39,7 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
|
||||
#endif
|
||||
{
|
||||
if (request is TriggeringDEXJobCommand command)
|
||||
if (request is TriggeringProfileJobCommand command)
|
||||
{
|
||||
await SendReCRequestAsync(command, cancellationToken);
|
||||
}
|
||||
@@ -53,20 +47,30 @@ namespace ECMJobRunner.Application.Behaviors
|
||||
return await next();
|
||||
}
|
||||
|
||||
private async Task SendReCRequestAsync(TriggeringDEXJobCommand command, CancellationToken cancel)
|
||||
private async Task SendReCRequestAsync(TriggeringProfileJobCommand command, CancellationToken cancel)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _reCClient.RecActions.InvokeAsync(command.Job.ProfileId, new InvokeReferences()
|
||||
command.RecActionResult = await ReCClient.RecActions.InvokeAsync(command.Job.ProfileId, new InvokeReferences()
|
||||
{
|
||||
BatchId = command.BatchId,
|
||||
}, cancel);
|
||||
|
||||
Logger.LogInformation(
|
||||
"ReC request completed successfully. Profile ID: {ProfileId} | Job name: {JobName} | Batch ID: {BatchId} | Total action count: {TotalActionCount} | Action exception count: {ActionExceptionCount}",
|
||||
command.Job.ProfileId,
|
||||
command.Job.Name,
|
||||
command.BatchId,
|
||||
command.RecActionResult?.TotalActionCount ?? 0,
|
||||
command.RecActionResult?.ActionExceptionCount ?? 0);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_options.Error.ReCRequest.OnSending == ErrorAction.Stop)
|
||||
if (Options.Error.ReCRequest.OnSending == ErrorAction.Stop)
|
||||
{
|
||||
throw new JobHttpException(
|
||||
profileId: command.Job.ProfileId,
|
||||
jobName: "Triggering DEX",
|
||||
processName: "ReC Http Request",
|
||||
batchId: command.BatchId,
|
||||
@@ -6,13 +6,13 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
namespace ECMJobRunner.Application.Profiles.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Command to trigger DEX job batch for a profile
|
||||
/// Executes all SQL jobs associated with a profile ID
|
||||
/// </summary>
|
||||
public class TriggeringDEXJobBatchCommand : IRequest<Unit>
|
||||
public class TriggeringProfileJobBatchCommand : IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Profile ID to trigger all associated SQL jobs
|
||||
@@ -26,7 +26,7 @@ namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
/// Validates that the profile is active before execution
|
||||
/// </summary>
|
||||
public class TriggeringDEXJobBatchCommandHandler(ICfgProfileRepository profileRepo, IProfileSqlJobRepository jobRepo, ISender sender)
|
||||
: IRequestHandler<TriggeringDEXJobBatchCommand, Unit>
|
||||
: IRequestHandler<TriggeringProfileJobBatchCommand, Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the TriggeringDEXJobBatchCommand
|
||||
@@ -36,7 +36,7 @@ namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Unit value indicating completion</returns>
|
||||
/// <exception cref="InactiveProfileException">Thrown when the profile is not active</exception>
|
||||
public async Task<Unit> Handle(TriggeringDEXJobBatchCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Unit> Handle(TriggeringProfileJobBatchCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var batchId = CreateBatchId();
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
await sender.Send(new TriggeringDEXJobCommand
|
||||
await sender.Send(new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = job,
|
||||
BatchId = batchId
|
||||
@@ -1,9 +1,13 @@
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using ECMJobRunner.Application.ProfileHistories.Commands;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using ECMJobRunner.Domain.ValueObjects;
|
||||
using MediatR;
|
||||
using ReC.Client.Api;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
namespace ECMJobRunner.Application.Profiles.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Command to trigger a single DEX job execution
|
||||
@@ -12,7 +16,7 @@ namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
/// 2. CheckQueryExecutionBehavior - validates with check SQL query
|
||||
/// 3. ReCRequestExecutionBehavior - invokes ReC HTTP request
|
||||
/// </summary>
|
||||
public class TriggeringDEXJobCommand : IRequest<Unit>
|
||||
public class TriggeringProfileJobCommand : IRequest<Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// The SQL job to execute
|
||||
@@ -23,6 +27,8 @@ namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
/// Unique batch identifier for this execution
|
||||
/// </summary>
|
||||
public string BatchId { get; set; } = null!;
|
||||
|
||||
internal BatchRecActionViewResponse? RecActionResult { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -30,7 +36,7 @@ namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
/// All execution logic is delegated to pipeline behaviors
|
||||
/// This handler simply returns completion after behaviors execute
|
||||
/// </summary>
|
||||
public class TriggeringDEXJobCommandHandler : IRequestHandler<TriggeringDEXJobCommand, Unit>
|
||||
public class TriggeringDEXJobCommandHandler(ISender Sender) : IRequestHandler<TriggeringProfileJobCommand, Unit>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the TriggeringDEXJobCommand
|
||||
@@ -39,13 +45,24 @@ namespace ECMJobRunner.Application.DEXJob.Commands
|
||||
/// <param name="request">The command containing job and batch ID</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Unit value indicating completion</returns>
|
||||
public Task<Unit> Handle(TriggeringDEXJobCommand request, CancellationToken cancellationToken)
|
||||
public async Task<Unit> Handle(TriggeringProfileJobCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// All execution logic is handled by pipeline behaviors:
|
||||
// - MainQueryExecutionBehavior
|
||||
// - CheckQueryExecutionBehavior
|
||||
// - ReCRequestExecutionBehavior
|
||||
return Task.FromResult(Unit.Value);
|
||||
|
||||
var cmd = new CreateProfileHistoryCommand
|
||||
{
|
||||
Result = ResultType.Ok,
|
||||
ResultText = $"Job '{request.Job.Name}' erfolgreich abgeschlossen. | Batch-ID: {request.BatchId} | Verarbeitete Aktionen: {request.RecActionResult?.TotalActionCount ?? 0} | Fehlgeschlagene Aktionen: {request.RecActionResult?.ActionExceptionCount ?? 0}",
|
||||
ProfileId = request.Job.ProfileId,
|
||||
AddedWho = "ECMJobRunner"
|
||||
};
|
||||
|
||||
await Sender.Send(cmd, cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,13 @@ using AutoMapper;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Domain.Interfaces;
|
||||
using MediatR;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ECMJobRunner.Application.DEXJob.Queries
|
||||
namespace ECMJobRunner.Application.Profiles.Queries
|
||||
{
|
||||
/// <summary>
|
||||
/// Query to retrieve profiles with flexible filtering options
|
||||
@@ -58,12 +59,23 @@ namespace ECMJobRunner.Application.DEXJob.Queries
|
||||
private readonly ICfgProfileRepository _profileRepository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="profileRepository">Repository for profile data access</param>
|
||||
/// <param name="mapper">AutoMapper instance for entity-to-DTO mapping</param>
|
||||
public GetProfileQueryHandler(ICfgProfileRepository profileRepository, IMapper mapper)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="GetProfileQuery"/> by retrieving and mapping profiles
|
||||
/// </summary>
|
||||
/// <param name="request">The query containing optional filter parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>List of matched profiles mapped to <see cref="CfgProfileDto"/></returns>
|
||||
public async Task<List<CfgProfileDto>> Handle(GetProfileQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
IEnumerable<Domain.Entities.CfgProfile> profiles;
|
||||
@@ -112,8 +124,11 @@ namespace ECMJobRunner.Application.DEXJob.Queries
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ProfileName))
|
||||
{
|
||||
var searchName = request.ProfileName.ToLowerInvariant();
|
||||
profiles = profiles.Where(p => p.ProfileName.ToLowerInvariant().Contains(searchName));
|
||||
#if NET
|
||||
profiles = profiles.Where(p => p.ProfileName.Contains(request.ProfileName, StringComparison.OrdinalIgnoreCase));
|
||||
#else
|
||||
profiles = profiles.Where(p => p.ProfileName.IndexOf(request.ProfileName!, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ECMJobRunner.Domain.ValueObjects;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
@@ -35,6 +36,18 @@ namespace ECMJobRunner.Domain.Entities
|
||||
[Column("RESULT_ID")]
|
||||
public byte ResultId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the result type of the job execution.
|
||||
/// This property is not mapped to a database column; it wraps <see cref="ResultId"/>.
|
||||
/// If <see cref="ResultId"/> does not correspond to a defined <see cref="ResultType"/> value, returns <see cref="ResultType.Unknown"/>.
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public ResultType Result
|
||||
{
|
||||
get => Enum.IsDefined(typeof(ResultType), ResultId) ? (ResultType)ResultId : ResultType.Unknown;
|
||||
set => ResultId = (byte)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result text/message
|
||||
/// </summary>
|
||||
|
||||
@@ -90,10 +90,5 @@ namespace ECMJobRunner.Domain.Interfaces
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>True if entity was found and deleted, false otherwise</returns>
|
||||
Task<bool> DeleteSingleAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Save all changes asynchronously
|
||||
/// </summary>
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
27
ECMJobRunner.Domain/ValueObjects/ResultType.cs
Normal file
27
ECMJobRunner.Domain/ValueObjects/ResultType.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace ECMJobRunner.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result type of a job execution.
|
||||
/// </summary>
|
||||
public enum ResultType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The operation completed successfully.
|
||||
/// </summary>
|
||||
Ok = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The operation encountered an error.
|
||||
/// </summary>
|
||||
Error = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The operation completed with warnings.
|
||||
/// </summary>
|
||||
Warning = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The operation result is unknown.
|
||||
/// </summary>
|
||||
Unknown = 255
|
||||
}
|
||||
@@ -31,7 +31,7 @@ namespace ECMJobRunner.Infrastructure.Repositories
|
||||
/// </summary>
|
||||
public async Task<CfgProfile?> GetByIdWithSqlJobsAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.CfgProfiles
|
||||
return await Context.CfgProfiles
|
||||
.Include(p => p.SqlJobs)
|
||||
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ namespace ECMJobRunner.Infrastructure.Repositories
|
||||
/// </summary>
|
||||
public async Task<List<CfgProfile>> GetAllActiveWithSqlJobsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.CfgProfiles
|
||||
return await Context.CfgProfiles
|
||||
.Include(p => p.SqlJobs)
|
||||
.Where(p => p.Active)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -20,145 +20,144 @@ namespace ECMJobRunner.Infrastructure.Repositories
|
||||
/// Uses AutoMapper for DTO mapping
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">Entity type</typeparam>
|
||||
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
|
||||
/// <remarks>
|
||||
/// Constructor
|
||||
/// </remarks>
|
||||
public class Repository<TEntity>(JobRunnerDbContext context, IMapper mapper) : IRepository<TEntity> where TEntity : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Database context
|
||||
/// </summary>
|
||||
protected readonly JobRunnerDbContext _context;
|
||||
protected readonly JobRunnerDbContext Context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
|
||||
/// <summary>
|
||||
/// DbSet for the entity
|
||||
/// </summary>
|
||||
protected readonly DbSet<TEntity> _dbSet;
|
||||
protected readonly DbSet<TEntity> DbSet = context.Set<TEntity>();
|
||||
|
||||
/// <summary>
|
||||
/// AutoMapper instance for DTO mapping
|
||||
/// </summary>
|
||||
protected readonly IMapper _mapper;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public Repository(JobRunnerDbContext context, IMapper mapper)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
_dbSet = context.Set<TEntity>();
|
||||
}
|
||||
protected readonly IMapper Mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<TEntity?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET48
|
||||
return await _dbSet.FindAsync(cancellationToken, id);
|
||||
return await DbSet.FindAsync(cancellationToken, id);
|
||||
#else
|
||||
return await _dbSet.FindAsync(new object[] { id }, cancellationToken);
|
||||
return await DbSet.FindAsync([id], cancellationToken);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbSet.ToListAsync(cancellationToken);
|
||||
return await DbSet.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbSet.Where(predicate).ToListAsync(cancellationToken);
|
||||
return await DbSet.Where(predicate).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<TEntity?> SingleOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbSet.SingleOrDefaultAsync(predicate, cancellationToken);
|
||||
return await DbSet.SingleOrDefaultAsync(predicate, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<TEntity> AddAsync<TDto>(TDto dto, CancellationToken cancellationToken = default) where TDto : class
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
if (dto == null) throw new ArgumentNullException(nameof(dto));
|
||||
|
||||
var entity = _mapper.Map<TEntity>(dto);
|
||||
|
||||
#if NET48
|
||||
_dbSet.Add(entity);
|
||||
await Task.CompletedTask;
|
||||
#else
|
||||
await _dbSet.AddAsync(entity, cancellationToken);
|
||||
#endif
|
||||
|
||||
await SaveChangesAsync(cancellationToken);
|
||||
var entity = Mapper.Map<TEntity>(dto);
|
||||
|
||||
#if NET48
|
||||
DbSet.Add(entity);
|
||||
#else
|
||||
await DbSet.AddAsync(entity, cancellationToken);
|
||||
#endif
|
||||
|
||||
await Context.SaveChangesAsync(cancellationToken);
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<int> AddRangeAsync<TDto>(IEnumerable<TDto> dtos, CancellationToken cancellationToken = default) where TDto : class
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
if (dtos == null) throw new ArgumentNullException(nameof(dtos));
|
||||
|
||||
var dtoList = dtos.ToList();
|
||||
if (!dtoList.Any())
|
||||
return 0;
|
||||
|
||||
var entities = _mapper.Map<List<TEntity>>(dtoList);
|
||||
|
||||
#if NET48
|
||||
_dbSet.AddRange(entities);
|
||||
await Task.CompletedTask;
|
||||
#else
|
||||
await _dbSet.AddRangeAsync(entities, cancellationToken);
|
||||
#endif
|
||||
|
||||
return await SaveChangesAsync(cancellationToken);
|
||||
var dtoList = dtos.ToList();
|
||||
if (dtoList.Count == 0)
|
||||
return 0;
|
||||
|
||||
var entities = Mapper.Map<List<TEntity>>(dtoList);
|
||||
|
||||
#if NET48
|
||||
DbSet.AddRange(entities);
|
||||
#else
|
||||
await DbSet.AddRangeAsync(entities, cancellationToken);
|
||||
#endif
|
||||
|
||||
return await Context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<int> UpdateAsync<TDto>(Expression<Func<TEntity, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
if (dto == null) throw new ArgumentNullException(nameof(dto));
|
||||
#endif
|
||||
|
||||
var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken);
|
||||
var entities = await DbSet.Where(predicate).ToListAsync(cancellationToken);
|
||||
|
||||
if (!entities.Any())
|
||||
if (entities.Count == 0)
|
||||
return 0;
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
_mapper.Map(dto, entity);
|
||||
Mapper.Map(dto, entity);
|
||||
}
|
||||
|
||||
return await SaveChangesAsync(cancellationToken);
|
||||
return await Context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<bool> UpdateSingleAsync<TDto>(Expression<Func<TEntity, bool>> predicate, TDto dto, CancellationToken cancellationToken = default) where TDto : class
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
if (dto == null) throw new ArgumentNullException(nameof(dto));
|
||||
#endif
|
||||
|
||||
var entity = await SingleOrDefaultAsync(predicate, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
return false;
|
||||
|
||||
_mapper.Map(dto, entity);
|
||||
Mapper.Map(dto, entity);
|
||||
|
||||
await SaveChangesAsync(cancellationToken);
|
||||
await Context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<int> DeleteAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entities = await _dbSet.Where(predicate).ToListAsync(cancellationToken);
|
||||
var entities = await DbSet.Where(predicate).ToListAsync(cancellationToken);
|
||||
|
||||
if (!entities.Any())
|
||||
if (entities.Count == 0)
|
||||
return 0;
|
||||
|
||||
_dbSet.RemoveRange(entities);
|
||||
DbSet.RemoveRange(entities);
|
||||
|
||||
return await SaveChangesAsync(cancellationToken);
|
||||
return await Context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -169,20 +168,10 @@ namespace ECMJobRunner.Infrastructure.Repositories
|
||||
if (entity == null)
|
||||
return false;
|
||||
|
||||
_dbSet.Remove(entity);
|
||||
DbSet.Remove(entity);
|
||||
|
||||
await SaveChangesAsync(cancellationToken);
|
||||
await Context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
#if NET48
|
||||
return await _context.SaveChangesAsync();
|
||||
#else
|
||||
return await _context.SaveChangesAsync(cancellationToken);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#if NET48
|
||||
using System.Data.Entity;
|
||||
using System.Linq;
|
||||
#else
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
#endif
|
||||
@@ -8,6 +7,7 @@ using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Infrastructure.Data;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
|
||||
namespace ECMJobRunner.Infrastructure.Services
|
||||
{
|
||||
@@ -41,16 +41,16 @@ namespace ECMJobRunner.Infrastructure.Services
|
||||
// Entity Framework 6 implementation
|
||||
var result = await _context.Database
|
||||
.SqlQuery<TResult>(sql)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return result;
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return result.FirstOrDefault();
|
||||
#else
|
||||
// Entity Framework Core implementation
|
||||
var result = await _context.Database
|
||||
.SqlQueryRaw<TResult>(sql)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return result;
|
||||
return result.FirstOrDefault();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using ECMJobRunner.Application.Behaviors;
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Application.DEXJob.Commands.Behaviors;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using MediatR;
|
||||
@@ -21,7 +21,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
{
|
||||
private readonly Mock<ISQLExecutor> _mockExecutor;
|
||||
private readonly Mock<IOptions<DexJobOptions>> _mockOptions;
|
||||
private readonly CheckQueryExecutionBehavior<TriggeringDEXJobCommand, Unit> _behavior;
|
||||
private readonly CheckQueryExecutionBehavior<TriggeringProfileJobCommand, Unit> _behavior;
|
||||
private readonly Mock<RequestHandlerDelegate<Unit>> _mockNext;
|
||||
|
||||
public CheckQueryExecutionBehaviorTests()
|
||||
@@ -29,7 +29,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
_mockExecutor = new Mock<ISQLExecutor>();
|
||||
_mockOptions = new Mock<IOptions<DexJobOptions>>();
|
||||
_mockOptions.Setup(o => o.Value).Returns(new DexJobOptions());
|
||||
_behavior = new CheckQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
_behavior = new CheckQueryExecutionBehavior<TriggeringProfileJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
_mockNext = new Mock<RequestHandlerDelegate<Unit>>();
|
||||
_mockNext.Setup(n => n()).ReturnsAsync(Unit.Value);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithValidCheckQuery_ExecutesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -68,7 +68,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithPositiveReturnValue_DoesNotThrow(int returnValue)
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -96,7 +96,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithZeroOrNegativeReturnValue_ThrowsDEXJobException(int returnValue)
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -122,7 +122,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithNullCheckQuery_IgnoresByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlCheckQuery = null },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -157,8 +157,8 @@ namespace ECMJobRunner.Tests.Application
|
||||
};
|
||||
_mockOptions.Setup(o => o.Value).Returns(options);
|
||||
|
||||
var behavior = new CheckQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var behavior = new CheckQueryExecutionBehavior<TriggeringProfileJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlCheckQuery = null },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -181,7 +181,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
{
|
||||
// Arrange
|
||||
var batchId = "20260711143025123456";
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table WHERE BatchId = '#INT#BATCH_ID'" },
|
||||
BatchId = batchId
|
||||
@@ -209,7 +209,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithNullResult_ThrowsDEXJobException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlCheckQuery = "SELECT COUNT(*) AS [Return Value] FROM Table" },
|
||||
BatchId = "20260711143025123456"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using ECMJobRunner.Application.Behaviors;
|
||||
using ECMJobRunner.Application.Common.Constants;
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using ECMJobRunner.Application.Common.Interfaces;
|
||||
using ECMJobRunner.Application.Common.Options;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Application.DEXJob.Commands.Behaviors;
|
||||
using ECMJobRunner.Domain.Entities;
|
||||
using FluentAssertions;
|
||||
using MediatR;
|
||||
@@ -21,7 +21,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
{
|
||||
private readonly Mock<ISQLExecutor> _mockExecutor;
|
||||
private readonly Mock<IOptions<DexJobOptions>> _mockOptions;
|
||||
private readonly MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit> _behavior;
|
||||
private readonly MainQueryExecutionBehavior<TriggeringProfileJobCommand, Unit> _behavior;
|
||||
private readonly Mock<RequestHandlerDelegate<Unit>> _mockNext;
|
||||
|
||||
public MainQueryExecutionBehaviorTests()
|
||||
@@ -29,7 +29,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
_mockExecutor = new Mock<ISQLExecutor>();
|
||||
_mockOptions = new Mock<IOptions<DexJobOptions>>();
|
||||
_mockOptions.Setup(o => o.Value).Returns(new DexJobOptions());
|
||||
_behavior = new MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
_behavior = new MainQueryExecutionBehavior<TriggeringProfileJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
_mockNext = new Mock<RequestHandlerDelegate<Unit>>();
|
||||
_mockNext.Setup(n => n()).ReturnsAsync(Unit.Value);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithValidMainQuery_ExecutesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -65,7 +65,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithNullReturnValue_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -90,7 +90,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithNonNullReturnValue_ThrowsDEXJobException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table VALUES (1)" },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -116,7 +116,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithNullMainQuery_IgnoresByDefault()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = null },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -151,8 +151,8 @@ namespace ECMJobRunner.Tests.Application
|
||||
};
|
||||
_mockOptions.Setup(o => o.Value).Returns(options);
|
||||
|
||||
var behavior = new MainQueryExecutionBehavior<TriggeringDEXJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var behavior = new MainQueryExecutionBehavior<TriggeringProfileJobCommand, Unit>(_mockExecutor.Object, _mockOptions.Object);
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = null },
|
||||
BatchId = "20260711143025123456"
|
||||
@@ -175,7 +175,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
{
|
||||
// Arrange
|
||||
var batchId = "20260711143025123456";
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INSERT INTO Table (BatchId) VALUES ('#INT#BATCH_ID')" },
|
||||
BatchId = batchId
|
||||
@@ -203,7 +203,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
public async Task Handle_WithExecutionError_ThrowsDEXJobException()
|
||||
{
|
||||
// Arrange
|
||||
var command = new TriggeringDEXJobCommand
|
||||
var command = new TriggeringProfileJobCommand
|
||||
{
|
||||
Job = new ProfileSqlJob { SqlMainQuery = "INVALID SQL" },
|
||||
BatchId = "20260711143025123456"
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
.Setup(r => r.FindAsync(It.IsAny<Expression<Func<ProfileSqlJob, bool>>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(jobs);
|
||||
|
||||
var command = new TriggeringDEXJobBatchCommand { ProfileId = profileId };
|
||||
var command = new TriggeringProfileJobBatchCommand { ProfileId = profileId };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
@@ -54,7 +54,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
// Assert
|
||||
result.Should().Be(Unit.Value);
|
||||
_mockSender.Verify(
|
||||
s => s.Send(It.IsAny<TriggeringDEXJobCommand>(), It.IsAny<CancellationToken>()),
|
||||
s => s.Send(It.IsAny<TriggeringProfileJobCommand>(), It.IsAny<CancellationToken>()),
|
||||
Times.Exactly(2),
|
||||
"Should send command for each job");
|
||||
}
|
||||
@@ -68,7 +68,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
.Setup(r => r.FindAsync(It.IsAny<Expression<Func<ProfileSqlJob, bool>>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<ProfileSqlJob>());
|
||||
|
||||
var command = new TriggeringDEXJobBatchCommand { ProfileId = profileId };
|
||||
var command = new TriggeringProfileJobBatchCommand { ProfileId = profileId };
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
@@ -76,7 +76,7 @@ namespace ECMJobRunner.Tests.Application
|
||||
// Assert
|
||||
result.Should().Be(Unit.Value);
|
||||
_mockSender.Verify(
|
||||
s => s.Send(It.IsAny<TriggeringDEXJobCommand>(), It.IsAny<CancellationToken>()),
|
||||
s => s.Send(It.IsAny<TriggeringProfileJobCommand>(), It.IsAny<CancellationToken>()),
|
||||
Times.Never,
|
||||
"Should not send any commands when no jobs found");
|
||||
}
|
||||
@@ -115,13 +115,13 @@ namespace ECMJobRunner.Tests.Application
|
||||
.Setup(r => r.FindAsync(It.IsAny<Expression<Func<ProfileSqlJob, bool>>>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new[] { job });
|
||||
|
||||
TriggeringDEXJobCommand? capturedCommand = null;
|
||||
TriggeringProfileJobCommand? capturedCommand = null;
|
||||
_mockSender
|
||||
.Setup(s => s.Send(It.IsAny<TriggeringDEXJobCommand>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IRequest<Unit>, CancellationToken>((cmd, _) => capturedCommand = cmd as TriggeringDEXJobCommand)
|
||||
.Setup(s => s.Send(It.IsAny<TriggeringProfileJobCommand>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<IRequest<Unit>, CancellationToken>((cmd, _) => capturedCommand = cmd as TriggeringProfileJobCommand)
|
||||
.ReturnsAsync(Unit.Value);
|
||||
|
||||
var command = new TriggeringDEXJobBatchCommand { ProfileId = profileId };
|
||||
var command = new TriggeringProfileJobBatchCommand { ProfileId = profileId };
|
||||
|
||||
// Act
|
||||
await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||
|
||||
<!-- ReC.Client for HTTP mocking -->
|
||||
<PackageReference Include="ReC.Client" Version="2.0.0-beta" />
|
||||
<PackageReference Include="ReC.Client" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- .NET 8.0 specific packages -->
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>ECMJobRunner.WebCron</Product>
|
||||
<Version>1.0.0</Version>
|
||||
<FileVersion>1.0.0.0</FileVersion>
|
||||
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||
<InformationalVersion>1.0.0</InformationalVersion>
|
||||
<Version>1.1.0</Version>
|
||||
<FileVersion>1.1.0.1</FileVersion>
|
||||
<AssemblyVersion>1.1.0.1</AssemblyVersion>
|
||||
<InformationalVersion>1.1.0</InformationalVersion>
|
||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
<PackageTags>digital data job runner</PackageTags>
|
||||
<UserSecretsId>cf893b96-c71a-4a96-a6a7-40004249e1a3</UserSecretsId>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using ECMJobRunner.Application.Common.Dtos;
|
||||
using ECMJobRunner.Application.DEXJob.Commands;
|
||||
using ECMJobRunner.Application.Profiles.Commands;
|
||||
using MediatR;
|
||||
|
||||
namespace ECMJobRunner.WebCron.Extensions;
|
||||
@@ -27,10 +27,10 @@ public static class DtoExtensions
|
||||
/// Converts a profile DTO to a DEX job batch command.
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile configuration DTO.</param>
|
||||
/// <returns>A <see cref="TriggeringDEXJobBatchCommand"/> ready to execute the profile job.</returns>
|
||||
public static TriggeringDEXJobBatchCommand ToJob(this CfgProfileDto profile)
|
||||
/// <returns>A <see cref="TriggeringProfileJobBatchCommand"/> ready to execute the profile job.</returns>
|
||||
public static TriggeringProfileJobBatchCommand ToJob(this CfgProfileDto profile)
|
||||
{
|
||||
return new TriggeringDEXJobBatchCommand
|
||||
return new TriggeringProfileJobBatchCommand
|
||||
{
|
||||
ProfileId = profile.Id,
|
||||
};
|
||||
|
||||
@@ -112,7 +112,7 @@ public static class HealthCheckHtmlGenerator
|
||||
<h1 class=""mb-2"">");
|
||||
sb.Append(icon);
|
||||
sb.Append($@" Health Check Status</h1>
|
||||
<p class=""text-muted mb-0"">Last checked: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</p>
|
||||
<p class=""text-muted mb-0"">Last checked: {DateTime.Now:yyyy-MM-dd HH:mm:ss} UTC</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2><span class=""badge {badgeClass} fs-3"">{report.Status}</span></h2>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using ECMJobRunner.Application.Common.Exceptions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
|
||||
namespace ECMJobRunner.WebCron.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Middleware for handling exceptions globally in the application.
|
||||
/// Captures exceptions thrown during the request pipeline execution,
|
||||
/// logs them, and returns an appropriate HTTP response with a JSON error details.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="ExceptionHandlingMiddleware"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="Next">The next middleware in the request pipeline.</param>
|
||||
/// <param name="Logger">The logger instance for logging exceptions.</param>
|
||||
public class ExceptionHandlingMiddleware(RequestDelegate Next, ILogger<ExceptionHandlingMiddleware> Logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes the middleware to handle the HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context of the current request.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Next(context); // Continue down the pipeline
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleExceptionAsync(context, ex, Logger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles exceptions by logging them and writing an appropriate JSON response.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP context of the current request.</param>
|
||||
/// <param name="exception">The exception that occurred.</param>
|
||||
/// <param name="logger">The logger instance for logging the exception.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
ValidationProblemDetails details;
|
||||
|
||||
switch (exception)
|
||||
{
|
||||
case JobException jobEx:
|
||||
logger.LogWarning(jobEx, "Job exception occurred.");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
details = new()
|
||||
{
|
||||
Title = "Job Exception",
|
||||
Detail = jobEx.Message
|
||||
};
|
||||
break;
|
||||
default:
|
||||
logger.LogError(exception, "Unhandled exception occurred.");
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
details = new()
|
||||
{
|
||||
Title = "Internal Server Error",
|
||||
Detail = "An unexpected error occurred. Please try again later."
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
if (details is not null)
|
||||
await context.Response.WriteAsJsonAsync(details);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ internal class ProfileWorkerOptionsValidator : IValidateOptions<ProfileWorkerOpt
|
||||
/// <param name="options">The options to validate.</param>
|
||||
/// <returns>
|
||||
/// <see cref="ValidateOptionsResult.Success"/> if valid,
|
||||
/// or <see cref="ValidateOptionsResult.Fail"/> with error message if invalid.
|
||||
/// or <see cref="ValidateOptionsResult.Fail(string)"/> with error message if invalid.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Validation rules:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using ECMJobRunner.Application.DEXJob.Queries;
|
||||
using ECMJobRunner.Application.Profiles.Queries;
|
||||
using ECMJobRunner.WebCron.Extensions;
|
||||
using Hangfire;
|
||||
using MediatR;
|
||||
|
||||
@@ -67,7 +67,7 @@ public class ProfileWorker(
|
||||
await work.ExecuteAsync(stoppingToken);
|
||||
|
||||
// Success - update health state
|
||||
_lastSuccessfulRun = DateTime.UtcNow;
|
||||
_lastSuccessfulRun = DateTime.Now;
|
||||
_consecutiveFailures = 0;
|
||||
_lastException = null;
|
||||
|
||||
@@ -151,7 +151,7 @@ public class ProfileWorker(
|
||||
));
|
||||
}
|
||||
|
||||
var timeSinceLastSuccess = DateTime.UtcNow - _lastSuccessfulRun.Value;
|
||||
var timeSinceLastSuccess = DateTime.Now - _lastSuccessfulRun.Value;
|
||||
|
||||
// Adaptive multiplier: 3x for fast intervals (<10s), 1.5x for slower intervals
|
||||
// This ensures faster problem detection when using longer intervals (e.g., 60s)
|
||||
|
||||
@@ -2,6 +2,7 @@ using ECMJobRunner.Application;
|
||||
using ECMJobRunner.Infrastructure;
|
||||
using ECMJobRunner.WebCron;
|
||||
using ECMJobRunner.WebCron.HealthCheck;
|
||||
using ECMJobRunner.WebCron.Middleware;
|
||||
using ECMJobRunner.WebCron.ProfileWorker;
|
||||
using Hangfire;
|
||||
using Hangfire.SqlServer;
|
||||
@@ -65,7 +66,7 @@ builder.Services.AddJobRunnerInfrastructure(cnnStr);
|
||||
|
||||
var recClientApiUrl = builder.Configuration.GetValue<string>("ReC:ApiUrl")
|
||||
?? throw new InvalidOperationException("ReC:ApiUrl not found.");
|
||||
builder.Services.AddJobRunnerServices(recClientApiUrl);
|
||||
builder.Services.AddJobRunnerServices(recClientApiUrl, builder.Configuration);
|
||||
|
||||
// Get Hangfire storage configuration
|
||||
var useInMemory = builder.Configuration.GetValue<bool>("Hangfire:InMemory");
|
||||
@@ -105,7 +106,7 @@ builder.Services.AddSwaggerGen();
|
||||
|
||||
// Add health checks
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck<ProfileWorker>("profile-worker", tags: new[] { "ready", "worker" });
|
||||
.AddCheck<ProfileWorker>("profile-worker", tags: ["ready", "worker"]);
|
||||
|
||||
// Add Serilog.UI with SQLite provider - use same path from configuration
|
||||
var serilogUiLogDirectory = builder.Configuration.GetValue<string>("Application:LogDirectory")
|
||||
@@ -123,6 +124,8 @@ builder.Services.AddSerilogUi(logUIOpt =>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -171,7 +174,7 @@ app.MapHealthChecks("/health", new Microsoft.AspNetCore.Diagnostics.HealthChecks
|
||||
var result = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
status = report.Status.ToString(),
|
||||
timestamp = DateTime.UtcNow,
|
||||
timestamp = DateTime.Now,
|
||||
checks = report.Entries.Select(e => new
|
||||
{
|
||||
name = e.Key,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<WebPublishMethod>Package</WebPublishMethod>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<ProjectGuid>c60bc965-d293-ea64-b153-1941f0648df4</ProjectGuid>
|
||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\JobRunner\PreRelease\WebCron\net8\$(Version)\JobRunner.WebCron.zip</DesktopBuildPackageLocation>
|
||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||
<DeployIisAppPath>JobRunner.WebCron</DeployIisAppPath>
|
||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"SDD-VMP04-SQL17": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;Encrypt=false;TrustServerCertificate=True;"
|
||||
"SDD-VMP04-SQL17": "Server=SDD-VHP04-SQL19\\DD_TESTING01;Database=DD_ECM;User Id=sa;Password=123456789dD!;Encrypt=false;TrustServerCertificate=True;"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"HostingOptions": {
|
||||
@@ -23,5 +23,28 @@
|
||||
},
|
||||
"ProfileWorker": {
|
||||
"IntervalMS": 60000
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user