This article is a work in progress. Check back later.
In this post, I will highlight how, and why, I've created an incremental source generator in order to enforce a contract on Akka.NET actors.
Motivation
As you may well know from working with Akka, the framework enforces a disconnect between actors by abstracting actors away, using an indirection called an ActorRef.
From the docs: (An ActorRef is an) immutable and serializable handle to an actor, which may or may not reside on the local host or inside the same ActorSystem.
In plain terms: an ActorRef shields an actor and it's state from the outside world, while providing an interface for message passing.
This is, of course, one of the main benefits of the actor model. However, due to the way it's implemented in Akka, using a plain ActorRef comes with a few drawbacks to those used to more standard .NET code.
One of these drawbacks is that an ActorRef does not in itself enforce or guarantee a strict contract.
Concretely, this means that messages passed to an actor through its ActorRef lack compile-time checks, potentially leading to runtime errors if the wrong message type is sent. Of course, it's always possible to wrap an ActorRef in a separate class, which may be a service, thus defining a contract.
In this experiment, we will take precisely this approach, with a twist: we will automate, by means of an incremental source generator, the wrapping of the ActorRef.
A note on source generators
It's a well-known fact in the .NET community that source generators used to be a pain to write and maintain. As such, while fascinated by the technology (yay metaprogramming!), I did not watch the space actively for a few years.
For the readers who, like me, assume this is still the case: I was pleasantly surprised to find that both writing, using, and debugging source generators has matured significantly, to the point where I've used my usual IDE (JetBrains Rider) to both write, run, and debug this generator, and had a flawless experience. I would assume the experience to be similar in Visual Studio.
As such, while this experiment is by no means production-ready, I find myself less hesistant to include such code in a production project.
Design
Let's first walk through the kind of code we will be generating, and provide an example of some user code we will be generating against.
The contract
This one is easy enough - we need to define a contract, more commonly known in C# as an interface. In order to not distract from the essence: A toy example.
We, taking on the role of the user of our generator, define an interface, IScreamActor. It has one method: Scream, which takes the text to scream, the volume in db, and whether or not we're screaming during a scrum meeting.
public interface IScreamActor
{
public void Scream(string text, int db, bool scrumMeeting);
}
The actor
Next up, the actual actor. This will implement the actual business logic, and fulfill the IScreamActor contract. Let's also make it a ReceiveActor for now.
You will note a few things:
- this class is partial
- we're not defining any Receive<SomeMessage>(message => {...});
Of course: this is the very stuff we want to generate ;)
public partial class ScreamBloodyGoreActor : ReceiveActor, IScreamActor
{
private void Scream(string text, int db, bool scrumMeeting)
{
if (scrumMeeting)
{
Console.WriteLine("In a scrum meeting, nobody can hear you scream.");
}
if (db > 90)
{
Console.WriteLine(text.ToUpperInvariant());
}
else
{
Console.WriteLine(text);
}
}
}
The proxy
I call this the proxy, as that is, to me, fundamentally what it is, but you may call it the wrapper, indirection, or anything else that makes sense to you.
// TODO
Returning
TODO: Notes on not breaking asynchronous messaging flow
Other approaches
In the spirit of being my own critic: "Why not write a Roslyn analyzer, which will output a warning or error when sending a message to an actor that is not configured to receive the message?"
This would involve somehow tracking, at compile-time, which actor instance would be occupying which IActorRef. While this may be doable in simple instances, such as creating an actor and immediately sending a message to it, Akka's flexibility allows you, for instance, to hot-swap actors and their behavior at runtime. As such, this kind of analyzer becomes useless in all but the most straightforward use cases.
Furthermore, achieving the goal in the way we have done now, we receive serveral additional benefits:
- We can make the choice between using a ContractActor, and using a plain ActorRef
- Our IDE/IntelliSense will provide the usual hints when working with a ContractActor, thus making Akka feel more familiar to developers not used to working with the framework
References
[1] Creating an incremental source generator - Andrew Lock