Biblioteca diseñada para crear flujo de trabajo personalizado, orcestación entre componentes, microservicios y otras utilidades de automatización, creadas en Dotnet versión 6
Esta biblioteca ha sido diseñada para tener un ecosistema de entidades orientadas a la orquestación de cualquier tipo de proceso. Desde los procesos más simples, como las llamadas entre varias de nuestras clases, a procesos más complejos, como llamadas a microservicios, etc. Siempre, sin embargo, de manera declarativa y buscar código en términos de anidación tediosa y compleja de "si", "más" ... etc. Estructura de estructura
En este repositorio, serán las clases centrales de este ecosistema, agrupadas en el espacio de nombres Magnett.automation.core. Algunas de estas clases se pueden usar fuera del alcance de crear un flujo de trabajo, ya que son lo suficientemente genéricos como para ser útiles de forma independiente.
En este espacio de nombres encontramos la clase de utilidades utilizada dentro de las clases de Libray
El contexto conceptual es algo muy genérico y se usa en varios campos. Para nosotros, un contexto será un espacio común donde los valores se almacenan para compartirse entre varios componentes, ya que son valores del tipo heterogéneo.
Por lo tanto, estamos frente a un sistema de llave/valor, donde los valores serán de cualquier tipo.
La estructura del contexto es simple, se forma solo por la clase de contexto , que será nuestra entrada y recuperación de la clase de valores, y la interfaz IcontextVault , que será la definición de la bóveda donde se almacenan los valores.
Por defecto, tendremos una implementación del IcontextVault , donde almacenará los valores en la memoria, pero estará abierto para cualquier otra implementación que almacene estos valores de cualquier otra manera.
Usaremos la clase contextfield para obtener y establecer valores en un contexto, con esta clase podemos definir el tipo y el nombre de la clase.
Ejemplo de cómo obtener / establecer valor en un contexto
var context = Context . Create ( ) ;
var field = ContextField < int > . Create ( "FieldName" ) ;
//Set Value
context . Store ( field , random . Next ( 1000 ) ) ;
//Get Value
var value = context . Value ( field ) ; Tenemos a nuestra disposición varias interfaces para la definición de una máquina de estado, así como su ejecución. La definición de la máquina se separará de la ejecución de la máquina misma, para evitar acoplamientos y una clara separación de responsabilidades.
La interfaz principal es la iMachine . Con esta interfaz tendremos acceso al estado actual, la interfaz ISTA y la posibilidad de transición a otro estado utilizando códigos de acción que generen una transición, entidad Itransaction a otro estado.
No es posible pasar de un estado a otro directamente, solo a través de una transición, de modo que tengamos un modelo a los que podamos ir de uno en particular.
Se puede dar un estado sin transiciones definidas y esto significa que el estado es terminal. De esta manera, podemos definir máquinas finitas o no finitas.
Con respecto a la parte de tiempo de ejecución, la definición de una máquina se realizará desde la interfaz iMachinedEfinition , que se generará a partir de la clase MachineFinitionBuilder .
Ejemplo del código de definición de la máquina
//Helper class with states enumeration
public class State : Enumeration
{
public static readonly State Init = new State ( 1 , nameof ( Init ) ) ;
public static readonly State Working = new State ( 2 , nameof ( Working ) ) ;
public static readonly State Paused = new State ( 3 , nameof ( Paused ) ) ;
public static readonly State Finished = new State ( 4 , nameof ( Finished ) ) ;
private State ( int id , string name ) : base ( id , name )
{
}
}
//Helper class with action enumerations
public class Action : Enumeration
{
public static readonly Action Start = new Action ( 1 , nameof ( Start ) ) ;
public static readonly Action Pause = new Action ( 2 , nameof ( Pause ) ) ;
public static readonly Action Continue = new Action ( 3 , nameof ( Continue ) ) ;
public static readonly Action Finish = new Action ( 4 , nameof ( Finish ) ) ;
private Action ( int id , string name ) : base ( id , name )
{
}
}
//Now we can create a definition
_definition = MachineDefinitionBuilder . Create ( )
. InitialState ( State . Init )
. OnAction ( Action . Start ) . ToState ( State . Working )
. Build ( )
. AddState ( State . Working )
. OnAction ( Action . Pause ) . ToState ( State . Paused )
. OnAction ( Action . Finish ) . ToState ( State . Finished )
. Build ( )
. AddState ( State . Paused )
. OnAction ( Action . Continue ) . ToState ( State . Working )
. Build ( )
. AddState ( State . Finished )
. Build ( )
. BuildDefinition ( ) ;Ejemplo de creación de máquinas y código de uso.
var machine = Machine
. Create ( SimpleMachineDefinition . GetDefinition ( ) ) ;
machine . Dispatch ( Action . Start ) ;
var currentState = machine . State ; En este espacio de nombres, tendremos las clases necesarias para definir un flujo de trabajo y ejecutarlo. Como en la sección anterior, mantendremos la definición del flujo de trabajo separada de la ejecución.
Esta separación se realizará utilizando las interfaces IWorkFlowDefinition e IWorkflowrunner .
Para encapsular la definición y la ejecución, tenemos la interfaz Iflow , esta interfaz también nos permitirá en el futuro construir subfluos, crear flujos que se encapsulen como un servicio dentro de aplicaciones más complejas ... etc.
Si pensamos en un flujo básico, el nodo justo y inicial para restablecer los valores de campo, el siguiente nodo solo para cacular a números aleatorios, y un nodo final para sumar ambos valores La definición debe ser algo así.
Ejemplo de código de definición de flujo de trabajo.
var contextDefinition = ContextDefinition . Create ( ) ;
var definition = FlowDefinitionBuilder . Create ( )
. WithInitialNode ( ResetValue . Create ( Node . Reset , contextDefinition ) )
. OnExitCode ( ResetValue . ExitCode . Ok ) . GoTo ( Node . SetValue )
. Build ( )
. WithNode ( SetValue . Create ( Node . SetValue , contextDefinition ) )
. OnExitCode ( SetValue . ExitCode . Assigned ) . GoTo ( Node . SumValue )
. Build ( )
. WithNode ( SumValue . Create ( Node . SumValue , contextDefinition ) ) . Build ( )
. BuildDefinition ( ) ;Anteriormente, ha definido algunas clases de ayuda como contextefinition, es solo una clase para contener un campo de contexto y evitar la duplicación con las definiciones de nombres.
internal class ContextDefinition
{
public ContextField < int > FirstDigit { get ; }
public ContextField < int > SecondDigit { get ; }
public ContextField < int > Result { get ; }
private ContextDefinition ( )
{
FirstDigit = ContextField < int > . Create ( "FieldOne" ) ;
SecondDigit = ContextField < int > . Create ( "FieldTwo" ) ;
Result = ContextField < int > . Create ( "FieldResult" ) ;
}
public static ContextDefinition Create ( )
{
return new ContextDefinition ( ) ;
}
}Hemos creado también la clase abstracta común que utilizaremos como clase base para todas nuestras clases de nodos, utilizaremos como una forma de garantizar que todos los nodos tengan conxtextdefinition avaliable
internal abstract class Common : Core . WorkFlows . Implementations . Node
{
protected ContextDefinition ContextDefinition { get ; }
protected Common ( CommonNamedKey key , ContextDefinition contextDefinition ) : base ( key )
{
ContextDefinition = contextDefinition
?? throw new ArgumentNullException ( nameof ( contextDefinition ) ) ;
}
}Tenemos dos tipos de nodos sincronos y async, debajo de las interfaces inodo e inodeasync , por lo que podemos usar nodos como envoltura de ambos tipos de proceso. En este ejemplo solo tenemos la implementación de sincronización.
En nuestro ejemplo usaremos solo nodos de sincronización.
Nodo de ejemplo
internal class ResetValue : Common
{
#region ExitCodes
public class ExitCode : Enumeration
{
public static readonly ExitCode Ok = new ExitCode ( 1 , "Ok" ) ;
private ExitCode ( int id , string name ) : base ( id , name )
{
}
}
#endregion
private ResetValue ( CommonNamedKey key , ContextDefinition contextDefinition ) :
base ( key , contextDefinition )
{
}
public override NodeExit Execute ( Context context )
{
context . Store ( ContextDefinition . FirstDigit , 0 ) ;
context . Store ( ContextDefinition . SecondDigit , 0 ) ;
context . Store ( ContextDefinition . Result , 0 ) ;
return NodeExit . Create ( ExitCode . Ok . Name ) ;
}
public static ResetValue Create ( CommonNamedKey name , ContextDefinition contextDefinition )
{
return new ResetValue ( name , contextDefinition ) ;
}
}La clase interna de salida de la clase es solo otra clase de ayuda, construir sobre la clase de enumeración con la definición de códigos de salida avalables para este nodo, también usamos Somthin Similira
Un corredor, para instanciarse a sí mismo, deberá recibir la definición de flujo de trabajo y una instancia de contexto que se utilizará para compartir información entre nodos. Una vez que se ha ejecutado el corredor, podemos recuperar los valores de retorno del contexto si los hay.
Tenemos la clase abstracta FlowRunnerBase para que podamos implementar nuestros corredores personalizados, pase a paso, distribuido, etc.
Ejemplo de corredor de flujo
var flowRunner = FlowRunner . Create ( definition , Context . Create ( ) ) ;
var exit = await flowRunner . Start ( ) ;El flujo de clase, como dijimos antes, es un envoltorio para todo este proceso, ahora tiene funcionalidades básicas, pero en futuras versiones se utilizará como clase principal para la gestión del flujo de trabajo.
var definition = SimpleFlowDefinition . GetDefinition ( ) ;
var context = Context . Create ( ) ;
var flow = Flow . Create ( FlowRunner . Create ( definition , context ) ) ;
var exit = await flow . Run ( ) ;