[.NET项目实战] Elsa开源工作流组件应用(三):实战演练

使用.net 构建 Elsa Workflow 对接过蓝凌OA 也基于泛微OA数据库原型重新研发上线过产品,自研的开源的也上线过 每个公司对OA流程引擎介绍 都不一样的, 比如Elsa 这块微软MVP 开源产品 这个,基于跨平台开发的技术含量高,专门做OA的同行推过对应文章。二章 改成API 使用的预发行版本 即还没有成为正式发布的版本!然后就潜规则 官网居然 直接就 下一页 不告诉怎么搞了 哈哈哈哈。---分割线------------------------已知的是1用这个就能拿到token了还要其他干啥。三章 开始用用核心点的东西了。 阅读详情
AI摘要

本文详细介绍了如何在Elsa开源工作流库中创建一个审批工作流,包括文章发布、审批流程和不同活动的实现,如下发问卷、通知、等待填写和定时任务。此外,还讨论了如何处理并发任务和常见问题解决方案。

补充

之前的文章简单介绍了工作流和Elsa工作流库,这里再补充说明两点

  1. 工作流的使用场景非常广泛,几乎涵盖了所有需要进行业务流程自动化管理的领域。

  2. 学习一个开源库,最简单的方法就是看源码,Elsa的工作流引擎源码非常简单易懂,并且提供了非常丰富的示例代码,举一个例子:审批工作流示例.\src\samples\aspnet\Elsa.Samples.AspNet.DocumentApproval

在这里插入图片描述
这个审批流是这样的:
作者发来一个文章,有两个审批人需要全部审批通过,文章才算通过,否则退回。

我们尝试阅读工作流源代码DocumentApprovalWorkflow.cs,并运行此项目,用postman发送请求

第一步:

假设这名叫Amanda的作者要发布文章,请求发送后,作者浏览器显示发送成功稍安勿躁之类的提示

同时后台打印作者信息和4个链接,分别是Jack和Lucy两位审批人“通过”和“退回”的url链接

Activities =
{
    new HttpEndpoint
    {
        Path = new("/documents"),
        SupportedMethods = new(new[] { HttpMethods.Post }),
        ParsedContent = new(documentVariable),
        CanStartWorkflow = true
    },
    new WriteLine(context => $"Document received from {documentVariable.Get<dynamic>(context)!.Author.Name}."),
    new WriteHttpResponse
    {
        Content = new("<h1>Request for Approval Sent</h1><p>Your document has been received and will be reviewed shortly.</p>"),
        ContentType = new(MediaTypeNames.Text.Html),
        StatusCode = new(HttpStatusCode.OK),
        ResponseHeaders = new(new HttpHeaders { ["X-Powered-By"] = new[] { "Elsa 3.0" } })
    },

第二步:

Jack觉得文章不错,通过浏览器请求了“通过”链接,而Lucy觉得文章还不够好,需改进,她在浏览器中请求了“退回”链接。

两位审批人的审批结果存储于approvedVariable变量中

同时他们的浏览器返回的响应内容:Thanks for the approval 或 Sorry to hear that

    new Fork
    {
        JoinMode = ForkJoinMode.WaitAll,
        Branches =
        {
            // Jack
            new Sequence
            {
                Activities =
                {
                    new WriteLine(context => $"Jack approve url: \n {GenerateSignalUrl(context, "Approve:Jack")}"),
                    new WriteLine(context => $"Jack reject url: \n {GenerateSignalUrl(context, "Reject:Jack")}"),
                    new Fork
                    {
                        JoinMode = ForkJoinMode.WaitAny,
                        Branches =
                        {
                            // Approve
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Approve:Jack"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(true)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Thanks for the approval, Jack!"),
                                    }
                                }
                            },

                            // Reject
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Reject:Jack"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(false)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Sorry to hear that, Jack!"),
                                    }
                                }
                            }
                        }
                    }
                }
            },

            // Lucy
            new Sequence
            {
                Activities =
                {
                    new WriteLine(context => $"Lucy approve url: \n {GenerateSignalUrl(context, "Approve:Lucy")}"),
                    new WriteLine(context => $"Lucy reject url: \n {GenerateSignalUrl(context, "Reject:Lucy")}"),
                    new Fork
                    {
                        JoinMode = ForkJoinMode.WaitAny,
                        Branches =
                        {
                            // Approve
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Approve:Lucy"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(true)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Thanks for the approval, Lucy!"),
                                    }
                                }
                            },

                            // Reject
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Reject:Lucy"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(false)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Sorry to hear that, Lucy!"),
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },

