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);
    }
}

上述活动接收两个输入 AB,并返回一个输出 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 接口。如果你需要运行多个活动,只需简单地将一个容器活动,如 SequenceFlowchart 安装为根活动即可。


依赖注入

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)用来响应某些外部事件启动工作流的一种方式。在后台,工作流运行时调用较低层次的服务来运行相应的工作流。

在本文档中