Skip to content

Commit

Permalink
added documentation for DI features in Akka.Hosting (#171)
Browse files Browse the repository at this point in the history
* added documentation for DI features in Akka.Hosting

documents #169 and #170

* addressed comments
  • Loading branch information
Aaronontheweb authored Dec 27, 2022
1 parent aff4b76 commit bf2713d
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 15 deletions.
98 changes: 97 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TKey>` - 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.

Expand All @@ -105,6 +114,93 @@ registry.TryRegister<Index>(indexer); // register for DI
registry.Get<Index>(); // use in DI
```

### Injecting Actors with `IRequiredActor<TKey>`

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<MyActorType> actor)
{
_actor = actor.ActorRef;
}

public async Task<string> Say(string word)
{
return await _actor.Ask<string>(word, TimeSpan.FromSeconds(3));
}
}
```

The `IRequiredActor<MyActorType>` will cause the Microsoft.Extensions.DependencyInjection mechanism to resolve `MyActorType` from the `ActorRegistry` and inject it into the `IRequired<Actor<MyActorType>` instance passed into `MyConsumer`.

The `IRequiredActor<TActor>` exposes a single property:

```csharp
public interface IRequiredActor<TActor>
{
/// <summary>
/// The underlying actor resolved via <see cref="ActorRegistry"/> using the given <see cref="TActor"/> key.
/// </summary>
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<MyActorType>(actor);
});
});
services.AddScoped<MyConsumer>();
})
.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<TActor>` for that type.

#### Resolving `IRequiredActor<TKey>` 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<IReplyGenerator, DefaultReplyGenerator>();
builder.Services.AddAkka("MyActorSystem", configurationBuilder =>
{
configurationBuilder
.WithRemoting(hostname: "localhost", port: 8110)
.WithClustering(new ClusterOptions{SeedNodes = new []{ "akka.tcp://MyActorSystem@localhost:8110", }})
.WithShardRegion<Echo>(
typeName: "myRegion",
entityPropsFactory: (_, _, resolver) =>
{
// uses DI to inject `IReplyGenerator` into EchoActor
return s => resolver.Props<EchoActor>(s);
},
extractEntityId: ExtractEntityId,
extractShardId: ExtractShardId,
shardOptions: new ShardOptions());
});
```

The `dependencyResolver.Props<MySingletonDiActor>()` call will leverage the `ActorSystem`'s built-in `IDependencyResolver` to instantiate the `MySingletonDiActor` and inject it with all of the necessary dependences, including `IRequiredActor<TKey>`.

## Microsoft.Extensions.Logging Integration

__Logger Configuration Support__
Expand Down
34 changes: 25 additions & 9 deletions src/Examples/Akka.Hosting.SimpleDemo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)}");
});
}
}
Expand All @@ -32,32 +47,33 @@ 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<IReplyGenerator, DefaultReplyGenerator>();
builder.Services.AddAkka("MyActorSystem", configurationBuilder =>
{
configurationBuilder
.WithRemoting(hostname: "localhost", port: 8110)
.WithClustering(new ClusterOptions{SeedNodes = new []{ "akka.tcp://MyActorSystem@localhost:8110", }})
.WithShardRegion<Echo>(
typeName: "myRegion",
entityPropsFactory: PropsFactory,
entityPropsFactory: (_, _, resolver) =>
{
return s => resolver.Props<EchoActor>(s);
},
extractEntityId: ExtractEntityId,
extractShardId: ExtractShardId,
shardOptions: new ShardOptions());
});

var app = builder.Build();

app.MapGet("/", async (context) =>
app.MapGet("/", async (HttpContext context, IRequiredActor<Echo> echoActor) =>
{
var echo = context.RequestServices.GetRequiredService<ActorRegistry>().Get<Echo>();
var echo = echoActor.ActorRef;
var body = await echo.Ask<string>(
message: context.TraceIdentifier,
cancellationToken: context.RequestAborted)
Expand Down
4 changes: 2 additions & 2 deletions src/Examples/Akka.Hosting.SqlSharding/Actors/Indexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public sealed class Indexer : ReceiveActor

private Dictionary<string, UserDescriptor> _users = new Dictionary<string, UserDescriptor>();

public Indexer(IActorRef userActionsShardRegion)
public Indexer(IRequiredActor<UserActionsEntity> userActionsShardRegion)
{
_userActionsShardRegion = userActionsShardRegion;
_userActionsShardRegion = userActionsShardRegion.ActorRef;

Receive<UserDescriptor>(d =>
{
Expand Down
6 changes: 3 additions & 3 deletions src/Examples/Akka.Hosting.SqlSharding/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
.WithShardRegion<UserActionsEntity>("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<UserActionsEntity>();
var indexer = system.ActorOf(Props.Create(() => new Indexer(userActionsShard)), "index");
var props = resolver.Props<Indexer>();
var indexer = system.ActorOf(props, "index");
registry.TryRegister<Index>(indexer); // register for DI
});
})
Expand Down

0 comments on commit bf2713d

Please sign in to comment.