第三步:

根据approvedVariable变量判定文章是否被审核通过。

如果通过则在控制台打印Document document-1 approved!, 否则打印Document document-1 rejected!

    new WriteLine(context => $"Approved: {approvedVariable.Get<bool>(context)}"),
    new If(context => approvedVariable.Get<bool>(context))
    {
        Then = new WriteLine(context => $"Document ${documentVariable.Get<dynamic>(context)!.Id} approved!"),
        Else = new WriteLine(context => $"Document ${documentVariable.Get<dynamic>(context)!.Id} rejected!")
    }
}

Elsa工作流源码还提供了大量的Sample,这里就不一一列举了,

需求描述

根据不同的时间规则,发送下发问卷给客户填写。

下发问卷给用户填写,且填写有超时时间,期间要提醒用户答题,

如果问卷未在规定的时间内作答则,则作废,并提醒用户。

需求分析

我们将需求尽可能分解成为单一职责的功能单元,并定义这些功能单元的输入输出。

下发问卷任务 PublishQuestionnaireActivity

下发问卷是将问卷(Questionnaire)实例化成问卷实例(Survey),问卷实例绑定用户Id,用户在问卷实例上作答。明确输入和输出:

  • 输入:问卷ID
  • 输出:问卷实例对象SurveyDto

通知任务 NotificationActivity

通知在这个需求中需要发送问卷状态,时间等内容给对应的用户,同通至少包含标题和内容。

  • 输入:标题和内容
  • 输出:无

问卷状态跟踪任务 WaitFillInSurveyActivity

这个任务要追踪问卷实例的状态,当问卷实例状态为已完成时,可以继续执行后续任务。

  • 输入:问卷实例ID
  • 输出:无

定时和延时任务

用于延时执行每个下发问卷的时间,等待问卷超时,以及延时发送通知等。

  • 输入:开始日期,延时日期,间隔时间或cron表达式
  • 输出:无

根任务

根任务包含所有的子任务,完成这个任务后,整个流程结束。在这个需求中根任务只需要知道将什么问卷,发送给哪位用户,以及在何时发送这三个问题。

  • 输入:问卷ID,用户ID,发送时间
  • 输出:无

各子任务参数对于他们的根任务是透明的(Invisible),根任务只需要关心是否完成,而不需要知道任务参数。

代码实现

下发问卷活动 PublishQuestionnaireActivity

下发问卷任务可以抽象成为下发问卷活动 PublishQuestionnaireActivity
创建PublishQuestionnaireActivity类并设置输入QuestionnaireId,输出SurveyDto

public class PublishQuestionnaireActivity : Activity<SurveyDto>
{
    public PublishQuestionnaireActivity()
    {

    }
    public PublishQuestionnaireActivity(long questionnaireId)
    {
        QuestionnaireId = new Input<long>(questionnaireId);
    }


    
    public Input<long> QuestionnaireId { get; set; } = default!;
}


重写ExecuteAsync方法,完成问卷下发逻辑

protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
    var _surveyAppService = context.GetRequiredService<ISurveyAppService>();
    if (_surveyAppService != null)
    {
        var currentUserId = await context.GetInputValueAsync<Guid>("UserId");
        var survey = await _surveyAppService.PublishAsync(new PublishInput()
        {
            QuestionnaireId = this.QuestionnaireId.Get<long>(context),
            UserId = currentUserId

        }) ?? throw new Exception("创建问卷失败");
        context.SetResult(survey);
    }


    await context.CompleteActivityAsync();

}

如此,其他的任务分别抽象成为相应的活动,这里展示完整代码

通知活动:NotificationActivity

public class NotificationActivity : Activity
{


    public NotificationActivity()
    {

    }
    public NotificationActivity(string title, string content)
    {
        Content = new Input<string>(content);
        Title = new Input<string>(title);
    }

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var notificationManager = context.GetRequiredService<NotificationManager>();

