Workflow Core: un motor de procesos de negocio para .Net Core

imagen


Hola a todos!


Decidimos apoyar el tema de la migración del proyecto utilizando Windows Workflow Foundation a .Net Core , que fue iniciado por colegas de DIRECTUM, porque enfrentamos un problema similar hace un par de años y seguimos nuestro propio camino.


Empecemos con la historia.


Nuestro producto estrella, Avanpost IDM, es un sistema de gestión de acceso de empleados y ciclo de vida de la cuenta. Él sabe cómo administrar el acceso de forma automática según el modelo a seguir y de acuerdo con las solicitudes. En los albores de la formación del producto, teníamos un sistema de autoservicio bastante simple con un flujo de trabajo simple paso a paso, para el cual el motor no era necesario en principio.


imagen


Sin embargo, cuando nos enfrentamos con grandes clientes, nos dimos cuenta de que se requería una herramienta mucho más flexible, ya que sus requisitos para los procesos de coordinación de los derechos de acceso buscaban las reglas de un flujo de trabajo bueno y pesado. Después de analizar los requisitos, decidimos desarrollar nuestro propio editor de procesos en formato BPMN, adecuado para nuestras necesidades. Hablaremos sobre el desarrollo del editor usando React.js + SVG un poco más tarde, y hoy discutiremos el tema del backend: el motor de flujo de trabajo o el motor de procesos de negocios.


Requisitos


Al comienzo del desarrollo del sistema, teníamos los siguientes requisitos para el motor:


  • Soporte para diagramas de proceso, un formato comprensible, la capacidad de transmitir desde nuestro formato al formato del motor.
  • Proceso de almacenamiento de estado
  • Soporte de versiones de proceso
  • Soporte para ejecución paralela (ramas) del proceso
  • Una licencia adecuada para usar la solución en un producto comercial replicado
  • Soporte para escalado horizontal

Después de analizar el mercado (para 2014), nos decidimos por una solución prácticamente no alternativa para .Net: Windows Workflow Foundation.


Windows Workflow Foundation (WWF)


WWF es la tecnología de Microsoft para definir, ejecutar y administrar flujos de trabajo.


La base de su lógica es un conjunto de contenedores para acciones (actividades) y la capacidad de construir procesos secuenciales a partir de estos contenedores. El contenedor puede ser ordinario, un cierto paso en el proceso donde se realiza la actividad. Puede ser un administrador, que contiene la lógica de ramificación.


Puede dibujar un proceso directamente en Visual Studio. El diagrama de proceso de negocio compilado se almacena en Haml, lo cual es muy conveniente: se describe el formato y es posible crear un diseñador de proceso autoescrito. Esto es por un lado. Por otro lado, Xaml no es el formato más conveniente para almacenar una descripción: el esquema compilado para un proceso más o menos real resulta enorme, sobre todo debido a la redundancia. Es muy difícil de entender, pero tendrás que entenderlo.


Pero si, tarde o temprano, uno puede comprender el zen con los esquemas y aprender a leerlos, entonces la falta de transparencia del motor se suma a la molestia que ya tienen los usuarios durante el funcionamiento del sistema. Cuando el error proviene de las entrañas de Wf, no siempre es posible averiguar al 100% cuál fue exactamente la razón del fallo. La fuente cerrada y la relativa monstruosidad no ayudan al caso. A menudo, las correcciones de errores se debieron a síntomas.


Para ser justos, vale la pena aclarar aquí que los problemas descritos anteriormente, en su mayor parte, nos han afectado debido a la fuerte personalización sobre Wf. Uno de los lectores dirá con seguridad que nosotros mismos creamos un montón de problemas y luego los resolvimos heroicamente. Era necesario hacer un motor hecho a sí mismo desde el principio. En general, tendrán razón.


