Migrating to Akka.Hosting can introduce several challenges, particularly when dealing with child actors and their dependencies in a Dependency Injection (DI) environment. This article explores multiple approaches to managing child actors within Akka.NET including leveraging Akka’s DependencyResolver to simplify actor creation.
From manually passing dependencies and creating child actors within your main actor to utilizing IServiceProvider or IServiceScopeFactory for managing scoped dependencies, each method comes with its own trade-offs. We'll also demonstrate how the DependencyResolver can streamline the process, helping to decouple actors from their dependencies and improving maintainability.
By comparing these various techniques, you'll be able to choose the best approach for your use case, whether you're looking to minimize code complexity, enhance testability, or maintain more granular control over your actor system.
First, ensure you have the necessary dependencies in your project. You will need the Akka.Hosting and Akka.DependencyInjection package, which can be added via NuGet:
dotnet add package Akka.Hosting
dotnet add package Akka.DependencyInjectionNext, set up your ActorSystem in the Program.cs of your application:
builder.Services.AddAkka("MessageProcessor",(configurationBuilder)=>{configurationBuilder.WithActors((system,registry,resolver)=>{//register actors here});});Once your ActorSystem is set up, you can set up your main actor that will be responsible for creating child actors later:
- Define Your Main Actor: Create a main actor that will handle messages and create child actors as needed. Here is an example of a main actor with message handling:
usingAkka.Actor;usingMessageProcessor.Models;namespaceMessageProcessor.Actors;publicclassMainActor:ReceiveActor{publicMainActor(){Receive<LowWeatherForecast>(forecast =>{Console.WriteLine($"Received a low weather forecast with temperature {forecast.ForecastType}");});Receive<MediumWeatherForecast>(forecast =>{Console.WriteLine($"Received a medium weather forecast with temperature {forecast.ForecastType}");});Receive<HighWeatherForecast>(forecast =>{Console.WriteLine($"Received a High weather forecast with temperature {forecast.ForecastType}");});}}- Register the Actor: Use the Akka.Hosting API to register your actor in your
Program.cs.
varbuilder=WebApplication.CreateBuilder(args);builder.Services.AddAkka("MessageProcessor",(configurationBuilder)=>{configurationBuilder.WithActors((system,registry,resolver)=>{//propsvarmainActorProps=resolver.Props<MainActor>().WithSupervisorStrategy(SupervisorStrategy.DefaultStrategy);//instancevarmainActor=system.ActorOf(mainActorProps,nameof(MainActor));//registryregistry.Register<MainActor>(mainActor);});});varapp=builder.Build();3.Refactor MainActor into Constituent Child Actors
publicclassHighWeatherForecastActor:ReceiveActor{publicHighWeatherForecastActor(){Receive<HighWeatherForecast>(forecast =>{Console.WriteLine($"Received a high weather forecast with temperature {forecast.ForecastType}");});}}publicclassLowWeatherForecastActor:ReceiveActor{publicLowWeatherForecastActor(WeatherServiceweatherService){Receive<LowWeatherForecast>(forecast =>{Console.WriteLine($"Received a low weather forecast with temperature {forecast.ForecastType}");Console.WriteLine($"The weather is {weatherService.GetWeather()}");});}}publicclassMediumWeatherForecastActor:ReceiveActor{publicMediumWeatherForecastActor(){Receive<MediumWeatherForecast>(forecast =>{Console.WriteLine($"Received a medium weather forecast with temperature {forecast.ForecastType}");});}}Without the DependencyResolver,we could do a number of things to create child actors in the main actor.
- One of the ways is to manually create the child actors and forward messages to them. Since one of our Child Actors has a dependency, we will need to pass the dependency to the child actor's constructor; but that also means that the main actor will need to have access to the dependency. This is not ideal as it couples the main actor to the dependency, making it harder to test and maintain. Below is an example :
usingAkka.Actor;usingMessageProcessor.Models;usingMessageProcessor.Services;namespaceMessageProcessor.Actors{publicclassMainActor:ReceiveActor{//main actor takes a dependency on the external servicepublicMainActor(WeatherServiceweatherService){// Create child actors with their dependenciesvarlowWeatherForecastActor=Context.ActorOf(Props.Create(()=>newLowWeatherForecastActor(weatherService)),nameof(LowWeatherForecastActor));varmediumWeatherForecastActor=Context.ActorOf(Props.Create(()=>newMediumWeatherForecastActor()),nameof(MediumWeatherForecastActor));varhighWeatherForecastActor=Context.ActorOf(Props.Create(()=>newHighWeatherForecastActor()),nameof(HighWeatherForecastActor));// Define message handlingReceive<LowWeatherForecast>(forecast =>{lowWeatherForecastActor.Forward(forecast);});Receive<MediumWeatherForecast>(forecast =>{mediumWeatherForecastActor.Forward(forecast);});Receive<HighWeatherForecast>(forecast =>{highWeatherForecastActor.Forward(forecast);});}}}For the parameterless child actors, you could use the generic Props.Create method to create the child actors like so:
varmediumWeatherForecastActor=Context.ActorOf(Props.Create<MediumWeatherForecastActor>(),nameof(MediumWeatherForecastActor));Afterthis the main actor can be registered in the `Program.cs` as shown below:
```csharp
varbuilder=WebApplication.CreateBuilder(args);// add transient or singleton services for actor dependencies.//scoped usually causes`TypeLoadException`builder.Services.AddSingleton<WeatherService>();// Configure Akka.NET ActorSystembuilder.Services.AddAkka("MessageProcessor",(configurationBuilder,provider)=>{configurationBuilder.WithActors((system,registry)=>{varweatherService=provider.GetRequiredService<WeatherService>();varmainActorProps=Props.Create(()=>newMainActor(weatherService));varmainActor=system.ActorOf(mainActorProps,nameof(MainActor));registry.Register<MainActor>(mainActor);});});varapp=builder.Build();app.Run();It is immediately visible how much more we needed to write to manage one dependency for one child actor. Imagine if this actor had five more and the other child actors, also had two each; this could immediately evolve into a very hard to manage mess, causing readability to suffer.
2. IServiceScopeFactory, IServiceProvider to the rescue.
A very useful middle ground in manually creating child actors is to take a dependency only on the IServiceProvider or the IServiceScopeFactory then creating your own scope and resolving all required dependencies yourself. This way, the main actor is not coupled to the dependency, and the child actors can be created with their dependencies.
Also, the IServiceProvider or IServiceScopeFactory can be directly injected into the main actor and passed to the child actors without any need to configure additional dependencies(while registering the main actor). It is also important to note that the IServiceProvider and IServiceScopeFactory allow you to create your own scope, making it possible to resolve and use scoped dependencies without any issues.
Our actors and configurations should look now like this:
//configurationvarbuilder=WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddScoped<WeatherService>();builder.Services.AddAkka("MessageProcessor",(configurationBuilder)=>{configurationBuilder.WithActors((system,registry,resolver)=>{//propsvarmainActorProps=resolver.Props<MainActor>().WithSupervisorStrategy(SupervisorStrategy.DefaultStrategy);//instancevarmainActor=system.ActorOf(mainActorProps,nameof(MainActor));//registryregistry.Register<MainActor>(mainActor);//nothing changes in the actor registration});});varapp=builder.Build();//main actorpublicclassMainActor:ReceiveActor{//main actor takes a dependency on the IServiceScopeFactorypublicMainActor(IServiceScopeFactoryserviceScopeFactory){varlowWeatherForecastActor=Context.ActorOf(Props.Create(()=>newLowWeatherForecastActor(serviceScopeFactory)),nameof(LowWeatherForecastActor));//Or//var lowWeatherForecastActor = Context.ActorOf(Props.Create <LowWeatherForecastActor >(serviceScopeFactory),nameof(LowWeatherForecastActor));//which I consider cleaner. plust it avoids the use of the 'new' keyword.varmediumWeatherForecastActor=Context.ActorOf(Props.Create<MediumWeatherForecastActor>(),nameof(MediumWeatherForecastActor));varhighWeatherForecastActor=Context.ActorOf(Props.Create<HighWeatherForecastActor>(),nameof(HighWeatherForecastActor));Receive<LowWeatherForecast>(forecast =>{lowWeatherForecastActor.Forward(forecast);});}}//child actor with dependencypublicclassLowWeatherForecastActor:ReceiveActor{publicLowWeatherForecastActor(IServiceScopeFactoryserviceScopeFactory){usingvarscope=serviceScopeFactory.CreateScope();varweatherService=scope.ServiceProvider.GetRequiredService<WeatherService>();Receive<LowWeatherForecast>(forecast =>{Console.WriteLine($"Received a low weather forecast with temperature {forecast.ForecastType}");Console.WriteLine($"The weather is {weatherService.GetWeather()}");});}}- Recreate the Child Actors: In your main actor using the
DependencyResolver.For. We refactor the same Main Actor to swap out the various manual initialisations in favour of the dependency resolver.
usingAkka.Actor;usingAkka.DependencyInjection;usingMessageProcessor.Models;namespaceMessageProcessor.Actors;publicclassMainActor:ReceiveActor{publicMainActor(){varresolver=DependencyResolver.For(Context.System);varlowWeatherForecastActor=Context.ActorOf(resolver.Props<LowWeatherForecastActor>(),nameof(LowWeatherForecastActor));varmediumWeatherForecastActor=Context.ActorOf(resolver.Props<MediumWeatherForecastActor>(),nameof(MediumWeatherForecastActor));varhighWeatherForecastActor=Context.ActorOf(resolver.Props<HighWeatherForecastActor>(),nameof(HighWeatherForecastActor));Receive<LowWeatherForecast>(forecast =>{lowWeatherForecastActor.Forward(forecast);});Receive<MediumWeatherForecast>(forecast =>{mediumWeatherForecastActor.Forward(forecast);});Receive<HighWeatherForecast>(forecast =>{highWeatherForecastActor.Forward(forecast);});}}Actors in Akka are designed to be long-lived and can be thought of as "singletons" within their context. This characteristic has implications for how dependencies are managed, particularly when dealing with scoped dependencies. Scoped dependencies are typically created per request or per operation, which contrasts with the long-lived nature of actors.
When injecting scoped dependencies into actors that use the DependencyResolver, it important to note that scoped dependencies will throw a TypeLoadException. Registering dependencies as singletons or transient however work. Depending on your use case, this may or may not pose challenges to other parts of your system and should be considered carefully.
I suggest however that you take dependencies on IServiceScopeFactory,or IServiceProvider instead to avoid any issues in this regard.
varbuilder=WebApplication.CreateBuilder(args);// add transient or singleton services for actor dependencies.//scoped usually causes`TypeLoadException`//or use IServiceScopeFactory,IserviceProviderbuilder.Services.AddTransient<WeatherService>();varapp=builder.Build();//directly consume dependency in child actorpublicclassLowWeatherForecastActor:ReceiveActor{publicLowWeatherForecastActor(WeatherServiceweatherService){Receive<LowWeatherForecast>(forecast =>{Console.WriteLine($"Received a low weather forecast with temperature {forecast.ForecastType}");Console.WriteLine($"The weather is {weatherService.GetWeather()}");});}}//OR depend on IServiceScopeFactorypublicclassLowWeatherForecastActor:ReceiveActor{publicLowWeatherForecastActor(IServiceScopeFactoryserviceScopeFactory){usingvarscope=serviceScopeFactory.CreateScope();varweatherService=scope.ServiceProvider.GetRequiredService<WeatherService>();Receive<LowWeatherForecast>(forecast =>{Console.WriteLine($"Received a low weather forecast with temperature {forecast.ForecastType}");Console.WriteLine($"The weather is {weatherService.GetWeather()}");});}}By utilizing the DependencyResolver in Akka.DependencyInjection, you can effectively manage child actors and their dependencies, simplifying the migration process.
You can also redesign your actors to only depend on the service provider to help reduce the number of dependencies injected during manual child actor creation.
These approaches not only enhance the maintainability of your code but also aligns with modern dependency injection practices while creating child actors.
With the steps outlined above, you can escape the complexities of child actor management and focus on building robust Akka applications.