服务可以看作是随时间分布的流程,您可以通过它来影响服务的结果(通过门户取消,拒绝代理,将有关服务状态变化的信息发送到门户以及发送其提供的结果)有几个方面。在这方面,每个服务在此过程中都会经历其自己的生命周期,累积有关用户请求,接收到的错误,服务结果等数据。这使您能够随时控制和决定处理服务的进一步措施。
我们将进一步讨论如何以及在何种帮助下可以组织此类处理。
选择业务流程自动化引擎
组织数据的处理,也有业务流程自动化库和系统,广泛市场上:从嵌入式解决方案到功能全面的系统,为过程控制提供框架。我们选择Workflow Core作为自动化业务流程的工具。做出此选择的原因有几个:首先,该引擎是用C#编写的.NET Core平台(这是我们的主要开发平台),因此与Camunda BPM不同,将其包含在整个产品概述中更加容易。此外,它是一个嵌入式引擎,为管理业务流程实例提供了充足的机会。其次,在众多受支持的存储选项中,我们的解决方案中还使用了PostgreSQL。第三,引擎提供了一种简单的语法,用于以流畅的API形式描述流程(不过,在JSON文件中描述流程也是一种变体,由于很难在过程的实际执行之前检测到该过程的描述中的错误,因此使用起来似乎不太方便。
业务流程
在用于描述业务流程的公认工具中,应注意BPMN标记。例如,以BPMN表示法解决FizzBuzz问题的方法可能如下所示:
Workflow Core引擎包含该符号中显示的大多数构建基块和语句,并且如上所述,允许您使用流畅的API或JSON数据来描述特定的流程。通过Workflow Core引擎实现的此过程可以采用以下形式:
// .
public class FizzBuzzWfData
{
public int Counter { get; set; } = 1;
public StringBuilder Output { get; set; } = new StringBuilder();
}
// .
public class FizzBuzzWorkflow : IWorkflow<FizzBuzzWfData>
{
public string Id => "FizzBuzz";
public int Version => 1;
public void Build(IWorkflowBuilder<FizzBuzzWfData> builder)
{
builder
.StartWith(context => ExecutionResult.Next())
.While(data => data.Counter <= 100)
.Do(a => a
.StartWith(context => ExecutionResult.Next())
.Output((step, data) => data.Output.Append(data.Counter))
.If(data => data.Counter % 3 == 0 || data.Counter % 5 == 0)
.Do(b => b
.StartWith(context => ExecutionResult.Next())
.Output((step, data) => data.Output.Clear())
.If(data => data.Counter % 3 == 0)
.Do(c => c
.StartWith(context => ExecutionResult.Next())
.Output((step, data) =>
data.Output.Append("Fizz")))
.If(data => data.Counter % 5 == 0)
.Do(c => c
.StartWith(context => ExecutionResult.Next())
.Output((step, data) =>
data.Output.Append("Buzz"))))
.Then(context => ExecutionResult.Next())
.Output((step, data) =>
{
Console.WriteLine(data.Output.ToString());
data.Output.Clear();
data.Counter++;
}));
}
}
}
当然,可以通过在基数检查之后的步骤中添加所需值的输出来更简单地描述该过程。但是,使用当前的实现,您可以看到每个步骤都可以对过程数据的常规“存钱罐”进行一些更改,并且还可以利用先前步骤的结果。在这种情况下,过程数据存储在一个实例中
FizzBuzzWfData
,在执行该实例时,每个步骤都可以访问该实例。
方法
Build
以流程构建器对象作为参数,它作为调用顺序描述业务流程步骤的扩展方法链的起点。反过来,扩展方法可以直接在当前代码中以作为参数传递的lambda表达式的形式包含对动作的描述,也可以对其进行参数化。在清单中介绍的第一种情况下,一种简单的算法可以转化为相当复杂的指令集。在第二部分中,步骤的逻辑隐藏在从类型继承的单独类中Step
(或AsyncStep
对于异步变体),这使您可以将复杂的过程放入更简洁的描述中。实际上,第二种方法似乎更合适,而第一种方法对于简单的示例或极其简单的业务流程就足够了。
实际的流程描述类实现了参数化的接口
IWorkflow
,并且在执行合同时包含流程标识符和版本号。借助这些信息,引擎可以在内存中生成流程实例,将其填充数据并在存储中修复其状态。版本控制支持使您可以创建新的流程变体,而不会影响现有存储库实例的风险。要创建新版本,只需创建现有描述的副本,为属性分配下一个数字Version
并根据需要更改此过程的行为即可(标识符应保持不变)。
在我们的任务范围内的业务流程示例如下:
- – .
- – , , .
- – .
- – , .
从示例中可以看到,所有过程都按条件细分为“循环”,其执行涉及周期性重复,而“线性”则在特定语句的上下文中执行,但是,并不排除其内部存在某些循环结构。
让我们来看一个解决方案中用于轮询传入请求队列的流程之一的示例:
public class LoadRequestWf : IWorkflow<LoadRequestWfData>
{
public const string DefinitionId = "LoadRequest";
public string Id => DefinitionId;
public int Version => 1;
public void Build(IWorkflowBuilder<LoadRequestWfData> builder)
{
builder
.StartWith(then => ExecutionResult.Next())
.While(d => !d.Quit)
.Do(x => x
.StartWith<LoadRequestStep>() // *
.Output(d => d.LoadRequest_Output, s => s.Output)
.If(d => d.LoadRequest_Output.Exception != null)
.Do(then => then
.StartWith(ctx => ExecutionResult.Next()) // *
.Output((s, d) => d.Quit = true))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType
== ReqType.Unknown)
.Do(then => then
.StartWith<LogInfoAboutFaultResponseStep>() // *
.Input((s, d) =>
{ s.Input = d.LoadRequest_Output?.Result?.Fault; })
.Output((s, d) => d.Quit = false))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType
== ReqType.DataRequest)
.Do(then => then
.StartWith<StartWorkflowStep>() // *
.Input(s => s.Input, d => BuildEpguNewApplicationWfData(d))
.Output((s, d) => d.Quit = false))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType == ReqType.Empty)
.Do(then => then
.StartWith(ctx => ExecutionResult.Next()) // *
.Output((s, d) => d.Quit = true))
.If(d => d.LoadRequest_Output.Exception == null
&& d.LoadRequest_Output.Result.SmevReqType
== ReqType.CancellationRequest)
.Do(then => then
.StartWith<StartWorkflowStep>() // *
.Input(s => s.Input, d => BuildCancelRequestWfData(d))
.Output((s, d) => d.Quit = false)));
}
}
在标有*的行中,您可以看到参数化扩展方法的使用,这些扩展方法指示引擎使用与类型参数相对应的步骤类(稍后再介绍)。借助扩展方法
Input
,Output
我们能够在开始执行之前设置传递给步骤的初始数据,并因此根据该步骤执行的操作来更改过程数据(它们由类的实例表示LoadRequestWfData
)。这是该流程在BPMN图上的外观:
脚步
如上所述,将步骤的逻辑放在单独的类中是合理的。除了使过程更简洁外,它还允许您为常见操作创建可重用的步骤。
根据在我们的解决方案中执行的操作的唯一性程度,这些步骤分为两类:常规步骤和特定步骤。前者可以在任何项目的任何模块中重用,因此它们被放置在共享解决方案库中。通过每个客户在相应设计模块中的位置,后者对于每个客户都是唯一的。常见步骤的示例包括:
发送对响应的Ack请求。
- 将文件上传到文件存储。
- 从SMEV包等提取数据
具体步骤:
- 在IAS中创建对象,使操作员能够提供服务。
- .
- ..
在描述过程中的步骤时,我们遵守每个步骤的有限责任原则。这不允许在步骤中隐藏高级业务流程逻辑的片段,而不能在流程描述中明确表达它。例如,如果在应用程序数据中发现错误,则有必要向SMEV发送有关拒绝处理该应用程序的消息,那么条件的相应块将直接位于业务流程的代码中,并且不同的类将对应于确定错误事实并对其做出响应的步骤。
应该注意的是,必须在依赖项容器中注册步骤,以便引擎能够在每个进程在其生命周期中移动时使用步骤实例。
每个步骤都是包含该流程的高级描述的代码与解决应用程序问题的代码-服务之间的连接链接。
服务
服务是解决问题的下一个较低层次。通常,履行职责的每个步骤都取决于一项或多项服务(注意,在此上下文中,“服务”的概念更接近于领域特定设计(DDD)领域中类似的“应用程序级服务”概念)。
服务示例包括:
- 用于从SMEV响应队列接收响应的服务准备SOAP格式的相应数据包,将其发送到SMEV并将响应转换为适合于进一步处理的形式。
- 用于从SMEV存储库下载文件的服务-使用FTP协议从文件存储库中的门户中读取附加到应用程序的文件。
- 用于获取服务提供结果的服务-从IAS读取有关服务结果的数据并形成相应的对象,在此基础上,另一个服务将构建SOAP请求以发送到门户。
- 用于将与服务结果相关的文件上载到SMEV文件存储的服务。
解决方案中的服务根据系统及其提供的交互作用分为几类:
- SMEV服务。
- IAS服务。
用于与集成解决方案的内部基础结构一起使用的服务(记录有关数据包的信息,将集成解决方案的实体与IAS对象链接等)。
从体系结构方面来说,服务是最低级别的服务,但是,它们也可以依靠实用程序类来解决其问题。因此,例如,在解决方案中,存在一层代码,用于解决不同版本的SMEV协议的SOAP数据包的序列化和反序列化的问题。一般而言,以上描述可以归纳为类图:
接口
IWorkflow
和抽象类与引擎直接相关StepBodyAsync
(但是,您也可以使用其同步模拟StepBody)。下图显示了“构建块”的实现-具体类,其中包含Workflow业务流程及其中使用的步骤的描述(Step
)。在较低的级别,提供了服务,从本质上讲,服务已经特定于解决方案的此特定实现,并且与过程和步骤不同,它不是强制性的。
服务,例如步骤,必须在依赖项容器中注册,以便使用其服务的步骤可以通过构造函数注入来获取它们的必要实例。
将引擎嵌入解决方案中
在开始使用门户创建集成系统时,Nuget存储库中提供了该引擎的2.1.2版本。它是在
ConfigureServices
类方法中以标准方式内置到依赖项容器中的Startup
:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddWorkflow(opts =>
opts.UsePostgreSQL(connectionString, false, false, schemaName));
// ...
}
该发动机可以被配置为支持的数据仓库中的一个(有他人其中:的MySQL,MS SQL,SQLite的,MongoDB的)。对于PostgreSQL,引擎使用Code First变体中的Entity Framework Core来处理进程。因此,如果数据库为空,则可以应用迁移并获得所需的表结构。迁移的使用是可选的,可以使用方法参数来控制它
UsePostgreSQL
:第二(canCreateDB
)和第三(canMigrateDB
)布尔类型参数允许您告诉引擎是否可以创建数据库(如果数据库不存在)并应用迁移。
由于在引擎的下一次更新中,更改其数据模型的可能性不为零,并且下一次迁移的相应使用可能会损坏已经累积的数据,因此我们决定放弃此选项,并根据我们在其他项目中使用的数据库组件的机制自行维护数据库结构。
因此,已经解决了在依赖容器中存储数据和注册引擎的问题,让我们继续启动引擎。对于此任务,出现了托管服务选项,在这里请参阅用于创建此类服务的基类示例)。略微修改了作为基础的代码以维护模块化,这意味着将集成解决方案(称为“ Onyx”)划分为一个通用部分,该部分提供引擎初始化和某些服务过程的执行,而每个部分都特定于每个特定客户(集成模块) ...
每个模块都包含流程描述,用于执行业务逻辑的基础结构以及一些统一代码,以使开发的集成系统能够识别流程描述并将其动态加载到Workflow Core引擎实例中:
注册和启动业务流程
现在,我们已经对业务流程和连接到解决方案的引擎进行了现成的描述,是时候告诉引擎它将使用哪些流程了。
这可以通过以下代码完成,该代码可以位于前面提到的托管服务中(可以在此处放置用于启动已连接模块中进程的注册的代码):
public async Task RunWorkflowsAsync(IWorkflowHost host,
CancellationToken token)
{
host.RegisterWorkflow<LoadRequestWf, LoadRequestWfData>();
// ...
await host.StartAsync(token);
token.WaitHandle.WaitOne();
host.Stop();
}
结论
一般而言,我们介绍了在集成解决方案中使用Workflow Core所需采取的步骤。该引擎使您可以灵活方便地描述业务流程。考虑到我们正在通过SMEV处理与“ Gosuslug”门户网站集成的任务这一事实,应该预期的是,预计的业务流程将涵盖一系列相当多样化的任务(轮询队列,上传/下载文件,确保符合交换协议并确保确认数据接收,不同阶段的错误处理等)。因此,很自然地期望乍一看会出现一些不明显的实现时刻,而正是他们将致力于下一个周期的最后一篇文章。