You signed in with another tab or window.Reload to refresh your session.You signed out in another tab or window.Reload to refresh your session.You switched accounts on another tab or window.Reload to refresh your session.Dismiss alert
This package lets you easily create asynchronous & type safe node workflows (called blueprints).It consists in 3 main projects :
SafeNodes.Design : The contracts necessary to define your nodes system, such as nodes, events, types, etc...
SafeNodes.Runtime : The runtime that executes blueprints and generates schemes representing your nodes system definition.
SafeNodes.Internal : A set of tools used internally.
Note
By design, user facing contracts such as events, values or nodes must implement marker interfaces.This enforces contracts to be explicitly define and decoupled from logic.
Getting started
A blueprint is made of at least one event and one node.Each blueprint requires exactly one event which is the entrypoint.The entrypoint nodes are the nodes triggered by the events which eventually trigger other nodes.You may want data to flow between your event and nodes. To shape this data, custom values can be defined.Events, nodes, values and their properties can be referenced in the blueprint definition.Use theApi attribute to explicitly create explicit compile-time references.
Using SafeNodes assemblies
// when defining contractsusingSafeNodes.Design;// when using the runtimeusingSafeNodes.Runtime;
Values can define initializers, they create a new instance of the value from a raw string value.This initializer takes raw values such as trim this and returns a trimmedTextValue (trim this).
// you could have as much data as you want here, I just keep things simplepublicsealedrecordBlankData:IEventData;[Api("my-blank-event")]publicsealedclassBlankEvent:IEvent<BlankData>{publicvoidDefine(BlankDatadata){// initializes the event using the data if needed}// Whether this event will be triggerd, you can use the event data here.publicboolIsActivated()=>true;}
Defining a node
[Api("my-print-node")]publicsealedclassPrintNode(IInput<TextValue>textToPrint,ITriggerdone,IOutput<TextValue>textOutput):INode{[Api("print-text")]publicIInput<TextValue>TextToPrint{get;}=textToPrint;[Api("text")]publicIOutput<TextValue>TextOutput{get;}=textOutput;[Api("done")]publicITriggerDone{get;}=done;// this is where the logic goespublicasyncTask<ErrorOr<Success>>Execute(CancellationTokencancellationToken){vartextToPrint=TextToPrint.Get();Console.WriteLine(textToPrint);TextOutput.Set(textToPrint);// awaits for the children nodes (and their children) to completeawaitDone.Trigger(cancellationToken);returnResult.Success;}}
Pipelines
Pipelines let you define logic around execution of nodes. You can either target all nodes or specific ones using interfaces.In this example, the pipeline only targets the nodes implementingIBenchmarkMe.
// make nodes to benchmark implement this interfacepublicinterfaceIBenchmarkMe;publicsealedclassBenchmarkNodes<TNode>:INodeContextPipeline<TNode>whereTNode:IBenchmarkMe,INode{publicasyncTask<IErrorOr>Next(TNodenode,NodeContextPipelineNextnext,CancellationTokencancellationToken){varstopwatch=newStopwatch();varresult=awaitnext();Console.WriteLine($"{typeof(TNode)} execution time ticks :{stopwatch.ElapsedTicks}");returnresult;}}
Running a blueprint
// setup a DI containervarbuilder=newContainerBuilder();// don't forget to add the SafeNodesModule in the DI containerbuilder.RegisterModule<SafeNodesModule>();// your defined types need to be added to the DI container !!// the recommended lifetime is Transient (InstancePerDependency)builder.RegisterTypes([typeof(BlankEvent),typeof(PrintNode),typeof(TrimTextInitializer)]).AsImplementedInterfaces().InstancePerDependency();builder.RegisterGeneric(typeof(BenchmarkNodes<>)).AsImplementedInterfaces().InstancePerDependency();varapp=builder.Build();awaitusingvarscope=app.BeginLifetimeScope();varblueprintRuntime=scope.Resolve<IBlueprintRuntime>();Console.WriteLine("Runtime ...");// this is your blueprint definition.// use the API references here.varblueprint=newBlueprint{Event=newBlueprintEvent{EventReference="my-blank-event"},Nodes=[newBlueprintNode{// this id is a runtime id,// this allows to have multiple nodes of the same typesId="node-1",NodeReference="my-print-node",IsEntrypoint=true,// this node is triggered by the eventInputs=[newBlueprintNodeInput{InputReference="print-text",// set the input value from a raw value and an initializerInitializer=newBlueprintNodeInputInitializer{InitializerReference="my-trim-text-initializer",RawValue=" Hello, World ! "}}],},newBlueprintNode{Id="node-2",NodeReference="my-print-node",Inputs=[newBlueprintNodeInput{InputReference="print-text",// this is the value to set to input to,// it comes from the output 'text' of the node 'node-id'Source=newBlueprintNodeInputSource{NodeId="node-1",OutputReference="text"}}],// this node is not triggered by the event,// rather by the 'done' trigger of the 'node-1' nodeTrigger=newBlueprintNodeTrigger{NodeId="node-1",TriggerReference="done"}}]};// execute the blueprint, throws if the event data is not compatible with the blueprint's eventawaitblueprintRuntime.ExecuteMandatory(blueprint,newBlankData());// execute the blueprint or skip when the event data is not compatible// var blueprintWasExecuted = await blueprintRuntime.Execute(blueprint, new BlankData());