En resumen, la solución funcionó de manera estable y entró en producción con éxito. Pero la transición de nuestros productos a .Net Core nos obligó a abandonar WWF y buscar otro motor de procesos comerciales, porque A partir de mayo de 2019, Windows Workflow Foundation no se ha migrado a .Net Core. Como estábamos buscando un nuevo motor, el tema de un artículo separado, pero al final nos decidimos por Workflow Core.


Núcleo de flujo de trabajo


Workflow Core es un motor de proceso empresarial gratuito. Está desarrollado bajo la licencia MIT, es decir, se puede usar de forma segura en el desarrollo comercial.


Una persona la realiza activamente, varias más realizan periódicamente una solicitud de extracción. Hay puertos para otros idiomas (Java, Python y varios más).


El motor está posicionado como ligero. De hecho, este es solo un host para la ejecución secuencial de acciones agrupadas por cualquier regla de negocios.


El proyecto tiene documentación wiki . Desafortunadamente, no describe todas las características del motor. Sin embargo, será descarado requerir documentación completa: el proyecto de código abierto es respaldado por un entusiasta. Por lo tanto, el Wiki será suficiente para comenzar.


Fuera de la caja hay soporte para almacenar el estado del proceso en almacenamiento externo (almacenamiento de persistencia). Los proveedores son estándar para:


  • Mongodb
  • Servidor SQL
  • PostgreSQL
  • Sqlite
  • Amazon DynamoDB

Escribir a su proveedor no es un problema. Tomamos las fuentes de cualquier estándar y lo hacemos como un ejemplo.


Se admite el escalado horizontal, es decir, puede ejecutar el motor en varios nodos a la vez, mientras tiene un punto de almacenamiento de estados de proceso (un almacenamiento de persistencia). En este caso, la colocación de la cola de tareas interna del motor debe estar en el almacenamiento general (rabbitMQ, como opción). Para excluir la ejecución de una tarea por varios nodos, se proporciona un administrador de bloqueos al mismo tiempo. Por analogía con los proveedores de almacenamiento externo, hay implementaciones estándar:


  • Arrendamientos de almacenamiento de Azure
  • Redis
  • AWS DynamoDB
  • SQLServer (en la fuente hay, pero no se dice nada en la documentación)

Conocer algo nuevo es más fácil comenzar con un ejemplo. Entonces hagámoslo. Describiré la construcción de un proceso simple desde el principio, junto con una explicación. Un ejemplo puede parecer imposiblemente simple. Estoy de acuerdo, es simple. Lo más para empezar.


Vamos


Paso


Un paso es un paso en el proceso en el que se realizan las acciones. Todo el proceso se construye a partir de una secuencia de pasos. Un paso puede realizar muchas acciones, puede repetirse, por ejemplo, para algún evento desde el exterior. Hay un conjunto de pasos dotados de la lógica "lista para usar":


  • Esperar
  • Si
  • Mientras que
  • Foreach
  • Retraso
  • Paralelo
  • Horario
  • Recurrir

Por supuesto, en algunas primitivas integradas no puede soportar el proceso. Necesitamos pasos que completen las tareas comerciales. Por lo tanto, por ahora, déjelos a un lado y tome medidas con nuestra propia lógica. Para hacer esto, debe heredar de la abstracción StepBody .


public abstract class StepBody : IStepBody { public abstract ExecutionResult Run(IStepExecutionContext context); } 

El método Run se ejecuta cuando el proceso ingresa en un paso. Es necesario colocar la lógica necesaria en él.


  public abstract class StepBody : IStepBody { public abstract ExecutionResult Run(IStepExecutionContext context); } 

Los pasos admiten la inyección de dependencia. Para hacer esto, es suficiente registrarlos en el mismo contenedor que las dependencias necesarias.


Obviamente, el proceso necesita su propio contexto, un lugar donde se pueden agregar resultados intermedios de ejecución. Wf core tiene su propio contexto para la ejecución de un proceso que almacena información sobre su estado actual. Puede acceder a ella utilizando la variable de contexto desde el método Run (). Además del incorporado, podemos usar nuestro contexto.


