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;