Refactor triggers
This commit is contained in:
parent
161a70e429
commit
6b7caddca8
|
|
@ -2,6 +2,7 @@
|
|||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Computables/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pomelo/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Populator/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=startable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Syntaxes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tupled/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpublish/@EntryIndexedValue">True</s:Boolean>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,6 @@ public class Event : Activity
|
|||
var hasher = context.GetRequiredService<IHasher>();
|
||||
var eventName = context.Get(EventName)!;
|
||||
var hash = hasher.Hash(eventName);
|
||||
context.SetBookmark(hash);
|
||||
context.CreateBookmark(hash);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,33 @@ using Elsa.Models;
|
|||
|
||||
namespace Elsa.Contracts;
|
||||
|
||||
public interface IActivity : INode
|
||||
/// <summary>
|
||||
/// Represents an activity, which is an atomic unit of operation within a workflow.
|
||||
/// </summary>
|
||||
public interface IActivity
|
||||
{
|
||||
/// <summary>
|
||||
/// An identifier that is unique within the workflow.
|
||||
/// </summary>
|
||||
string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The logical type name of the activity.
|
||||
/// </summary>
|
||||
string TypeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A value indicating whether this activity can start instances of the workflow it is a part of.
|
||||
/// </summary>
|
||||
public bool CanStartWorkflow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Can contain application-specific information about this activity.
|
||||
/// </summary>
|
||||
IDictionary<string, object> Metadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the activity executes.
|
||||
/// </summary>
|
||||
ValueTask ExecuteAsync(ActivityExecutionContext context);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
namespace Elsa.Contracts;
|
||||
|
||||
public interface IPayloadSerializer
|
||||
public interface IBookmarkDataSerializer
|
||||
{
|
||||
T Deserialize<T>(string json) where T : notnull;
|
||||
string Serialize<T>(T payload) where T : notnull;
|
||||
12
src/core/Elsa.Core/Contracts/IEventGenerator.cs
Normal file
12
src/core/Elsa.Core/Contracts/IEventGenerator.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Elsa.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by trigger activities that generate events. For example, a timer activity is an event generator.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When an event generator activity is marked as startable, it generates new workflow instances. Otherwise, it schedules new workflow executions for the current workflow instance, regardless of whether or not the activity is currently blocking.
|
||||
/// For example, a timer activity will continuously generate events, causing the workflow instance to execute the timer event and subsequent activities, even if the timer activity wasn't scheduled with the workflow execution stack.
|
||||
/// </remarks>
|
||||
public interface IEventGenerator : ITrigger
|
||||
{
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace Elsa.Contracts;
|
||||
|
||||
public interface INode
|
||||
{
|
||||
string Id { get; set; }
|
||||
string NodeType { get; set; }
|
||||
IDictionary<string, object> Metadata { get; set; }
|
||||
}
|
||||
|
|
@ -3,21 +3,12 @@ using Elsa.Models;
|
|||
namespace Elsa.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Activities that act as a trigger should implement this interface.
|
||||
/// Implement this method if your activity needs to provide bookmark data that will be used when it is marked as a trigger.
|
||||
/// </summary>
|
||||
public interface ITrigger : INode
|
||||
public interface ITrigger : IActivity
|
||||
{
|
||||
/// <summary>
|
||||
/// A value indicating whether this trigger can create a new workflow instances when triggered, or execute the workflow instance this trigger belongs to.
|
||||
/// Implementors should return a list of objects where each object represents a bookmark datum. For each datum, a trigger is created.
|
||||
/// </summary>
|
||||
TriggerMode TriggerMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Implementing triggers should return zero or more objects that represent a lookup value.
|
||||
/// </summary>
|
||||
///<example>
|
||||
/// For example, if your trigger can be configured with a signal name to listen for, you would return an object with said signal name as a field.
|
||||
/// Your auxiliary services would then use this signal name to create a hash and use it to lookup a trigger stored in the database by this hash.
|
||||
/// </example>
|
||||
ValueTask<IEnumerable<object>> GetTriggerPayloadsAsync(TriggerIndexingContext context, CancellationToken cancellationToken = default);
|
||||
ValueTask<IEnumerable<object>> GetTriggerDataAsync(TriggerIndexingContext context);
|
||||
}
|
||||
|
|
@ -1,24 +1,9 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Models;
|
||||
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
public static class NodeExtensions
|
||||
{
|
||||
public static IEnumerable<Input> GetInputs(this INode node)
|
||||
{
|
||||
var inputProps = node.GetType().GetProperties().Where(x => typeof(Input).IsAssignableFrom(x.PropertyType)).ToList();
|
||||
|
||||
var query =
|
||||
from inputProp in inputProps
|
||||
select (Input?)inputProp.GetValue(node)
|
||||
into input
|
||||
where input != null
|
||||
select input;
|
||||
|
||||
return query.Select(x => x!).ToList();
|
||||
}
|
||||
|
||||
public static IEnumerable<ActivityNode> Flatten(this ActivityNode root)
|
||||
{
|
||||
yield return root;
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@ namespace Elsa.Models;
|
|||
|
||||
public abstract class Activity : IActivity
|
||||
{
|
||||
protected Activity() => NodeType = TypeNameHelper.GenerateTypeName(GetType());
|
||||
protected Activity(string activityType) => NodeType = activityType;
|
||||
protected Activity() => TypeName = TypeNameHelper.GenerateTypeName(GetType());
|
||||
protected Activity(string activityType) => TypeName = activityType;
|
||||
|
||||
public string Id { get; set; } = default!;
|
||||
public string NodeType { get; set; }
|
||||
public string TypeName { get; set; }
|
||||
public bool CanStartWorkflow { get; set; }
|
||||
public IDictionary<string, object> Metadata { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
protected virtual ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
|
|
@ -18,6 +19,11 @@ public abstract class Activity : IActivity
|
|||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<IEnumerable<object>> GetBookmarkPayloadsAsync(TriggerIndexingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected virtual void Execute(ActivityExecutionContext context)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,29 +47,28 @@ public class ActivityExecutionContext
|
|||
ScheduleActivity(activity, completionCallback);
|
||||
}
|
||||
|
||||
public void SetBookmarks(IEnumerable<object> payloads, ExecuteActivityDelegate? callback = default)
|
||||
public void SetBookmarks(IEnumerable<object> bookmarkData, ExecuteActivityDelegate? callback = default)
|
||||
{
|
||||
foreach (var hashInput in payloads)
|
||||
SetBookmark(payloads, callback);
|
||||
foreach (var bookmarkDatum in bookmarkData)
|
||||
CreateBookmark(bookmarkDatum, callback);
|
||||
}
|
||||
|
||||
public void SetBookmarks(IEnumerable<Bookmark> bookmarks) => _bookmarks.AddRange(bookmarks);
|
||||
public void SetBookmark(Bookmark bookmark) => _bookmarks.Add(bookmark);
|
||||
public void CreateBookmark(Bookmark bookmark) => _bookmarks.Add(bookmark);
|
||||
|
||||
public void SetBookmark(object? payload, ExecuteActivityDelegate? callback = default)
|
||||
public void CreateBookmark(object? bookmarkDatum, ExecuteActivityDelegate? callback = default)
|
||||
{
|
||||
var hasher = GetRequiredService<IHasher>();
|
||||
|
||||
var identityGenerator = GetRequiredService<IIdentityGenerator>();
|
||||
var payloadSerializer = GetRequiredService<IPayloadSerializer>();
|
||||
var payloadJson = payload != null ? payloadSerializer.Serialize(payload) : default;
|
||||
var hash = payloadJson != null ? hasher.Hash(payloadJson) : default;
|
||||
var bookmarkDataSerializer = GetRequiredService<IBookmarkDataSerializer>();
|
||||
var bookmarkDatumJson = bookmarkDatum != null ? bookmarkDataSerializer.Serialize(bookmarkDatum) : default;
|
||||
var hash = bookmarkDatumJson != null ? hasher.Hash(bookmarkDatumJson) : default;
|
||||
|
||||
SetBookmark(new Bookmark(
|
||||
CreateBookmark(new Bookmark(
|
||||
identityGenerator.GenerateId(),
|
||||
Activity.NodeType,
|
||||
Activity.TypeName,
|
||||
hash,
|
||||
payloadJson,
|
||||
bookmarkDatumJson,
|
||||
Activity.Id,
|
||||
Id,
|
||||
callback?.Method.Name));
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ public record Bookmark(
|
|||
string Id,
|
||||
string Name,
|
||||
string? Hash,
|
||||
string? Payload,
|
||||
string? Data,
|
||||
string ActivityId,
|
||||
string ActivityInstanceId,
|
||||
string? CallbackMethodName = default
|
||||
|
|
|
|||
14
src/core/Elsa.Core/Models/EventGenerator.cs
Normal file
14
src/core/Elsa.Core/Models/EventGenerator.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Contracts;
|
||||
|
||||
namespace Elsa.Models;
|
||||
|
||||
public abstract class EventGenerator : Trigger, IEventGenerator
|
||||
{
|
||||
protected EventGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
protected EventGenerator(string triggerType) : base(triggerType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -6,15 +6,17 @@ public class ExpressionExecutionContext
|
|||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ExpressionExecutionContext(IServiceProvider serviceProvider, Register register, ExpressionExecutionContext? parentContext)
|
||||
public ExpressionExecutionContext(IServiceProvider serviceProvider, Register register, ExpressionExecutionContext? parentContext, CancellationToken cancellationToken)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
Register = register;
|
||||
ParentContext = parentContext;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public Register Register { get; }
|
||||
public ExpressionExecutionContext? ParentContext { get; set; }
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public RegisterLocation GetLocation(RegisterLocationReference locationReference) => GetLocationInternal(locationReference) ?? throw new InvalidOperationException();
|
||||
public object Get(RegisterLocationReference locationReference) => GetLocation(locationReference).Value!;
|
||||
|
|
|
|||
|
|
@ -8,29 +8,28 @@ public abstract class Trigger : Activity, ITrigger
|
|||
{
|
||||
}
|
||||
|
||||
protected Trigger(string triggerType) : base(triggerType)
|
||||
protected Trigger(string activityType) : base(activityType)
|
||||
{
|
||||
}
|
||||
|
||||
public TriggerMode TriggerMode { get; set; } = TriggerMode.WorkflowDefinition;
|
||||
ValueTask<IEnumerable<object>> ITrigger.GetTriggerPayloadsAsync(TriggerIndexingContext context, CancellationToken cancellationToken) => GetTriggerPayloadsAsync(context, cancellationToken);
|
||||
|
||||
ValueTask<IEnumerable<object>> ITrigger.GetTriggerDataAsync(TriggerIndexingContext context) => GetTriggerDataAsync(context);
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a list of trigger payloads.
|
||||
/// Override this method to return trigger data.
|
||||
/// </summary>
|
||||
protected virtual ValueTask<IEnumerable<object>> GetTriggerPayloadsAsync(TriggerIndexingContext context, CancellationToken cancellationToken = default)
|
||||
protected virtual ValueTask<IEnumerable<object>> GetTriggerDataAsync(TriggerIndexingContext context)
|
||||
{
|
||||
var hashes = GetTriggerPayloads(context);
|
||||
var hashes = GetTriggerData(context);
|
||||
return ValueTask.FromResult(hashes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a list of trigger payloads.
|
||||
/// Override this method to return trigger data.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<object> GetTriggerPayloads(TriggerIndexingContext context) => new[]{ GetTriggerPayload(context) };
|
||||
protected virtual IEnumerable<object> GetTriggerData(TriggerIndexingContext context) => new[]{ GetTriggerDatum(context) };
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a trigger payload.
|
||||
/// Override this method to return a trigger datum.
|
||||
/// </summary>
|
||||
protected virtual object GetTriggerPayload(TriggerIndexingContext context) => new();
|
||||
protected virtual object GetTriggerDatum(TriggerIndexingContext context) => new();
|
||||
}
|
||||
|
|
@ -7,12 +7,14 @@ public class TriggerIndexingContext
|
|||
public WorkflowIndexingContext WorkflowIndexingContext { get; }
|
||||
public ExpressionExecutionContext ExpressionExecutionContext { get; }
|
||||
public ITrigger Trigger { get; }
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public TriggerIndexingContext(WorkflowIndexingContext workflowIndexingContext, ExpressionExecutionContext expressionExecutionContext, ITrigger trigger)
|
||||
public TriggerIndexingContext(WorkflowIndexingContext workflowIndexingContext, ExpressionExecutionContext expressionExecutionContext, ITrigger trigger, CancellationToken cancellationToken)
|
||||
{
|
||||
WorkflowIndexingContext = workflowIndexingContext;
|
||||
ExpressionExecutionContext = expressionExecutionContext;
|
||||
Trigger = trigger;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public T? Get<T>(Input<T>? input) => ExpressionExecutionContext.Get(input);
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
namespace Elsa.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A set of trigger modes.
|
||||
/// </summary>
|
||||
public enum TriggerMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The trigger will create a new workflow instance and execute said instance.
|
||||
/// </summary>
|
||||
WorkflowDefinition,
|
||||
|
||||
/// <summary>
|
||||
/// The trigger will schedule the trigger for execution within the context of the workflow instance.
|
||||
/// </summary>
|
||||
WorkflowInstance
|
||||
}
|
||||
|
|
@ -4,13 +4,15 @@ namespace Elsa.Models;
|
|||
|
||||
public class WorkflowIndexingContext
|
||||
{
|
||||
public WorkflowIndexingContext(Workflow workflow)
|
||||
public WorkflowIndexingContext(Workflow workflow, CancellationToken cancellationToken)
|
||||
{
|
||||
Workflow = workflow;
|
||||
CancellationToken = cancellationToken;
|
||||
}
|
||||
|
||||
public Workflow Workflow { get; }
|
||||
public IDictionary<ITrigger, Register> Registers { get; } = new Dictionary<ITrigger, Register>();
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public Register GetOrCreateRegister(ITrigger activity)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ public class ActivityInvokerMiddleware : IActivityExecutionMiddleware
|
|||
{
|
||||
var activity = context.Activity;
|
||||
var workflowExecutionContext = context.WorkflowExecutionContext;
|
||||
var logEntry = new WorkflowExecutionLogEntry(activity.Id, activity.NodeType, _clock.UtcNow, eventName, message, source, payload);
|
||||
var logEntry = new WorkflowExecutionLogEntry(activity.Id, activity.TypeName, _clock.UtcNow, eventName, message, source, payload);
|
||||
workflowExecutionContext.ExecutionLog.Add(logEntry);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class ActivityInvoker : IActivityInvoker
|
|||
// Setup an activity execution context.
|
||||
var register = workflowExecutionContext.Workflow.CreateRegister();
|
||||
|
||||
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, parentActivityExecutionContext?.ExpressionExecutionContext);
|
||||
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, parentActivityExecutionContext?.ExpressionExecutionContext, cancellationToken);
|
||||
var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, parentActivityExecutionContext, expressionExecutionContext, activity, cancellationToken);
|
||||
|
||||
// Declare locations.
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ using Elsa.Contracts;
|
|||
|
||||
namespace Elsa.Services;
|
||||
|
||||
public class PayloadSerializer : IPayloadSerializer
|
||||
public class BookmarkDataSerializer : IBookmarkDataSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _settings;
|
||||
|
||||
public PayloadSerializer()
|
||||
public BookmarkDataSerializer()
|
||||
{
|
||||
_settings = new JsonSerializerOptions
|
||||
{
|
||||
|
|
@ -6,16 +6,16 @@ namespace Elsa.Services;
|
|||
|
||||
public class Hasher : IHasher
|
||||
{
|
||||
private readonly IPayloadSerializer _payloadSerializer;
|
||||
private readonly IBookmarkDataSerializer _bookmarkDataSerializer;
|
||||
|
||||
public Hasher(IPayloadSerializer payloadSerializer)
|
||||
public Hasher(IBookmarkDataSerializer bookmarkDataSerializer)
|
||||
{
|
||||
_payloadSerializer = payloadSerializer;
|
||||
_bookmarkDataSerializer = bookmarkDataSerializer;
|
||||
}
|
||||
|
||||
public string Hash(object value)
|
||||
{
|
||||
var json = _payloadSerializer.Serialize(value);
|
||||
var json = _bookmarkDataSerializer.Serialize(value);
|
||||
return Hash(json);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ public class IdentityGraphService : IIdentityGraphService
|
|||
if (!string.IsNullOrWhiteSpace(activityNode.NodeId))
|
||||
return activityNode.NodeId;
|
||||
|
||||
var type = activityNode.Activity.NodeType;
|
||||
var type = activityNode.Activity.TypeName;
|
||||
var index = GetNextIndexFor(type, identityCounters);
|
||||
var name = $"{Camelize(type)}{index + 1}";
|
||||
return name;
|
||||
|
|
|
|||
|
|
@ -124,11 +124,12 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer
|
|||
{
|
||||
ActivityExecutionContext CreateActivityExecutionContext(ActivityExecutionContextState activityExecutionContextState)
|
||||
{
|
||||
var cancellationToken = workflowExecutionContext.CancellationToken;
|
||||
var activity = workflowExecutionContext.FindActivityById(activityExecutionContextState.ScheduledActivityId);
|
||||
var register = new Register(activityExecutionContextState.Register.Locations);
|
||||
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, default);
|
||||
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, default, cancellationToken);
|
||||
var properties = activityExecutionContextState.Properties;
|
||||
var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, default, expressionExecutionContext, activity, workflowExecutionContext.CancellationToken)
|
||||
var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, default, expressionExecutionContext, activity, cancellationToken)
|
||||
{
|
||||
Id = activityExecutionContextState.Id,
|
||||
Properties = properties
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
using Elsa.Management.Models;
|
||||
|
||||
namespace Elsa.Management.Contracts;
|
||||
|
||||
public interface ITriggerDescriber
|
||||
{
|
||||
ValueTask<TriggerDescriptor> DescribeTriggerAsync(Type triggerType, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Management.Models;
|
||||
|
||||
namespace Elsa.Management.Contracts;
|
||||
|
||||
public interface ITriggerFactory
|
||||
{
|
||||
ITrigger Create(Type type, TriggerConstructorContext context);
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Elsa.Management.Models;
|
||||
|
||||
namespace Elsa.Management.Contracts;
|
||||
|
||||
public interface ITriggerProvider
|
||||
{
|
||||
ValueTask<IEnumerable<TriggerDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
using Elsa.Management.Models;
|
||||
|
||||
namespace Elsa.Management.Contracts;
|
||||
|
||||
public interface ITriggerRegistry
|
||||
{
|
||||
void Add(object provider, TriggerDescriptor descriptor);
|
||||
void AddMany(object provider, IEnumerable<TriggerDescriptor> descriptors);
|
||||
void Clear();
|
||||
void ClearProvider(object provider);
|
||||
IEnumerable<TriggerDescriptor> ListAll();
|
||||
IEnumerable<TriggerDescriptor> ListByProvider(object provider);
|
||||
TriggerDescriptor? Find(Func<TriggerDescriptor, bool> predicate);
|
||||
TriggerDescriptor? Find(string triggerType);
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Elsa.Management.Contracts;
|
||||
|
||||
public interface ITriggerRegistryPopulator
|
||||
{
|
||||
ValueTask PopulateRegistryAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
@ -21,11 +21,6 @@ public static class ServiceCollectionExtensions
|
|||
.AddSingleton<IPropertyOptionsResolver, PropertyOptionsResolver>()
|
||||
.AddSingleton<IActivityProvider, TypedActivityProvider>()
|
||||
.AddSingleton<IActivityFactory, ActivityFactory>()
|
||||
.AddSingleton<ITriggerDescriber, TriggerDescriber>()
|
||||
.AddSingleton<ITriggerRegistry, TriggerRegistry>()
|
||||
.AddSingleton<ITriggerRegistryPopulator, TriggerRegistryPopulator>()
|
||||
.AddSingleton<ITriggerFactory, TriggerFactory>()
|
||||
.AddSingleton<ITriggerProvider, TypedTriggerProvider>()
|
||||
.AddSingleton<IWellKnownTypeRegistry, WellKnownTypeRegistry>()
|
||||
.AddSingleton<IExpressionSyntaxRegistry, ExpressionSyntaxRegistry>()
|
||||
.AddSingleton<IExpressionSyntaxProvider, DefaultExpressionSyntaxProvider>()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Elsa.Management.Models;
|
|||
|
||||
public class TriggerDescriptor : NodeDescriptor
|
||||
{
|
||||
[JsonIgnore] public Func<TriggerConstructorContext, ITrigger> Constructor { get; init; } = default!;
|
||||
[JsonIgnore] public Func<TriggerConstructorContext, IEventGenerator> Constructor { get; init; } = default!;
|
||||
}
|
||||
|
||||
public record TriggerConstructorContext(JsonElement Element, JsonSerializerOptions SerializerOptions);
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Management.Contracts;
|
||||
using Elsa.Management.Models;
|
||||
using Elsa.Management.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Elsa.Management.Providers;
|
||||
|
||||
public class TypedTriggerProvider : ITriggerProvider
|
||||
{
|
||||
private readonly ITriggerDescriber _triggerDescriber;
|
||||
private readonly ApiOptions _options;
|
||||
|
||||
public TypedTriggerProvider(IOptions<ApiOptions> options, ITriggerDescriber triggerDescriber)
|
||||
{
|
||||
_triggerDescriber = triggerDescriber;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public async ValueTask<IEnumerable<TriggerDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var triggerTypes = _options.TriggerTypes;
|
||||
var descriptors = await DescribeTriggerTypesAsync(triggerTypes, cancellationToken).ToListAsync(cancellationToken);
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<TriggerDescriptor> DescribeTriggerTypesAsync(IEnumerable<Type> triggerTypes, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var triggerType in triggerTypes)
|
||||
{
|
||||
var descriptor = await _triggerDescriber.DescribeTriggerAsync(triggerType, cancellationToken);
|
||||
yield return descriptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ public class FlowchartJsonConverter : JsonConverter<Flowchart>
|
|||
|
||||
var model = new
|
||||
{
|
||||
NodeType = value.NodeType,
|
||||
NodeType = value.TypeName,
|
||||
Id = value.Id,
|
||||
Metadata = value.Metadata,
|
||||
Start = value.Start?.Id,
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Management.Contracts;
|
||||
using Elsa.Management.Models;
|
||||
|
||||
namespace Elsa.Management.Serialization.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// (De)serializes objects of type <see cref="ITrigger"/>.
|
||||
/// </summary>
|
||||
public class TriggerJsonConverter : JsonConverter<ITrigger>
|
||||
{
|
||||
private readonly ITriggerRegistry _triggerRegistry;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public TriggerJsonConverter(ITriggerRegistry triggerRegistry, IServiceProvider serviceProvider)
|
||||
{
|
||||
_triggerRegistry = triggerRegistry;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public override ITrigger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (!JsonDocument.TryParseValue(ref reader, out var doc))
|
||||
throw new JsonException("Failed to parse JsonDocument");
|
||||
|
||||
if (!doc.RootElement.TryGetProperty("nodeType", out var triggerTypeNameElement))
|
||||
throw new JsonException("Failed to extract trigger type property");
|
||||
|
||||
var triggerType = triggerTypeNameElement.GetString()!;
|
||||
var triggerDescriptor = _triggerRegistry.Find(triggerType);
|
||||
|
||||
if (triggerDescriptor == null)
|
||||
throw new Exception($"Trigger of type {triggerType} not found in registry");
|
||||
|
||||
var newOptions = new JsonSerializerOptions(options);
|
||||
newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider));
|
||||
|
||||
var context = new TriggerConstructorContext(doc.RootElement, newOptions);
|
||||
var activity = triggerDescriptor.Constructor(context);
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ITrigger value, JsonSerializerOptions options)
|
||||
{
|
||||
var newOptions = new JsonSerializerOptions(options);
|
||||
newOptions.Converters.Add(new InputJsonConverterFactory(_serviceProvider));
|
||||
JsonSerializer.Serialize(writer, value, value.GetType(), newOptions);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Management.Contracts;
|
||||
|
||||
namespace Elsa.Management.Serialization.Converters;
|
||||
|
||||
public class TriggerJsonConverterFactory : JsonConverterFactory
|
||||
{
|
||||
private readonly ITriggerRegistry _triggerRegistry;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public TriggerJsonConverterFactory(ITriggerRegistry triggerRegistry, IServiceProvider serviceProvider)
|
||||
{
|
||||
_triggerRegistry = triggerRegistry;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
// Notice that this factory only creates converters when the type to convert is ITrigger.
|
||||
// The TriggerJsonConverter will create concrete trigger objects, which then uses regular serialization
|
||||
public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ITrigger);
|
||||
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new TriggerJsonConverter(_triggerRegistry, _serviceProvider);
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ public class WorkflowSerializerOptionsProvider
|
|||
Create<JsonStringEnumConverter>(),
|
||||
Create<TypeJsonConverter>(),
|
||||
Create<ActivityJsonConverterFactory>(),
|
||||
Create<TriggerJsonConverterFactory>(),
|
||||
Create<ExpressionJsonConverterFactory>(),
|
||||
Create<FlowchartJsonConverter>()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class ActivityDescriber : IActivityDescriber
|
|||
var properties = activityType.GetProperties();
|
||||
var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType) || x.GetCustomAttribute<InputAttribute>() != null).ToList();
|
||||
var outputProperties = properties.Where(x => typeof(Output).IsAssignableFrom(x.PropertyType)).ToList();
|
||||
var isTrigger = activityType.IsAssignableTo(typeof(ITrigger));
|
||||
var isTrigger = activityType.IsAssignableTo(typeof(IEventGenerator));
|
||||
|
||||
var descriptor = new ActivityDescriptor
|
||||
{
|
||||
|
|
@ -66,7 +66,7 @@ public class ActivityDescriber : IActivityDescriber
|
|||
Constructor = context =>
|
||||
{
|
||||
var activity = _activityFactory.Create(activityType, context);
|
||||
activity.NodeType = fullTypeName;
|
||||
activity.TypeName = fullTypeName;
|
||||
return activity;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Elsa.Attributes;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Management.Contracts;
|
||||
using Elsa.Management.Models;
|
||||
using Elsa.Models;
|
||||
using Humanizer;
|
||||
|
||||
namespace Elsa.Management.Services;
|
||||
|
||||
public class TriggerDescriber : ITriggerDescriber
|
||||
{
|
||||
private readonly IPropertyOptionsResolver _optionsResolver;
|
||||
private readonly IPropertyDefaultValueResolver _defaultValueResolver;
|
||||
private readonly ITriggerFactory _activityFactory;
|
||||
|
||||
public TriggerDescriber(IPropertyOptionsResolver optionsResolver, IPropertyDefaultValueResolver defaultValueResolver, ITriggerFactory activityFactory)
|
||||
{
|
||||
_optionsResolver = optionsResolver;
|
||||
_defaultValueResolver = defaultValueResolver;
|
||||
_activityFactory = activityFactory;
|
||||
}
|
||||
|
||||
public ValueTask<TriggerDescriptor> DescribeTriggerAsync(Type triggerType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ns = TypeNameHelper.GenerateNamespace(triggerType);
|
||||
var typeName = triggerType.Name;
|
||||
var fullTypeName = TypeNameHelper.GenerateTypeName(triggerType, ns);
|
||||
var displayNameAttr = triggerType.GetCustomAttribute<DisplayNameAttribute>();
|
||||
var displayName = displayNameAttr?.DisplayName ?? typeName.Humanize(LetterCasing.Title);
|
||||
var categoryAttr = triggerType.GetCustomAttribute<CategoryAttribute>();
|
||||
var category = categoryAttr?.Category ?? TypeNameHelper.GetCategoryFromNamespace(ns) ?? "Miscellaneous";
|
||||
var descriptionAttr = triggerType.GetCustomAttribute<DescriptionAttribute>();
|
||||
var description = descriptionAttr?.Description;
|
||||
var properties = triggerType.GetProperties();
|
||||
var inputProperties = properties.Where(x => typeof(Input).IsAssignableFrom(x.PropertyType)).ToList();
|
||||
|
||||
var descriptor = new TriggerDescriptor
|
||||
{
|
||||
Category = category,
|
||||
Description = description,
|
||||
NodeType = fullTypeName,
|
||||
DisplayName = displayName,
|
||||
InputProperties = DescribeInputProperties(inputProperties).ToList(),
|
||||
Constructor = context =>
|
||||
{
|
||||
var trigger = _activityFactory.Create(triggerType, context);
|
||||
trigger.NodeType = fullTypeName;
|
||||
return trigger;
|
||||
}
|
||||
};
|
||||
|
||||
return ValueTask.FromResult(descriptor);
|
||||
}
|
||||
|
||||
private IEnumerable<InputDescriptor> DescribeInputProperties(IEnumerable<PropertyInfo> properties)
|
||||
{
|
||||
foreach (var propertyInfo in properties)
|
||||
{
|
||||
var inputAttribute = propertyInfo.GetCustomAttribute<InputAttribute>();
|
||||
var descriptionAttribute = propertyInfo.GetCustomAttribute<DescriptionAttribute>();
|
||||
var wrappedPropertyType = propertyInfo.PropertyType.GenericTypeArguments[0];
|
||||
|
||||
yield return new InputDescriptor
|
||||
(
|
||||
inputAttribute?.Name ?? propertyInfo.Name,
|
||||
wrappedPropertyType,
|
||||
InputUIHints.GetUIHint(wrappedPropertyType, inputAttribute),
|
||||
inputAttribute?.DisplayName ?? propertyInfo.Name.Humanize(LetterCasing.Title),
|
||||
descriptionAttribute?.Description,
|
||||
_optionsResolver.GetOptions(propertyInfo),
|
||||
inputAttribute?.Category,
|
||||
inputAttribute?.Order ?? 0,
|
||||
_defaultValueResolver.GetDefaultValue(propertyInfo),
|
||||
inputAttribute?.DefaultSyntax,
|
||||
//inputAttribute?.SupportedSyntaxes, TODO: Come up with a different way to specify support languages for activity inputs. By default, maybe all pros should support all registered scripting languages?
|
||||
inputAttribute?.IsReadOnly ?? false,
|
||||
inputAttribute?.IsBrowsable ?? true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Management.Contracts;
|
||||
using Elsa.Management.Models;
|
||||
|
||||
namespace Elsa.Management.Services;
|
||||
|
||||
public class TriggerFactory : ITriggerFactory
|
||||
{
|
||||
public ITrigger Create(Type type, TriggerConstructorContext context)
|
||||
{
|
||||
var trigger = (ITrigger)context.Element.Deserialize(type, context.SerializerOptions)!;
|
||||
return trigger;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
using Elsa.Management.Contracts;
|
||||
using Elsa.Management.Models;
|
||||
|
||||
namespace Elsa.Management.Services;
|
||||
|
||||
public class TriggerRegistry : ITriggerRegistry
|
||||
{
|
||||
private readonly IDictionary<object, ICollection<TriggerDescriptor>> _providedTriggerDescriptors = new Dictionary<object, ICollection<TriggerDescriptor>>();
|
||||
private readonly IDictionary<string, TriggerDescriptor> _triggerDescriptors = new Dictionary<string, TriggerDescriptor>();
|
||||
|
||||
public void Add(object provider, TriggerDescriptor descriptor) => Add(descriptor, GetOrCreateDescriptors(provider));
|
||||
|
||||
public void AddMany(object provider, IEnumerable<TriggerDescriptor> descriptors)
|
||||
{
|
||||
var target = GetOrCreateDescriptors(provider);
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
Add(descriptor, target);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_triggerDescriptors.Clear();
|
||||
_providedTriggerDescriptors.Clear();
|
||||
}
|
||||
|
||||
public void ClearProvider(object provider)
|
||||
{
|
||||
var descriptors = ListByProvider(provider).ToList();
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
_triggerDescriptors.Remove(descriptor.NodeType);
|
||||
|
||||
_providedTriggerDescriptors.Remove(provider);
|
||||
}
|
||||
|
||||
public IEnumerable<TriggerDescriptor> ListAll() => _triggerDescriptors.Values;
|
||||
public IEnumerable<TriggerDescriptor> ListByProvider(object provider) => _providedTriggerDescriptors.TryGetValue(provider, out var descriptors) ? descriptors : ArraySegment<TriggerDescriptor>.Empty;
|
||||
public TriggerDescriptor? Find(Func<TriggerDescriptor, bool> predicate) => _triggerDescriptors.Values.FirstOrDefault(predicate);
|
||||
public TriggerDescriptor? Find(string triggerType) => _triggerDescriptors.TryGetValue(triggerType, out var descriptor) ? descriptor : null;
|
||||
|
||||
private void Add(TriggerDescriptor descriptor, ICollection<TriggerDescriptor> target)
|
||||
{
|
||||
_triggerDescriptors.Add(descriptor.NodeType, descriptor);
|
||||
target.Add(descriptor);
|
||||
}
|
||||
|
||||
private ICollection<TriggerDescriptor> GetOrCreateDescriptors(object provider)
|
||||
{
|
||||
if (_providedTriggerDescriptors.TryGetValue(provider, out var descriptors))
|
||||
return descriptors;
|
||||
|
||||
descriptors = new List<TriggerDescriptor>();
|
||||
_providedTriggerDescriptors.Add(provider, descriptors);
|
||||
|
||||
return descriptors;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
using Elsa.Management.Contracts;
|
||||
|
||||
namespace Elsa.Management.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Populates the <see cref="ITriggerRegistry"/> with descriptors provided by <see cref="ITriggerProvider"/>s.
|
||||
/// </summary>
|
||||
public class TriggerRegistryPopulator : ITriggerRegistryPopulator
|
||||
{
|
||||
private readonly IEnumerable<ITriggerProvider> _providers;
|
||||
private readonly ITriggerRegistry _registry;
|
||||
|
||||
public TriggerRegistryPopulator(IEnumerable<ITriggerProvider> providers, ITriggerRegistry registry)
|
||||
{
|
||||
_providers = providers;
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public async ValueTask PopulateRegistryAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var provider in _providers)
|
||||
{
|
||||
var descriptors = await provider.GetDescriptorsAsync(cancellationToken);
|
||||
_registry.AddMany(provider, descriptors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ public static class ServiceCollectionExtensions
|
|||
.AddSingleton<IHasher, Hasher>()
|
||||
.AddSingleton<IIdentityGenerator, IdentityGenerator>()
|
||||
.AddSingleton<ISystemClock, SystemClock>()
|
||||
.AddSingleton<IPayloadSerializer, PayloadSerializer>()
|
||||
.AddSingleton<IBookmarkDataSerializer, BookmarkDataSerializer>()
|
||||
|
||||
// DSL.
|
||||
.AddDsl()
|
||||
|
|
@ -75,5 +75,4 @@ public static class ServiceCollectionExtensions
|
|||
public static IServiceCollection AddWorkflowProvider<T>(this IServiceCollection services) where T : class, IWorkflowProvider => services.AddSingleton<IWorkflowProvider, T>();
|
||||
public static IServiceCollection AddStimulusHandler<T>(this IServiceCollection services) where T : class, IStimulusHandler => services.AddSingleton<IStimulusHandler, T>();
|
||||
public static IServiceCollection AddInstructionInterpreter<T>(this IServiceCollection services) where T : class, IWorkflowInstructionInterpreter => services.AddSingleton<IWorkflowInstructionInterpreter, T>();
|
||||
public static IServiceCollection ConfigureWorkflowRuntime(this IServiceCollection services, Action<WorkflowRuntimeOptions> configure) => services.Configure(configure);
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
using System;
|
||||
[assembly: CLSCompliant(true)]
|
||||
[assembly: CLSCompliant(false)]
|
||||
|
|
@ -21,7 +21,7 @@ public static class TypeSystemExtensions
|
|||
{
|
||||
var kind = TypeKind.Unknown;
|
||||
var isActivity = typeof(IActivity).IsAssignableFrom(type);
|
||||
var isTrigger = typeof(ITrigger).IsAssignableFrom(type);
|
||||
var isTrigger = typeof(IEventGenerator).IsAssignableFrom(type);
|
||||
var isPrimitive = type.IsPrimitive;
|
||||
var isObject = type.IsClass || type.IsValueType;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Azure.Messaging.ServiceBus;
|
||||
using Elsa.Attributes;
|
||||
using Elsa.Formatting.Contracts;
|
||||
using Elsa.Models;
|
||||
|
|
@ -33,12 +32,12 @@ public class MessageReceived : Trigger
|
|||
/// </summary>
|
||||
public IFormatter? Formatter { get; set; }
|
||||
|
||||
protected override object GetTriggerPayload(TriggerIndexingContext context) => GetPayload(context.ExpressionExecutionContext);
|
||||
protected override object GetTriggerDatum(TriggerIndexingContext context) => GetBookmarkData(context.ExpressionExecutionContext);
|
||||
|
||||
protected override void Execute(ActivityExecutionContext context)
|
||||
{
|
||||
var payload = GetPayload(context.ExpressionExecutionContext);
|
||||
context.SetBookmark(payload, Resume);
|
||||
var bookmarkData = GetBookmarkData(context.ExpressionExecutionContext);
|
||||
context.CreateBookmark(bookmarkData, Resume);
|
||||
}
|
||||
|
||||
private async ValueTask Resume(ActivityExecutionContext context)
|
||||
|
|
@ -52,7 +51,7 @@ public class MessageReceived : Trigger
|
|||
context.Set(ReceivedMessageBody, body);
|
||||
}
|
||||
|
||||
private object GetPayload(ExpressionExecutionContext context)
|
||||
private object GetBookmarkData(ExpressionExecutionContext context)
|
||||
{
|
||||
var queueOrTopic = context.Get(QueueOrTopic)!;
|
||||
var subscription = context.Get(Subscription);
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ namespace Elsa.Modules.AzureServiceBus.Handlers;
|
|||
/// </summary>
|
||||
public class UpdateWorkers : INotificationHandler<WorkflowTriggersIndexed>, INotificationHandler<WorkflowBookmarksDeleted>, INotificationHandler<WorkflowBookmarksSaved>
|
||||
{
|
||||
private readonly IPayloadSerializer _serializer;
|
||||
private readonly IBookmarkDataSerializer _serializer;
|
||||
private readonly IWorkerManager _workerManager;
|
||||
|
||||
public UpdateWorkers(IPayloadSerializer serializer, IWorkerManager workerManager)
|
||||
public UpdateWorkers(IBookmarkDataSerializer serializer, IWorkerManager workerManager)
|
||||
{
|
||||
_serializer = serializer;
|
||||
_workerManager = workerManager;
|
||||
|
|
@ -28,8 +28,8 @@ public class UpdateWorkers : INotificationHandler<WorkflowTriggersIndexed>, INot
|
|||
/// </summary>
|
||||
public async Task HandleAsync(WorkflowTriggersIndexed notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var added = notification.IndexedWorkflowTriggers.AddedTriggers.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
var removed = notification.IndexedWorkflowTriggers.RemovedTriggers.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
var added = notification.IndexedWorkflowTriggers.AddedTriggers.Filter<MessageReceived>().Select(x => DeserializePayload(x.Data!));
|
||||
var removed = notification.IndexedWorkflowTriggers.RemovedTriggers.Filter<MessageReceived>().Select(x => DeserializePayload(x.Data!));
|
||||
|
||||
await StopWorkersAsync(removed, cancellationToken);
|
||||
await StartWorkersAsync(added, cancellationToken);
|
||||
|
|
@ -40,7 +40,7 @@ public class UpdateWorkers : INotificationHandler<WorkflowTriggersIndexed>, INot
|
|||
/// </summary>
|
||||
public async Task HandleAsync(WorkflowBookmarksDeleted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var payloads = notification.Bookmarks.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
var payloads = notification.Bookmarks.Filter<MessageReceived>().Select(x => DeserializePayload(x.Data!));
|
||||
await StopWorkersAsync(payloads, cancellationToken);
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ public class UpdateWorkers : INotificationHandler<WorkflowTriggersIndexed>, INot
|
|||
/// </summary>
|
||||
public async Task HandleAsync(WorkflowBookmarksSaved notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var payloads = notification.Bookmarks.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
var payloads = notification.Bookmarks.Filter<MessageReceived>().Select(x => DeserializePayload(x.Data!));
|
||||
await StartWorkersAsync(payloads, cancellationToken);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using Elsa.Modules.Http.Models;
|
|||
|
||||
namespace Elsa.Modules.Http;
|
||||
|
||||
public class HttpTrigger : Trigger
|
||||
public class HttpEndpoint : Trigger
|
||||
{
|
||||
[Input] public Input<string> Path { get; set; } = default!;
|
||||
|
||||
|
|
@ -20,14 +20,14 @@ public class HttpTrigger : Trigger
|
|||
|
||||
[Output] public Output<HttpRequestModel>? Result { get; set; }
|
||||
|
||||
protected override IEnumerable<object> GetTriggerPayloads(TriggerIndexingContext context) => GetHashInputs(context.ExpressionExecutionContext);
|
||||
protected override void Execute(ActivityExecutionContext context) => context.SetBookmarks(GetHashInputs(context.ExpressionExecutionContext));
|
||||
protected override IEnumerable<object> GetTriggerData(TriggerIndexingContext context) => GetBookmarkData(context.ExpressionExecutionContext);
|
||||
protected override void Execute(ActivityExecutionContext context) => context.SetBookmarks(GetBookmarkData(context.ExpressionExecutionContext));
|
||||
|
||||
private IEnumerable<object> GetHashInputs(ExpressionExecutionContext context)
|
||||
private IEnumerable<object> GetBookmarkData(ExpressionExecutionContext context)
|
||||
{
|
||||
// Generate a bookmark hash for path and selected methods.
|
||||
// Generate bookmark data for path and selected methods.
|
||||
var path = context.Get(Path);
|
||||
var methods = context.Get(SupportedMethods);
|
||||
return methods!.Select(x => new HttpTriggerPayload(path!, x.ToLowerInvariant())).Cast<object>().ToArray();
|
||||
return methods!.Select(x => new HttpBookmarkData(path!, x.ToLowerInvariant())).Cast<object>().ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,8 @@ public class HttpTriggerMiddleware
|
|||
var path = GetPath(httpContext);
|
||||
var method = httpContext.Request.Method!.ToLowerInvariant();
|
||||
var abortToken = httpContext.RequestAborted;
|
||||
var hash = _hasher.Hash(new HttpTriggerPayload(path, method));
|
||||
var activityTypeName = TypeNameHelper.GenerateTypeName<HttpTrigger>();
|
||||
var hash = _hasher.Hash(new HttpBookmarkData(path, method));
|
||||
var activityTypeName = TypeNameHelper.GenerateTypeName<HttpEndpoint>();
|
||||
var stimulus = Stimulus.Standard(activityTypeName, hash);
|
||||
var executionResults = (await workflowServer.ExecuteStimulusAsync(stimulus, abortToken)).ToList();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
namespace Elsa.Modules.Http.Models;
|
||||
|
||||
public record HttpTriggerPayload
|
||||
public record HttpBookmarkData
|
||||
{
|
||||
private readonly string _path = default!;
|
||||
private readonly string _method = default!;
|
||||
|
||||
public HttpTriggerPayload(string path, string method)
|
||||
public HttpBookmarkData(string path, string method)
|
||||
{
|
||||
Path = path;
|
||||
Method = method;
|
||||
|
|
@ -16,7 +16,7 @@ public class Delay : Activity
|
|||
var resumeAt = clock.UtcNow.Add(timeSpan);
|
||||
var payload = new DelayPayload(resumeAt);
|
||||
|
||||
context.SetBookmark(payload);
|
||||
context.CreateBookmark(payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public class WorkflowBookmarkScheduler : IWorkflowBookmarkScheduler
|
|||
// Schedule a trigger for each bookmark.
|
||||
foreach (var bookmark in delayBookmarks)
|
||||
{
|
||||
var payload = JsonSerializer.Deserialize<DelayPayload>(bookmark.Payload!)!;
|
||||
var payload = JsonSerializer.Deserialize<DelayPayload>(bookmark.Data!)!;
|
||||
var resumeAt = payload.ResumeAt;
|
||||
var job = new ResumeWorkflowJob(workflowInstanceId, bookmark.ToBookmark());
|
||||
var schedule = new SpecificInstantSchedule(resumeAt);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class WorkflowTriggerScheduler : IWorkflowTriggerScheduler
|
|||
foreach (var trigger in timerTriggers)
|
||||
{
|
||||
// Schedule trigger.
|
||||
var (dateTime, timeSpan) = JsonSerializer.Deserialize<TimerPayload>(trigger.Payload!)!;
|
||||
var (dateTime, timeSpan) = JsonSerializer.Deserialize<TimerPayload>(trigger.Data!)!;
|
||||
var groupKeys = new[] { RootGroupKey, trigger.WorkflowDefinitionId };
|
||||
await _jobScheduler.ScheduleAsync(new RunWorkflowJob(trigger.WorkflowDefinitionId), new RecurringSchedule(dateTime, timeSpan), groupKeys, cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ using Elsa.Models;
|
|||
namespace Elsa.Modules.Scheduling.Triggers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a timer to periodically trigger the workflow it is associated with.
|
||||
/// Represents a timer to periodically trigger the workflow.
|
||||
/// </summary>
|
||||
public class Timer : Trigger
|
||||
public class Timer : EventGenerator
|
||||
{
|
||||
[Input] public Input<TimeSpan> Interval { get; set; } = default!;
|
||||
|
||||
protected override IEnumerable<object> GetTriggerPayloads(TriggerIndexingContext context)
|
||||
protected override IEnumerable<object> GetTriggerData(TriggerIndexingContext context)
|
||||
{
|
||||
var interval = context.ExpressionExecutionContext.Get(Interval);
|
||||
var clock = context.ExpressionExecutionContext.GetRequiredService<ISystemClock>();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ public class WorkflowBookmark : Entity
|
|||
{
|
||||
public string Name { get; init; } = default!;
|
||||
public string? Hash { get; set; }
|
||||
public string? Payload { get; set; }
|
||||
public string? Data { get; set; }
|
||||
public string WorkflowDefinitionId { get; init; } = default!;
|
||||
public string WorkflowInstanceId { get; init; } = default!;
|
||||
public string CorrelationId { get; init; } = default!;
|
||||
|
|
@ -14,7 +14,7 @@ public class WorkflowBookmark : Entity
|
|||
public string ActivityInstanceId { get; init; } = default!;
|
||||
public string? CallbackMethodName { get; set; }
|
||||
|
||||
public Bookmark ToBookmark() => new(Id, Name, Hash, Payload, ActivityId, ActivityInstanceId, CallbackMethodName);
|
||||
public Bookmark ToBookmark() => new(Id, Name, Hash, Data, ActivityId, ActivityInstanceId, CallbackMethodName);
|
||||
|
||||
public static WorkflowBookmark FromBookmark(Bookmark bookmark, WorkflowInstance workflowInstance) =>
|
||||
new()
|
||||
|
|
@ -24,7 +24,7 @@ public class WorkflowBookmark : Entity
|
|||
WorkflowInstanceId = workflowInstance.Id,
|
||||
CorrelationId = workflowInstance.CorrelationId,
|
||||
Hash = bookmark.Hash,
|
||||
Payload = bookmark.Payload,
|
||||
Data = bookmark.Data,
|
||||
Name = bookmark.Name,
|
||||
ActivityId = bookmark.ActivityId,
|
||||
ActivityInstanceId = bookmark.ActivityInstanceId,
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ public class WorkflowTrigger : Entity
|
|||
public string WorkflowDefinitionId { get; set; } = default!;
|
||||
public string Name { get; set; } = default!;
|
||||
public string? Hash { get; set; }
|
||||
public string? Payload { get; set; }
|
||||
public string? Data { get; set; }
|
||||
}
|
||||
|
|
@ -7,5 +7,5 @@ namespace Elsa.Persistence.Requests;
|
|||
|
||||
public record FindWorkflowTriggersByName(string Name, string? Hash = default) : IRequest<ICollection<WorkflowTrigger>>
|
||||
{
|
||||
public static FindWorkflowTriggersByName ForTrigger<T>(string? hash = default) where T : ITrigger => new(TypeNameHelper.GenerateTypeName<T>(), hash);
|
||||
public static FindWorkflowTriggersByName ForTrigger<T>(string? hash = default) where T : IEventGenerator => new(TypeNameHelper.GenerateTypeName<T>(), hash);
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ public class WorkflowOperatorActor : IActor
|
|||
{
|
||||
Id = x.Id,
|
||||
Hash = x.Hash,
|
||||
Payload = x.Payload,
|
||||
Payload = x.Data,
|
||||
Name = x.Name,
|
||||
ActivityId = x.ActivityId,
|
||||
ActivityInstanceId = x.ActivityInstanceId,
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ public class ProtoActorWorkflowInvoker : IWorkflowInvoker
|
|||
Id = bookmark.Id,
|
||||
Name = bookmark.Name,
|
||||
Hash = bookmark.Hash,
|
||||
Payload = bookmark.Payload,
|
||||
Payload = bookmark.Data,
|
||||
ActivityId = bookmark.ActivityId,
|
||||
ActivityInstanceId = bookmark.ActivityInstanceId,
|
||||
CallbackMethodName = bookmark.CallbackMethodName,
|
||||
|
|
|
|||
|
|
@ -6,21 +6,7 @@ namespace Elsa.Runtime.HostedServices;
|
|||
public class RegisterDescriptors : IHostedService
|
||||
{
|
||||
private readonly IActivityRegistryPopulator _activityRegistryPopulator;
|
||||
private readonly ITriggerRegistryPopulator _triggerRegistryPopulator;
|
||||
|
||||
public RegisterDescriptors(IActivityRegistryPopulator activityRegistryPopulator, ITriggerRegistryPopulator triggerRegistryPopulator)
|
||||
{
|
||||
_activityRegistryPopulator = activityRegistryPopulator;
|
||||
_triggerRegistryPopulator = triggerRegistryPopulator;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var activityRegistryTask = _activityRegistryPopulator.PopulateRegistryAsync(cancellationToken).AsTask();
|
||||
var triggerRegistryTask = _triggerRegistryPopulator.PopulateRegistryAsync(cancellationToken).AsTask();
|
||||
|
||||
await Task.WhenAll(activityRegistryTask, triggerRegistryTask);
|
||||
}
|
||||
|
||||
public RegisterDescriptors(IActivityRegistryPopulator activityRegistryPopulator) => _activityRegistryPopulator = activityRegistryPopulator;
|
||||
public async Task StartAsync(CancellationToken cancellationToken) => await _activityRegistryPopulator.PopulateRegistryAsync(cancellationToken).AsTask();
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ public class ResumeWorkflowInstructionInterpreter : WorkflowInstructionInterpret
|
|||
}
|
||||
|
||||
// Resume workflow instance.
|
||||
var bookmark = new Bookmark(workflowBookmark.Id, workflowBookmark.Name, workflowBookmark.Hash, workflowBookmark.Payload, workflowBookmark.ActivityId, workflowBookmark.ActivityInstanceId, workflowBookmark.CallbackMethodName);
|
||||
var bookmark = new Bookmark(workflowBookmark.Id, workflowBookmark.Name, workflowBookmark.Hash, workflowBookmark.Data, workflowBookmark.ActivityId, workflowBookmark.ActivityInstanceId, workflowBookmark.CallbackMethodName);
|
||||
var workflowState = workflowInstance.WorkflowState;
|
||||
var input = instruction.Input;
|
||||
var workflowExecutionResult = await _workflowRunner.RunAsync(workflow, workflowState, bookmark, input, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -113,26 +113,79 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
|
||||
private async IAsyncEnumerable<WorkflowTrigger> GetTriggersAsync(Workflow workflow, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var context = new WorkflowIndexingContext(workflow);
|
||||
var triggerNodes = _activityWalker.Walk(workflow.Root).Flatten().Where(x => x.Activity is ITrigger { TriggerMode: TriggerMode.WorkflowDefinition }).ToList();
|
||||
var context = new WorkflowIndexingContext(workflow, cancellationToken);
|
||||
|
||||
// Get a list of activities that are configured as "startable".
|
||||
var startableNodes = _activityWalker
|
||||
.Walk(workflow.Root)
|
||||
.Flatten()
|
||||
.Where(x => x.Activity.CanStartWorkflow)
|
||||
.ToList();
|
||||
|
||||
foreach (var triggerNode in triggerNodes)
|
||||
// For each startable node, create triggers.
|
||||
foreach (var node in startableNodes)
|
||||
{
|
||||
var triggers = await GetTriggersAsync(workflow, context, (ITrigger)triggerNode.Activity, cancellationToken);
|
||||
var triggers = await GetTriggersAsync(context, (IEventGenerator)node.Activity, cancellationToken);
|
||||
|
||||
foreach (var trigger in triggers)
|
||||
yield return trigger;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<WorkflowTrigger>> GetTriggersAsync(Workflow workflow, WorkflowIndexingContext context, ITrigger trigger, CancellationToken cancellationToken)
|
||||
private async Task<IEnumerable<WorkflowTrigger>> GetTriggersAsync(WorkflowIndexingContext context, IActivity activity, CancellationToken cancellationToken)
|
||||
{
|
||||
// If the activity implements ITrigger, request its trigger data. Otherwise, create one trigger datum.
|
||||
if (activity is ITrigger trigger)
|
||||
return await CreateWorkflowTriggersAsync(context, trigger);
|
||||
|
||||
// Else, create a single workflow trigger with no additional data.
|
||||
var simpleTrigger = CreateWorkflowTrigger(context, activity);
|
||||
|
||||
return new[] { simpleTrigger };
|
||||
}
|
||||
|
||||
private WorkflowTrigger CreateWorkflowTrigger(WorkflowIndexingContext context, IActivity activity)
|
||||
{
|
||||
var workflow = context.Workflow;
|
||||
return new WorkflowTrigger
|
||||
{
|
||||
Id = _identityGenerator.GenerateId(),
|
||||
WorkflowDefinitionId = workflow.Identity.DefinitionId,
|
||||
Name = activity.TypeName
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ICollection<WorkflowTrigger>> CreateWorkflowTriggersAsync(WorkflowIndexingContext context, ITrigger trigger)
|
||||
{
|
||||
var workflow = context.Workflow;
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var expressionExecutionContext = await CreateExpressionExecutionContextAsync(context, trigger);
|
||||
|
||||
var triggerIndexingContext = new TriggerIndexingContext(context, expressionExecutionContext, trigger, cancellationToken);
|
||||
var triggerData = await TryGetTriggerDataAsync(trigger, triggerIndexingContext);
|
||||
var triggerTypeName = trigger.TypeName;
|
||||
|
||||
var triggers = triggerData.Select(x => new WorkflowTrigger
|
||||
{
|
||||
Id = _identityGenerator.GenerateId(),
|
||||
WorkflowDefinitionId = workflow.Identity.DefinitionId,
|
||||
Name = triggerTypeName,
|
||||
Hash = _hasher.Hash(x),
|
||||
Data = JsonSerializer.Serialize(x)
|
||||
});
|
||||
|
||||
return triggers.ToList();
|
||||
}
|
||||
|
||||
private async Task<ExpressionExecutionContext> CreateExpressionExecutionContextAsync(WorkflowIndexingContext context, ITrigger trigger)
|
||||
{
|
||||
var inputs = trigger.GetInputs();
|
||||
var assignedInputs = inputs.Where(x => x.LocationReference != null!).ToList();
|
||||
var register = context.GetOrCreateRegister(trigger);
|
||||
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, default);
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, register, default, cancellationToken);
|
||||
|
||||
// Evaluate trigger inputs.
|
||||
// Evaluate activity inputs before requesting trigger data.
|
||||
foreach (var input in assignedInputs)
|
||||
{
|
||||
var locationReference = input.LocationReference;
|
||||
|
|
@ -148,32 +201,18 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
}
|
||||
}
|
||||
|
||||
var triggerIndexingContext = new TriggerIndexingContext(context, expressionExecutionContext, trigger);
|
||||
var payloads = await TryGetPayloadsAsync(trigger, triggerIndexingContext, cancellationToken);
|
||||
var triggerType = trigger.GetType();
|
||||
var triggerTypeName = TypeNameHelper.GenerateTypeName(triggerType);
|
||||
|
||||
var triggers = payloads.Select(x => new WorkflowTrigger
|
||||
{
|
||||
Id = _identityGenerator.GenerateId(),
|
||||
WorkflowDefinitionId = workflow.Identity.DefinitionId,
|
||||
Name = triggerTypeName,
|
||||
Hash = _hasher.Hash(x),
|
||||
Payload = JsonSerializer.Serialize(x)
|
||||
});
|
||||
|
||||
return triggers;
|
||||
return expressionExecutionContext;
|
||||
}
|
||||
|
||||
private async Task<ICollection<object>> TryGetPayloadsAsync(ITrigger trigger, TriggerIndexingContext context, CancellationToken cancellationToken)
|
||||
private async Task<ICollection<object>> TryGetTriggerDataAsync(ITrigger trigger, TriggerIndexingContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await trigger.GetTriggerPayloadsAsync(context, cancellationToken)).ToList();
|
||||
return (await trigger.GetTriggerDataAsync(context)).ToList();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Failed to get hash inputs");
|
||||
_logger.LogWarning(e, "Failed to get trigger data for activity {ActivityId}", trigger.Id);
|
||||
}
|
||||
|
||||
return Array.Empty<object>();
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ services
|
|||
.AddActivity<WriteLines>()
|
||||
.AddActivity<ReadLine>()
|
||||
.AddActivity<If>()
|
||||
.AddActivity<HttpTrigger>()
|
||||
.AddActivity<HttpEndpoint>()
|
||||
.AddActivity<Flowchart>()
|
||||
.AddActivity<Delay>()
|
||||
.AddActivity<Timer>()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class ForkedHttpWorkflow : IWorkflow
|
|||
{
|
||||
Activities =
|
||||
{
|
||||
new HttpTrigger
|
||||
new HttpEndpoint
|
||||
{
|
||||
Path = new Input<string>("/fork"),
|
||||
SupportedMethods = new Input<ICollection<string>>(new[] { HttpMethods.Get })
|
||||
|
|
@ -34,7 +34,7 @@ public class ForkedHttpWorkflow : IWorkflow
|
|||
{
|
||||
Activities =
|
||||
{
|
||||
new HttpTrigger { Path = new Input<string>("/fork/branch-1"), },
|
||||
new HttpEndpoint { Path = new Input<string>("/fork/branch-1"), },
|
||||
new WriteLine("Branch 1 continues!")
|
||||
}
|
||||
},
|
||||
|
|
@ -42,7 +42,7 @@ public class ForkedHttpWorkflow : IWorkflow
|
|||
{
|
||||
Activities =
|
||||
{
|
||||
new HttpTrigger { Path = new Input<string>("/fork/branch-2"), },
|
||||
new HttpEndpoint { Path = new Input<string>("/fork/branch-2"), },
|
||||
new WriteLine("Branch 2 continues!")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ public class HttpWorkflow : IWorkflow
|
|||
{
|
||||
Activities =
|
||||
{
|
||||
new HttpTrigger
|
||||
new HttpEndpoint
|
||||
{
|
||||
Path = new Input<string>("/hello-world"),
|
||||
SupportedMethods = new Input<ICollection<string>>(new[] { HttpMethods.Get })
|
||||
|
|
@ -34,7 +34,7 @@ public class HttpWorkflow : IWorkflow
|
|||
Condition = new Input<bool>(true),
|
||||
Then = new Sequence(
|
||||
new WriteLine("It's true!"),
|
||||
new HttpTrigger
|
||||
new HttpEndpoint
|
||||
{
|
||||
Path = new Input<string>("/hello-world/true"),
|
||||
SupportedMethods = new Input<ICollection<string>>(new[] { HttpMethods.Post })
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ using Elsa.Extensions;
|
|||
using Elsa.Modules.Http.Extensions;
|
||||
using Elsa.Persistence.InMemory.Extensions;
|
||||
using Elsa.Pipelines.WorkflowExecution.Components;
|
||||
using Elsa.Runtime.Middleware;
|
||||
using Elsa.Runtime.Extensions;
|
||||
using Elsa.Runtime.ProtoActor.Extensions;
|
||||
using Elsa.Samples.Web2.Workflows;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using WorkflowExecutionBuilderExtensions = Elsa.Runtime.Extensions.WorkflowExecutionBuilderExtensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
|
@ -27,7 +26,7 @@ builder.Services
|
|||
var app = builder.Build();
|
||||
|
||||
// Configure workflow engine execution pipeline.
|
||||
app.Services.ConfigureDefaultWorkflowExecutionPipeline(pipeline => WorkflowExecutionBuilderExtensions.UsePersistence(pipeline)
|
||||
app.Services.ConfigureDefaultWorkflowExecutionPipeline(pipeline => pipeline.UsePersistence()
|
||||
.UseActivityScheduler()
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ typeSystem.Register("Variable<>", typeof(Variable<>));
|
|||
typeSystem.Register<Sequence>();
|
||||
typeSystem.Register<ReadLine>();
|
||||
typeSystem.Register<WriteLine>();
|
||||
typeSystem.Register<HttpTrigger>();
|
||||
typeSystem.Register<HttpEndpoint>();
|
||||
typeSystem.Register<Timer>();
|
||||
|
||||
functionActivityRegistry.RegisterFunction("print", nameof(WriteLine), new[] { nameof(WriteLine.Text) });
|
||||
|
|
|
|||
Loading…
Reference in a new issue