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.
This commit is contained in:
Sipke Schoorstra 2024-05-10 15:30:32 +02:00 committed by GitHub
parent 016fa22145
commit a2908960a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 149 additions and 31 deletions

View file

@ -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);
}

View file

@ -23,12 +23,12 @@ public class DispatchAlterationJobs : CodeActivity
{
PlanId = new Input<string>(planId);
}
/// <inheritdoc />
public DispatchAlterationJobs([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The ID of the alteration plan.
/// </summary>
@ -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<IAlterationJobDispatcher>();
foreach (var jobId in alterationJobIds)
await alterationJobDispatcher.DispatchAsync(jobId, cancellationToken);
// Update status.
plan.Status = AlterationPlanStatus.Running;
await alterationPlanStore.SaveAsync(plan, cancellationToken);

View file

@ -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<int>
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;
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Alterations;
public static class AlterationFaultCategories
{
public const string Alteration = "Alteration";
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Alterations;
public static class AlterationFaultCodes
{
public const string PlanNotFound = "PlanNotFound";
}

View file

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=constants/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -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);
}

View file

@ -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);

View file

@ -0,0 +1,6 @@
namespace Elsa.Http;
public static class HttpFaultCategories
{
public const string Http = "HTTP";
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Http;
public static class HttpFaultCodes
{
public const string NoHttpContext = "NoHttpContext";
}

View file

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Chttpendpoint/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Chttptrigger/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Chttptrigger/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=constants/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -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;
}

View file

@ -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
{
/// <inheritdoc />
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)
{
}
/// <summary>
/// Creates a fault activity.
/// </summary>
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)
};
}
/// <summary>
/// Code to categorize the fault.
/// </summary>
[Input(Description = "Code to categorize the fault.")]
public Input<string> Code { get; set; } = default!;
/// <summary>
/// Category to categorize the fault. Examples: HTTP, Alteration, Azure, etc.
/// </summary>
[Input(Description = "Category to categorize the fault. Examples: HTTP, Alteration, Azure, etc.")]
public Input<string> Category { get; set; } = default!;
/// <summary>
/// The type of fault. Examples: System, Business, Integration, etc.
/// </summary>
[Input(
DisplayName = "Type",
Description = "The type of fault. Examples: System, Business, Integration, etc."
)]
public Input<string> FaultType { get; set; } = default!;
/// <summary>
/// The message to include with the fault.
/// </summary>
[Input(Description = "The message to include with the fault.")]
public Input<string> Message { get; set; } = default!;
public Input<string?> Message { get; set; } = default!;
/// <inheritdoc />
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);
}
}

View file

@ -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";
}

View file

@ -7,6 +7,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Cprimitives_005Csetname/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Csignaling_005Cactivities_005Csignalreceived/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Cworkflows_005Cfreeflowchart/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=constants/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=contexts/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=enums/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -6,7 +6,26 @@ namespace Elsa.Workflows.Exceptions;
public class FaultException : Exception
{
/// <inheritdoc />
public FaultException(string? message) : base(message)
public FaultException(string code, string category, string type, string? message) : base(message)
{
Code = code;
Category = category;
Type = type;
}
/// <summary>
/// Code that identifies the fault type.
/// </summary>
public string Code { get; }
/// <summary>
/// Category to categorize the fault. E.g. "HTTP", "Alteration", "Azure", etc.
/// This is used to distinguish error codes between modules.
/// </summary>
public string Category { get; }
/// <summary>
/// Kind of fault. E.g. "System", "Business", "Integration", etc.
/// </summary>
public string Type { get; }
}

View file

@ -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;
}

View file

@ -123,9 +123,7 @@ public class DispatchWorkflow : Activity<object>
// 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;
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Workflows.Runtime;
public static class RuntimeFaultCategories
{
public const string Dispatch = "Dispatch";
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Workflows.Runtime;
public static class RuntimeFaultCodes
{
public const string UnknownChannel = "UnknownChannel";
}

View file

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=constants/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -1,18 +1,37 @@
using Elsa.Workflows.Exceptions;
namespace Elsa.Workflows.Runtime.Responses;
/// <summary>
/// Represents the response of a dispatch action for a workflow definition.
/// </summary>
public record DispatchWorkflowResponse(bool Succeeded, string? ErrorMessage)
public record DispatchWorkflowResponse(FaultException? Fault)
{
/// <summary>
/// Creates a response indicating that the dispatch action was successful.
/// </summary>
/// <returns></returns>
public static DispatchWorkflowResponse Success() => new DispatchWorkflowResponse(true, default);
public static DispatchWorkflowResponse Success() => new(default(FaultException?));
/// <summary>
/// Creates a response indicating that the specified channel does not exist.
/// </summary>
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."));
/// <summary>
/// Gets a value indicating whether the dispatch of a workflow definition succeeded.
/// </summary>
/// <value>
/// <c>true</c> if the dispatch succeeded; otherwise, <c>false</c>.
/// </value>
public bool Succeeded => Fault == null;
/// <summary>
/// Throws an exception if the dispatch failed.
/// </summary>
public void ThrowIfFailed()
{
if (Fault != null)
throw Fault;
}
}

View file

@ -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;