$npx -y skills add Aaronontheweb/dotnet-skills --skill akka-hosting-actor-patternsPatterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.
| 1 | # Akka.Hosting Actor Patterns |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Use this skill when: |
| 6 | - Building entity actors that represent domain objects (users, orders, invoices, etc.) |
| 7 | - Need actors that work in both unit tests (no clustering) and production (cluster sharding) |
| 8 | - Setting up scheduled tasks with akka-reminders |
| 9 | - Registering actors with Akka.Hosting extension methods |
| 10 | - Creating reusable actor configuration patterns |
| 11 | |
| 12 | ## Core Principles |
| 13 | |
| 14 | 1. **Execution Mode Abstraction** - Same actor code runs locally (tests) or clustered (production) |
| 15 | 2. **GenericChildPerEntityParent for Local** - Mimics sharding semantics without cluster overhead |
| 16 | 3. **Message Extractors for Routing** - Reuse Akka.Cluster.Sharding's IMessageExtractor interface |
| 17 | 4. **Akka.Hosting Extension Methods** - Fluent configuration that composes well |
| 18 | 5. **ITimeProvider for Testability** - Use ActorSystem.Scheduler instead of DateTime.Now |
| 19 | |
| 20 | ## Execution Modes |
| 21 | |
| 22 | Define an enum to control actor behavior: |
| 23 | |
| 24 | ```csharp |
| 25 | /// <summary> |
| 26 | /// Determines how Akka.NET should be configured |
| 27 | /// </summary> |
| 28 | public enum AkkaExecutionMode |
| 29 | { |
| 30 | /// <summary> |
| 31 | /// Pure local actor system - no remoting, no clustering. |
| 32 | /// Use GenericChildPerEntityParent instead of ShardRegion. |
| 33 | /// Ideal for unit tests and simple scenarios. |
| 34 | /// </summary> |
| 35 | LocalTest, |
| 36 | |
| 37 | /// <summary> |
| 38 | /// Full clustering with ShardRegion. |
| 39 | /// Use for integration testing and production. |
| 40 | /// </summary> |
| 41 | Clustered |
| 42 | } |
| 43 | ``` |
| 44 | |
| 45 | ## GenericChildPerEntityParent |
| 46 | |
| 47 | A lightweight parent actor that routes messages to child entities, mimicking cluster sharding semantics without requiring a cluster: |
| 48 | |
| 49 | ```csharp |
| 50 | using Akka.Actor; |
| 51 | using Akka.Cluster.Sharding; |
| 52 | |
| 53 | /// <summary> |
| 54 | /// A generic "child per entity" parent actor. |
| 55 | /// </summary> |
| 56 | /// <remarks> |
| 57 | /// Reuses Akka.Cluster.Sharding's IMessageExtractor for consistent routing. |
| 58 | /// Ideal for unit tests where clustering overhead is unnecessary. |
| 59 | /// </remarks> |
| 60 | public sealed class GenericChildPerEntityParent : ReceiveActor |
| 61 | { |
| 62 | public static Props CreateProps( |
| 63 | IMessageExtractor extractor, |
| 64 | Func<string, Props> propsFactory) |
| 65 | { |
| 66 | return Props.Create(() => |
| 67 | new GenericChildPerEntityParent(extractor, propsFactory)); |
| 68 | } |
| 69 | |
| 70 | private readonly IMessageExtractor _extractor; |
| 71 | private readonly Func<string, Props> _propsFactory; |
| 72 | |
| 73 | public GenericChildPerEntityParent( |
| 74 | IMessageExtractor extractor, |
| 75 | Func<string, Props> propsFactory) |
| 76 | { |
| 77 | _extractor = extractor; |
| 78 | _propsFactory = propsFactory; |
| 79 | |
| 80 | ReceiveAny(message => |
| 81 | { |
| 82 | var entityId = _extractor.EntityId(message); |
| 83 | if (entityId is null) return; |
| 84 | |
| 85 | // Get existing child or create new one |
| 86 | Context.Child(entityId) |
| 87 | .GetOrElse(() => Context.ActorOf(_propsFactory(entityId), entityId)) |
| 88 | .Forward(_extractor.EntityMessage(message)); |
| 89 | }); |
| 90 | } |
| 91 | } |
| 92 | ``` |
| 93 | |
| 94 | ## Message Extractors |
| 95 | |
| 96 | Create extractors that implement `IMessageExtractor` from Akka.Cluster.Sharding: |
| 97 | |
| 98 | ```csharp |
| 99 | using Akka.Cluster.Sharding; |
| 100 | |
| 101 | /// <summary> |
| 102 | /// Routes messages to entity actors based on a strongly-typed ID. |
| 103 | /// </summary> |
| 104 | public sealed class OrderMessageExtractor : HashCodeMessageExtractor |
| 105 | { |
| 106 | public const int DefaultShardCount = 40; |
| 107 | |
| 108 | public OrderMessageExtractor(int maxNumberOfShards = DefaultShardCount) |
| 109 | : base(maxNumberOfShards) |
| 110 | { |
| 111 | } |
| 112 | |
| 113 | public override string? EntityId(object message) |
| 114 | { |
| 115 | return message switch |
| 116 | { |
| 117 | IWithOrderId msg => msg.OrderId.Value.ToString(), |
| 118 | _ => null |
| 119 | }; |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // Define an interface for messages that target a specific entity |
| 124 | public interface IWithOrderId |
| 125 | { |
| 126 | OrderId OrderId { get; } |
| 127 | } |
| 128 | |
| 129 | // Use strongly-typed IDs |
| 130 | public readonly record struct OrderId(Guid Value) |
| 131 | { |
| 132 | public static OrderId New() => new(Guid.NewGuid()); |
| 133 | public override string ToString() => Value.ToString(); |
| 134 | } |
| 135 | ``` |
| 136 | |
| 137 | ## Akka.Hosting Extension Methods |
| 138 | |
| 139 | Create extension methods that abstract the execution mode: |
| 140 | |
| 141 | ```csharp |
| 142 | using Akka.Cluster.Hosting; |
| 143 | using Akka.Cluster.Sharding; |
| 144 | using Akka.Hosting; |
| 145 | |
| 146 | public static class OrderActorHostingExtensions |
| 147 | { |
| 148 | /// <summary> |
| 149 | /// Adds OrderActor with support for both local and clustered modes. |
| 150 | /// </summary> |
| 151 | public static AkkaConfigurationBuilder WithOrderActor( |
| 152 | this AkkaConfigurationBuilder builder, |
| 153 | AkkaExecutionMode executionMode = AkkaExecutionMode.Clustered, |
| 154 | string? clusterRole = null) |
| 155 | { |
| 156 | if (executionMode == AkkaExecutionMode.LocalTest) |
| 157 | { |
| 158 | // Non-clustered mode: Use GenericChildPerEntityParent |
| 159 | builder.WithActors((system, regist |