Add event-driven quick action dispatch

This commit is contained in:
Drechsler, Meik
2026-07-22 11:57:32 +02:00
parent fb45f1c42b
commit 9bd3896ba0
20 changed files with 591 additions and 139 deletions

View File

@@ -0,0 +1,62 @@
using System;
using System.Threading.Tasks;
using F4SD.ActionConnector.Enumerations;
using F4SD.ActionConnector.Models;
using static C4IT.Logging.cLogManager;
namespace F4SD.ActionConnector.Execution
{
/// <summary>
/// Routes a single action definition to the correct executor based on its <see cref="ActionType"/>.
/// </summary>
internal sealed class ActionDispatcher
{
private readonly Func<QuickActionRequest, Task<ActionResult>> _quickActionExecutor;
internal ActionDispatcher(Func<QuickActionRequest, Task<ActionResult>> quickActionExecutor)
{
_quickActionExecutor = quickActionExecutor;
}
internal async Task<ActionResult> DispatchAsync(ActionDefinitionBase action, ActionContext context)
{
if (!action.IsEnabled)
return ActionResult.Skip(action.Id);
switch (action.Type)
{
case ActionType.QuickAction:
return await ExecuteQuickActionAsync((QuickActionDefinition)action, context);
case ActionType.HttpCall:
case ActionType.Webhook:
throw new NotImplementedException(
$"ActionType '{action.Type}' is not yet implemented.");
default:
LogEntry($"ActionDispatcher: unknown ActionType '{action.Type}' for action '{action.Id}'.",
C4IT.Logging.LogLevels.Warning);
return ActionResult.Skip(action.Id);
}
}
private async Task<ActionResult> ExecuteQuickActionAsync(QuickActionDefinition definition, ActionContext context)
{
if (_quickActionExecutor == null)
throw new InvalidOperationException(
$"A QuickAction ('{definition.Id}') was triggered but no {nameof(Bus.ActionBusOptions.QuickActionExecutor)} was registered.");
var resolvedParameters = PayloadVariableResolver.Resolve(definition.Parameters, context.Payload);
var request = new QuickActionRequest
{
ActionId = definition.Id,
QuickActionRef = definition.QuickActionRef,
Parameters = resolvedParameters,
Context = context,
};
return await _quickActionExecutor(request);
}
}
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using F4SD.ActionConnector.Payloads;
namespace F4SD.ActionConnector.Execution
{
/// <summary>
/// Resolves template variables such as {{Case.Id}} or {{Case.ClosedAt:yyyy-MM-ddTHH:mm:ssZ}}
/// against a payload object.
///
/// Variable format: {{ObjectName.PropertyName[:FormatSpecifier]}}
/// </summary>
internal static class PayloadVariableResolver
{
private static readonly Regex VariablePattern =
new Regex(@"\{\{(?<obj>[^.}]+)\.(?<prop>[^:}]+)(?::(?<fmt>[^}]+))?\}\}",
RegexOptions.Compiled);
/// <summary>
/// Returns a new dictionary with every value in <paramref name="rawParameters"/> having
/// its template variables replaced by the corresponding payload property values.
/// Pure single-variable templates (e.g. <c>{{Case.ClosedAt}}</c>) yield the raw typed
/// object; mixed or formatted templates yield a resolved string.
/// </summary>
internal static IDictionary<string, object> Resolve(
IDictionary<string, string> rawParameters,
PayloadBase payload)
{
var resolved = new Dictionary<string, object>(rawParameters.Count);
foreach (var pair in rawParameters)
resolved[pair.Key] = ResolveValue(pair.Value, payload);
return resolved;
}
private static object ResolveValue(string template, PayloadBase payload)
{
var match = VariablePattern.Match(template);
// Pure single variable with no format specifier → return raw object
if (match.Success && match.Value == template && !match.Groups["fmt"].Success)
return ResolveRawProperty(match, payload);
// Mixed template, literal, or explicit format specifier → return resolved string
return ResolveString(template, payload);
}
private static object ResolveRawProperty(Match match, PayloadBase payload)
{
string propertyName = match.Groups["prop"].Value;
PropertyInfo property = payload.GetType().GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (property == null)
return match.Value;
return property.GetValue(payload);
}
private static string ResolveString(string template, PayloadBase payload)
{
return VariablePattern.Replace(template, match => ResolveMatch(match, payload));
}
private static string ResolveMatch(Match match, PayloadBase payload)
{
string propertyName = match.Groups["prop"].Value;
string formatSpecifier = match.Groups["fmt"].Success ? match.Groups["fmt"].Value : null;
PropertyInfo property = payload.GetType().GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (property == null)
return match.Value;
object value = property.GetValue(payload);
if (value == null)
return string.Empty;
if (formatSpecifier != null && value is IFormattable formattable)
return formattable.ToString(formatSpecifier, CultureInfo.InvariantCulture);
return value.ToString();
}
}
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using F4SD.ActionConnector.Models;
namespace F4SD.ActionConnector.Execution
{
public sealed class QuickActionRequest
{
/// <summary>
/// Matches <see cref="QuickActionDefinition.Id"/> from the configuration.
/// </summary>
public string ActionId { get; internal set; }
/// <summary>
/// The name of the Quick Action to invoke, e.g. "Document case in JIRA".
/// </summary>
public string QuickActionRef { get; internal set; }
/// <summary>
/// Template variables resolved against the event payload.
/// Values are the raw payload values; callers are responsible for casting.
/// </summary>
public IDictionary<string, object> Parameters { get; internal set; }
/// <summary>
/// Full context of the triggering event.
/// </summary>
public ActionContext Context { get; internal set; }
}
}