Add IResilienceStrategy Abstraction with Category Matching and Expression-Based Configuration (#6637)
* Add resilience module with core interfaces and services Introduced a new `Elsa.Resilience` module and its core components to support resilient services and activities. This includes resilience strategies, providers, and attributes, along with integration into the existing HTTP module for enhanced fault tolerance. Added solution and project references for proper dependency management. * Add resilience strategy framework with HTTP strategy support Introduced a resilience strategy architecture, including a configurable `HttpResilienceStrategy` with retry capabilities, strategy serialization, and integration with existing modules. Enhanced ResilienceFeature to support registration of strategy types and updated application configuration to enable resilience strategies. This change ensures more robust and fault-tolerant HTTP request handling. * Add JSON serialization support for resilience configuration Introduced `ConfigurationExtensions` to enable JSON serialization of configuration sections. Updated resilience strategies to utilize the new extension methods and adjusted JSON serialization logic to support polymorphism with `$type` discriminator. Minor modifications were made to support deserialization and property mutability. * Add resilience strategy support to workflows and API clients Introduced resilience strategy configuration, serialization, and execution support across workflows and API clients. Added new APIs, models, and services to enhance fault tolerance capabilities for activities and HTTP interactions. * Refactor resilience services for improved modularity. Replaced `IResilienceService` with new modular interfaces (`IResilienceStrategyCatalog`, `IResilienceStrategyConfigEvaluator`, `IResilientActivityInvoker`) and corresponding implementations. Enhanced maintainability by simplifying components and responsibilities, ensuring better separation of concerns. * Rename methods in ResilienceStrategyCatalog for clarity Updated method names in `ResilienceStrategyCatalog` and its interfaces for better readability and alignment with naming conventions. Replaced `GetAllStrategiesAsync` with `ListAsync` and `GetStrategyAsync` with `GetAsync` across the codebase. * Refactor resilience handling in HTTP activities. Replaced `ResilienceCategory` property with `ResilienceCategoryAttribute` for a cleaner implementation. Updated `IResilientActivity` to simplify its interface and adjusted related modifications accordingly. Introduced `IResilientActivityInvoker` to enhance resilience strategy execution. * Add support for additional resilience and scripting features Extended resilience strategy handling with serialization support, added `HttpResilienceStrategy` type in JavaScript handler, and refined object conversion logic for interfaces. Minor adjustments to `Expression` class properties for consistency. * Remove `UseResilience` call from Program.cs This call was redundant and no longer necessary for the application. Its removal simplifies the code and ensures only required middleware is used. * Remove commented-out JSON converter code in serializer setup Cleaned up unused and commented-out converter initialization code in `ResilienceStrategySerializer`. This improves readability and removes unnecessary clutter from the file. * Fix typo in method names from 'Resiliency' to 'Resilience' Renamed methods to maintain consistency in naming conventions across the codebase. This change ensures clarity and alignment with established terminology. * Refactor namespace for ConfigurationResilienceStrategySource Updated the namespace of ConfigurationResilienceStrategySource to "StrategySources" for better alignment with naming conventions and structure. Removed an unused namespace reference in ResilienceFeature for cleanup. * Mark EnableResiliency as obsolete in SendHttpRequestBase. The EnableResiliency property is now marked with the [Obsolete] attribute. Developers are encouraged to use the common Resilience Strategy setting instead for managing HTTP request resiliency. This change ensures better consistency and alignment with the broader resilience strategy. * Restrict ResilienceCategoryAttribute to class targets only Removed support for using ResilienceCategoryAttribute on properties. This change enforces a stricter and more focused usage of the attribute, ensuring it applies only to class-level declarations. * Add documentation for IResilientActivityInvoker interface Include summaries and parameter descriptions for the `InvokeAsync` method. This improves code clarity and helps developers understand the functionality and usage of the resilient activity invocation process. * Fix logical operator precedence in type comparison check Parentheses were added to ensure correct evaluation of conditions when checking type compatibility. This prevents potential logical errors when determining the target type in object conversions. * Add support for resilience source identification Introduce the `ResilienceSourceNameAttribute` to allow naming of resilience sources. Updated `ResilienceStrategyCatalog` to utilize the attribute for prefixing strategy IDs, improving source identification and traceability. Applied the attribute to `ConfigurationResilienceStrategySource` as an example. * Revert "Add support for resilience source identification" This reverts commit 19b4e7121d6330b5de4f692b78da4c1e4a2d1f67. * Reapply "Add support for resilience source identification" This reverts commit 8bcba9d040c4eb247077aec6d90dc02817adcbd5. * Revert "Reapply "Add support for resilience source identification"" This reverts commit ee04d35e7930956c752dda3ed150ca34a535e66c.
This commit is contained in:
parent
81cd6b0d5e
commit
9aa239719d
17
Elsa.sln
17
Elsa.sln
|
|
@ -397,6 +397,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bounty", "bounty", "{9B80A7
|
|||
doc\bounty\bounty-quality-standards.md = doc\bounty\bounty-quality-standards.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resilience", "resilience", "{CD7DC0D1-FFDC-417A-89BE-7F32408F583E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Resilience.Core", "src\modules\Elsa.Resilience.Core\Elsa.Resilience.Core.csproj", "{70593549-8B26-4D63-9857-6BA8BB3E31DB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Resilience", "src\modules\Elsa.Resilience\Elsa.Resilience.csproj", "{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -821,6 +827,14 @@ Global
|
|||
{DB20B168-CD90-41F2-BE08-3D8C6F142459}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -966,6 +980,9 @@ Global
|
|||
{8A050229-DB79-4E0B-9AFF-7565E87F2954} = {B08B4E00-C2AB-48F3-8389-449F42AEF179}
|
||||
{0A04B1FD-06C0-4271-A910-A08C263DBC44} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6}
|
||||
{9B80A705-2E31-4012-964A-83963DCDB384} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6}
|
||||
{CD7DC0D1-FFDC-417A-89BE-7F32408F583E} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
|
||||
{70593549-8B26-4D63-9857-6BA8BB3E31DB} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E}
|
||||
{E7137FB0-1988-4562-AD8D-D0D9D2EE85F6} = {CD7DC0D1-FFDC-417A-89BE-7F32408F583E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ using Elsa.MongoDb.Modules.Management;
|
|||
using Elsa.MongoDb.Modules.Runtime;
|
||||
using Elsa.MongoDb.Modules.Tenants;
|
||||
using Elsa.OpenTelemetry.Middleware;
|
||||
using Elsa.Resilience.Extensions;
|
||||
using Elsa.Retention.Extensions;
|
||||
using Elsa.Retention.Models;
|
||||
using Elsa.Secrets.Extensions;
|
||||
|
|
@ -105,9 +106,9 @@ const bool useTenantsFromConfiguration = true;
|
|||
const bool useSecrets = false;
|
||||
const bool disableVariableWrappers = false;
|
||||
const bool disableVariableCopying = false;
|
||||
const bool useManualOtelInstrumentation = true;
|
||||
const bool useManualOtelInstrumentation = false;
|
||||
|
||||
ObjectConverter.StrictMode = false;
|
||||
ObjectConverter.StrictMode = true;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var services = builder.Services;
|
||||
|
|
|
|||
|
|
@ -271,5 +271,23 @@
|
|||
"Url": "https://localhost:6001/webhooks/run-task"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Resilience": {
|
||||
"Strategies": [
|
||||
{
|
||||
"$type": "HttpResilienceStrategy",
|
||||
"Id": "1",
|
||||
"DisplayName": "Retry 3 times",
|
||||
"RetryCount": 3,
|
||||
"BackoffFactor": 2.0
|
||||
},
|
||||
{
|
||||
"$type": "HttpResilienceStrategy",
|
||||
"Id": "2",
|
||||
"DisplayName": "Retry 10 times",
|
||||
"RetryCount": 10,
|
||||
"BackoffFactor": 2.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Elsa.Api.Client.Resources.ResilienceStrategies.Models;
|
||||
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
|
||||
using Elsa.Api.Client.Shared.Models;
|
||||
|
||||
|
|
@ -193,4 +194,23 @@ public static class ActivityExtensions
|
|||
/// Sets the commit state behavior for the specified activity.
|
||||
/// </summary>
|
||||
public static void SetCommitStrategy(this JsonObject activity, string? name) => activity.SetProperty(JsonValue.Create(name), "customProperties", "commitStrategyName");
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resilience strategy for the specified activity.
|
||||
/// </summary>
|
||||
public static ResilienceStrategyConfig? GetResilienceStrategy(this JsonObject activity)
|
||||
{
|
||||
var node = activity.GetProperty("customProperties", "resilienceStrategy");
|
||||
return ResilienceStrategyConfig.Deserialize(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the resilience strategy for the specified activity.
|
||||
/// </summary>
|
||||
public static void SetResilienceStrategy(this JsonObject activity, ResilienceStrategyConfig? config)
|
||||
{
|
||||
var node = config?.SerializeToNode();
|
||||
activity.SetProperty(node, "customProperties", "resilienceStrategy");
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ using Elsa.Api.Client.Resources.Features.Contracts;
|
|||
using Elsa.Api.Client.Resources.Identity.Contracts;
|
||||
using Elsa.Api.Client.Resources.IncidentStrategies.Contracts;
|
||||
using Elsa.Api.Client.Resources.LogPersistenceStrategies;
|
||||
using Elsa.Api.Client.Resources.ResilienceStrategies.Contracts;
|
||||
using Elsa.Api.Client.Resources.Scripting.Contracts;
|
||||
using Elsa.Api.Client.Resources.StorageDrivers.Contracts;
|
||||
using Elsa.Api.Client.Resources.Tasks.Contracts;
|
||||
|
|
@ -76,6 +77,7 @@ public static class DependencyInjectionExtensions
|
|||
services.AddApi<IIncidentStrategiesApi>(builderOptions);
|
||||
services.AddApi<ILogPersistenceStrategiesApi>(builderOptions);
|
||||
services.AddApi<ICommitStrategiesApi>(builderOptions);
|
||||
services.AddApi<IResilienceStrategiesApi>(builderOptions);
|
||||
services.AddApi<ILoginApi>(builderOptions);
|
||||
services.AddApi<IFeaturesApi>(builderOptions);
|
||||
services.AddApi<IJavaScriptApi>(builderOptions);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace Elsa.Api.Client.Extensions;
|
||||
|
||||
public static class ResilienceStrategyExtensions
|
||||
{
|
||||
public static string GetResilienceStrategyId(this JsonObject strategyJsonObject)
|
||||
{
|
||||
return strategyJsonObject.GetProperty<string>("id")!;
|
||||
}
|
||||
|
||||
public static string GetResilienceStrategyDisplayName(this JsonObject strategyJsonObject)
|
||||
{
|
||||
return strategyJsonObject.GetProperty<string>("displayName")!;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Elsa.Api.Client.Shared.Models;
|
||||
using Refit;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.ResilienceStrategies.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Provides operations for managing and retrieving resilience strategies within the application.
|
||||
/// </summary>
|
||||
public interface IResilienceStrategiesApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a list of resilience strategies from the resilience strategies API endpoint.
|
||||
/// </summary>
|
||||
/// <param name="category">The category to filter the strategies by.</param>
|
||||
/// <param name="cancellationToken">An optional token to cancel the operation.</param>
|
||||
/// <returns>A <see cref="ListResponse{JsonObject}"/> with the list of resilience strategies.</returns>
|
||||
[Get("/resilience/strategies")]
|
||||
Task<ListResponse<JsonObject>> LisAsync(string category, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Api.Client.Resources.Scripting.Models;
|
||||
using Elsa.Api.Client.Serialization;
|
||||
|
||||
namespace Elsa.Api.Client.Resources.ResilienceStrategies.Models;
|
||||
|
||||
public class ResilienceStrategyConfig
|
||||
{
|
||||
public ResilienceStrategyConfigMode Mode { get; set; }
|
||||
public string? StrategyId { get; set; }
|
||||
public Expression? Expression { get; set; }
|
||||
|
||||
public JsonNode SerializeToNode()
|
||||
{
|
||||
return JsonSerializer.SerializeToNode(this, SerializerOptions.ResilienceStrategyConfigSerializerOptions)!;
|
||||
}
|
||||
|
||||
public static ResilienceStrategyConfig? Deserialize(JsonNode? node)
|
||||
{
|
||||
return node.Deserialize<ResilienceStrategyConfig?>(SerializerOptions.ResilienceStrategyConfigSerializerOptions)!;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace Elsa.Api.Client.Resources.ResilienceStrategies.Models;
|
||||
|
||||
public enum ResilienceStrategyConfigMode
|
||||
{
|
||||
Identifier,
|
||||
Expression,
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ public class Expression
|
|||
/// </summary>
|
||||
/// <param name="type">The type of the expression.</param>
|
||||
/// <param name="value">The expression.</param>
|
||||
public Expression(string type, string? value = default)
|
||||
public Expression(string type, string? value = null)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
|
|
@ -29,7 +29,7 @@ public class Expression
|
|||
/// <summary>
|
||||
/// Gets or sets the expression type.
|
||||
/// </summary>
|
||||
public string Type { get; set; } = default!;
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value representing the expression.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Api.Client.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides options for serializing and deserializing objects.
|
||||
/// </summary>
|
||||
public static class SerializerOptions
|
||||
{
|
||||
public static JsonSerializerOptions CommonSerializerOptions { get; } = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the serializer options for the log persistence configuration.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions LogPersistenceConfigSerializerOptions { get; } = new(CommonSerializerOptions);
|
||||
|
||||
public static JsonSerializerOptions ResilienceStrategyConfigSerializerOptions { get; } = new(CommonSerializerOptions);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ public static class DependencyInjectionExtensions
|
|||
/// <summary>
|
||||
/// Installs and configures the specified feature. If the feature was already installed, it is not added twice, which means it is safe to call this method multiple times.
|
||||
/// </summary>
|
||||
public static IModule Use<T>(this IModule module, Action<T>? configure = default) where T: class, IFeature
|
||||
public static IModule Use<T>(this IModule module, Action<T>? configure = null) where T: class, IFeature
|
||||
{
|
||||
module.Configure(configure);
|
||||
return module;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
public static string GetSectionAsJson(this IConfiguration configuration, string sectionKey, JsonSerializerOptions? options = null)
|
||||
{
|
||||
var section = configuration.GetSection(sectionKey);
|
||||
var element = ConvertToJsonElement(section);
|
||||
return JsonSerializer.Serialize(element, options);
|
||||
}
|
||||
|
||||
private static JsonElement ConvertToJsonElement(IConfigurationSection section)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(ToObject(section)));
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
|
||||
private static object? ToObject(IConfigurationSection section)
|
||||
{
|
||||
var children = section.GetChildren().ToList();
|
||||
if (!children.Any())
|
||||
return section.Value;
|
||||
|
||||
if (children.All(c => int.TryParse(c.Key, out _)))
|
||||
{
|
||||
// Treat as array
|
||||
return children
|
||||
.OrderBy(c => int.Parse(c.Key))
|
||||
.Select(ToObject)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var dict = new Dictionary<string, object?>();
|
||||
foreach (var child in children)
|
||||
{
|
||||
dict[child.Key] = ToObject(child);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,26 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Common.Serialization;
|
||||
|
||||
public static class SerializerOptions
|
||||
{
|
||||
public static JsonSerializerOptions CommonSerializerOptions { get; } = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public static JsonSerializerOptions ConfigurationJsonSerializerOptions { get; } = new()
|
||||
{
|
||||
Converters = { new ConfigurationJsonConverter() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the serializer options for the log persistence configuration.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions LogPersistenceConfigSerializerOptions { get; } = new(CommonSerializerOptions);
|
||||
|
||||
public static JsonSerializerOptions ResilienceStrategyConfigSerializerOptions { get; } = new(CommonSerializerOptions);
|
||||
}
|
||||
|
|
@ -178,7 +178,7 @@ public static class ObjectConverter
|
|||
|
||||
var internalSerializerOptions = InternalSerializerOptions;
|
||||
|
||||
if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && underlyingTargetType.IsClass)
|
||||
if (typeof(IDictionary<string, object>).IsAssignableFrom(underlyingSourceType) && (underlyingTargetType.IsClass || underlyingTargetType.IsInterface))
|
||||
{
|
||||
if (typeof(ExpandoObject) == underlyingTargetType)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Elsa.Http;
|
|||
public class SendHttpRequest : SendHttpRequestBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public SendHttpRequest([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
|
||||
public SendHttpRequest([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Net.Http.Headers;
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.UIHints;
|
||||
using Elsa.Resilience;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Attributes;
|
||||
using Elsa.Workflows.UIHints;
|
||||
|
|
@ -16,7 +17,8 @@ namespace Elsa.Http;
|
|||
/// Base class for activities that send HTTP requests.
|
||||
/// </summary>
|
||||
[Output(IsSerializable = false)]
|
||||
public abstract class SendHttpRequestBase(string? source = null, int? line = null) : Activity<HttpResponseMessage>(source, line)
|
||||
[ResilienceCategory("HTTP")]
|
||||
public abstract class SendHttpRequestBase(string? source = null, int? line = null) : Activity<HttpResponseMessage>(source, line), IResilientActivity
|
||||
{
|
||||
/// <summary>
|
||||
/// The URL to send the request to.
|
||||
|
|
@ -94,6 +96,8 @@ public abstract class SendHttpRequestBase(string? source = null, int? line = nul
|
|||
/// <summary>
|
||||
/// Indicates whether resiliency mechanisms should be enabled for the HTTP request.
|
||||
/// </summary>
|
||||
[Obsolete("Use the common Resilience Strategy setting instead.")]
|
||||
[Input(Description = "Obsolete. Use the common Resilience Strategy setting instead.")]
|
||||
public Input<bool> EnableResiliency { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -145,7 +149,7 @@ public abstract class SendHttpRequestBase(string? source = null, int? line = nul
|
|||
|
||||
try
|
||||
{
|
||||
var response = await SendRequestAsync();
|
||||
var response = await SendRequestAsync(context);
|
||||
var parsedContent = await ParseContentAsync(context, response);
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var responseHeaders = new HttpHeaders(response.Headers);
|
||||
|
|
@ -180,15 +184,17 @@ public abstract class SendHttpRequestBase(string? source = null, int? line = nul
|
|||
|
||||
return;
|
||||
|
||||
async Task<HttpResponseMessage> SendRequestAsync()
|
||||
async Task<HttpResponseMessage> SendRequestAsync(ActivityExecutionContext activityExecutionContext)
|
||||
{
|
||||
// Keep this for backward compatibility.
|
||||
if (resiliencyEnabled)
|
||||
{
|
||||
var pipeline = BuildResiliencyPipeline(context);
|
||||
return await pipeline.ExecuteAsync(async ct => await SendRequestAsyncCore(ct), cancellationToken);
|
||||
}
|
||||
|
||||
return await SendRequestAsyncCore();
|
||||
|
||||
var resilienceService = activityExecutionContext.GetRequiredService<IResilientActivityInvoker>();
|
||||
return await resilienceService.InvokeAsync(this, activityExecutionContext, async () => await SendRequestAsyncCore(cancellationToken), cancellationToken);
|
||||
}
|
||||
|
||||
async Task<HttpResponseMessage> SendRequestAsyncCore(CancellationToken ct = default)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@
|
|||
<PackageReference Include="FluentStorage"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Resilience"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Resilience"/>
|
||||
<PackageReference Include="Polly" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Liquid\Elsa.Liquid.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.Resilience.Core\Elsa.Resilience.Core.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Resilience\Elsa.Resilience.csproj" />
|
||||
<ProjectReference Include="..\Elsa.SasTokens\Elsa.SasTokens.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj"/>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ using Elsa.Http.Handlers;
|
|||
using Elsa.Http.Options;
|
||||
using Elsa.Http.Parsers;
|
||||
using Elsa.Http.PortResolvers;
|
||||
using Elsa.Http.Resilience;
|
||||
using Elsa.Http.Selectors;
|
||||
using Elsa.Http.Services;
|
||||
using Elsa.Http.Tasks;
|
||||
using Elsa.Http.UIHints;
|
||||
using Elsa.Resilience.Extensions;
|
||||
using Elsa.Resilience.Features;
|
||||
using Elsa.Workflows;
|
||||
using FluentStorage;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -28,6 +31,7 @@ namespace Elsa.Http.Features;
|
|||
/// Installs services related to HTTP services and activities.
|
||||
/// </summary>
|
||||
[DependsOn(typeof(HttpJavaScriptFeature))]
|
||||
[DependsOn(typeof(ResilienceFeature))]
|
||||
public class HttpFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
private Func<IServiceProvider, IHttpEndpointRoutesProvider> _httpEndpointRouteProvider = sp => sp.GetRequiredService<DefaultHttpEndpointRoutesProvider>();
|
||||
|
|
@ -137,6 +141,8 @@ public class HttpFeature(IModule module) : FeatureBase(module)
|
|||
|
||||
management.AddActivitiesFrom<HttpFeature>();
|
||||
});
|
||||
|
||||
Module.UseResilience(resilience => resilience.AddResilienceStrategyType<HttpResilienceStrategy>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
26
src/modules/Elsa.Http/Resilience/HttpResilienceStrategy.cs
Normal file
26
src/modules/Elsa.Http/Resilience/HttpResilienceStrategy.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using Elsa.Resilience;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace Elsa.Http.Resilience;
|
||||
|
||||
[ResilienceCategory("HTTP")]
|
||||
public class HttpResilienceStrategy : IResilienceStrategy
|
||||
{
|
||||
public string Id { get; set; } = null!;
|
||||
public string DisplayName { get; set; } = null!;
|
||||
public int RetryCount { get; set; } = 3;
|
||||
public double BackoffFactor { get; set; } = 2.0;
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(Func<Task<T>> action)
|
||||
{
|
||||
AsyncRetryPolicy policy = Policy
|
||||
.Handle<Exception>()
|
||||
.WaitAndRetryAsync(
|
||||
RetryCount,
|
||||
retryAttempt => TimeSpan.FromSeconds(Math.Pow(BackoffFactor, retryAttempt))
|
||||
);
|
||||
|
||||
return await policy.ExecuteAsync(action);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Http.Resilience;
|
||||
using Elsa.JavaScript.Notifications;
|
||||
using Elsa.JavaScript.TypeDefinitions.Builders;
|
||||
using Elsa.JavaScript.TypeDefinitions.Contracts;
|
||||
using Elsa.JavaScript.TypeDefinitions.Models;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Workflows;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Http.Scripting.JavaScript;
|
||||
|
|
@ -30,6 +32,7 @@ public class HttpJavaScriptHandler : INotificationHandler<EvaluatingJavaScript>,
|
|||
var engine = notification.Engine;
|
||||
engine.RegisterType<HttpHeaders>();
|
||||
engine.RegisterType<Downloadable>();
|
||||
engine.RegisterType<HttpResilienceStrategy>();
|
||||
|
||||
var activityExecutionContext = notification.Context;
|
||||
|
||||
|
|
@ -63,6 +66,7 @@ public class HttpJavaScriptHandler : INotificationHandler<EvaluatingJavaScript>,
|
|||
{
|
||||
yield return _typeDescriber.DescribeType(typeof(HttpHeaders));
|
||||
yield return _typeDescriber.DescribeType(typeof(Downloadable));
|
||||
yield return _typeDescriber.DescribeType(typeof(HttpResilienceStrategy));
|
||||
}
|
||||
|
||||
private IEnumerable<FunctionDefinition> GetFunctionDefinitions(TypeDefinitionContext context)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
namespace Elsa.Resilience;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ResilienceCategoryAttribute(string category) : Attribute
|
||||
{
|
||||
public string Category { get; } = category;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategy
|
||||
{
|
||||
string Id { get; set; }
|
||||
string DisplayName { get; set; }
|
||||
Task<T> ExecuteAsync<T>(Func<Task<T>> action);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategyCatalog
|
||||
{
|
||||
Task<IEnumerable<IResilienceStrategy>> ListAsync(CancellationToken cancellationToken = default);
|
||||
Task<IResilienceStrategy?> GetAsync(string id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Resilience.Models;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategyConfigEvaluator
|
||||
{
|
||||
Task<IResilienceStrategy?> EvaluateAsync(ResilienceStrategyConfig? config, ExpressionExecutionContext context, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilienceStrategySource
|
||||
{
|
||||
Task<IEnumerable<IResilienceStrategy>> GetStrategiesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public interface IResilientActivity : IActivity;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to execute an activity's logic with a specified resilience strategy, or execute the action directly if no resilience configuration is applicable.
|
||||
/// </summary>
|
||||
public interface IResilientActivityInvoker
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes a resilient activity execution with the provided action using the activity's selected resilience strategy configuration.
|
||||
/// If no resilience configuration is provided or otherwise does not evaluate to a strategy, the action is called as-is.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result produced by the activity.</typeparam>
|
||||
/// <param name="activity">The resilient activity being invoked.</param>
|
||||
/// <param name="context">The execution context in which the activity runs.</param>
|
||||
/// <param name="action">The action representing the activity's execution logic.</param>
|
||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||
/// <returns>The result of the invoked action.</returns>
|
||||
Task<T> InvokeAsync<T>(IResilientActivity activity, ActivityExecutionContext context, Func<Task<T>> action, CancellationToken cancellationToken = default);
|
||||
}
|
||||
15
src/modules/Elsa.Resilience.Core/Elsa.Resilience.Core.csproj
Normal file
15
src/modules/Elsa.Resilience.Core/Elsa.Resilience.Core.csproj
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides core services and types for implementing resilient services and activities.
|
||||
</Description>
|
||||
<PackageTags>elsa module resilience core</PackageTags>
|
||||
<RootNamespace>Elsa.Resilience</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<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/=attributes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=contracts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
3
src/modules/Elsa.Resilience.Core/FodyWeavers.xml
Normal file
3
src/modules/Elsa.Resilience.Core/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Common.Serialization;
|
||||
using Elsa.Expressions.Models;
|
||||
|
||||
namespace Elsa.Resilience.Models;
|
||||
|
||||
public class ResilienceStrategyConfig
|
||||
{
|
||||
public ResilienceStrategyConfigMode Mode { get; set; }
|
||||
public string? StrategyId { get; set; }
|
||||
public Expression? Expression { get; set; }
|
||||
|
||||
public JsonNode SerializeToNode()
|
||||
{
|
||||
return JsonSerializer.SerializeToNode(this, SerializerOptions.ResilienceStrategyConfigSerializerOptions)!;
|
||||
}
|
||||
|
||||
public static ResilienceStrategyConfig? Deserialize(JsonNode? node)
|
||||
{
|
||||
return node.Deserialize<ResilienceStrategyConfig?>(SerializerOptions.ResilienceStrategyConfigSerializerOptions)!;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace Elsa.Resilience.Models;
|
||||
|
||||
public enum ResilienceStrategyConfigMode
|
||||
{
|
||||
Identifier,
|
||||
Expression,
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Resilience.Options;
|
||||
|
||||
public class ResilienceOptions
|
||||
{
|
||||
public ICollection<Type> StrategyTypes { get; set; } = new List<Type>();
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Elsa.Common.Converters;
|
||||
using Elsa.Resilience.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Resilience.Serialization;
|
||||
|
||||
public class ResilienceStrategySerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
public ResilienceStrategySerializer(IOptions<ResilienceOptions> options)
|
||||
{
|
||||
var serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver().WithAddedModifier(typeInfo =>
|
||||
{
|
||||
if (typeInfo.Type != typeof(IResilienceStrategy))
|
||||
return;
|
||||
|
||||
if (typeInfo.Kind != JsonTypeInfoKind.Object)
|
||||
return;
|
||||
|
||||
var polymorphismOptions = new JsonPolymorphismOptions
|
||||
{
|
||||
TypeDiscriminatorPropertyName = "$type"
|
||||
};
|
||||
|
||||
foreach (var type in options.Value.StrategyTypes.ToList())
|
||||
polymorphismOptions.DerivedTypes.Add(new(type, type.Name));
|
||||
|
||||
typeInfo.PolymorphismOptions = polymorphismOptions;
|
||||
}),
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString
|
||||
};
|
||||
|
||||
_serializerOptions = serializerOptions;
|
||||
}
|
||||
|
||||
public JsonSerializerOptions SerializerOptions => _serializerOptions;
|
||||
public string Serialize(IResilienceStrategy strategy) => JsonSerializer.Serialize(strategy, _serializerOptions);
|
||||
public string SerializeMany(IEnumerable<IResilienceStrategy> strategies) => JsonSerializer.Serialize(strategies, _serializerOptions);
|
||||
public IResilienceStrategy Deserialize(string json) => JsonSerializer.Deserialize<IResilienceStrategy>(json, _serializerOptions)!;
|
||||
public IEnumerable<IResilienceStrategy> DeserializeMany(string json) => JsonSerializer.Deserialize<IEnumerable<IResilienceStrategy>>(json, _serializerOptions)!;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
namespace Elsa.Resilience;
|
||||
|
||||
public class ResilienceStrategyCatalog : IResilienceStrategyCatalog
|
||||
{
|
||||
private readonly Lazy<Task<IEnumerable<IResilienceStrategy>>> _strategies;
|
||||
private readonly IEnumerable<IResilienceStrategySource> _providers;
|
||||
|
||||
public ResilienceStrategyCatalog(IEnumerable<IResilienceStrategySource> providers)
|
||||
{
|
||||
_providers = providers;
|
||||
_strategies = new(GetStrategiesInternalAsync, LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<IResilienceStrategy>> ListAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _strategies.Value;
|
||||
}
|
||||
|
||||
public async Task<IResilienceStrategy?> GetAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var strategies = await ListAsync(cancellationToken);
|
||||
return strategies.FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<IResilienceStrategy>> GetStrategiesInternalAsync()
|
||||
{
|
||||
var strategies = new List<IResilienceStrategy>();
|
||||
foreach (var provider in _providers) strategies.AddRange(await provider.GetStrategiesAsync());
|
||||
return strategies;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using System.Dynamic;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Resilience.Models;
|
||||
using Elsa.Resilience.Serialization;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class ResilienceStrategyConfigEvaluator(IResilienceStrategyCatalog catalog, IExpressionEvaluator expressionEvaluator, ResilienceStrategySerializer strategySerializer) : IResilienceStrategyConfigEvaluator
|
||||
{
|
||||
public async Task<IResilienceStrategy?> EvaluateAsync(ResilienceStrategyConfig? config, ExpressionExecutionContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (config == null) return null;
|
||||
|
||||
var mode = config.Mode;
|
||||
|
||||
return mode switch
|
||||
{
|
||||
ResilienceStrategyConfigMode.Identifier => await ResolveByIdentifierAsync(config, cancellationToken),
|
||||
ResilienceStrategyConfigMode.Expression => await ResolveByExpressionAsync(config, context, cancellationToken),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IResilienceStrategy?> ResolveByIdentifierAsync(ResilienceStrategyConfig config, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var strategyId = config.StrategyId;
|
||||
return string.IsNullOrWhiteSpace(strategyId)
|
||||
? null
|
||||
: await catalog.GetAsync(strategyId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<IResilienceStrategy?> ResolveByExpressionAsync(ResilienceStrategyConfig config, ExpressionExecutionContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var expression = config.Expression;
|
||||
|
||||
if (expression == null)
|
||||
return null;
|
||||
|
||||
var result = await expressionEvaluator.EvaluateAsync<object>(expression, context, ExpressionEvaluatorOptions.Empty);
|
||||
|
||||
return result switch
|
||||
{
|
||||
string strategyId => await catalog.GetAsync(strategyId, cancellationToken),
|
||||
IResilienceStrategy strategy => strategy,
|
||||
ExpandoObject expando => expando.ConvertTo<IResilienceStrategy>(new(strategySerializer.SerializerOptions)),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Resilience.Models;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Resilience;
|
||||
|
||||
public class ResilientActivityInvoker(IResilienceStrategyConfigEvaluator configEvaluator) : IResilientActivityInvoker
|
||||
{
|
||||
private const string ResilienceStrategyIdPropKey = "resilienceStrategy";
|
||||
|
||||
public async Task<T> InvokeAsync<T>(IResilientActivity activity, ActivityExecutionContext context, Func<Task<T>> action, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var strategyConfig = GetStrategyConfig(activity);
|
||||
var strategy = await configEvaluator.EvaluateAsync(strategyConfig, context.ExpressionExecutionContext, cancellationToken);
|
||||
return strategy == null ? await action() : await strategy.ExecuteAsync(action);
|
||||
}
|
||||
|
||||
private ResilienceStrategyConfig? GetStrategyConfig(IResilientActivity resilientActivity)
|
||||
{
|
||||
return !resilientActivity.CustomProperties.TryGetValue(ResilienceStrategyIdPropKey, out var value)
|
||||
? null
|
||||
: value.ConvertTo<ResilienceStrategyConfig>();
|
||||
}
|
||||
}
|
||||
15
src/modules/Elsa.Resilience/Elsa.Resilience.csproj
Normal file
15
src/modules/Elsa.Resilience/Elsa.Resilience.csproj
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides features and services for implementing resilient services and activities.
|
||||
</Description>
|
||||
<PackageTags>elsa module resilience</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Resilience.Core\Elsa.Resilience.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Elsa.Abstractions;
|
||||
using Elsa.Models;
|
||||
using Elsa.Resilience.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Resilience.Endpoints.ResilienceStrategies.List;
|
||||
|
||||
public class Endpoint(IResilienceStrategyCatalog catalog, ResilienceStrategySerializer serializer) : ElsaEndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/resilience/strategies");
|
||||
ConfigurePermissions("read:*", "read:resilience-strategies");
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
var strategies = (await catalog.ListAsync(ct)).ToList();
|
||||
var response = new ListResponse<IResilienceStrategy>(strategies);
|
||||
|
||||
await HttpContext.Response.WriteAsJsonAsync(response, serializer.SerializerOptions, ct);
|
||||
}
|
||||
}
|
||||
13
src/modules/Elsa.Resilience/Extensions/ModuleExtensions.cs
Normal file
13
src/modules/Elsa.Resilience/Extensions/ModuleExtensions.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Resilience.Features;
|
||||
|
||||
namespace Elsa.Resilience.Extensions;
|
||||
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
public static IModule UseResilience(this IModule module, Action<ResilienceFeature>? configure = null)
|
||||
{
|
||||
return module.Use(configure);
|
||||
}
|
||||
}
|
||||
43
src/modules/Elsa.Resilience/Features/ResilienceFeature.cs
Normal file
43
src/modules/Elsa.Resilience/Features/ResilienceFeature.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Resilience.Modifiers;
|
||||
using Elsa.Resilience.Options;
|
||||
using Elsa.Resilience.Serialization;
|
||||
using Elsa.Resilience.StrategySources;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Resilience.Features;
|
||||
|
||||
public class ResilienceFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
public ResilienceFeature AddResilienceStrategyType<T>() where T : IResilienceStrategy
|
||||
{
|
||||
return AddResilienceStrategyType(typeof(T));
|
||||
}
|
||||
|
||||
public ResilienceFeature AddResilienceStrategyType(Type strategyType)
|
||||
{
|
||||
Services.Configure<ResilienceOptions>(options => options.StrategyTypes.Add(strategyType));
|
||||
return this;
|
||||
}
|
||||
|
||||
public override void Configure()
|
||||
{
|
||||
Module.AddFastEndpointsAssembly<ResilienceFeature>();
|
||||
}
|
||||
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddOptions<ResilienceOptions>();
|
||||
|
||||
Services
|
||||
.AddSingleton<ResilienceStrategySerializer>()
|
||||
.AddSingleton<IActivityDescriptorModifier, ResilientActivityDescriptorModifier>()
|
||||
.AddScoped<IResilienceStrategyCatalog, ResilienceStrategyCatalog>()
|
||||
.AddScoped<IResilienceStrategyConfigEvaluator, ResilienceStrategyConfigEvaluator>()
|
||||
.AddScoped<IResilientActivityInvoker, ResilientActivityInvoker>()
|
||||
.AddScoped<IResilienceStrategySource, ConfigurationResilienceStrategySource>();
|
||||
}
|
||||
}
|
||||
3
src/modules/Elsa.Resilience/FodyWeavers.xml
Normal file
3
src/modules/Elsa.Resilience/FodyWeavers.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Resilience.Modifiers;
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the <see cref="ActivityDescriptor"/> of the <see cref="Cron"/> activity.
|
||||
/// </summary>
|
||||
public class ResilientActivityDescriptorModifier : IActivityDescriptorModifier
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Modify(ActivityDescriptor descriptor)
|
||||
{
|
||||
if (!descriptor.CustomProperties.TryGetValue("Type", out var typeObj))
|
||||
return;
|
||||
|
||||
if(typeObj is not Type type)
|
||||
return;
|
||||
|
||||
// Check if this type implements IResilientActivity.
|
||||
if (!typeof(IResilientActivity).IsAssignableFrom(type))
|
||||
return;
|
||||
|
||||
var categoryAttr = descriptor.Attributes.FirstOrDefault(x => x is ResilienceCategoryAttribute) as ResilienceCategoryAttribute;
|
||||
var category = categoryAttr?.Category ?? "Default";
|
||||
descriptor.CustomProperties["Resilient"] = true;
|
||||
descriptor.CustomProperties["ResilienceCategory"] = category;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Resilience.Serialization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Elsa.Resilience.StrategySources;
|
||||
|
||||
public class ConfigurationResilienceStrategySource(IConfiguration configuration, ResilienceStrategySerializer serializer) : IResilienceStrategySource
|
||||
{
|
||||
public Task<IEnumerable<IResilienceStrategy>> GetStrategiesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(GetStrategies());
|
||||
}
|
||||
|
||||
private IEnumerable<IResilienceStrategy> GetStrategies()
|
||||
{
|
||||
var json = configuration.GetSectionAsJson("Resilience:Strategies");
|
||||
return string.IsNullOrWhiteSpace(json) ? [] : serializer.DeserializeMany(json);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,17 +13,17 @@ public class ActivityDescriptor
|
|||
/// <summary>
|
||||
/// The fully qualified name of the activity type.
|
||||
/// </summary>
|
||||
public string TypeName { get; set; } = default!;
|
||||
public string TypeName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The namespace of the activity type.
|
||||
/// </summary>
|
||||
public string Namespace { get; set; } = default!;
|
||||
public string Namespace { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The name of the activity type.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = default!;
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The version of the activity type.
|
||||
|
|
@ -33,7 +33,7 @@ public class ActivityDescriptor
|
|||
/// <summary>
|
||||
/// The category of the activity type.
|
||||
/// </summary>
|
||||
public string Category { get; set; } = default!;
|
||||
public string Category { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The display name of the activity type.
|
||||
|
|
@ -64,7 +64,7 @@ public class ActivityDescriptor
|
|||
/// Instantiates a concrete instance of an <see cref="IActivity"/>.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<ActivityConstructorContext, IActivity> Constructor { get; set; } = default!;
|
||||
public Func<ActivityConstructorContext, IActivity> Constructor { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The kind of activity.
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
|
|||
IsStart = isStart,
|
||||
IsTerminal = isTerminal,
|
||||
Attributes = attributes,
|
||||
CustomProperties =
|
||||
{
|
||||
["Type"] = activityType
|
||||
},
|
||||
Constructor = context =>
|
||||
{
|
||||
var activity = activityFactory.Create(activityType, context);
|
||||
|
|
@ -143,7 +147,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
|
|||
|
||||
if (wrappedPropertyType.IsNullableType())
|
||||
wrappedPropertyType = wrappedPropertyType.GetTypeOfNullable();
|
||||
|
||||
|
||||
var uiSpecification = await propertyUIHandlerResolver.GetUIPropertiesAsync(propertyInfo, null, cancellationToken);
|
||||
|
||||
return new InputDescriptor
|
||||
|
|
@ -183,7 +187,7 @@ public class ActivityDescriber(IPropertyDefaultValueResolver defaultValueResolve
|
|||
{
|
||||
return await DescribeOutputPropertiesAsync(GetOutputProperties(activityType), cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
public static string GetUIHint(Type wrappedPropertyType, InputAttribute? inputAttribute = null)
|
||||
{
|
||||
if (inputAttribute?.UIHint != null)
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public class WorkflowDefinitionActivityProvider(IWorkflowDefinitionStore store,
|
|||
|
||||
ports.Insert(0, rootPort);
|
||||
|
||||
return new ActivityDescriptor
|
||||
return new()
|
||||
{
|
||||
TypeName = typeName,
|
||||
Name = typeName,
|
||||
|
|
|
|||
Loading…
Reference in a new issue