        if (notificationManager != null)
        {
            var title = this.Title.Get(context);
            var content = this.Content.Get(context);
            var currentUserId = await context.GetInputValueAsync<Guid>("UserId");
            var data = new CreatePrivateMessageNotificationEto(currentUserId, title, content);
            await notificationManager.Send(data);
        }


    await context.CompleteActivityAsync();

}

    public Input<string> Title { get; set; } = default!;
    public Input<string> Content { get; set; } = default!;
}

等待问卷完成活动:WaitFillInSurveyActivity

public class WaitFillInSurveyActivity : Activity
{
    public WaitFillInSurveyActivity()
    {

    }
    public WaitFillInSurveyActivity(Func<ExpressionExecutionContext, long?> surveyId)
: this(Expression.DelegateExpression(surveyId))
    {
    }

    public WaitFillInSurveyActivity(long surveyId) => SurveyId = new Input<long>(surveyId);

    public WaitFillInSurveyActivity(Expression expression) => SurveyId = new Input<long>(expression, new MemoryBlockReference());


    /// <inheritdoc />
    protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var surveyId = SurveyId.Get(context);
        if (surveyId == default)
        {
            var survey = context.ExpressionExecutionContext.GetLastResult<SurveyDto>();
            surveyId = survey.Id;
        }
        var payload = new WaitFillInSurveyBookmarkPayload(surveyId);
        context.CreateBookmark(new CreateBookmarkArgs
        {
            Payload = payload,
            Callback = Resume,
            BookmarkName = Type,
            IncludeActivityInstanceId = false
        });
        return ValueTask.CompletedTask;
    }

    private async ValueTask Resume(ActivityExecutionContext context)
    {
        await context.CompleteActivityAsync();
    }

    public Input<long> SurveyId { get; set; } = default!;
}

此任务需要等待,我们创建一个Bookmark,注意创建Bookmark时,我们根据问卷实例SurveyId判断是否完成问卷的回答,因此指定IncludeActivityInstanceId为false,创建携带SurveyId的Payload类型:

public record WaitFillInSurveyBookmarkPayload(long SurveyId);

在回调OnResumeAsync中,我们使用context.CompleteActivityAsync来完成任务。

定时和延时活动:

Elsa.Scheduling库提供了用于定时和延时任务的触发器(触发器属于工作流的一种)

在这里插入图片描述

在[.NET项目实战] Elsa开源工作流组件应用(二):内核解读 一文 "构建 - 构建活动 "章节 列出了Elsa所有内建的活动。

这里使用Elsa内建的三个触发器:

StartAt 在未来特定的时间戳触发工作流触发器
Delay 延迟执行工作流触发器。
Timer 定期触发工作流触发器。

问卷活动:QuestionnaireActivity

问卷活动是下发问卷,通知,等待填写问卷等活动的父级。

Elsa定义了容器类型的活动Container类型,其中的Activities可以包含其他活动。

在这里插入图片描述

Sequence和Parallel都是容器类型,是Activity的子类,它们分别表示并行和顺序执行。

除此之外我们还需要两个内建活动:

Fork:分支,用于分支并行执行,与Parallel类似,但比它多了一个等待完成功能。

通过ForkJoinMode属性,可以指定分支任务的执行方式,ForkJoinMode.WaitAny:等待任意一个任务完成,ForkJoinMode.WaitAll:等待所有任务完成。

Fault:故障,用于在工作流执行过程中,遇到异常时,触发故障。并结束工作流。

创建问卷活动类型QuestionnaireActivity,继承自Sequence类型,并设置一些属性,如问卷Id,问卷填写超时时间等。

[可选]Elsa在注册工作流时,Activity对象是会被序列化并存储到WorflowDefinition表中的, 因此这些属性可以被持久化到数据库中。

public class QuestionnaireActivity : Sequence
{
    //可选,用于持久化一些属性
    public TimeSpan Delay { get; set; }
    public DateTime StartAt { get; set; }
    public TimeSpan Interval { get; set; }
    public string Cron { get; set; }
    public TimeSpan Duration { get; set; }
    public long QuestionnaireId { get; set; }
    public TimeSpan FillInTimeout { get; set; } = TimeSpan.FromHours(2);

