Practical ASP.NET

Using Predicates in Akka.NET Receive Actors

Create precise logic dictating which messages an actor will handle and how they will react to them.

When using Akka.NET receive actors, one advanced feature is the ability to specify predicates to further refine whether an incoming message will be handled or not. A predicate is simply a function which takes the message, performs some logic, and returns a Boolean that determines if the message will be handled. Using receive predicates allow us to not only determine if a message will be handled but also can allow us to replace if/switch statements in our actor code.

Example Sans Receive Predicates
As an example, suppose we have an actor that represents a player in a video game. We can send this PlayerCharacterActor a TakeDamageMessage to simulate the player being attacked.

To start, Listing 1 shows an enumeration to decide on what type of damage is being taken and a message stating how much damage is being inflicted.

Listing 1: Defining TakeDamageMessage

internal enum DamageKind
{
   Fire,
   Magic,
Sword
}

internal class TakeDamageMessage
{
   public DamageKind DamageKind { get; private set; }
   public int Damage { get; private set; }

   public TakeDamageMessage(DamageKind damageKind, int damage)
   {
      DamageKind = damageKind;
      Damage = damage;
   }
}

We can create a simple console application that will create an actor system and an instance of a PlayerCharacterActor and send it several TakeDamageMessages as Listing 2 shows.

Listing 2: Console Application Sending TakeDamageMessages

class Program
{
   static void Main(string[] args)
   {
      var actorSystem = ActorSystem.Create("GameSystem");

      IActorRef actor = actorSystem.ActorOf<PlayerCharacterActor>();

      // Player is alive - send an "unhandled" message
      actor.Tell(new TakeDamageMessage(DamageKind.Sword, 200));

      actor.Tell(new TakeDamageMessage(DamageKind.Fire, 200));

      actor.Tell(new TakeDamageMessage(DamageKind.Magic, 99));

      actor.Tell(new TakeDamageMessage(DamageKind.Magic, 2000));

      // Player is now dead
      actor.Tell(new TakeDamageMessage(DamageKind.Fire, 250));
      // DamageKind.Sword will now be handled as health < 1
      actor.Tell(new TakeDamageMessage(DamageKind.Sword, 200));

      Console.ReadLine();            
   }
}

We can now define a receive actor to receive these messages. The PlayerCharacterActor is able to resist different types of damage by varying amounts. The actor will handle magic and fire damage messages but will be unable to handle sword damage if the player is alive. Sending a DamageKind.Sword message while the player is alive will result in an unhandled message. When dead we allow the handling of any kind of damage and simply write "Can't take any more damage when dead" to the console.

Listing 3 shows a version of the PlayerCharacterActor that uses a switch statement to evaluate the damage kind.

Listing 3: PlayerCharacterActor Without Receive Predicates

class PlayerCharacterActor : ReceiveActor
{
   private int _health;
   private readonly int _fireResistance;
   private readonly int _magicResistance;

   public PlayerCharacterActor()
   {
      _health = 1000;
      _fireResistance = 100;
      _magicResistance = 100;

      Console.WriteLine("Starting Health {0}", _health);

      Receive<TakeDamageMessage>(message => TakeDamage(message));
   }

   private void TakeDamage(TakeDamageMessage message)
   {
      if (_health < 1)
      {
         Console.WriteLine("Can't take any more damage when dead");
         return;
      }

      switch (message.DamageKind)
      {
         case DamageKind.Fire:
            var modifiedFireDamage = 
               Math.Max(message.Damage - _fireResistance, 0);
            _health -= modifiedFireDamage;
            Console.WriteLine("Taking {0} points of {1} damage",
               modifiedFireDamage, message.DamageKind);
            break;
         case DamageKind.Magic:
            var modifiedMagicDamage = 
               Math.Max(message.Damage - _magicResistance, 0);
            _health -= modifiedMagicDamage;
            Console.WriteLine("Taking {0} points of {1} damage", 
               modifiedMagicDamage, message.DamageKind);
            break;
         default:
            Unhandled(message);
            break;
      }

      if (_health < 1)
      {
         Console.WriteLine("Player is dead");
      }
      }
} 

The actor in Listing 3 will handle TakeDamageMessages unless we send a TakeDamageMessage with DamageKind.Sword, in this case we manually instruct Akka.NET that the message was unhandled as DamageKind.Sword does not have a matching switch label.

Refactoring To Use Receive Predicates
There are a number of overloads of the Receive() method. Two of these overloads allow a predicate to be supplied. We can either specify the predicate before the message handling action, or after as a matter of personal choice/style. Listing 4 shows the switch statement replaced with Receive methods that use predicates to determine if the message will be handled.

Listing 4: PlayerCharacterActor2 Using Receive Predicates

class PlayerCharacterActor2 : ReceiveActor
{
   private int _health;
   private readonly int _fireResistance;
   private readonly int _magicResistance;

