The short version of this issue:
Now that ActorSystemSetup is available, it allows us to easily pass in external code dependencies into an ActorSystem in an immutable way before it gets instantiated. It should now be possible to accomplish the following:
Microsoft.Extenions.*, which resolves one of my bugaboos for years; andActorSystem to grab ahold of the current DI container without end-users needing to manually pass it around anyway.I want to revisit the trail that @Horusiath blazed with https://github.com/akkadotnet/akka.net/pull/3862 - never felt comfortable changing the default pipeline to use Microsoft.Extensions.DependencyIjnection directly (QA surface area is enormous) but making it possible to opt-into using the pipeline using an updated Setup introduced by Akka.DI.Common should take the risk factor down significantly.
Going to open this issue to cover the architectural bits, user requests, and comments from contributors.
Should also help resolve https://github.com/akkadotnet/akka.net/issues/1675, https://github.com/akkadotnet/akka.net/issues/3050, and https://github.com/akkadotnet/akka.net/issues/1267
Wonderful, I can't waiting for. 馃憤
My team is going to re-design software using "Actor model" after to release Akka.DI.Common.
Regards.
Okay, so I'm experimenting with dependency injection and would really like to bootstrap my actors with proper constructor argument injection. As I'm also passing literals to my child actors, such as IActorRefs, strings, etc. the standard injection is not feasible. So I came up with the following idea:
The constructor of my actor looks like this:
```c#
public MyActor(IActorRef other, [AkkaInject]IOptions
{
// more code
}
Note the AkkaInject attribute.
The parent actor initializes the actor in the following way:
```c#
private void Initialize()
{
var props = Context.DI2(() => new MyActor(Self, null));
_actorRef= Context.ActorOf(props, "MyActor");
}
The magic starts in _DI2()_:
```c#
public static Props DI2(this IActorContext context, Expression
{
var newExpression = factory.Body.AsInstanceOf
if (newExpression == null)
throw new ArgumentException("The create function must be a 'new T (args)' expression");
var arguments = newExpression.GetArguments().ToArray();
return Props.Create(typeof(InjectedActorProvider), newExpression.Constructor, arguments);
}
Here I create a Props instance with the a IIndirectActorProducer actor type of InjectedActorProvider. I also pass in the constructor and the original arguments. I need the constructor since I still need to inspect the reflection of the argument types.
Alternatively I can do the reflection here and collect an array of types and arguments to be passed to the InjectedActorProvider.
Finally, the InjectedActorProvider can now create a new instance of the actual actor by invoking the constructor with the original arguments. The arguments that were passed in as null and have the AkkaInject attribute are resolved using the service provider:
```c#
public class InjectedActorProvider : IIndirectActorProducer
{
public static IServiceProvider Provider;
private readonly Type _baseActor;
private readonly ConstructorInfo _constructor;
private readonly object[] _arguments;
public Type ActorType => _baseActor;
public InjectedActorProvider(ConstructorInfo constructor, object[] arguments)
{
this._baseActor = constructor.DeclaringType;
this._constructor = constructor;
this._arguments = arguments;
}
public ActorBase Produce()
{
var parameters = _constructor.GetParameters();
for (var i = 0; i < parameters.Length; ++i)
{
if (_arguments[i] is null && parameters[i].GetCustomAttributes().Any(a => a.GetType().Name.Contains("AkkaInject", StringComparison.CurrentCultureIgnoreCase)))
{
var value = Provider.GetService(parameters[i].ParameterType);
_arguments[i] = value;
}
}
return _constructor.Invoke(_arguments) as ActorBase;
}
public void Release(ActorBase actor)
{
//@tbd
}
}
I know this is still a quick and dirty implementation and I should retrieve my services from a correct place. I'm sure that with some modification child scopes should also be handled correctly. The GetCustomAttributes could be the specific AkkaInjectAttribute, however I don't want my actor library to be depending on the library that handles the DI.
I would love to get some feedback from the community.
I'm a great fan of Akka.net and really enjoyed the Petabridge course last year.
Regards,
Kevin
@kevinvanblokland I think this is a great start - I'm hoping we can come up with a solution that can avoid users having to do this:
Context.DI2
But I have a strong feeling that remaining backwards compatible for all of the cases that the current ActorOf implementation currently covers using a more "implicit" approach will be very difficult. Still, it's worth going for.
Your implementation idea is great this is definitely an improvement beyond what we have right now.
Let me emphasize that last part more strongly - your approach is an _amazing_ start to this @kevinvanblokland
Microsoft.Extensions.DependencyInjection uses scopes to host a logical context.
To have a single scope for the whole actor system will not end well.
An actor would need to get the scope from its nearest parent with one and this is not always/never possible in actor-model.
The other option is to handle the scope in user space
but then the scope needs to be always provided to the factory method.
The class Microsoft.Extensions.DependencyInjection.ActivatorUtilities has methods to already create instance of objects.
I fixed it with a simple extension to the the Root ServiceProvider and then create and manage the scope in actor user-space
public class SampleActor : ActorBase, IWithUnboundedStash
{
public IStash Stash { get; set; }
readonly IServiceScope _scope = Context.CreateServiceScope();
readonly CancellationTokenSource _cts = new CancellationTokenSource();
readonly MySettings _settings;
MyClient _client;
public VipIbcUserWebClientActor(MySettings settings)
{
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
public static Props GetProps(MySettings settings)
{
return Props.Create(() => new SampleActor(settings));
}
protected override void PreStart()
{
BecomeIdle();
}
protected override void PostStop()
{
_cts.Cancel();
_scope.Dispose();
}
void BecomeIdle()
{
bool Idle(object message)
{
switch (message)
{
case Commands.Connect cmd:
BecomeConnecting();
return true;
case Commands.Disconnect _:
return true;
default:
return false;
}
}
Become(Idle, null);
Stash.UnstashAll();
_client = null;
}
void BecomeConnecting()
{
if (string.IsNullOrEmpty(_settings.Password))
{
Log.Error("Password not set for user {UserName}", _settings.UserName);
BecomeIdle();
return;
}
bool Connecting(object message)
{
switch (message)
{
case Status.Success { Status: "connect" }:
BecomeProcessing();
return true;
case Status.Failure m:
Log.Error(m.Cause, "User[{UserName}] connecting error", _settings.UserName);
BecomeIdle();
return true;
default:
Stash.Stash();
return true;
}
}
Become(Connecting, null);
_client = _scope.ServiceProvider.GetRequiredService<MyWebClient>();
_client.SomeValue = _settings.SomeValue;
_client.LoginAsync(_settings.UserName, _settings.Password, _cts.Token)
.PipeTo(Self, null,
() => new Status.Success("connect"),
ex => new Status.Failure(ex));
}
//other stuff
}
public sealed class PNetServiceExtension : IExtension
{
public IServiceProvider Services { get; private set; }
public PNetServiceExtension(IServiceProvider services)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
}
public static PNetServiceExtension Get(ActorSystem system)
{
return system.GetExtension<PNetServiceExtension>();
}
}
public sealed class PNetServiceExtensionIdProvider : ExtensionIdProvider<PNetServiceExtension>
{
public IServiceProvider Services { get; private set; }
public PNetServiceExtensionIdProvider(IServiceProvider services)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
}
public override PNetServiceExtension CreateExtension(ExtendedActorSystem system)
{
return new PNetServiceExtension(Services);
}
}
public static class PNetServiceExtensions
{
public static IServiceProvider GetServiceProvider(this IActorContext context)
{
return PNetServiceExtension.Get(context.System).Services;
}
public static IServiceProvider GetServiceProvider(this ActorSystem system)
{
return PNetServiceExtension.Get(system).Services;
}
public static T GetRequiredService<T>(this IActorContext context)
{
return context.GetServiceProvider().GetRequiredService<T>();
}
public static T GetRequiredService<T>(this ActorSystem system)
{
return system.GetServiceProvider().GetRequiredService<T>();
}
public static T GetService<T>(this IActorContext context)
{
return context.GetServiceProvider().GetService<T>();
}
public static T GetService<T>(this ActorSystem system)
{
return system.GetServiceProvider().GetService<T>();
}
public static IEnumerable<T> GetServices<T>(this IActorContext context)
{
return context.GetServiceProvider().GetServices<T>();
}
public static IEnumerable<T> GetServices<T>(this ActorSystem system)
{
return system.GetServiceProvider().GetServices<T>();
}
public static IServiceScope CreateServiceScope(this IActorContext context)
{
return context.GetServiceProvider().CreateScope();
}
public static IServiceScope CreateServiceScope(this ActorSystem system)
{
return system.GetServiceProvider().CreateScope();
}
public static T CreateInstance<T>(this IActorContext context, params object[] parameters)
{
return ActivatorUtilities.CreateInstance<T>(context.GetServiceProvider(), parameters);
}
public static object CreateInstance(this IActorContext context, Type instanceType, params object[] parameters)
{
return ActivatorUtilities.CreateInstance(context.GetServiceProvider(), instanceType, parameters);
}
public static T CreateInstance<T>(this ActorSystem system, params object[] parameters)
{
return ActivatorUtilities.CreateInstance<T>(system.GetServiceProvider(), parameters);
}
public static object CreateInstance(this ActorSystem system, Type instanceType, params object[] parameters)
{
return ActivatorUtilities.CreateInstance(system.GetServiceProvider(), instanceType, parameters);
}
}
To have a single scope for the whole actor system will not end well.
Yes, I strongly agree with that - each actor should have its own scope for anything that isn't explicitly registered as a singleton
Thank you all very much for the comments - I borrowed from both @Zetanova and @kevinvanblokland and @Horusiath in implementing this: https://github.com/akkadotnet/akka.net/pull/4708
I kept it extremely simple for now but I think it's pretty effective. I would love to hear your thoughts. By design, I did not want to get into managing people's scopes for them. I'd rather document what the best practices are for using DI with actors in broad strokes and let the users decide for themselves what works best for them.
Yes, looks ok.
Only one point/idea at https://github.com/akkadotnet/akka.net/pull/4708#discussion_r551918473
@Zetanova trying out your suggestions - not sure if I'm doing this correctly though? https://github.com/akkadotnet/akka.net/pull/4708#discussion_r553067731
@Aaronontheweb
I think you are trying to solve to different things.
1) Providing the actor instance with DI-services like IMemoryCache, DbContext and other services.
2) Using DI to create or provider actors reference to the outside world.
Following code makes no sense to me, because it can not and should not
host an actor instance without a name-path into the ServiceContainer (aka scope).
fixture.Services.AddScoped<ScopedActor>();
fixture.Services.AddTransient<MixedActor>();
and the idea to register an Actor-type and return an IActorRef will not go well,
because the MS-DI does only support query and return by different types.
The Props (Actor Instance Factory) can provide the user the Func<IServiceProvider,ActorBase>
or use a default one over (ActorBase)ActivatorUtilities.CreateInstance(sp, Type, Arguments)
Props need to accept a IServiceProvider reference from user-code.
The same DI-scope can and should be used for the children of an actor too.
For convenience the user-code inside an actor need to get the reference to the IServiceProvider.
It need to do it, to create instances of unknown types on demand or provide the reference to a child-props
(see https://github.com/akkadotnet/akka.net/issues/4590#issuecomment-741509615)
If the Props is creating a DI-scope for the actor-instance and the actor is providing it to its children.
I don't really know what should happen on an actor-restart of the parent.
most likely the scope should not be incarnated and only disposed on "Actor.PostStop"
This can only be done over a wrapper implantation.
the actorsystem or IActorFactory need to be registered into or created into the DI-container
//service collection
services.AddScoped<IMyService, MyService>();
sealed class MyService : IMyService
{
public IActorRef Ref { get; private set; }
public MyService(ActorSystem system)
{
Ref = _system.ActorOf<MyServiceActor>("myservice");
//or Ref = _system.ActorSelection("/user/myserver");
}
}
Ok, I see where I went wrong there. Looks like I just needed to change my syntax some on the original sample and not rewrite it the way I did.
Most helpful comment
Okay, so I'm experimenting with dependency injection and would really like to bootstrap my actors with proper constructor argument injection. As I'm also passing literals to my child actors, such as IActorRefs, strings, etc. the standard injection is not feasible. So I came up with the following idea:
The constructor of my actor looks like this: options)
```c#
public MyActor(IActorRef other, [AkkaInject]IOptions
{
// more code
}
The magic starts in _DI2()_:
```c#> factory)();
public static Props DI2(this IActorContext context, Expression
{
var newExpression = factory.Body.AsInstanceOf
if (newExpression == null)
throw new ArgumentException("The create function must be a 'new T (args)' expression");
I know this is still a quick and dirty implementation and I should retrieve my services from a correct place. I'm sure that with some modification child scopes should also be handled correctly. The GetCustomAttributes could be the specific AkkaInjectAttribute, however I don't want my actor library to be depending on the library that handles the DI.
I would love to get some feedback from the community.
I'm a great fan of Akka.net and really enjoyed the Petabridge course last year.
Regards,
Kevin