    public QuestionnaireActivity()
    {

    }   
}

重写构造函数,并设置Activities属性

public QuestionnaireActivity(long questionnaireId, TimeSpan fillInTimeout)
{
    this.QuestionnaireId = questionnaireId;
    this.FillInTimeout = fillInTimeout;
    var currentSurvey = new Variable<SurveyDto>();
    Variables.Add(currentSurvey);
    Activities = new List<IActivity>()
    {
        //流程开始打印
        new WriteLine("问卷流程开始"),

        //下发问卷任务
        new PublishQuestionnaireActivity(QuestionnaireId)
        {
            Name="PublishQuestionnaire",
            Result=new Output<Questionnaire.Survey.Dto.SurveyDto> (currentSurvey)
        },
        //问卷到达提醒             
        new NotificationActivity("新问卷提醒", "您有新的问卷,请查收"),

        //问卷处理分支 
        new Fork
        {
            JoinMode = ForkJoinMode.WaitAny,
            Branches =
            {
                //问卷即将过期提醒 
                new Sequence
                {
                    Activities =
                    {
                        //等待
                        new Delay
                        {
                            Name = "RemindDelay",
                            TimeSpan = new(RemindDelay)
                        },
                        //通知
                        new NotificationActivity("问卷即将超时", "问卷即将超时,请尽快回答")
                    }
                },
                //问卷过期处理以及提醒 
                new Sequence
                {
                    Activities =
                    {
                        //等待
                        new Delay
                        {
                            Name = "TimeoutDelay",
                            TimeSpan = new(FillInTimeout)
                        },
                        //通知
                        new NotificationActivity("问卷已过期", "问卷已过期,请等待工作人员处理"),

                        //处理
                        new Fault()
                        {
                            Message=new ("问卷回答超时")
                        }
                    }
                },
                //问卷状态跟踪 
                new Sequence
                {
                    Activities =
                    {
                        new WriteLine("开始等待问卷提交信号"),
                        new WaitFillInSurveyActivity(context => currentSurvey.Get<SurveyDto>(context)?.Id)

                    }
                }
            }
        },
        //流程结束打印 
        new WriteLine("完成流程结束"),
        new Finish(),
    };
}

创建工作流

现在我们来创建测试工作流,

  1. 添加一个工作流参数UserId,用于各活动中对用户的查询依赖。
  2. 分别实现4个并行任务:延时发送问卷,定时发送问卷,定期间隔发送问卷,根据Cron表达式执行。和一个串行任务
public class Test1Workflow : WorkflowBase
{
    public Guid UserId { get; set; }
    protected override void Build(IWorkflowBuilder workflow)
    {
        var startTime = new Variable<DateTimeOffset>();
        workflow.Inputs.Add(
                    new InputDefinition() { Name = "UserId", Type = typeof(Guid), StorageDriverType = typeof(WorkflowStorageDriver) }
            );
        workflow.WithVariable(startTime);

        workflow.Root = new Sequence
        {
            Activities =
                {
                    new WriteLine("Start"),
                    new SetVariable<DateTimeOffset>
                    {
                        Variable = startTime,
                        Value = new (DateTime.Now )
                    },
                    
                    new Parallel()
                    {
                        Activities =
                        {
                            //并行任务1:延时发送问卷
                            new Sequence()
                            {
                                Activities =
                                {
                                    //问卷1 将在工作流启动后1小时执行

                                    new Delay(TimeSpan.FromHours(1)),
                                    new QuestionnaireActivity(1),
                                }
                            },
                            //并行任务2:定时发送问卷
                            new Sequence()
                            {
                                Activities =
                                {
                                    //问卷2 将在 2024-4-1 08:30:00 执行
                                    new StartAt(new DateTime(2024,4,1,8,30,0)),
                                    new Delay(TimeSpan.FromHours(2)),
                                    new QuestionnaireActivity(2),
                                }
                            },
                            //并行任务3:定期间隔发送问卷
                            new Sequence()
                            {
                                Activities =
                                {
                                    //问卷3 每隔两个小时执行
                                    new Timer(new TimeSpan(2,0,0)),

                                    new Delay(TimeSpan.FromHours(2)),
                                    new QuestionnaireActivity(3),
                                }
                            },
                            //并行任务4:根据Cron表达式执行
                            new Sequence()
                            {
                                Activities =
                                {
                                    //问卷4 每个月的最后一天上午10点执行任务
                                    new Cron(cronExpression:"0 0 10 L * ?"),

                                    new Delay(TimeSpan.FromHours(2)),
                                    new QuestionnaireActivity(4),
                                }
                            },
                            //并行任务5:根据某时间发送问卷
                            new Sequence()
                            {
                                Activities =
                                {
                                    new StartAt(context=> startTime.Get(context).AddMinutes(90)),

                                    new Delay(TimeSpan.FromHours(2)),
                                    new QuestionnaireActivity(5),
                                }
                            },



                            //串行任务
                            new Sequence()
                            {
                                Activities =
                                {
                                    //问卷3 将在工作流启动后2小时执行

                                    new Delay(TimeSpan.FromHours(2)),
                                    new QuestionnaireActivity(3),

                                        //问卷4 将在问卷3完成1天后执行

                                    new Delay(TimeSpan.FromDays(1)),
                                    new QuestionnaireActivity(4),

                                    //问卷5 将在问卷4完成3天后执行

                                    new Delay(TimeSpan.FromDays(3)),
                                    new QuestionnaireActivity(5),
                                }
                            }
                        }
                    },
                    new Finish(),
            },
        };
    }
}

