ActorSrcGen 1.0.1

dotnet add package ActorSrcGen --version 1.0.1
NuGet\Install-Package ActorSrcGen -Version 1.0.1
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ActorSrcGen" Version="1.0.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ActorSrcGen --version 1.0.1
#r "nuget: ActorSrcGen, 1.0.1"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install ActorSrcGen as a Cake Addin
#addin nuget:?package=ActorSrcGen&version=1.0.1

// Install ActorSrcGen as a Cake Tool
#tool nuget:?package=ActorSrcGen&version=1.0.1

Welcome To ActorSrcGen

ActorSrcGen is a C# Source Generator allowing the conversion of simple C# classes into Dataflow compatible pipelines supporting the actor model.

ActorSrcGen is currently a solo effort to create a useful and powerful source code generator to simplify the creation of high performance pipeline code conforming to the actor model. We welcome any feedback, suggestions, and contributions from the community.

If you encounter any issues or have any questions, please don't hesitate to submit an issue report. This helps me understand any problems or limitations of the project and allows me to address them promptly.

If you have an idea for a new feature or enhancement, I encourage you to submit a feature request. Your input will shape the future direction of ActorSrcGen and help make it even better.

If you have any code changes or improvements you'd like to contribute, I welcome pull requests (PRs). Please follow the guidelines provided in our project's contribution guidelines and README file. I will review your changes and provide feedback, helping you ensure a smooth integration process.

How Do You Use It?

It's remarkably easy to use ActorSrcGen to inject pipeline processing code into your project.

  1. Install the Nuget Package into your project

    dotnet add package ActorSrcGen --version 0.3.5
    
  2. Adorn your actor class with the Actor Attribute

    [Actor]
    public class MyActor{ . . . }
    
  3. Define the initial starting step of your pipeline, being sure to indicate what step comes next

    [InitialStep]
    [NextStep(nameof(DecodeMsg))]
    [NextStep(nameof(LogMsg))]
    public string ReceiveMsgFromSomewhere(string x){ . . . }
    
  4. Add a sequence of intermediate steps

    [Step, NextStep(nameof(ProcessMsg))]
    public Request DecodeMsg(string x){ . . . }
    
    [Step]
    public void LogMsg(string x){ . . . }
    
  5. Finish up with the last step

    [LastStep]
    public void ProcessMsg(Request req){ . . . }
    

Behind the scenes, the source generator will generate the wiring for your actor, so that all you then need to do is invoke the actor with a call to Call or Cast depending on whether you want the invocation to be blocking or not.

var a = new MyActor();
a.Call("hello world!");

Naturally there are various other details related to DataflowEx and TPL dataflow that you can take advantage of, but the gist is to make the actor as simple as that to write. The generator will create the wiring. You just need to implement the steps of the pipeline itself.

What It Does

The source generator in the provided code is a tool that automatically generates additional code based on a simple C# class. Its purpose is to simplify the usage of TPL Dataflow, a library that helps with writing robust and performant asynchronous and concurrent code in .NET. In this specific case, the source generator takes a regular C# class and extends it by generating the necessary boilerplate code to use TPL Dataflow. The generated code creates a pipeline of dataflow components that support the actor model.

The generated code includes the following components

  • TransformManyBlock: This block transforms input data and produces multiple output data items.
  • ActionBlock: This block performs an action on the input data without producing any output.
  • DataflowLinkOptions: This class specifies options for linking dataflow blocks together.
  • ExecutionDataflowBlockOptions: This class specifies options for configuring the execution behavior of dataflow blocks.

The generated code also includes the necessary wiring to connect the methods of the original class together using the TPL Dataflow components. This allows the methods to be executed in a coordinated and concurrent manner.

Overall, the source generator simplifies the process of using TPL Dataflow by automatically generating the code that would otherwise need to be written manually. It saves developers from writing a lot of boilerplate code and allows them to focus on the core logic of their application.

[Actor]
public partial class MyActor
{
    public List<int> Results { get; set; } = [];

    [InitialStep(next: "DoTask2")]
    public string DoTask1(int x)
    {
        Console.WriteLine("DoTask1");
        return x.ToString();
    }

    [Step(next: "DoTask3")]
    public string DoTask2(string x)
    {
        Console.WriteLine("DoTask2");
        return $"100{x}";
    }

    [LastStep]
    public void DoTask3(string input)
    {
        Console.WriteLine("DoTask3");
        int result = int.Parse(input);
        Results.Add(result);
    }
}

And the source generator will extend it, adding the boilerplate TPL Dataflow code to wire the methods together in a clean way:

#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
#pragma warning disable CS0108 // hides inherited member.

using ActorSrcGen;
namespace ConsoleApp2;
using System.Threading.Tasks.Dataflow;
using Gridsum.DataflowEx;

