Allows you to create a simple workflow structure from specially marked up handler methods, in which the input and output types are the links between the work items.
This implementation is an IoC framework that allows you to execute a sequence of specially marked up class methods, in the order determined by the input and output value types of those methods.
public class SimpleComponent
{
[TwEntrypoint]
public int FirstRun()
=> 1;
[TwEntrypoint]
public float SecondRun(int i)
=> (float)i;
}
public class OtherSimpleComponent
{
[TwEntrypoint]
public void ThirdRun(float i){}
}
For such a system to work, one mandatory rule must be observed - uniqueness of the return type of the handler method.
The following code cannot be executed:
public class SimpleComponent
{
[TwEntrypoint]
public int Run()
=> 1;
}
public class OtherSimpleComponent
{
[TwEntrypoint]
public int Run()
=> 2;
}
The second mandatory rule is that - all return types must be in the input parameters of the handler methods.
When trying to execute this schema, the framework will throw an exception due to the presence of an unused int
parameter
public class SimpleComponent
{
[TwEntrypoint]
public (int, float) Run()
=> (1, 2.0);
}
public class OtherSimpleComponent
{
[TwEntrypoint]
public void Run(float i){}
}
Each handler can return multiple values (types) in one execution as a value tuple.
Each type in such a tuple will be considered a separate result of execution and can be used separately from neighboring types as input values to other handler methods.
var builder = new TwContainerBuilder();
var container = builder
.AddAssemblies(typeof(SimpleComponent).Assembly)
.Build();
container.Run().AsTask().Wait();
This code will create a container for executing a sequence of handler methods and execute one cycle of work while waiting for the execution result.
The container's Run
method is thread-safe and can be used any number of times during the lifetime of the container.
Dependency injection is supported for constructors (Constructor Injection) and static methods of the TwInject
tagged classes that contain the tagged handler methods.
This can be done through the implementation of the TypedWorkflow.IResolver
interface and passing an instance of such a class when creating a container.
var resolver = new MyDiResolver();
var builder = new TwContainerBuilder();
var container = builder
.AddAssemblies(typeof(SimpleComponent).Assembly)
.RegisterExternalDi(resolver)
.Build();
It is possible to create static handler methods
public static class SingletonComponent
{
[TwEntrypoint]
public static void Run(int i, float f){}
}
At each cycle of the container, an arbitrary number of values can be entered into the system
var builder = new TwContainerBuilder();
var container = builder
.AddAssemblies(typeof(SimpleComponent).Assembly)
.Build<(int, float)>();
container.Run((1, 2.0)).AsTask().Wait();
It is necessary that at least one handler method has a dependence on the input values.
public class SimpleComponent
{
[TwEntrypoint]
public void Run(int i, float f){}
}
Upon completion of the work, the system can generate a value with the specified type as a result of the entire system
var builder = new TwContainerBuilder();
var container = builder
.AddAssemblies(typeof(SimpleComponent).Assembly)
.Build<int, float>();
float result = container.Run(2).AsTask().Result;
public class SimpleComponent
{
[TwEntrypoint]
public float Run(int i)
=> i * 2.0;
}
To cancel the long execution of the component graph, a special token System.Threading.CancellationToken
is used, which is passed when the Run
execution method is started as an additional optional parameter.
var componentType = typeof(TypedWorkflowTests.OtherComponents.AsyncCancellationTest.WoInputAndOutput.LongTimeExecutionComponent);
var builder = new TwContainerBuilder();
var container = builder
.AddAssemblies(componentType.Assembly)
.AddNamespaces(componentType.Namespace)
.Build();
var cancellation = new CancellationTokenSource();
var t = container.Run(cancellation.Token).AsTask();
Task.Delay(500).Wait();
cancellation.Cancel();
var ex = Assert.CatchAsync<TaskCanceledException>(() => t);
The framework does not interact with this token in any way, so all work with it must be implemented inside the handler method of the custom component (passed as one of the parameters of the call to the handler method).
public class LongTimeExecutionComponent
{
[TwEntrypoint]
public async Task Run(CancellationToken cancellation)
{
await Task.Delay(-1, cancellation);
}
}
The cache-aside
template implemented in the proactive cache from the ProactiveCache
library is used.
Using the cache allows you to save the results of the execution of the graph of components in memory for a specified time with a specified background update on demand (proactive update).
Caching is available only for systems with input and output parameters (passing parameters and returning results), where the input parameters will become the key value in the cache,
and the results of work are cached value.
var builder = new TwContainerBuilder();
var container = builder
.AddAssemblies(typeof(SlowProducerComponent).Assembly)
.AddNamespaces(typeof(SlowProducerComponent).Namespace)
.WithCache(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(1))
.Build<int, long>();
var result1 = container.Run(42).Result;
var result2 = container.Run(42).Result;
In the example, the int
type will be used as the key, and the returned result with the long
type will be used as the cached value.
result1
will be obtained after the execution of the given component graph from the typeof (SlowProducerComponent) .Namespace
namespace.
result2
will be taken from the cache in memory and equal to the value in result1
.
After the expiration of one second (the second parameter of the WithCache
method), the subsequent request will re-execute the graph, and the immediately following request will receive the old value from the cache.
Completing the re-execution of the graph will update the value in the cache and specify the lifetime of this value in two seconds (the first parameter of the WithCache
method).
Skip execution of a handler method depending on the value of the result.
If the result model is wrapped in a TypedWorkflow.Option
wrapper, then it will allow returning an empty value as a response.
Handler methods whose model values cannot be empty (not wrapped by TypedWorkflow.Option
) will be skipped if the values of these models are empty.
Thus, all handler methods dependent on the execution result will be skipped.
public class SimpleComponent
{
[TwEntrypoint]
public TypedWorkflow.Option<int> Run()
=> TypedWorkflow.Option<float>.None;
[TwEntrypoint]
public float NeverRun(int i)
=> i * 2.0;
[TwEntrypoint]
public void NeverRun(float i) {}
}
They are implemented as the TwConstraintAttribute
attribute and are intended to block the execution of a handler method based on the presence or absence of a given model value.
Using such restrictions, it is possible to implement the operation of the cache, this is when the presence of a value in the cache blocks the execution of an operation to obtain this value from its slower storage (for example, a database).
A constraint condition can be assigned both to the base or main class of the component, and to the handler method itself.
The result of a blocked handler method will have an empty value, so all handler methods dependent on this value will be skipped and their results will also have an empty value.
[TwConstraint(typeof(FromCache), HasNone = true)]
public class ConstrainedComponent
{
[TwEntrypoint]
public Option<FromDb> GetModelFromDb()
{
return Option.Create(new FromDb(new SomeModel()));
}
}
A more detailed example can be found in the tests TypedWorkflowTests.Components.11-ConstrainedComponent.cs
- Return instances of classes, not structures (the mechanism of boxing and unboxing in cases of calling handler methods through the use of reflection nullifies all the advantages of structures).
- Use
singlеton
components whenever possible (using static methods as handler methods).
Can be found in the project TypedWorkflowTests
in the class WorkflowBuilderTest
(all components are located in the folder Components
of the specified project)
Icons made by Freepik from www.flaticon.com