开始工作流

工作流启动参数需设置Input对象


var input = new Dictionary<string, object>
{

    {"UserId", "D1522DBC-5BFC-6173-EB60-3A114454350C"},

};

var startWorkflowOptions = new StartWorkflowRuntimeOptions
{
    Input = input,
    VersionOptions = versionOptions,
    InstanceId = instanceId,
};


// Start the workflow.
var result = await _workflowRuntime.StartWorkflowAsync(workflowDefinition.DefinitionId, startWorkflowOptions);

下面进入喜闻乐见的踩坑填坑环节

TroubleShooting

  1. 在活动中执行异步操作时,会导致报错:

    如下面的代码,执行Excute方法中的 context.CompleteActivityAsync()方法,时报错

在这里插入图片描述

在这里插入图片描述

原因分析:scope资源被提前释放

代码先执行到了112行,scope释放

在这里插入图片描述

解决:带有异步的操作一定要使用ExecuteAsync方法

在这里插入图片描述

  1. delay之后,Workflow的Input无法访问

原因分析:

Delay或其他Schedule类型的Activity,通过创建Bookmark挂起任务,当任务被唤醒时,input被workflowState.Output替换掉,和原先的input不一样了。

在这里插入图片描述

解决:

虽然input被替换了,但数据库的input还在,可以通过workflowInstanceId先取回workflowInstance对象,再通过instance.WorkflowState.Input.TryGetValue方法获取原始input值。

可以创建一个一个扩展方法GetInputValueAsync,Delay之后的活动中调用即可。

public static async Task<TValue> GetInputValueAsync<TValue>(this ActivityExecutionContext context, string name)
{
    TValue value;
    if (!context.TryGetWorkflowInput(name, out value))
    {
        var workflowInstanceStore = context.GetRequiredService<IWorkflowInstanceStore>();

        var instance = await workflowInstanceStore.FindAsync(new WorkflowInstanceFilter()
        {
            Id = context.WorkflowExecutionContext.Id

        });
        if (instance != null)
        {
            instance.WorkflowState.Input.TryGetValue(name, out value);
        }
    }

    return value;


}

在Activity中调用:

await context.GetInputValueAsync<Guid>("UserId");

持续更新中…

–完结–

Elsa Workflow 一些概念 这些概念构成了.NET Core Elsa Workflow的核心,理解它们是使用该框架的关键。通过结合这些概念,你可以构建复杂的工作流应用程序来自动化业务流程和任务。.NET Core Elsa Workflow包含许多重要的概念,这些概念是理解和使用工作流引擎的关键。 阅读详情

相关推荐

Elsa vs CCFlow:.NET开源工作流引擎的7大对决!你选对了吗?

