From bf2713d64c5b93244f8ba396111974f08d8ae462 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 27 Dec 2022 14:23:24 -0600 Subject: [PATCH] added documentation for DI features in Akka.Hosting (#171) * added documentation for DI features in Akka.Hosting documents https://github.com/akkadotnet/Akka.Hosting/pull/169 and https://github.com/akkadotnet/Akka.Hosting/pull/170 * addressed comments --- README.md | 98 ++++++++++++++++++- .../Akka.Hosting.SimpleDemo/Program.cs | 34 +++++-- .../Actors/Indexer.cs | 4 +- .../Akka.Hosting.SqlSharding/Program.cs | 6 +- 4 files changed, 127 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fad824d..6d87edd 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,16 @@ builder.Services.AddAkka("MyActorSystem", configurationBuilder => }) ``` -#### `ActorRegistry` +## Dependency Injection Outside and Inside Akka.NET + +One of the other design goals of Akka.Hosting is to make the dependency injection experience with Akka.NET as seamless as any other .NET technology. We accomplish this through two new APIs: + +* The `ActorRegistry`, a DI container that is designed to be populated with `Type`s for keys and `IActorRef`s for values, just like the `IServiceCollection` does for ASP.NET services. +* The `IRequiredActor` - you can place this type the constructor of any DI'd resource and it will automatically resolve a reference to the actor stored inside the `ActorRegistry` with `TKey`. This is how we inject actors into ASP.NET, SignalR, gRPC, and other Akka.NET actors! + +> **N.B.** The `ActorRegistry` and the `ActorSystem` are automatically registered with the `IServiceCollection` / `IServiceProvider` associated with your application. + +### Registering Actors with the `ActorRegistry` As part of Akka.Hosting, we need to provide a means of making it easy to pass around top-level `IActorRef`s via dependency injection both within the `ActorSystem` and outside of it. @@ -105,6 +114,93 @@ registry.TryRegister(indexer); // register for DI registry.Get(); // use in DI ``` +### Injecting Actors with `IRequiredActor` + +Suppose we have a class that depends on having a reference to a top-level actor, a router, a `ShardRegion`, or perhaps a `ClusterSingleton` (common types of actors that often interface with non-Akka.NET parts of a .NET application): + +```csharp +public sealed class MyConsumer +{ + private readonly IActorRef _actor; + + public MyConsumer(IRequiredActor actor) + { + _actor = actor.ActorRef; + } + + public async Task Say(string word) + { + return await _actor.Ask(word, TimeSpan.FromSeconds(3)); + } +} +``` + +The `IRequiredActor` will cause the Microsoft.Extensions.DependencyInjection mechanism to resolve `MyActorType` from the `ActorRegistry` and inject it into the `IRequired` instance passed into `MyConsumer`. + +The `IRequiredActor` exposes a single property: + +```csharp +public interface IRequiredActor +{ + /// + /// The underlying actor resolved via using the given key. + /// + IActorRef ActorRef { get; } +} +``` + +By default, you can automatically resolve any actors registered with the `ActorRegistry` without having to declare anything special on your `IServiceCollection`: + +```csharp +using var host = new HostBuilder() + .ConfigureServices(services => + { + services.AddAkka("MySys", (builder, provider) => + { + builder.WithActors((system, registry) => + { + var actor = system.ActorOf(Props.Create(() => new MyActorType()), "myactor"); + registry.Register(actor); + }); + }); + services.AddScoped(); + }) + .Build(); + await host.StartAsync(); +``` + +Adding your actor and your type key into the `ActorRegistry` is sufficient - no additional DI registration is required to access the `IRequiredActor` for that type. + +#### Resolving `IRequiredActor` within Akka.NET + +Akka.NET does not use dependency injection to start actors by default primarily because actor lifetime is unbounded by default - this means reasoning about the scope of injected dependencies isn't trivial. ASP.NET, by contrast, is trivial: all HTTP requests are request-scoped and all web socket connections are connection-scoped - these are objects have _bounded_ and typically short lifetimes. + +Therefore, users have to explicitly signal when they want to use Microsoft.Extensions.DependencyInjection via [the `IDependencyResolver` interface in Akka.DependencyInjection](https://getakka.net/articles/actors/dependency-injection.html) - which is easy to do in most of the Akka.Hosting APIs for starting actors: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddScoped(); +builder.Services.AddAkka("MyActorSystem", configurationBuilder => +{ + configurationBuilder + .WithRemoting(hostname: "localhost", port: 8110) + .WithClustering(new ClusterOptions{SeedNodes = new []{ "akka.tcp://MyActorSystem@localhost:8110", }}) + .WithShardRegion( + typeName: "myRegion", + entityPropsFactory: (_, _, resolver) => + { + // uses DI to inject `IReplyGenerator` into EchoActor + return s => resolver.Props(s); + }, + extractEntityId: ExtractEntityId, + extractShardId: ExtractShardId, + shardOptions: new ShardOptions()); +}); +``` + +The `dependencyResolver.Props()` call will leverage the `ActorSystem`'s built-in `IDependencyResolver` to instantiate the `MySingletonDiActor` and inject it with all of the necessary dependences, including `IRequiredActor`. + ## Microsoft.Extensions.Logging Integration __Logger Configuration Support__ diff --git a/src/Examples/Akka.Hosting.SimpleDemo/Program.cs b/src/Examples/Akka.Hosting.SimpleDemo/Program.cs index dee1a8c..85e25b0 100644 --- a/src/Examples/Akka.Hosting.SimpleDemo/Program.cs +++ b/src/Examples/Akka.Hosting.SimpleDemo/Program.cs @@ -5,14 +5,29 @@ namespace Akka.Hosting.SimpleDemo; +public interface IReplyGenerator +{ + string Reply(object input); +} + +public class DefaultReplyGenerator : IReplyGenerator +{ + public string Reply(object input) + { + return input.ToString()!; + } +} + public class EchoActor : ReceiveActor { private readonly string _entityId; - public EchoActor(string entityId) + private readonly IReplyGenerator _replyGenerator; + public EchoActor(string entityId, IReplyGenerator replyGenerator) { _entityId = entityId; + _replyGenerator = replyGenerator; ReceiveAny(message => { - Sender.Tell($"{Self} rcv {message}"); + Sender.Tell($"{Self} rcv {_replyGenerator.Reply(message)}"); }); } } @@ -32,14 +47,12 @@ public class Program string id => (id.GetHashCode() % NumberOfShards).ToString(), _ => null }; - - private static Props PropsFactory(string entityId) - => Props.Create(() => new EchoActor(entityId)); - + public static void Main(params string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.Services.AddScoped(); builder.Services.AddAkka("MyActorSystem", configurationBuilder => { configurationBuilder @@ -47,7 +60,10 @@ public static void Main(params string[] args) .WithClustering(new ClusterOptions{SeedNodes = new []{ "akka.tcp://MyActorSystem@localhost:8110", }}) .WithShardRegion( typeName: "myRegion", - entityPropsFactory: PropsFactory, + entityPropsFactory: (_, _, resolver) => + { + return s => resolver.Props(s); + }, extractEntityId: ExtractEntityId, extractShardId: ExtractShardId, shardOptions: new ShardOptions()); @@ -55,9 +71,9 @@ public static void Main(params string[] args) var app = builder.Build(); - app.MapGet("/", async (context) => + app.MapGet("/", async (HttpContext context, IRequiredActor echoActor) => { - var echo = context.RequestServices.GetRequiredService().Get(); + var echo = echoActor.ActorRef; var body = await echo.Ask( message: context.TraceIdentifier, cancellationToken: context.RequestAborted) diff --git a/src/Examples/Akka.Hosting.SqlSharding/Actors/Indexer.cs b/src/Examples/Akka.Hosting.SqlSharding/Actors/Indexer.cs index 74d5566..2ed6b45 100644 --- a/src/Examples/Akka.Hosting.SqlSharding/Actors/Indexer.cs +++ b/src/Examples/Akka.Hosting.SqlSharding/Actors/Indexer.cs @@ -17,9 +17,9 @@ public sealed class Indexer : ReceiveActor private Dictionary _users = new Dictionary(); - public Indexer(IActorRef userActionsShardRegion) + public Indexer(IRequiredActor userActionsShardRegion) { - _userActionsShardRegion = userActionsShardRegion; + _userActionsShardRegion = userActionsShardRegion.ActorRef; Receive(d => { diff --git a/src/Examples/Akka.Hosting.SqlSharding/Program.cs b/src/Examples/Akka.Hosting.SqlSharding/Program.cs index df8840c..b0992ce 100644 --- a/src/Examples/Akka.Hosting.SqlSharding/Program.cs +++ b/src/Examples/Akka.Hosting.SqlSharding/Program.cs @@ -23,10 +23,10 @@ .WithShardRegion("userActions", s => UserActionsEntity.Props(s), new UserMessageExtractor(), new ShardOptions(){ StateStoreMode = StateStoreMode.DData, Role = "myRole"}) - .WithActors((system, registry) => + .WithActors((system, registry, resolver) => { - var userActionsShard = registry.Get(); - var indexer = system.ActorOf(Props.Create(() => new Indexer(userActionsShard)), "index"); + var props = resolver.Props(); + var indexer = system.ActorOf(props, "index"); registry.TryRegister(indexer); // register for DI }); })