public partial class MyActor : Dataflow<Int32>, IActor<Int32>
{

    public MyActor() : base(DataflowOptions.Default)
    {
        _DoTask1 = new TransformManyBlock<Int32, String>(       async (Int32 x) => {
           var result = new List<String>();
           try
           {
               result.Add(DoTask1(x));
           }catch{}
           return result;
       },
            new ExecutionDataflowBlockOptions() {
                BoundedCapacity = 5,
                MaxDegreeOfParallelism = 8
        });
        RegisterChild(_DoTask1);
        _DoTask2 = new TransformManyBlock<String, String>(       async (String x) => {
           var result = new List<String>();
           try
           {
               result.Add(DoTask2(x));
           }catch{}
           return result;
       },
            new ExecutionDataflowBlockOptions() {
                BoundedCapacity = 5,
                MaxDegreeOfParallelism = 8
        });
        RegisterChild(_DoTask2);
        _DoTask3 = new ActionBlock<String>(        (String x) => {
            try
            {
                DoTask3(x);
            }catch{}
        },
            new ExecutionDataflowBlockOptions() {
                BoundedCapacity = 5,
                MaxDegreeOfParallelism = 8
        });
        RegisterChild(_DoTask3);
        _DoTask1.LinkTo(_DoTask2, new DataflowLinkOptions { PropagateCompletion = true });
        _DoTask2.LinkTo(_DoTask3, new DataflowLinkOptions { PropagateCompletion = true });
    }
    TransformManyBlock<Int32, String> _DoTask1;

    TransformManyBlock<String, String> _DoTask2;

    ActionBlock<String> _DoTask3;

    public override ITargetBlock<Int32> InputBlock { get => _DoTask1; }

    public bool Call(Int32 input)
    => InputBlock.Post(input);

    public async Task<bool> Cast(Int32 input)
    => await InputBlock.SendAsync(input);

}

Invocation of your class is a straightforward call to send a message to the actor:

static async Task Main(string[] args)
{
    await Console.Out.WriteLineAsync("Starting!");
    MyActor actor = new();
    actor.RegisterPostDataflowTask(AfterDone);
    await actor.Cast(50);
    await actor.SignalAndWaitForCompletionAsync();
    var x = actor.Results.First();
    await Console.Out.WriteLineAsync($"Result: {x}");
}

Which produces what you would expect:

Starting!
DoTask1
DoTask2
DoTask3
Finished
Result: 10050

Why Bother?

You might be wondering what the architectural benefits of using a model like this might be.

Writing robust and performant asynchronous and concurrent code in .NET is a laborious process. TPL Dataflow makes it easier - it "provides dataflow components to help increase the robustness of concurrency-enabled applications. This dataflow model promotes actor-based programming by providing in-process message passing for coarse-grained dataflow and pipelining tasks" (see docs).

ActorSrcGen allows you to take advantage of that model without needing to write a lot of the necessary boilerplate code.

The Actor Model

The Actor Model is a programming paradigm that is based on the concept of actors, which are autonomous units of computation. It has several benefits in programming:

  1. Concurrency: Actors can be executed concurrently, allowing for efficient use of multiple CPU cores. This can lead to significant performance improvements in systems that require concurrent execution.
  2. Fault tolerance: Actors can be designed to be fault-tolerant, meaning that if an actor fails or crashes, it can be restarted without affecting the rest of the system. This can improve the reliability and availability of the system.
  3. Encapsulation: Actors encapsulate their state and behavior, making it easier to reason about and test the code. This can lead to better code quality and maintainability.

TPL Dataflow

The Task Parallel Library (TPL) Dataflow in .NET provides a powerful framework for building high-throughput systems. Here are some benefits of using TPL Dataflow for high-throughput systems:

  1. Efficiency: TPL Dataflow is designed to optimize the execution of tasks and dataflows. It automatically manages the execution of tasks based on available resources, reducing unnecessary overhead and maximizing throughput.
  2. Scalability: TPL Dataflow allows you to easily scale your system by adding or removing processing blocks. You can dynamically adjust the number of processing blocks based on the workload, ensuring that your system can handle varying levels of throughput.
  3. Flexibility: TPL Dataflow provides a variety of processing blocks, such as buffers, transform blocks, and action blocks, which can be combined and customized to fit your specific requirements. This flexibility allows you to build complex dataflows that can handle different types of data and processing logic.

Acknowledgements

The generated source builds atop DataflowEx for a clean stateful object-oriented wrapper around your pipeline.

With thanks to:

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.1 25 4/28/2024
0.3.6 46 4/25/2024
0.3.5 44 4/24/2024
0.3.3 39 4/23/2024
0.3.0 213 11/4/2023
0.2.10 175 10/31/2023