🔥关注墨瑾轩,带你探索编程的奥秘!🚀🔥超萌技术攻略,轻松晋级编程高手🚀🔥技术宝库已备好,就等你来挖掘🚀🔥订阅墨瑾轩,智趣学习不孤单🚀🔥即刻启航,编程之旅更有趣🚀。

java专栏 1234

[.NET项目实战] Elsa开源工作流组件应用(一): Elsa工作流简介

工作流定义描述了一个工作流的结构,比如:名称,变量,包含的活动等,可以理解为一个工作流的模板。工作流定义在Elsa初始化时会“注册” 到资源池中,每个工作流定义都有一个唯一的ID。使用设计器生成的工作流,通过序列化成JSON字符串,并持久化到数据库。使用硬编码创建的工作流,在Elsa初始化时将工作流注册到工作流定义并持久化到数据库。实例化是根据定义创建一个工作流的实例,工作流实例包含工作流状态(WorkflowState)以及活动实例(ActivityState)。

jevonsflash的专栏 4257

flowable工作流 流程变量_.NET 5 开源工作流框架elsa技术研究

(给DotNet加星标,提升.Net技能)转自:Eric zhoucnblogs.com/tianqing/p/14221030.html研究.NET 5开源工作流框架elsa,现在分享给大家。一、框架简介elsa是一个开源的.NET Standard 工作流框架官方网站:https://elsa-workflows.github.io/elsa-core/以下列出了Elsa的一些关键功...

weixin_29886329的博客 3583

Elsa Workflows工作流引擎项目构建

注:本文主要采用.NET 6和Elsa工作引擎进行编写。

实践出真知 5041

揭秘.NET开源工作流引擎:Elsa Core如何重塑企业级自动化

扮演着“流程调度大脑”的角色。它不仅能自动化处理跨部门、跨系统的协作任务,还能通过可视化设计降低开发门槛,显著提升业务响应速度。的开源工作流引擎,专为 .NET 6+ 设计。部署为独立服务,并利用K8s的HPA实现自动扩缩容。机制暂停流程,等待外部事件触发恢复。字段管理版本,支持回滚与灰度发布。在复杂的企业级业务系统中,Elsa Core 是一个。

墨夶的博客 1232

通过asp.net core 页面发起Elsa工作流

通过asp.net core 页面发起Elsa工作流 工作流持久化,手工启动

[.NET项目实战] Elsa开源工作流组件应用(二):内核解读

Elsa工作原理可以抽象理解为管道中间件 + 异步模型Elsa中,活动的变量的获取和设置都是异步的。Elsa定义了Variable类型作为异步操作的结果或者说是异步操作的占位符,这个变量在运行的时候才会填充数值。这与我们熟悉C#中的Task,或者js里的promise对象作用相同。输入Input,OutPut都属于 Variable。Elsa模拟了内存寄存器(MemoryRegister)以及Set和Get访问器实现异步模型。

jevonsflash的专栏 3722

elsa-3.0.0-M5-API文档-中文版.zip

赠送jar包:elsa-3.0.0-M5.jar; 赠送原API文档:elsa-3.0.0-M5-javadoc.jar; 赠送源代码:elsa-3.0.0-M5-sources.jar; 赠送Maven依赖信息文件:elsa-3.0.0-M5.pom; 包含翻译后的API文档:elsa-3.0.0-M5-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.mapdb:elsa:3.0.0-M5; 标签:mapdb、elsa、中文文档、jar包、java; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,请放心使用。

Elsa Workflows: .NET 的开源工作流引擎简介

Elsa Workflows 是一个开源的、模块化的 .NET 库集合,旨在为 .NET 应用程序提供强大的工作流能力。它将复杂的工作流逻辑抽象化,让开发者(以及非开发者)能够更轻松地设计、执行和自动化各种业务流程。

Humbunklung的专栏 2436

激光SLAM总结——Fast LIO / Fast LIO2 / Faster LIO

激光SLAM总结——Fast LIO / Fast LIO2 / Faster LIO 在之前的工作中有接触过LOAM,最近在工作中又接触到Faster LIO相关的工作,于是想着对Fast LIO / Fast LIO2 / Faster LIO这一系列工作进行一个简单的总结,以加深自己对激光SLAM算法的理解,之前总结过的一些和激光SLAM算法相关的博客还有,感兴趣的同学可以一起学习讨论: 激光SLAM总结——VLOAM / LIMO算法解析 激光SLAM总结——LIO-Mapping / LIOM /

