elsa 活动
活动是工作流可执行的单元。活动可以链接在一起形成工作流。在 Elsa 中,活动由实现 IActivity
接口的类表示。
活动创建
以下是一个创建自定义活动的例子:
public class HelloWorld : Activity
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
Console.WriteLine("你好,世界!");
await CompleteAsync();
}
}
在上述示例中,HelloWorld
活动是一个简单的活动,它向控制台输出 "你好,世界!" 然后完成。
除了继承自 Activity
类,你还可以继承自 CodeActivity
类,该类包含一个 AutoCompleteBehavior
行为,当 ExecuteAsync
方法完成时自动完成活动:
public class HelloWorld : CodeActivity
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
Console.WriteLine("你好,世界!");
}
}
输入与输出
类似于函数,活动可以接收输入并返回输出。访问输入是通过传递给 ExecuteAsync
方法的 ActivityExecutionContext
对象提供的。同样,输出也可以通过 ActivityExecutionContext
对象设置。
以下是一个接收输入并返回输出的活动示例:
public class Sum : CodeActivity<int>
{
public Sum(Variable<int> a, Variable<int> b, Variable<int> result)
{
A = new(a);
B = new(b);
Result = new(result);
}
public Input<int> A { get; set; } = default!;
public Input<int> B { get; set; } = default!;
protected override void Execute(ActivityExecutionContext context)
{
var input1 = A.Get(context);
var input2 = B.Get(context);
var result = input1 + input2;
context.SetResult(result);
}
}
上述活动接收两个输入 A
和 B
,并返回一个输出 Result
。注意输入是如何使用 Get
方法访问的,而输出则使用 SetResult
方法设置。SetResult
方法是一个便捷方法,用于从 CodeActivity<T>
继承的 Result
属性设置结果。
我们现在可以在工作流中使用上述活动:
// 声明用于保存活动输出的工作流变量。
var a = new Variable<int>();
var b = new Variable<int>();
var sum = new Variable<int>();
var workflow = new Workflow
{
Root = new Sequence
{
Variables = {a, b, sum},
Activities =
{
new WriteLine("输入第一个值"),
new ReadLine(a),
new WriteLine("输入第二个值"),
new ReadLine(b),
new Sum(a, b, sum),
new WriteLine(context => $"{a.Get(context)} 和 {b.Get(context)} 的和是 {sum.Get(context)}")
}
}
};
元数据
活动可以用 ActivityAttribute
属性注解元数据。此元数据被设计器用来,例如,将活动分组到类别中并显示描述。
以下是一个用元数据注解活动的例子:
[Activity(
Namespace = "演示",
Category = "演示",
Description = "一个简单的活动,向控制台写入 \"你好,世界!\"。"
)]
输出结果
自定义活动输出结果可以通过在类上使用 FlowNodeAttribute
属性来定义,如下所示
[Activity("演示", "演示", "简单活动")]
[FlowNode("通过", "失败")]
这产生了活动的两个输出结果。在这种情况下,是 "通过" 或 "失败"。要产生一个输出结果,请使用 CompleteActivityWithOutcomesAsync
方法,如下所示
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var outcome = 2 > 1 ? "通过" : "失败";
await context.CompleteActivityWithOutcomesAsync(outcome);
}
复合活动
活动可以由其他活动组成。当你想要将一组活动组合在一起并作为一个单一单元执行时,或者你的活动包含一些决定接下来执行哪些活动的逻辑时,这很有用。
例如,这里是一个模拟简单 if
语句的自定义活动:
public class If : Activity
{
public Input<bool> Condition { get; set; } = default!;
public IActivity? Then { get; set; }
public IActivity? Else { get; set; }
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// 从评估的条件中获取结果。
var result = context.Get(Condition);
// 确定接下来要执行的活动。
var nextActivity = result ? Then : Else;
// 安排下一个活动执行。
await context.ScheduleActivityAsync(nextActivity, OnChildCompleted);
}
private async ValueTask OnChildCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext)
{
// 一旦这个子活动完成,就完成活动。
await context.CompleteActivityAsync();
}
}
注意,当你的活动调度子活动时,它还必须提供一个回调,该回调将在子活动完成后被调用。这是必要的,因为活动本身直到所有子活动都完成之后才被视为完成。
下面的示例展示了如何使用 If
活动:
var workflow = new Workflow
{
Root = new If
{
Condition = new(True),
Then = new WriteLine("你好,世界!"),
Else = new WriteLine("再见,残酷的世界!")
}
};
工作流根节点
请注意,我们将
If
活动赋值给了工作流的Root
属性。这样做是可行的,因为If
活动实现了IActivity
接口。如果你需要运行多个活动,只需简单地将一个容器活动,如Sequence
或Flowchart
安装为根活动即可。
依赖注入
Elsa 3.0 目前不直接支持依赖注入。
因此,按照传统方式调用依赖项,如下所示,将是不可能的。
public IApiService _apiService { get; set; }
public SimpleActivity(IApiService apiService)
{
_apiService = apiService;
}
如果需要依赖项,可以从 Elsa 活动执行上下文中使用 context.GetRequiredService
获取,如下所示:
public IApiService _apiService { get; set; }
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
_apiService = context.GetRequiredService<IApiService>();
await context.CompleteActivityAsync(new Outcomes(outcome));
}
确保在 Program.cs
文件中将依赖项添加到应用程序上下文中。
阻塞活动
阻塞活动是一种创建书签并等待外部事件恢复执行的活动。当你希望等待某些外部事件发生后再继续执行时,这非常有用。阻塞活动的例子包括 Event
活动和 Delay
活动。
下面是创建书签以供稍后用于恢复活动的一个阻塞活动示例:
public class MyEvent : Activity
{
protected override void Execute(ActivityExecutionContext context)
{
// 创建一个书签。创建的书签将存储在工作流状态中。
context.CreateBookmark();
// 此活动在事件发生之前不会完成。
}
}
以下是使用 Event
活动的一个示例:
var workflow = new Workflow
{
Root = new Sequence
{
Activities =
{
new WriteLine("工作流开始..."),
new MyEvent(), // 这将会阻塞后续执行,直到 MyEvent 的书签被恢复。
new WriteLine("事件发生!")
}
}
};
当我们运行这个工作流时,它会在控制台打印 "工作流开始...",然后等待名为 "MyEvent" 的事件发生。
var result = await workflowRunner.RunAsync(workflow);
result
变量包含了工作流状态,其中包含创建的书签。
要恢复工作流,我们需要提供书签 ID。在这个例子中,我们知道工作流只有一个书签,所以我们可以简单地从工作流状态中获取第一个书签。
var workflowState = result.WorkflowState;
var bookmark = workflowState.Bookmarks.Single(); // 获取由 MyEvent 活动创建的书签。
var options = new RunWorkflowOptions(BookmarkId: bookmark.Id);
// 恢复工作流。
await workflowRunner.RunAsync(workflow, workflowState, options);
当工作流恢复时,它会在控制台打印 "事件发生!" 然后完成。
书签管理
上面的示例使用低级服务手动检索和恢复书签。实际上,当你实现自定义阻塞活动时,通常会使用更高层次的服务,如
IWorkflowRuntime
来启动和恢复工作流。
触发器
触发器是一种特殊类型的活动,用于响应 HTTP 请求或消息队列中的消息等外部事件来启动工作流。
例如,我们可以将上一个示例中的 MyEvent
活动更新为一个触发器:
// 实现 ITrigger 接口。
public class MyEvent : Activity, ITrigger
{
protected override void Execute(ActivityExecutionContext context)
{
// 创建一个书签。创建的书签将存储在工作流状态中。
context.CreateBookmark();
// 此活动在事件发生之前不会完成。
}
// 实现 ITrigger 接口。
ValueTask<IEnumerable<object>> GetTriggerPayloadsAsync(TriggerIndexingContext context)
{
// 本例中我们不需要任何有效载荷。
return new Enumerable.Empty<object>().ToValueTask();
}
}
这些触发器是高层次服务(如 IWorkflowRuntime
)用来响应某些外部事件启动工作流的一种方式。在后台,工作流运行时调用较低层次的服务来运行相应的工作流。