   public PlayerCharacterActor2()
   {
      _health = 1000;
      _fireResistance = 100;
      _magicResistance = 100;

      Console.WriteLine("Starting Health {0}", _health);

      Receive<TakeDamageMessage>(
         handleIf => _health < 1,
         message =>
         {
            Console.WriteLine("Can't take any more damage when dead");
         });

      Receive<TakeDamageMessage>(
         handleIf => handleIf.DamageKind == DamageKind.Fire,
         message =>
         {
            var modifiedFireDamage = 
               Math.Max(message.Damage - _fireResistance, 0);
            _health -= modifiedFireDamage;
            Console.WriteLine("Taking {0} points of {1} damage", 
               modifiedFireDamage, message.DamageKind);
            AmIDead();
         });

      Receive<TakeDamageMessage>(
         handleIf => handleIf.DamageKind == DamageKind.Magic,
         message =>
         {
            var modifiedMagicDamage = 
               Math.Max(message.Damage - _magicResistance, 0);
            _health -= modifiedMagicDamage;
            Console.WriteLine("Taking {0} points of {1} damage", 
               modifiedMagicDamage, message.DamageKind);
            AmIDead();
         });

      // Don't have to manually call Unhandled() as if no 
      // receive method matches (predicates) then message is automatically unhandled
      }

      private void AmIDead()
      {
      if (_health < 1)
      {
         Console.WriteLine("Player is dead");
      }
   }
}

Combining Receive Predicates with Switchable Actor Behaviours As a final step, we could remove the receive predicate handleIf => _health < 1 and instead model the fact that the player can be alive or dead using switchable behaviors as Listing 5 shows.

Listing 5: Combining Receive Predicates with Switchable Actor Behavior

class PlayerCharacterActor3 : ReceiveActor
{
   private int _health;
   private readonly int _fireResistance;
   private readonly int _magicResistance;

   public PlayerCharacterActor3()
   {
      _health = 1000;
      _fireResistance = 100;
      _magicResistance = 100;

      Console.WriteLine("Starting Health {0}", _health);

      Alive();
   }

   private void Alive()
   {
      Receive<TakeDamageMessage>(
         handleIf => handleIf.DamageKind == DamageKind.Fire,
         message =>
         {
            var modifiedFireDamage = 
               Math.Max(message.Damage - _fireResistance, 0);
            _health -= modifiedFireDamage;
            Console.WriteLine("Taking {0} points of {1} damage", 
               modifiedFireDamage, message.DamageKind);
            AmIDead();
         });

      Receive<TakeDamageMessage>(
         handleIf => handleIf.DamageKind == DamageKind.Magic,
         message =>
         {
            var modifiedMagicDamage = 
               Math.Max(message.Damage - _magicResistance, 0);
            _health -= modifiedMagicDamage;
            Console.WriteLine("Taking {0} points of {1} damage", 
               modifiedMagicDamage, message.DamageKind);
            AmIDead();
         });
   }

   private void Dead()
   {
      Receive<TakeDamageMessage>(
         message =>
         {
            Console.WriteLine("Can't take any more damage when dead");
         });
   }

   private void AmIDead()
   {
      if (_health < 1)
      {
         Console.WriteLine("Player is dead");
         Become(Dead);
      }
   }
}

That's it for now. It's time to play.

About the Author

Jason Roberts is a Microsoft C# MVP with over 15 years experience. He writes a blog at http://dontcodetired.com, has produced numerous Pluralsight courses, and can be found on Twitter as @robertsjason.

comments powered by Disqus

Featured

  • Hands On: New VS Code Insiders Build Creates Web Page from Image in Seconds

    New Vision support with GitHub Copilot in the latest Visual Studio Code Insiders build takes a user-supplied mockup image and creates a web page from it in seconds, handling all the HTML and CSS.

  • Naive Bayes Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the naive Bayes regression technique, where the goal is to predict a single numeric value. Compared to other machine learning regression techniques, naive Bayes regression is usually less accurate, but is simple, easy to implement and customize, works on both large and small datasets, is highly interpretable, and doesn't require tuning any hyperparameters.

  • VS Code Copilot Previews New GPT-4o AI Code Completion Model

    The 4o upgrade includes additional training on more than 275,000 high-quality public repositories in over 30 popular programming languages, said Microsoft-owned GitHub, which created the original "AI pair programmer" years ago.

  • Microsoft's Rust Embrace Continues with Azure SDK Beta

    "Rust's strong type system and ownership model help prevent common programming errors such as null pointer dereferencing and buffer overflows, leading to more secure and stable code."

  • Xcode IDE from Microsoft Archrival Apple Gets Copilot AI

    Just after expanding the reach of its Copilot AI coding assistant to the open-source Eclipse IDE, Microsoft showcased how it's going even further, providing details about a preview version for the Xcode IDE from archrival Apple.

Subscribe on YouTube

Upcoming Training Events