weixin_44580210的博客 1万+

Xilinx Vitis arm-xilinx-eabi-gcc.exe: error: *.c: Invalid argument

This problem may be a bugof version 2021.1 only. Unfortunately, I'm just currently using this version. After Build Project: Find where this Makefile is through "make: Leaving directory" information. For me, it is located atZynq_CPU_wrapper_hw_platfo...

Ayka的博客 6661

.NET 5 开源工作流框架elsa技术研究

编者注:工作流框架 elsa 是.NET基金会下面的开源项目,2.0版本即将在3月份发布,目前正在紧锣密鼓的开发之中,2020年2月份我也介绍过推荐一个很棒的开源工作流elsa-cor...

dotNET跨平台 6948

【模型解读】【代码复现】Segment Anything Model(SAM)

Meta AI的一个新的人工智能模型,只需一次点击就能 "分割出 "任何图像中的任何物体! SAM是一个可提示的分割系统,对不熟悉的物体和图像具有zero-shot的概括性,并且不需要额外的训练。

lalula的博客 2万+

完整指南:通过14个.NET8项目掌握Elsa工作流自动化

Elsa工作流引擎是.NET Core生态系统中强大的流程自动化工具,通过practical-aspnetcore项目中的14个实战示例,开发者可以全面掌握工作流自动化的核心技术。本指南将带您深入了解Elsa工作流如何简化企业级应用开发流程。 ## 🎯 Elsa工作流核心概念 Elsa是一个开源的.NET工作流引擎库,可以构建复杂的业务流程应用。它支持可视化工作流设计、长时间运行流程、版本控

gitblog_00882的博客 1109

汽车以太网- IEEE 802.3标准

关于汽车以太网,大家看到最多的标准大概就是IEEE802.3bw,也就是我们车内使用的百兆以太网,专业称呼是100BASE-T1,该标准是车内百兆以太网的物理层协议,是我们学习汽车以太网必须要了解的协议。在正式了解协议之前,我们先介绍下什么是IEEE802.3标准。............

weixin_39974126的博客 7108

卡诺图化简法

目录 1.学前需要了解知识点 2.卡诺图(karnaugh map) 3.逻辑函数的卡诺图化简法 4.总结 1.学前需要了解知识点 最小项的定义 最小项的表示方法 最小项的相邻性 最小项的定义:一个函数的某个乘积项包含了函数的全部变量,其中每个变量都以原变量或反变量的形式出现,且仅出现一次,则这个乘积项称为该函数的一个标准积项,通常称为最小项。 最小项的表示方法:通常用来表...

hahasusu的专栏 17万+

推荐一个很棒的开源工作流elsa-core

开源项目orchard主要开发人员Sipke Schoorstra 开源了一个netcore 工作流项目,地址:https://github.com/elsa-workflows/elsa-core,最新版本1.2,正在开发2.0版本 。Spike 是参照Orchard Core的工作流引擎的原理,将其转化为一个可重用的.NET标准库,从而使*任何* .NET应用程序都能享受工作流的强大功能,...

寒冰屋的专栏 3550

不止于HTTPS请求:用VS2022编译的libcurl玩转文件上传和代理设置

本文详细介绍了如何使用VS2022编译的libcurl库实现文件上传和高级代理配置。通过实战代码示例,展示了多部分表单上传、二进制数据上传以及各种代理设置技巧,帮助C++开发者提升网络编程能力。

weixin_28595749的博客 206

开源的.Net 工作流引擎Elsa初试——创建工作流服务器和图形化工作流配置管理应用

微软的Workflow Foundation基于.Net Framework,并且没有向.Net Core迁移的计划。我们的很多项目使用了工作流引擎,这些项目向.Net Core以及更高版本迁移时遇到了不少问题,一直在寻找/开发适合的工作流引擎。找到一些开源的项目,可以解决一些问题,还有些项目不是基于.Net生态的,但我希望能有一个长期稳定的基于.Net生态的项目,能够满足多种场景,可以替换原来的Workflow Foundation。希望的特性包括:1、同时支持编程工作流定义和配置工作流定义,最好有图形化

