Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign the Dependency Injection API #1675

Closed
Horusiath opened this issue Jan 27, 2016 · 15 comments
Closed

Redesign the Dependency Injection API #1675

Horusiath opened this issue Jan 27, 2016 · 15 comments

Comments

@Horusiath
Copy link
Contributor

Right now our DI API is not sufficient to fluently operate with actors. The main issues here are:

  1. Using Props.DI().Create<InjectedActor>() is probably not the nicest way to abstract the fact, that we're working with some kind of DI container. This should be transparent.
  2. Current design doesn't allow us to make a selective injections - we either need to inject all or none of the actor parameters. This doesn't fit many real-life cases and should be definitely changed.
  3. Think about some way of adding additional scoping rules to containers allowing resolved objects to have lifecycle pinned to a particular actor context/actor or one of it's ancestors.

Some ideas here are:

Ad.1) Abstract DI container inside dedicated ActorProducers, which will be aware of their role without explicit notifying using Props.DI().

Ad.2) Allow to make a selective injections - I've actually have an idea here to create a feature inside Akka.DI.Core (which is shared by all DI plugins) i.e.:

Props.Create(() => new MyActor(Context.Parent, DI.Inject<MyService>()))

Context.Parent is example of pinning existing value to a constructor. Here an Inject method could work two ways:

  1. Simpler version: returns null/default(T). DI-aware actor producer would recognize this as a point, where container should resolve a dependency.
  2. More complex: returns a dynamic proxy replaced during actor incarnation with instance resolved by the container. This would be nicer for a reason, that dynamic proxy could convey some additional data informing about things like scoping or named/tagged parameters.

Before somebody mention it: I think, that part we shouldn't speak about scoping of actors themselves - akka has it's own lifecycle rules and none of DI libraries would fit into that.

@rogeralsing
Copy link
Contributor

