Skip to content

Commit

Permalink
Add Fault Code, Category and Type (#5362)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
sfmskywalker committed May 10, 2024
1 parent 016fa22 commit a290896
Show file tree
Hide file tree
Showing 23 changed files with 149 additions and 31 deletions.
Expand Up @@ -41,7 +41,7 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context
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);
}
Expand Down
Expand Up @@ -23,12 +23,12 @@ public DispatchAlterationJobs(Variable<string> planId, [CallerFilePath] string?
{
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>
Expand All @@ -47,7 +47,7 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context
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;
Expand All @@ -65,7 +65,7 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context
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);
Expand Down
Expand Up @@ -11,12 +11,8 @@
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;

Expand Down Expand Up @@ -68,7 +64,7 @@ private async Task<AlterationPlan> GetPlanAsync(ActivityExecutionContext context
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;
}
Expand Down
@@ -0,0 +1,6 @@
namespace Elsa.Alterations;

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

public static class AlterationFaultCodes
{
public const string PlanNotFound = "PlanNotFound";
}
@@ -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>
2 changes: 1 addition & 1 deletion src/modules/Elsa.Http/Activities/WriteFileHttpResponse.cs
Expand Up @@ -303,7 +303,7 @@ private async ValueTask OnResumeAsync(ActivityExecutionContext context)
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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/modules/Elsa.Http/Activities/WriteHttpResponse.cs
Expand Up @@ -86,7 +86,7 @@ private async ValueTask OnResumeAsync(ActivityExecutionContext context)
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);
Expand Down
6 changes: 6 additions & 0 deletions src/modules/Elsa.Http/Constants/HttpFaultCategories.cs
@@ -0,0 +1,6 @@
namespace Elsa.Http;

public static class HttpFaultCategories
{
public const string Http = "HTTP";
}
6 changes: 6 additions & 0 deletions src/modules/Elsa.Http/Constants/HttpFaultCodes.cs
@@ -0,0 +1,6 @@
namespace Elsa.Http;

public static class HttpFaultCodes
{
public const string NoHttpContext = "NoHttpContext";
}
3 changes: 2 additions & 1 deletion src/modules/Elsa.Http/Elsa.Http.csproj.DotSettings
@@ -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>
Expand Up @@ -67,7 +67,8 @@ public override async Task HandleAsync(Request request, CancellationToken cancel

if(!result.Succeeded)
{
AddError(result.ErrorMessage!);
var fault = result.Fault!;
AddError(fault.Message, fault.Code);
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
Expand Down
45 changes: 42 additions & 3 deletions src/modules/Elsa.Workflows.Core/Activities/Fault.cs
Expand Up @@ -4,6 +4,7 @@
using Elsa.Workflows.Exceptions;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
// ReSharper disable ExplicitCallerInfoArgument

namespace Elsa.Workflows.Activities;

Expand All @@ -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);
}
}
@@ -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";
}
Expand Up @@ -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>
21 changes: 20 additions & 1 deletion src/modules/Elsa.Workflows.Core/Exceptions/FaultException.cs
Expand Up @@ -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; }
}
Expand Up @@ -221,9 +221,7 @@ private async ValueTask<string> DispatchChildWorkflowAsync(ActivityExecutionCont
};

var dispatchResponse = await workflowDispatcher.DispatchAsync(request, options, context.CancellationToken);

if (!dispatchResponse.Succeeded)
throw new FaultException(dispatchResponse.ErrorMessage);
dispatchResponse.ThrowIfFailed();

return instanceId;
}
Expand Down
Expand Up @@ -123,9 +123,7 @@ private async ValueTask<string> DispatchChildWorkflowAsync(ActivityExecutionCont

// 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;
}
Expand Down
@@ -0,0 +1,6 @@
namespace Elsa.Workflows.Runtime;

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

public static class RuntimeFaultCodes
{
public const string UnknownChannel = "UnknownChannel";
}
@@ -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>
@@ -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;
}
}
Expand Up @@ -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");
Expand All @@ -20,7 +18,7 @@ protected override void Build(IWorkflowBuilder builder)
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;

Expand Down

0 comments on commit a290896

Please sign in to comment.