Analizaremos las formas de describir y registrar el proceso con más detalle a continuación, por ahora, simplemente definimos una determinada clase: el contexto.


  public class ProcessContext { public int Number1 {get;set;} public int Number2 {get;set;} public string StepResult {get;set;} public ProcessContext() { Number1 = 1; Number2 = 2; } } 

En las variables Número , escribimos números; en la variable StepResult : el resultado del paso.


Decidimos sobre el contexto. Puedes escribir tu propio paso:


  public class CustomStep : StepBody { private readonly Ilogger _log; public int Input1 { get; set; } public int Input2 { get; set; } public string Action { get; set; } public string Result { get; set; } public CustomStep(Ilogger log) { _log = log; } public override ExecutionResult Run(IStepExecutionContext context) { Result = ”none”; if (Action ==”sum”) { Result = Number1 + Number2; } if (Action ==”dif”){ Result = Number1 - Number2; } return ExecutionResult.Next(); } } 

La lógica es extremadamente simple: dos números y el nombre de la operación llegan a la entrada. El resultado de la operación se escribe en la variable de salida Resultado . Si la operación no está definida, el resultado será ninguno .


Decidimos sobre el contexto, hay un paso con la lógica que también necesitamos. Ahora necesitamos registrar nuestro proceso en el motor.


Descripción del proceso. Registro en el motor.


Hay dos formas de describir un proceso. La primera es la descripción en el código: el código duro.


El proceso se describe a través de la interfaz fluida . Es necesario heredar de la interfaz generalizada IWorkflow <T> , donde T es la clase de contexto del modelo. En nuestro caso, este es un ProcessContext .