虚幻私塾 4397

solidworks vba二次开发配合问题

solidworks vba二次开发配合问题二次开发装配的步骤 二次开发装配的步骤 solidworks可以使用宏录制配合过程从而得到配合的vba代码,步骤如下: 1.选择两个配合的面(点、线等) 2. 建立配合 例如将两个圆柱面同心(图中高亮): 配合后 我们看一下宏录制的代码: Sub main() //激活solidworks接口 Set swApp = Application.SldWorks Set Part = swApp.ActiveDoc //选择两个圆柱面,注意此处是根据坐标选择,不

Alejandro123的博客 3405

基于Python的电商平台商品价格监控与历史价格查询系统

随着电子商务的快速发展,消费者对商品价格透明度的需求日益增长。本文设计并实现了一个基于Python的电商平台商品价格监控与历史价格查询系统,能够自动抓取京东和淘宝商品信息,存储历史价格数据,并通过可视化界面展示价格走势。系统采用最新的异步爬虫技术、反爬虫绕过策略和现代Web框架,实现了高效稳定的数据采集与展示功能。关键词:Python爬虫、价格监控、历史价格、数据可视化、异步爬虫、电商平台实现了京东和淘宝商品信息的异步爬取,采用多种反爬虫策略提高爬取成功率;

2201_76125261的博客 682

Elsa Core工作流引擎终极指南:10个核心组件深度解析与实战应用

Elsa Core是一个功能强大的.NET工作流引擎库,它提供了完整的**工作流引擎架构**和可视化设计工具,让开发者能够轻松构建复杂的企业级业务流程。作为.NET生态中最受欢迎的工作流解决方案之一,Elsa Core通过其模块化设计和丰富的活动库,为现代应用开发带来了前所未有的灵活性和可扩展性。 ## 🔍 什么是Elsa Core工作流引擎? Elsa Core是一个基于.NET构建的开源

gitblog_00927的博客 1153

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

开源的 .NET 工作流引擎 Elsa 开源

前言微软的Workflow Foundation基于.NET Framework,并且没有向.NET Core迁移的计划。我们的很多项目使用了工作流引擎,这些项目向.NET Core以及更高版本迁移时遇到了不少问题,一直在寻找/开发适合的工作流引擎。找到一些开源的项目,可以解决一些问题,还有些项目不是基于.NET生态的,但我希望能有一个长期稳定的基于.NET生态的项目,能够满足多种场景,可以替换原...

zls365365的博客 1313

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Elsa 3.0终极指南:如何快速构建.NET工作流应用

Elsa是一个功能强大的.NET工作流库,能够帮助开发者轻松构建和管理复杂的工作流应用。无论你是需要实现业务流程自动化、数据处理管道,还是事件驱动的微服务编排,Elsa 3.0都提供了完整的解决方案。本文将为你详细介绍Elsa 3.0的核心功能、快速入门方法以及实际应用场景。 ## 🚀 什么是Elsa工作流库? Elsa是一个开源的.NET工作流引擎,专门为构建可扩展、高性能的工作流应用而设

gitblog_00195的博客 885

ABP Elsa 模块与工作流应用教程

ABP Elsa 模块与工作流应用教程 项目介绍 ABP Elsa 模块是专为ABP框架设计的扩展组件,它集成了Elsa Workflows,为.NET Core应用程序提供了强大的工作流程管理能力。这个模块允许开发者轻松创建、管理和执行复杂的工作流,支持Entity Framework和MongoDB作为存储后端,同时引入了全新的图形化工作流设计器系统。Elsa模块提供了广泛的功能,包括文件操...

gitblog_01067的博客 1030
上一篇: [.NET项目实战] Elsa开源工作流组件应用(二):内核解读
下一篇: [MAUI]集成高德地图组件至.NET MAUI Blazor项目
林晓lx
林晓lx 2017年优快云移动跨平台大赛优秀奖获得者 2017年优快云移动跨平台大赛优秀奖获得者
博客等级 码龄15年 3761粉丝 123原创
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林晓lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值