Working with Child Actors
6 minutes to readIn “Actor Hierarchies and Domain Modeling” we learned what an actor hierarchy is and why it exists. In this lesson, we will build one. Additionally, we will explore the other common actor base type we haven’t been using: the ReceiveActor
.
ReceiveActor
s and UntypedActor
s
We covered this in “Our First Akka.NET Application”, but there are two different common actor base types in the core Akka library:
UntypedActor
- the simplest possible actor type andReceiveActor
1 - an actor that uses strongly-typedReceive<T>
andReceiveAsync<T>
delegates to define message-handlers for our actors.
Both actor types are equally capable1, so the choice between them is largely a matter of preference and style.
In this particular lesson we’re going to create the WordCounterManager
actor using the ReceiveActor
syntax just so you can get exposed to both flavors.
Creating the WordCounterManager
Inside the AkkaWordCounter2.App/Actors/
folder, create a new file: WordCounterManager.cs
.
Then, enter the following code:
using System.Web;
namespace AkkaWordCounter2.App.Actors;
// Parent actor that manages the lifecycle of DocumentWordCounter actors
public sealed class WordCounterManager : ReceiveActor
{
public WordCounterManager()
{
Receive<IWithDocumentId>(s =>
{
string childName = $"word-counter-{HttpUtility.UrlEncode(s.DocumentId.ToString())}";
IActorRef child = Context.Child(childName);
if (child.IsNobody())
{
// start the child if it doesn't exist
child = Context.ActorOf(Props.Create(() => new DocumentWordCounter(s.DocumentId)), childName);
}
child.Forward(s);
});
}
}
ReceiveActor
s use strongly typed Receive<T>
and ReceiveAsync<T>
methods in order to process messages, rather than relying on switch
statements and expressions like the UntypedActor
.
In this particular case we’re making good on the “generic message routing” promise we made in “Behavior-Switching and Receive Timeouts” - the WordCounterManager
doesn’t need to receive every imaginable type of IWithDocumentId
message and handle those separately; it can just pattern match on the IWithDocumentId
interface directly.
Working with Child Actors
We’re using the “child-per-entity” pattern here to allocate a unique DocumentWordCounter
per AbsoluteUri
- this is an extremely common and good pattern to help make sure all data for a unique entity accumulates inside a single actor who represents it.
The key to making this work is naming all child actors after their respective entities - Akka.Cluster.Sharding does this automatically at large scale, for instance.
string childName = $"word-counter-{HttpUtility.UrlEncode(s.DocumentId.ToString())}";
IActorRef child = Context.Child(childName);
All actor names must be URI-friendly, or Akka.NET will throw an InvalidActorNameException
when calling ActorOf
!
For most simple strings they’re already URI-friendly but for things like URI’s themselves we’re going to call System.Web.HttpUtility.UrlEncode
to guarantee that this won’t be a problem.
Next, we’re going to use the WordCounterManager
’s Context.Child
method to see if we already have a child actor named word-counter-{Uri}
.
If a child actor exists, the returned IActorRef
is valid; otherwise, it will be ActorRefs.Nobody
. We use the IsNobody()
extension method to check for this, as it also handles things like null
checks for us:
if (child.IsNobody())
{
// start the child if it doesn't exist
child = Context.ActorOf(Props.Create(() => new DocumentWordCounter(s.DocumentId)), childName);
}
If IsNobody()
returns true
, no child exists with this name, so we create one using Context.ActorOf
.
This will create a new child actor named childName
- and if that actor is ever terminated at any point in the future its entry will be removed from the WordCounterManager
’s collection of children. Akka.NET guarantees that all ActorPaths within an ActorSystem are unique, ensuring no two child actors with the same name exist simultaneously under the same parent.
Wrapping Up
Now that we’ve implemented half of the actors we need to power AkkaWordCounter2
, it’s time we work on the remaining two actors and implement them. In the next lesson we’re going to learn how to work with async
and await
inside actor message processing routines.
Further Reading
-
As of Akka.NET v1.5, there is a slight performance advantage for
UntypedActor
s due to their ability to better leverage modern JIT techniques in .NET 7 and higher. However, we are working on addressing this in v1.6 as part of our AOT support initiative. ↩ ↩2