https://github.com/archi-Doc/BigMachines
  
  
    BigMachines is State Machine library for .NET 
    https://github.com/archi-Doc/BigMachines
  
        Last synced: 7 months ago 
        JSON representation
    
BigMachines is State Machine library for .NET
- Host: GitHub
- URL: https://github.com/archi-Doc/BigMachines
- Owner: archi-Doc
- License: mit
- Created: 2021-06-26T14:47:50.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2024-09-18T13:38:01.000Z (about 1 year ago)
- Last Synced: 2024-11-01T21:36:40.310Z (12 months ago)
- Language: C#
- Size: 842 KB
- Stars: 37
- Watchers: 2
- Forks: 0
- Open Issues: 0
- 
            Metadata Files:
            - Readme: README.md
- License: LICENSE
 
Awesome Lists containing this project
- RSCG_Examples - BigMachines - Doc/BigMachines (Contributors Welcome for those / 1. [ThisAssembly](https://ignatandrei.github.io/RSCG_Examples/v2/docs/ThisAssembly) , in the [EnhancementProject](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#enhancementproject) category)
- csharp-source-generators - BigMachines -   BigMachines is State Machine library for .NET. (Source Generators / Other)
README
          ## BigMachines is State Machine library for .NET
 
- Very versatile and easy to use.
- Running machines and sending commands to each machine is designed to be **lock-free**.
- Full serialization features integrated with [Tinyhand](https://github.com/archi-Doc/Tinyhand), [ValueLink](https://github.com/archi-Doc/ValueLink), [CrystalData](https://github.com/archi-Doc/CrystalData).
- Simplifies complex and long-running tasks.
  
## Table of Contents
- [Requirements](#requirements)
- [Quick Start](#quick-start)
- [Machine Types](#machine-types)
- [Identifier](#identifier)
- [Machine Class](#machine-class)
- [Other](#other)
  - [Service provider](#service-provider)
  - [Generic machine](#generic-machine)
  - [Loop checker](#loop-checker)
  - [Exception handling](#exception-handling)
## Requirements
**Visual Studio 2022** or later for Source Generator V2.
**C# 12** or later for generated codes.
**.NET 8** or later target framework.
## Quick Start
Install **BigMachines** using Package Manager Console.
```
Install-Package BigMachines
```
This is a small sample code to use **BigMachines**.
```csharp
using System;
using System.Threading.Tasks;
using Arc.Threading;
using BigMachines;
namespace QuickStart;
// Create a BigMachine class that acts as the root for managing machines.
// In particular, define an empty partial class, add a BigMachineObject attribute, and then add AddMachine attributes for the Machine you want to include.
[BigMachineObject]
[AddMachine]
public partial class BigMachine { }
[MachineObject] // Add a MachineObject attribute.
public partial class FirstMachine : Machine // Inherit Machine class. The type of an identifier is int.
{
    public FirstMachine()
    {
        this.DefaultTimeout = TimeSpan.FromSeconds(1); // The default time interval for machine execution.
        this.Lifespan = TimeSpan.FromSeconds(5); // The time until the machine automatically terminates.
    }
    public int Count { get; set; }
    [StateMethod(0)] // Add a StateMethod attribute and set the state method id 0 (default state).
    protected StateResult Initial(StateParameter parameter)
    {// This code is inside the machine's exclusive lock.
        Console.WriteLine($"FirstMachine {this.Identifier}: Initial");
        this.ChangeState(FirstMachine.State.One); // Change to state One.
        return StateResult.Continue; // Continue (StateResult.Terminate to terminate machine).
    }
    [StateMethod] // If a state method id is not specified, the hash of the method name is used.
    protected StateResult One(StateParameter parameter)
    {
        Console.WriteLine($"FirstMachine {this.Identifier}: One - {this.Count++}");
        return StateResult.Continue;
    }
    [CommandMethod] // Add a CommandMethod attribute to a method which receives and processes commands.
    protected CommandResult TestCommand(string message)
    {
        Console.WriteLine($"Command received: {message}");
        return CommandResult.Success;
    }
    protected override void OnTermination()
    {
        Console.WriteLine($"FirstMachine {this.Identifier}: Terminated");
        ThreadCore.Root.Terminate(); // Send a termination signal to the root.
    }
}
public class Program
{
    public static async Task Main(string[] args)
    {
        var bigMachine = new BigMachine(); // Create a BigMachine instance.
        bigMachine.Start(ThreadCore.Root); // Launch BigMachine to run machines and change the parent of the BigMachine thread to the application thread.
        var testMachine = bigMachine.FirstMachine.GetOrCreate(42); // Machine is created via an interface class and the identifier, not the machine class itself.
        testMachine.TryGetState(out var state); // Get the current state. You can operate machines using the interface class.
        Console.WriteLine($"FirstMachine state: {state}");
        testMachine = bigMachine.FirstMachine.GetOrCreate(42); // Get the created machine.
        testMachine.RunAsync().Wait(); // Run the machine manually.
        Console.WriteLine();
        var testControl = bigMachine.FirstMachine; // Control is a collection of machines.
        Console.WriteLine("Enumerates identifiers.");
        foreach (var x in testControl.GetIdentifiers())
        {
            Console.WriteLine($"Machine Id: {x}");
        }
        Console.WriteLine();
        await testMachine.Command.TestCommand("Test message"); // Send a command to the machine.
        Console.WriteLine();
        await ThreadCore.Root.WaitForTerminationAsync(-1); // Wait for the termination infinitely.
    }
}
```
## Machine Types
**BigMachines** supports several types of machine.
### Passive Machine
Passive machine can be run and the state can be changed by an external operation.
```csharp
[MachineObject]
public partial class PassiveMachine : Machine
{
    public static async Task Test(BigMachine bigMachine)
    {
        var machine = bigMachine.PassiveMachine.GetOrCreate(0);
        await machine.Command.ReceiveString("message 1"); // Send a command.
        await machine.RunAsync(); // Manually run the machine.
        var result = machine.ChangeState(State.First); // Change the state from State.Initial to State.First
        Console.WriteLine(result.ToString());
        await machine.RunAsync(); // Manually run the machine.
        result = machine.ChangeState(State.Second); // Change the state from State.First to State.Second (denied)
        Console.WriteLine(result.ToString());
        await machine.RunAsync(); // Manually run the machine.
        result = machine.ChangeState(State.Second); // Change the state from State.First to State.Second (approved)
        Console.WriteLine(result.ToString());
        await machine.RunAsync(); // Manually run the machine.
    }
    public PassiveMachine()
    {
    }
    public int Count { get; set; }
    [StateMethod(0)]
    protected StateResult Initial(StateParameter parameter)
    {
        Console.WriteLine($"PassiveMachine: Initial - {this.Count++}");
        return StateResult.Continue;
    }
    [StateMethod]
    protected StateResult First(StateParameter parameter)
    {
        Console.WriteLine($"PassiveMachine: First - {this.Count++}");
        return StateResult.Continue;
    }
    protected bool FirstCanExit()
    {// State Name + "CanExit": Determines if it is possible to change from the state.
        return true;
    }
    [StateMethod]
    protected StateResult Second(StateParameter parameter)
    {
        Console.WriteLine($"PassiveMachine: Second - {this.Count++}");
        return StateResult.Continue;
    }
    protected bool SecondCanEnter()
    {// State Name + "CanEnter": Determines if it is possible to change to the state.
        var result = this.Count > 2;
        var message = result ? "Approved" : "Denied";
        Console.WriteLine($"PassiveMachine: {this.GetState().ToString()} -> {State.Second.ToString()}: {message}");
        return result;
    }
    [CommandMethod]
    protected CommandResult ReceiveString(string message)
    {
        Console.WriteLine($"PassiveMachine command: {message}");
        return CommandResult.Success;
    }
}
```
### Intermittent Machine
Intermittent machine is a machine that runs at regular intervals.
```csharp
[MachineObject]
public partial class IntermittentMachine : Machine
{
    public static void Test(BigMachine bigMachine)
    {
        // The machine will run at regular intervals (1 second).
        var machine = bigMachine.IntermittentMachine.GetOrCreate(0);
    }
    public IntermittentMachine()
    {
        this.DefaultTimeout = TimeSpan.FromSeconds(1); // Default time interval for machine execution.
        this.Lifespan = TimeSpan.FromSeconds(5); // The time until the machine automatically terminates.
    }
    public int Count { get; set; }
    [StateMethod(0)]
    protected StateResult Initial(StateParameter parameter)
    {
        Console.WriteLine($"IntermittentMachine: Initial - {this.Count++}");
        if (this.Count > 2)
        {
            this.ChangeState(State.First);
        }
        return StateResult.Continue;
    }
    [StateMethod(1)]
    protected StateResult First(StateParameter parameter)
    {
        Console.WriteLine($"IntermittentMachine: First - {this.Count++}");
        this.TimeUntilRun = TimeSpan.FromSeconds(0.5); // Change the timeout of the machine.
        return StateResult.Continue;
    }
}
```
### Continuous Machine
Continuous machine is different from passive and intermittent machine (passive and intermittent machine are virtually the same).
It's designed for heavy and time-consuming tasks.
```csharp
[TinyhandObject]
[MachineObject(Control = MachineControlKind.Sequential, NumberOfTasks = 1)]
public partial class SequentialMachine : Machine
{// SequentialMachine executes one at a time, in the order of their creation.
    public static void Test(BigMachine bigMachine)
    {
        bigMachine.SequentialMachine.TryCreate(1);
        bigMachine.SequentialMachine.TryCreate(2);
        bigMachine.SequentialMachine.TryCreate(3);
    }
    public SequentialMachine()
    {
        this.Lifespan = TimeSpan.FromSeconds(5);
        this.DefaultTimeout = TimeSpan.FromSeconds(1);
    }
    [Key(10)]
    public int Count { get; set; }
    [StateMethod(0)]
    protected async Task Initial(StateParameter parameter)
    {
        Console.WriteLine($"SequentialMachine machine[{this.Identifier}]: {this.Count++}");
        await Task.Delay(500).ConfigureAwait(false); // Some heavy task
        if (this.Count >= 3)
        {
            return StateResult.Terminate;
        }
        return StateResult.Continue;
    }
}
```
To improve response and share resource, heavy task should not be done at once, but divided into several smaller tasks.
## Serialization
Thanks to [Tinyhand](https://github.com/archi-Doc/CrystalData) and [CrystalData](https://github.com/archi-Doc/CrystalData), serialization and persistence of **BigMachine** is very easy.
Add the `TinyhandObject` attribute to the **Machine** class, and use the code below to serialize and deserialize.
```csharp
[TinyhandObject]
[MachineObject]
public partial class TestMachine : Machine {}
```
```csharp
var bin = TinyhandSerializer.Serialize(bigMachine);
var bigMachine2 = TinyhandSerializer.Deserialize(bin);
```
If you want to save to a file, register it with **CrystalData** as shown in the following code.
```csharp
var builder = new CrystalControl.Builder()
    .ConfigureCrystal(context =>
    {
        context.AddCrystal(new()
        {
            FileConfiguration = new LocalFileConfiguration("Data/BigMachine.tinyhand"),
            SavePolicy = SavePolicy.Manual,
            SaveFormat = SaveFormat.Utf8,
            NumberOfFileHistories = 3,
        });
    });
```
## Identifier
`Identifier` is a key concept of `BigMachines`.
## Other
### Virtual methods
These are virtual functions of `Machine` class.
You can override and use them when necessary.
```csharp
/// 
/// Called when the machine is newly created.
/// Note that it is not called after deserialization.
///  ->  -> .
/// 
/// The parameters used when creating a machine.
protected virtual void OnCreate(object? createParam)
{
}
/// 
/// Called when the machine is ready to start
/// Note that it is called before the actual state method.
///  ->  -> .
/// 
protected virtual void OnStart()
{
}
/// 
/// Called when the machine is terminating.
///  This code is inside a semaphore lock.
///   ->  -> .
/// 
protected virtual void OnTerminate()
{
}
```
### Service provider
Since the machine is independent, you cannot pass parameters directly when creating an instance (and mainly for the deserialization process).
```csharp
```
```csharp
public class SomeService
{
    public void Print(string? text) => Console.WriteLine($"Some service : {text}");
}
// Machine depends on SomeService.
[MachineObject(UseServiceProvider = true)]
public partial class ServiceProviderMachine : Machine
{
    public static void Test(BigMachine bigMachine)
    {
        bigMachine.ServiceProviderMachine.GetOrCreate(0, "A"); // Create a machine and set a parameter.
    }
    public ServiceProviderMachine(SomeService service)
        : base()
    {
        this.Service = service;
        this.DefaultTimeout = TimeSpan.FromSeconds(1);
        this.Lifespan = TimeSpan.FromSeconds(3);
    }
    protected override void OnCreation(object? createParam)
    {// Receives the parameter at the time of creation. Note that it is not called during deserialization.
        this.Text = (string?)createParam;
    }
    public SomeService Service { get; }
    public string? Text { get; set; }
    [StateMethod(0)]
    protected StateResult Initial(StateParameter parameter)
    {
        this.Service.Print(this.Text);
        return StateResult.Continue;
    }
}
```
### Recursive calls checker
Relationships between machines can become complicated, and may lead to circular command issuing.
```csharp
```
### Exception handling
Each machine is designed to run independently.
So exceptions thrown in machines are handled by **BigMachine**'s main thread (`BigMachine.Core`), not by the caller.
In detail, exceptions are registered to **BigMachine** using `BigMachine.ReportException()`, and handled by the following method in **BigMachine**'s main thread.
```cahrp
private static void DefaultExceptionHandler(BigMachineException exception)
{
    throw exception.Exception;
}
```
You can set a custom exception handler using `BigMachine.SetExceptionHandler()`.