Se ve así:


  public class SimpleWorkflow : IWorkflow<ProcessContext> { public void Build(IWorkflowBuilder<ProcessContext> builder) { //    } public string Id => "SomeWorkflow"; public int Version => 1; } 

La descripción en sí estará dentro del método Build . Los campos Id y Versión también son obligatorios. Wf core admite el control de versiones de procesos: puede registrar n versiones de procesos con el mismo identificador. Esto es conveniente cuando necesita actualizar un proceso existente y al mismo tiempo dar "vida" a las tareas existentes.


Describimos un proceso simple:


  public class SimpleWorkflow : IWorkflow<ProcessContext> { public void Build(IWorkflowBuilder<ProcessContext> builder) { builder.StartWith<CustomStep>() .Input(step => step.Input1, data => data.Number1) .Input(step => step.Input2, data => data.Number2) .Input(step => step.Action, data => “sum”) .Output(data => data.StepResult, step => step.Result) .EndWorkflow(); } public string Id => "SomeWorkflow"; public int Version => 1; } 

Si se traduce al lenguaje "humano", resultará algo como esto: el proceso comienza con el paso CustomStep . El valor del campo de paso Entrada1 se toma del campo de contexto Número1 , El valor del campo de paso Entrada2 se toma del campo de contexto Número2 , el campo de acción está codificado con el valor "suma" . La salida del campo Resultado se escribe en el campo de contexto StepResult . Completa el proceso.


De acuerdo, el código resultó ser muy legible, es muy posible resolverlo, incluso sin un conocimiento especial en C #.


Agregue un paso más a nuestro proceso, que generará el resultado del paso anterior en el registro:


  public class CustomStep : StepBody { private readonly Ilogger _log; public string TextToOutput { get; set; } public CustomStep(Ilogger log) { //    _log = log; } public override ExecutionResult Run(IStepExecutionContext context) { _log.Debug(TextToOutput); return ExecutionResult.Next(); } } 

Y actualice el proceso:


  public class SimpleWorkflow : IWorkflow<ProcessContext> { public void Build(IWorkflowBuilder<ProcessContext> builder) { builder.StartWith<CustomStep>() .Input(step => step.Input1, data => data.Number1) .Input(step => step.Input2, data => data.Number2) .Input(step => step.Action, data => “sum”) .Output(data => data.StepResult, step => step.Result) .Then<OutputStep>.Input(step => step.TextToOutput, data => data.StepResult) .EndWorkflow(); } public string Id => "SomeWorkflow"; public int Version => 2; } 

Ahora, después del paso con la operación de suma, sigue el paso de enviar el resultado al registro. A la entrada, pasamos la variable Resultado y contexto en la que se escribió el resultado de la ejecución en el último paso. Me tomaré la libertad de afirmar que tal descripción a través de un código (hardcode) en sistemas reales sería de poca utilidad. A menos que sea para algunos procesos de oficina. Es mucho más interesante poder almacenar el circuito por separado. Como mínimo, no tenemos que volver a armar el proyecto cada vez que necesitemos cambiar algo en el proceso o agregar uno nuevo. Wf core proporciona esta característica almacenando el esquema json. Seguimos ampliando nuestro ejemplo.


Descripción del proceso de Json


Además, no proporcionaré una descripción a través del código. Esto no es particularmente interesante, y solo inflará el artículo.


Wf core admite la descripción del esquema en json. En mi opinión, json es más visual que xaml (un buen tema para holivar en los comentarios :)). La estructura del archivo es bastante simple:


 { "Id": "SomeWorkflow", "Version": 1, "DataType": "App.ProcessContext, App", "Steps": [ { /*step1*/ }, { /*step2*/ } ] } 

El campo DataType indica el nombre completo de la clase de contexto y el nombre del ensamblado en el que se describe. Steps almacena una colección de todos los pasos del proceso. Complete el elemento Pasos :


 { "Id": "SomeWorkflow", "Version": 1, "DataType": "App.ProcessContext, App", "Steps": [ { "Id": "Eval", "StepType": "App.CustomStep, App", "NextStepId": "Output", "Inputs": { "Input1": "data.Number1", "Input2": "data.Number2" }, "Outputs": { "StepResult": "step.Result" } }, { "Id": "Output", "StepType": "App.OutputStep, App", "Inputs": { "TextToOutput": "data.StepResult" } } ] } 

Echemos un vistazo más de cerca a la estructura de la descripción del paso a través de json.


Los campos Id y NextStepId almacenan el identificador de este paso y un indicador de qué paso puede ser el siguiente. Además, el orden de los elementos de la colección no es importante.


StepType es similar al campo DataType , contiene el nombre completo de la clase de paso (un tipo que hereda de StepBody e implementa la lógica del paso) y el nombre del ensamblado. Más interesantes son los objetos Entradas y Salidas . Se establecen en forma de mapeo.


En el caso de las entradas, el nombre del elemento json es el nombre del campo de clase de nuestro paso; El valor del elemento es el nombre del campo en la clase, el contexto del proceso.


Para las salidas, por el contrario, el nombre del elemento json es el nombre del campo en la clase, el contexto del proceso; El valor del elemento es el nombre del campo de clase de nuestro paso.


¿Por qué los campos de contexto se especifican mediante datos. {Field_name} , y en el caso de Output , paso. {Field_name} ? Debido a que wf core el valor del elemento se ejecuta como una expresión C # ( se usa la biblioteca Dynamic Expressions ). Esto es algo bastante útil, con su ayuda puede establecer cierta lógica comercial directamente dentro del esquema, si, por supuesto, el arquitecto aprueba tal desgracia :).


Diversificamos el esquema con primitivas estándar. Agregue un paso If condicional y procese un evento externo.


Si

Primitivo If . Aquí comienzan las dificultades. Si está acostumbrado a bpmn y dibuja procesos en esta notación, encontrará una configuración fácil. Según la documentación, el paso se describe de la siguiente manera:


 { "Id": "IfStep", "StepType": "WorkflowCore.Primitives.If, WorkflowCore", "NextStepId": "nextStep", "Inputs": { "Condition": "<<expression to evaluate>>" }, "Do": [ [ { /*do1*/ }, { /*do2*/ } ] ] } 

¿No hay sensación de que algo está mal aquí? Yo tengo uno La entrada de paso se establece en Condición - expresión. A continuación, establecemos la lista de pasos dentro de la matriz Do (acciones). Entonces, ¿dónde está la rama falsa ? ¿Por qué no hay una matriz Do para False? En realidad lo hay. Se entiende que la rama False es simplemente un paso más a lo largo del proceso, es decir, siguiendo el puntero en NextStepId . Al principio, estaba constantemente confundido debido a esto. De acuerdo, lo solucioné. Aunque no. Si las acciones del proceso en el caso de True deben colocarse dentro de Do , esto es lo que será "hermoso" json entonces. ¿Y si hay estos si están encerrados con una docena? Todo irá de lado. También dicen que el esquema en xaml es difícil de leer. Hay un pequeño truco. Simplemente tome el monitor más ancho. Se mencionó un poco más arriba que el orden de los pasos en la colección no importa, la transición sigue los signos. Se puede usar. Agrega un paso más:


 { "Id": "Jump", "StepType": "App.JumpStep, App", "NextStepId": "" } 

¿Adivina a qué estoy conduciendo? Es cierto que presentamos un paso de servicio, que en tránsito lleva el proceso a un paso en NextStepId .


Actualiza nuestro esquema:


 { "Id": "SomeWorkflow", "Version": 1, "DataType": "App.ProcessContext, App", "Steps": [ { "Id": "Eval", "StepType": "App.CustomStep, App", "NextStepId": "MyIfStep", "Inputs": { "Input1": "data.Number1", "Input2": "data.Number2" }, "Outputs": { "StepResult": "step.Result" } }, { "Id": "MyIfStep", "StepType": "WorkflowCore.Primitives.If, WorkflowCore", "NextStepId": "OutputEmptyResult", "Inputs": { "Condition": "!String.IsNullOrEmpty(data.StepResult)" }, "Do": [ [ { "Id": "Jump", "StepType": "App.JumpStep, App", "NextStepId": "Output" } ] ] }, { "Id": "Output", "StepType": "App.OutputStep, App", "Inputs": { "TextToOutput": "data.StepResult" } }, { "Id": "OutputEmptyResult", "StepType": "App.OutputStep, App", "Inputs": { "TextToOutput": "\"Empty result\"" } } ] } 

El paso If verifica si el resultado del paso Eval está vacío. Si no está vacío, mostramos el resultado; si está vacío, aparece el mensaje " Resultado vacío ". El paso Salto lleva el proceso al paso Salida , que está fuera de la colección Do. Por lo tanto, hemos mantenido la "verticalidad" del esquema. Además, de esta manera, uno puede saltar n pasos hacia atrás, es decir para organizar un ciclo. Hay primitivas incorporadas para bucles en wf core, pero no siempre son convenientes. En bpmn, por ejemplo, los bucles se organizan a través de If .


Utilice este enfoque o el estándar, depende de usted. Para nosotros, una organización así fue pasos más convenientes.


Esperar

La primitiva WaitFor permite que el mundo exterior influya en el proceso cuando ya se está ejecutando. Por ejemplo, si en la etapa del proceso se requiere la aprobación del curso posterior por parte de cualquier usuario. El proceso permanecerá en el paso Esperar hasta que reciba un evento al que esté suscrito.


Estructura primitiva:


 { "Id": "Wait", "StepType": "WorkflowCore.Primitives.WaitFor, WorkflowCore", "NextStepId": "NextStep", "CancelCondition": "If(cancel==true)", "Inputs": { "EventName": "\"UserAction\"", "EventKey": "\"DoSum\"", "EffectiveDate": "DateTime.Now" } } 

Explicaré los parámetros un poco.


CancelCondition : una condición para interrumpir una espera. Brinda la capacidad de interrumpir la espera de un evento y avanzar en el proceso. Por ejemplo, si un proceso está esperando n eventos diferentes al mismo tiempo (wf core admite la ejecución paralela de pasos), no es necesario esperar a que lleguen todos, en este caso CancelCondition nos ayudará. Agregamos un indicador lógico a las variables de contexto y al recibir el evento establecemos el indicador en verdadero : todos los pasos de WaitFor se completan.


EventName y EventKey : nombre y clave del evento. Los campos son necesarios para distinguir eventos, es decir, en un sistema real con una gran cantidad de procesos de trabajo simultáneos. para que el motor entienda qué evento está destinado para qué proceso y qué paso.


EffectiveDate : un campo opcional que agrega una marca de tiempo de evento. Puede ser útil si necesita publicar un evento "en el futuro". Para que se publique de inmediato, puede dejar el parámetro vacío o establecer la hora actual.


No en todos los casos es conveniente dar un paso separado para procesar las reacciones desde el exterior; más bien, incluso generalmente será redundante. Se puede evitar un paso adicional al agregar la expectativa de un evento externo y la lógica de su procesamiento al paso habitual. Complementamos el paso CustomStep suscribiéndonos a un evento externo:


  public class CustomStep : StepBody { private readonly Ilogger _log; public string TextToOutput { get; set; } public CustomStep(Ilogger log) { _log = log; } public override ExecutionResult Run(IStepExecutionContext context) { //-  return ExecutionResult.WaitForEvent("eventName", "eventKey", DateTime.Now); } } 

Utilizamos el método de extensión estándar WaitForEvent () . Acepta los parámetros anteriormente mencionados EventName , EventKey y EffectiveDate . Después de completar la lógica de dicho paso, el proceso esperará el evento descrito y volverá a llamar al método Run () en el momento en que el evento se publique en el bus del motor. Sin embargo, en la forma actual, no podemos distinguir entre los momentos de la entrada inicial en el paso y la entrada después del evento. Pero me gustaría separar de alguna manera la lógica antes-después en el nivel de paso. Y la bandera EventPublished nos ayudará con esto. Se encuentra dentro del contexto general del proceso, puede obtenerlo así:


 var ifEvent=context.ExecutionPointer.EventPublished; 

Según este indicador, puede dividir de forma segura la lógica en antes y después de un evento externo.


Una aclaración importante: según la idea del creador del motor, un paso solo se puede firmar en un evento y reaccionar ante él una vez. Para algunas tareas, esta es una limitación muy desagradable. Incluso tuvimos que "terminar" el motor para escapar de este matiz. Omitiremos su descripción en este artículo, de lo contrario el artículo nunca terminará :). Prácticas de uso más complejas y ejemplos de mejoras se cubrirán en artículos posteriores.


Proceso de registro en el motor. Publicación del evento en el autobús.


Entonces, con la implementación de la lógica de los pasos y las descripciones del proceso resuelto. Lo que queda es lo más importante, sin el cual el proceso no funcionará: la descripción debe registrarse.


Utilizaremos el método de extensión AddWorkflow () estándar, que colocará sus dependencias en nuestro contenedor IoC.


Se ve así:


 public static IServiceCollection AddWorkflow(this IServiceCollection services, Action<WorkflowOptions> setupAction = null) 

IServiceCollection - interfaz - un contrato para una colección de descripciones de servicios. Vive dentro de DI de Microsoft (puede leer más sobre esto aquí )


WorkflowOptions : configuración básica del motor. No es necesario configurarlos usted mismo; los valores estándar son bastante aceptables para el primer conocido. Vamos más lejos


Si el proceso se describió en código, entonces el registro se realiza así:


 var host = _serviceProvider.GetService<IWorkflowHost>(); host.RegisterWorkflow<SomeWorkflow, ProcessContext>(); 

Si el proceso se describe a través de json, debe registrarse de la siguiente manera (por supuesto, la descripción de json debe cargarse previamente desde la ubicación de almacenamiento):


 var host = _serviceProvider.GetService<IWorkflowHost>(); var definitionLoader = _serviceProvider.GetService<IDefinitionLoader>(); var definition = loader.LoadDefinition({*json  *}); 

Además, para ambas opciones, el código será el mismo:


 host.Start(); //      host.StartWorkflow(definitionId, version, context); //      /// host.Stop(); / /     

El parámetro definitionId es el identificador de proceso. Lo que está escrito en el campo Id del proceso. En este caso, id = SomeWorkflow .


El parámetro versión especifica qué versión del proceso ejecutar. El motor proporciona la capacidad de registrar inmediatamente n versiones de proceso con un identificador. Esto es conveniente cuando necesita hacer cambios en la descripción del proceso sin interrumpir las tareas que ya se están ejecutando: se crearán nuevas con la nueva versión, las antiguas vivirán en silencio en la anterior.


El parámetro de contexto es una instancia del contexto del proceso.


Los métodos host.Start () y host.Stop () inician y detienen el proceso de alojamiento. Si en la aplicación el inicio de procesos es una tarea aplicada y se realiza periódicamente, entonces el hosting debería detenerse. Si la aplicación se centra principalmente en la implementación de varios procesos, entonces el alojamiento no se puede detener.


Hay un método para enviar mensajes del mundo exterior al bus del motor, que luego los distribuirá entre los suscriptores:


 Task PublishEvent(string eventName, string eventKey, object eventData, DateTime effectiveDate = null); 

La descripción de sus parámetros fue mayor en el artículo ( ver la parte primitiva WaitFor ).


Conclusión


Definitivamente tomamos riesgos cuando decidimos a favor del Workflow Core - proyecto de código abierto, que es desarrollado activamente por una persona, e incluso con documentación muy pobre. Y lo más probable es que no encuentre prácticas reales de uso de wf core en sistemas de producción (excepto el nuestro). Por supuesto, después de haber seleccionado una capa separada de abstracciones, nos aseguramos contra un caso de falla y la necesidad de regresar rápidamente a WWF, por ejemplo, o una solución autoescrita, pero todo salió bastante bien y la falla no llegó.


El cambio al motor de código abierto Workflow Core resolvió un cierto número de problemas que nos impedían vivir pacíficamente en WWF. El más importante de ellos es, por supuesto, el soporte .Net Core y la falta de tal, incluso en los planes, WWF.


El siguiente es el código abierto. Trabajando con WWF y obteniendo varios errores de sus intestinos, la capacidad de al menos leer la fuente sería muy útil. Sin mencionar cambiar algo en ellos. Aquí con Workflow Core, completa libertad (incluyendo licencias - MIT). Si aparece un error repentinamente desde las entrañas del motor, simplemente descargue las fuentes de github y busque con calma la causa de su aparición. Sí, solo la capacidad de arrancar el motor en modo de depuración con puntos de interrupción ya facilita enormemente el proceso.


Por supuesto, al resolver algunos problemas, Workflow Core trajo sus propios problemas nuevos. Tuvimos que hacer una cantidad significativa de cambios en el núcleo del motor. Pero Trabajar en "terminar" por sí mismos cuesta menos en tiempo que desarrollar su propio motor desde cero. La solución final fue bastante aceptable en términos de velocidad y estabilidad, nos permitió olvidar los problemas con el motor y centrarnos en el desarrollo del valor comercial del producto.


PD: Si el tema resulta interesante, habrá más artículos sobre wf core, con un análisis más profundo del motor y soluciones a problemas empresariales complejos.

Source: https://habr.com/ru/post/475902/


All Articles