@Horusiath the Props.Create(() => uses Linq Expressions so it would be fairly easy to detect the DI.Inject code and replace that with resolved values.
No need for magic proxies or anything like that.

@Danthar
Copy link
Member

Danthar commented Jan 28, 2016

So I like idea number 1 because it makes DI more of an infrastructure concern instead of having it be in your face all the time.The concept of selective injections is great. Its something you will want to use any time your passing an IActorRef and DI injected services in an actors constructor.
Not sure if a proxy is the way to go with this. But anything that keeps DI as transparently as possible is good in my book.

About the scoping of dependencies. In my mind thats easy. You either have it scoped to the lifecycle of the actor its being injected in (which means its disposed/recreated on actor restart/creation). Or its singleton scoped.
You could also provide something which scopes it to the lifecycle of the parent actor. But im not sure how that would work with remoting.

@rogeralsing
Copy link
Contributor

Also, we need to think about how this fits with remote deployment.
Are dependencies resolved on the creating side, or on the target side?

@Horusiath
Copy link
Contributor Author

@rogeralsing My guess would be that dependencies should be resolved on the target side, since Props may also be passed inside messages and DI will be probably be used for things that won't be easily location-transparent - it's easy to imagine that things like database contexts or service proxies will be most likely injected.

For this reason I think, that dependency resolution should not work on Props.Create(() =>, but more during actor incarnation process itself.

@Danthar
Copy link
Member

Danthar commented Jan 28, 2016

I agree with @Horusiath the Props.Create(() =>, Should only convey intent.

@hvidgaard
Copy link

While we're at it, a container like SimpleInjector has a few different ways to do things. One of the things it does is verify all dependencies, so it knows at startup, that it can inject everything. A fail fast philosophy. This means that you cannot register Actors to verify their dependencies, as it fails when trying to create a new one. It would be nice for this to work one way or another, perhaps by a setting in the adaptor.

@llehn
Copy link

llehn commented Apr 19, 2017

We're badly in need of selective injections:
-Some of the parameters should be provided by DI
-A few must be provided by hand (PersistenceId of the persistent actor)

With Autofac, for example, it is very easy to mix both. I think we need that functionality in DI().Props<T>();

A very simple case (without adding more "abstractions") would be providing additional parameters to the method, allowing this:

DI().Props<MyActor>(param1, param2);

param1 and param2 would be matched by type.

For matching by name we would need an abstraction like NamedParameter.

I understand that we can do this now by explicitly resolving services from DI and using Props.Create(() => but for my taste this is too static and unflexible. This may do it for simple cases, but not when I want my code to create any type of an Aggregate Root Actor (persistent actor) - knowing that I need to pass an id and not knowing which exact services does this particular actor need from DI.

@Horusiath
Copy link
Contributor Author

Ok, so the more general design could look like this: instead of having actor producers and extension providers, we could create a single dependency resolver set as a field of ActorSystem. This dependency resolver would have methods used to register/resolve various types of components (not only actors). It could also be used as a single point to interop with third-party DI plugins. I think we should consider 2 basic types of scopes:

  • Scope of an actor system: basically equivalent of singleton scope.
  • Scope of a particular actor incarnation: just like transient scope.

Given this single abstraction, we could actually use it for variety of options:

  1. Actor system would register itself at the very beginning inside its dependency resolver. Since a lot of akka components are using ActorSystem or ExtendedActorSystem, they could be created via dependency resolver instead of usual Activator.CreateInstance.
  2. Most obvious example is an actor creation using ActorOf method.
  3. Creation of actor system extensions: if this would work, I think that having ExtensionIdProvider<> would no longer be needed. Extensions could be simply resolved and automatically also register themselves in actor system scope. This would allow for extension objects like Cluster, Persistence, DistributedData etc. to be used as parameters inside actor constructors and be injected as well.
  4. Serializers: most serializers make use of ActorSystem, they could be also injected instead of being created via activator.
  5. Event adapters.
  6. Probably all of the places where we use Activator or ConstructorInfo for object creation.

I've also though about how to allow users to partially define constructor params, such as IActorRefs. This could be done with anonymous types i.e. having new API method like Props.Dynamic<MyActor>(new { coordinatorRef = actorRef }) would be translated to props which defines that coordinatorRef parameter of MyActor constructor would be explicitly set to provided value, while the rest of the parameters would be left to be injected.

Of course this method is a heavier that standard Props.Create as it would need to infere some things from dynamic object, but it's only an option for people who desperately need it.

I'll try to present some PR for that. The biggest problem I see here is current actor creation pipeline, which is way more complex that is should be.

@arkadiuszwojcik
Copy link

@Horusiath any progress on scoped DI? Do you know what is the beast way to implement similar behaviour in current Akka implementation?

@Horusiath
Copy link
Contributor Author

Horusiath commented Mar 1, 2018

@arkadiuszwojcik the PR for this is essentially prepared (see #3057) - it already worked at the time, when it was send. I'm hoping to merge it for the v1.4 release.

The last missing piece was the way of registering types in container - since Akka.NET was purely dependent on HOCON for the configuration, and no suitable method for user code was prepared. This will be partially solved by @Aaronontheweb and his PR to split up HOCON from Akka - from there we'll be able to abstract the way of how and when configuration happens.

@srogovtsev
Copy link

If anybody's interested, I've just done a very compact Proof-of-Concept of what you probably call "partial injection":

public class ParameterActor : UntypedActor
{
  public ParameterActor(Service service, string parameter1, int parameter2)
  {
  }
}

//ParameterActor will get Service from DI, parameter1: "parameter1", parameter2: 2
Sys.ActorOf(Sys.DI().Props<ParameterActor>("parameter1", 2));

(and I've got this to work with Autofac)

The one major limitation is that parameter types have to be different, but in type-heavy environment such as Akka this is not an issue anyway.

While this is not a very "generic" way to do things, it might be implemented as a non-breaking change in Akka.DI with an additional interface for dependency resolvers to implement - which means people get to get it quite faster than major change.

(I can move this to separate issue if that's a preferable way)

@mclark1129
Copy link

mclark1129 commented Jul 31, 2019

Selective injections are definitely the biggest sticking point for us at the moment. It really limits us when we have to choose between every constructor parameter coming from DI, or from the actor's parent. Having something like Context.DI().Props<MyActor>(container => new MyActor("my parameter", container.Resolve<IMyService>())) would be a huge improvement in this regard.

@AndrewBoklashko
Copy link

I would like to share my experience with Akka.NET and dependency injection. I have an ASP.NET Core application with Microsoft DI container and I don't use any of Akka.NET DI. Instead I created a simple immutable classes which encapsulate dependencies for each actor type(including child actors) and resolve/pass them explicitly. For example:

public class RequestHandlerDependencies
{
    public IApplicationMetrics Metrics { get; }
    public ICanTell AuthorizationHandler { get; }
    public ConnectionHandlerDependencies ConnectionHandlerDependencies { get; }
    public MessageHandlerDependencies MessageHandlerDependencies { get; }

    public RequestHandlerDependencies(
        IApplicationMetrics metrics,
        ConnectionHandlerDependencies connectionHandlerDependencies,
        MessageHandlerDependencies messageHandlerDependencies,
        ICanTell authorizationHandler)
    {
        Metrics = metrics;
        AuthorizationHandler = authorizationHandler;
        ConnectionHandlerDependencies = connectionHandlerDependencies;
        MessageHandlerDependencies = messageHandlerDependencies;
    }
}

Then I register them on Startup, either resolving dependencies from DI or creating them directly. I also created SingletonActorProvider that allows to initialize and return refs for actors which
must exist as a single instance in actor system and to avoid concurrent initialization issues:

.AddSingletonActorProvider()
.AddMetrics(MetricsConfiguration.Configuration)
.AddSingleton<ConnectionHandlerDependencies>()
.AddSingleton<MessageHandlerDependencies>()
.AddSingleton(provider =>
{
    var actorProvider = provider.GetRequiredService<SingletonActorProvider>();
    var authorizationHandler = actorProvider.SingletonOf(
        AuthorizationHandler.Props(/* get dependencies from provider, configuration etc. */),
        "authorization");

    return new RequestHandlerDependencies(
        provider.GetRequiredService<IApplicationMetrics>(),
        authorizationHandler,
        provider.GetRequiredService<ConnectionHandlerDependencies>(),
        provider.GetRequiredService<MessageHandlerDependencies>()
    );
})

Usage:

app.MapWhen(ctx => ctx.WebSockets.IsWebSocketRequest, ws =>
{
    ws.Run(async context =>
    {
        var actorSystem = context.RequestServices.GetRequiredService<ActorSystem>();
        var requestHandler = actorSystem.ActorOf(
            RequestHandler.Props(
                context.RequestServices.GetRequiredService<RequestHandlerDependencies>(),
                context.Request),
            $"request-{context.Request.HttpContext.TraceIdentifier}");
    });
});

@AndrewBoklashko
Copy link

However I haven't yet figured out a way to reuse Akka actors(and configuration) between actor systems. Let's say I want to reuse AuthorizationHandler from the example above between different ASP.NET Core applications(actor systems) and provide a unified way for configuring it alongside with it's dependencies.

In non-Akka world I would do it using dependency injection by creating IServiceCollection extension method, like:

public static IServiceCollection AddAuthorizationHandler(
    this IServiceCollection services,
    /* configuration objects/delegates */)
{
    services
        // configure dependencies
        .AddSingleton(provider =>
        {
            // resolve dependencies
            // create and apply configuration
            return new AuthorizationHandler(/* dependencies, configuration */);
        });
}

For Akka actors it's hard to do this, because IActorRef is not typed. I was thinking to create an actor provider interface:

public interface IActorProvider<TActor>
{
    IActorRef GetActor();
}

Then it should be possible to implement this for the required DI scopes(singleton, transient etc.) and register them in DI, allowing consumers to resolve and use them to create actor instances. There is no way to provide extra parameters during actor resolving here, but I don't find it particularly useful anyway, because from my experience that's rarely needed.

@Aaronontheweb
Copy link
Member

closed via #4590

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants