From a2908960a47935d7b2130ee86d0c7069a17e4ce8 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 10 May 2024 15:30:32 +0200 Subject: [PATCH] Add Fault Code, Category and Type (#5362) * Add error handling with fault categories and codes The code now includes error handling through the introduction of fault categories and codes. New files containing constants for fault categories and codes have been added for different modules. FaultException has also been updated to include these properties. Changes are evident in various files where FaultException is thrown for error handling. * Add DotSettings file for Elsa.Alterations module A new DotSettings file is added for the Elsa.Alterations module. This includes configuration for namespace folders to be skipped during CodeInspection. * Renamed "DefaultFaultKinds" to "DefaultFaultTypes" and updated usages This commit renames the class "DefaultFaultKinds" to "DefaultFaultTypes" and updates all its references across the project files. The change is made keeping the more accurate naming context i.e., 'Types' suits better in the thrown exception scenarios. --- .../Activities/CompleteAlterationPlan.cs | 2 +- .../Activities/DispatchAlterationJobs.cs | 8 ++-- .../Activities/GenerateAlterationJobs.cs | 6 +-- .../Constants/AlterationFaultCategories.cs | 6 +++ .../Constants/AlterationFaultCodes.cs | 6 +++ .../Elsa.Alterations.csproj.DotSettings | 2 + .../Activities/WriteFileHttpResponse.cs | 2 +- .../Elsa.Http/Activities/WriteHttpResponse.cs | 2 +- .../Constants/HttpFaultCategories.cs | 6 +++ .../Elsa.Http/Constants/HttpFaultCodes.cs | 6 +++ .../Elsa.Http/Elsa.Http.csproj.DotSettings | 3 +- .../WorkflowDefinitions/Dispatch/Endpoint.cs | 3 +- .../Elsa.Workflows.Core/Activities/Fault.cs | 45 +++++++++++++++++-- .../Constants/DefaultFaultTypes.cs | 8 ++++ .../Elsa.Workflows.Core.csproj.DotSettings | 1 + .../Exceptions/FaultException.cs | 21 ++++++++- .../Activities/BulkDispatchWorkflows.cs | 4 +- .../Activities/DispatchWorkflow.cs | 4 +- .../Constants/RuntimeFaultCategories.cs | 6 +++ .../Constants/RuntimeFaultCodes.cs | 6 +++ .../Elsa.Workflows.Runtime.csproj.DotSettings | 2 + .../DispatchWorkflowDefinitionResponse.cs | 27 +++++++++-- .../Incidents/Workflows/FaultyWorkflow.cs | 4 +- 23 files changed, 149 insertions(+), 31 deletions(-) create mode 100644 src/modules/Elsa.Alterations/Constants/AlterationFaultCategories.cs create mode 100644 src/modules/Elsa.Alterations/Constants/AlterationFaultCodes.cs create mode 100644 src/modules/Elsa.Alterations/Elsa.Alterations.csproj.DotSettings create mode 100644 src/modules/Elsa.Http/Constants/HttpFaultCategories.cs create mode 100644 src/modules/Elsa.Http/Constants/HttpFaultCodes.cs create mode 100644 src/modules/Elsa.Workflows.Core/Constants/DefaultFaultTypes.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCategories.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCodes.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings diff --git a/src/modules/Elsa.Alterations/Activities/CompleteAlterationPlan.cs b/src/modules/Elsa.Alterations/Activities/CompleteAlterationPlan.cs index c4b664e2e..b63cd9715 100644 --- a/src/modules/Elsa.Alterations/Activities/CompleteAlterationPlan.cs +++ b/src/modules/Elsa.Alterations/Activities/CompleteAlterationPlan.cs @@ -41,7 +41,7 @@ public class CompleteAlterationPlan : CodeActivity var plan = await manager.GetPlanAsync(planId, cancellationToken); if (plan == null) - throw new FaultException($"Alteration Plan with ID {planId} not found."); + throw new FaultException(AlterationFaultCodes.PlanNotFound, AlterationFaultCategories.Alteration, DefaultFaultTypes.System, $"Alteration Plan with ID {planId} not found."); await manager.CompletePlanAsync(plan, cancellationToken); } diff --git a/src/modules/Elsa.Alterations/Activities/DispatchAlterationJobs.cs b/src/modules/Elsa.Alterations/Activities/DispatchAlterationJobs.cs index 495d7ea3d..a8c2c6fe5 100644 --- a/src/modules/Elsa.Alterations/Activities/DispatchAlterationJobs.cs +++ b/src/modules/Elsa.Alterations/Activities/DispatchAlterationJobs.cs @@ -23,12 +23,12 @@ public class DispatchAlterationJobs : CodeActivity { PlanId = new Input(planId); } - + /// public DispatchAlterationJobs([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - + /// /// The ID of the alteration plan. /// @@ -47,7 +47,7 @@ public class DispatchAlterationJobs : CodeActivity var plan = await alterationPlanStore.FindAsync(planFilter, cancellationToken); if (plan == null) - throw new FaultException($"Alteration Plan with ID {planId} not found."); + throw new FaultException(AlterationFaultCodes.PlanNotFound, AlterationFaultCategories.Alteration, DefaultFaultTypes.System, $"Alteration Plan with ID {planId} not found."); // Update status. plan.Status = AlterationPlanStatus.Dispatching; @@ -65,7 +65,7 @@ public class DispatchAlterationJobs : CodeActivity var alterationJobDispatcher = context.GetRequiredService(); foreach (var jobId in alterationJobIds) await alterationJobDispatcher.DispatchAsync(jobId, cancellationToken); - + // Update status. plan.Status = AlterationPlanStatus.Running; await alterationPlanStore.SaveAsync(plan, cancellationToken); diff --git a/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs b/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs index b56d9354d..fd2357501 100644 --- a/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs +++ b/src/modules/Elsa.Alterations/Activities/GenerateAlterationJobs.cs @@ -11,12 +11,8 @@ using Elsa.Workflows; using Elsa.Workflows.Attributes; using Elsa.Workflows.Contracts; using Elsa.Workflows.Exceptions; -using Elsa.Workflows.Management.Contracts; -using Elsa.Workflows.Management.Filters; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; -using Elsa.Workflows.Runtime.Contracts; -using Elsa.Workflows.Runtime.Filters; namespace Elsa.Alterations.Activities; @@ -68,7 +64,7 @@ public class GenerateAlterationJobs : CodeActivity var plan = await alterationPlanStore.FindAsync(planFilter, cancellationToken); if (plan == null) - throw new FaultException($"Alteration Plan with ID {planId} not found."); + throw new FaultException(AlterationFaultCodes.PlanNotFound, AlterationFaultCategories.Alteration, DefaultFaultTypes.System, $"Alteration Plan with ID {planId} not found."); return plan; } diff --git a/src/modules/Elsa.Alterations/Constants/AlterationFaultCategories.cs b/src/modules/Elsa.Alterations/Constants/AlterationFaultCategories.cs new file mode 100644 index 000000000..dc528751e --- /dev/null +++ b/src/modules/Elsa.Alterations/Constants/AlterationFaultCategories.cs @@ -0,0 +1,6 @@ +namespace Elsa.Alterations; + +public static class AlterationFaultCategories +{ + public const string Alteration = "Alteration"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Constants/AlterationFaultCodes.cs b/src/modules/Elsa.Alterations/Constants/AlterationFaultCodes.cs new file mode 100644 index 000000000..f3ea75e92 --- /dev/null +++ b/src/modules/Elsa.Alterations/Constants/AlterationFaultCodes.cs @@ -0,0 +1,6 @@ +namespace Elsa.Alterations; + +public static class AlterationFaultCodes +{ + public const string PlanNotFound = "PlanNotFound"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Alterations/Elsa.Alterations.csproj.DotSettings b/src/modules/Elsa.Alterations/Elsa.Alterations.csproj.DotSettings new file mode 100644 index 000000000..fa7725297 --- /dev/null +++ b/src/modules/Elsa.Alterations/Elsa.Alterations.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs b/src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs index 7d59eab75..e12de62c9 100644 --- a/src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs +++ b/src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs @@ -303,7 +303,7 @@ public class WriteFileHttpResponse : Activity var httpContext = httpContextAccessor.HttpContext; if (httpContext == null) - throw new FaultException("Cannot execute in a non-HTTP context"); + throw new FaultException(HttpFaultCodes.NoHttpContext, HttpFaultCategories.Http, DefaultFaultTypes.System, "Cannot execute in a non-HTTP context"); await WriteResponseAsync(context, httpContext); } diff --git a/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs b/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs index 1b1c6b92a..ab6c43685 100644 --- a/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs +++ b/src/modules/Elsa.Http/Activities/WriteHttpResponse.cs @@ -86,7 +86,7 @@ public class WriteHttpResponse : Activity if (httpContext == null) { // We're not in an HTTP context, so let's fail. - throw new FaultException("Cannot execute in a non-HTTP context"); + throw new FaultException(HttpFaultCodes.NoHttpContext, HttpFaultCategories.Http, DefaultFaultTypes.System, "Cannot execute in a non-HTTP context"); } await WriteResponseAsync(context, httpContext.Response); diff --git a/src/modules/Elsa.Http/Constants/HttpFaultCategories.cs b/src/modules/Elsa.Http/Constants/HttpFaultCategories.cs new file mode 100644 index 000000000..ff3d0ce06 --- /dev/null +++ b/src/modules/Elsa.Http/Constants/HttpFaultCategories.cs @@ -0,0 +1,6 @@ +namespace Elsa.Http; + +public static class HttpFaultCategories +{ + public const string Http = "HTTP"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Constants/HttpFaultCodes.cs b/src/modules/Elsa.Http/Constants/HttpFaultCodes.cs new file mode 100644 index 000000000..b67d1f75c --- /dev/null +++ b/src/modules/Elsa.Http/Constants/HttpFaultCodes.cs @@ -0,0 +1,6 @@ +namespace Elsa.Http; + +public static class HttpFaultCodes +{ + public const string NoHttpContext = "NoHttpContext"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/Elsa.Http.csproj.DotSettings b/src/modules/Elsa.Http/Elsa.Http.csproj.DotSettings index 875c6b6a6..45ef2789e 100644 --- a/src/modules/Elsa.Http/Elsa.Http.csproj.DotSettings +++ b/src/modules/Elsa.Http/Elsa.Http.csproj.DotSettings @@ -1,4 +1,5 @@  True True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs index bf33ed80c..924a864c4 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Dispatch/Endpoint.cs @@ -67,7 +67,8 @@ internal class Endpoint(IWorkflowDefinitionStore store, IWorkflowDispatcher work if(!result.Succeeded) { - AddError(result.ErrorMessage!); + var fault = result.Fault!; + AddError(fault.Message, fault.Code); await SendErrorsAsync(cancellation: cancellationToken); return; } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Fault.cs b/src/modules/Elsa.Workflows.Core/Activities/Fault.cs index eac4bb722..336c4ffe1 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Fault.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Fault.cs @@ -4,6 +4,7 @@ using Elsa.Workflows.Attributes; using Elsa.Workflows.Exceptions; using Elsa.Workflows.Models; using JetBrains.Annotations; +// ReSharper disable ExplicitCallerInfoArgument namespace Elsa.Workflows.Activities; @@ -15,20 +16,58 @@ namespace Elsa.Workflows.Activities; public class Fault : Activity { /// - public Fault([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + public Fault([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } + + /// + /// Creates a fault activity. + /// + public static Fault Create(string code, string category, string type, string? message = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) + { + return new Fault(source, line) + { + Code = new(code), + Message = new(message), + Category = new(category), + FaultType = new(type) + }; + } + + /// + /// Code to categorize the fault. + /// + [Input(Description = "Code to categorize the fault.")] + public Input Code { get; set; } = default!; + + /// + /// Category to categorize the fault. Examples: HTTP, Alteration, Azure, etc. + /// + [Input(Description = "Category to categorize the fault. Examples: HTTP, Alteration, Azure, etc.")] + public Input Category { get; set; } = default!; + + /// + /// The type of fault. Examples: System, Business, Integration, etc. + /// + [Input( + DisplayName = "Type", + Description = "The type of fault. Examples: System, Business, Integration, etc." + )] + public Input FaultType { get; set; } = default!; /// /// The message to include with the fault. /// [Input(Description = "The message to include with the fault.")] - public Input Message { get; set; } = default!; + public Input Message { get; set; } = default!; /// protected override void Execute(ActivityExecutionContext context) { + var code = Code.GetOrDefault(context) ?? "0"; + var category = Category.GetOrDefault(context) ?? "General"; + var type = FaultType.GetOrDefault(context) ?? "System"; var message = Message.GetOrDefault(context); - throw new FaultException(message); + throw new FaultException(code, category, type, message); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Constants/DefaultFaultTypes.cs b/src/modules/Elsa.Workflows.Core/Constants/DefaultFaultTypes.cs new file mode 100644 index 000000000..400ec3f03 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Constants/DefaultFaultTypes.cs @@ -0,0 +1,8 @@ +namespace Elsa.Workflows; + +public static class DefaultFaultTypes +{ + public const string System = "System"; + public const string Business = "Business"; + public const string Integration = "Integration"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings index d55c6dda2..3df78c204 100644 --- a/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Core/Elsa.Workflows.Core.csproj.DotSettings @@ -7,6 +7,7 @@ True True True + True True True True \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Exceptions/FaultException.cs b/src/modules/Elsa.Workflows.Core/Exceptions/FaultException.cs index 3017616e5..31a260341 100644 --- a/src/modules/Elsa.Workflows.Core/Exceptions/FaultException.cs +++ b/src/modules/Elsa.Workflows.Core/Exceptions/FaultException.cs @@ -6,7 +6,26 @@ namespace Elsa.Workflows.Exceptions; public class FaultException : Exception { /// - public FaultException(string? message) : base(message) + public FaultException(string code, string category, string type, string? message) : base(message) { + Code = code; + Category = category; + Type = type; } + + /// + /// Code that identifies the fault type. + /// + public string Code { get; } + + /// + /// Category to categorize the fault. E.g. "HTTP", "Alteration", "Azure", etc. + /// This is used to distinguish error codes between modules. + /// + public string Category { get; } + + /// + /// Kind of fault. E.g. "System", "Business", "Integration", etc. + /// + public string Type { get; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs index 485dc5639..3db53171e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/BulkDispatchWorkflows.cs @@ -221,9 +221,7 @@ public class BulkDispatchWorkflows : Activity }; var dispatchResponse = await workflowDispatcher.DispatchAsync(request, options, context.CancellationToken); - - if (!dispatchResponse.Succeeded) - throw new FaultException(dispatchResponse.ErrorMessage); + dispatchResponse.ThrowIfFailed(); return instanceId; } diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs index b57711595..5494f28da 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs @@ -123,9 +123,7 @@ public class DispatchWorkflow : Activity // Dispatch the child workflow. var dispatchResponse = await workflowDispatcher.DispatchAsync(request, options, context.CancellationToken); - - if (!dispatchResponse.Succeeded) - throw new FaultException(dispatchResponse.ErrorMessage); + dispatchResponse.ThrowIfFailed(); return instanceId; } diff --git a/src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCategories.cs b/src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCategories.cs new file mode 100644 index 000000000..4da6a4fda --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCategories.cs @@ -0,0 +1,6 @@ +namespace Elsa.Workflows.Runtime; + +public static class RuntimeFaultCategories +{ + public const string Dispatch = "Dispatch"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCodes.cs b/src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCodes.cs new file mode 100644 index 000000000..2e29a83ca --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Constants/RuntimeFaultCodes.cs @@ -0,0 +1,6 @@ +namespace Elsa.Workflows.Runtime; + +public static class RuntimeFaultCodes +{ + public const string UnknownChannel = "UnknownChannel"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings new file mode 100644 index 000000000..fa7725297 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Responses/DispatchWorkflowDefinitionResponse.cs b/src/modules/Elsa.Workflows.Runtime/Responses/DispatchWorkflowDefinitionResponse.cs index 744eeb8a2..a51c0e2f4 100644 --- a/src/modules/Elsa.Workflows.Runtime/Responses/DispatchWorkflowDefinitionResponse.cs +++ b/src/modules/Elsa.Workflows.Runtime/Responses/DispatchWorkflowDefinitionResponse.cs @@ -1,18 +1,37 @@ +using Elsa.Workflows.Exceptions; + namespace Elsa.Workflows.Runtime.Responses; /// /// Represents the response of a dispatch action for a workflow definition. /// -public record DispatchWorkflowResponse(bool Succeeded, string? ErrorMessage) +public record DispatchWorkflowResponse(FaultException? Fault) { /// /// Creates a response indicating that the dispatch action was successful. /// /// - public static DispatchWorkflowResponse Success() => new DispatchWorkflowResponse(true, default); - + public static DispatchWorkflowResponse Success() => new(default(FaultException?)); + /// /// Creates a response indicating that the specified channel does not exist. /// - public static DispatchWorkflowResponse UnknownChannel() => new DispatchWorkflowResponse(false, "The specified channel does not exist."); + public static DispatchWorkflowResponse UnknownChannel() => new(new FaultException(RuntimeFaultCodes.UnknownChannel, RuntimeFaultCategories.Dispatch, DefaultFaultTypes.System, "The specified channel does not exist.")); + + /// + /// Gets a value indicating whether the dispatch of a workflow definition succeeded. + /// + /// + /// true if the dispatch succeeded; otherwise, false. + /// + public bool Succeeded => Fault == null; + + /// + /// Throws an exception if the dispatch failed. + /// + public void ThrowIfFailed() + { + if (Fault != null) + throw Fault; + } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Incidents/Workflows/FaultyWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Incidents/Workflows/FaultyWorkflow.cs index ab175d157..ce3f08d97 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Incidents/Workflows/FaultyWorkflow.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Incidents/Workflows/FaultyWorkflow.cs @@ -9,8 +9,6 @@ namespace Elsa.Workflows.IntegrationTests.Scenarios.Incidents.Workflows; public class FaultyWorkflow : WorkflowBase { - - protected override void Build(IWorkflowBuilder builder) { var start = new WriteLine("Start"); @@ -20,7 +18,7 @@ public class FaultyWorkflow : WorkflowBase var step2A = new WriteLine("Step 2a"); var event2 = new Event("Event 2"); var step2B = new WriteLine("Step 2b"); - var fault = new Fault("Whoops!"); + var fault = Fault.Create("Whoops!", "Test", "Test"); builder.WorkflowOptions.IncidentStrategyType = TestSettings.IncidentStrategyType;