Skip to content

Commit c663bc2

Browse files
Abstraction of ServiceProvider, Improving Akka.DependencyInjection (#4814)
* Abstraction of ServiceProvider * introduced non-breaking Akka.DependencyInjection API changes * fixed unit tests / Props bug * fixed up DelegateInjectionSpecs * Added type checking for `Props(Type type, params object[] args)` * fixed non-generic `Props()` method Co-authored-by: Aaron Stannard <[email protected]>
1 parent 99afc0e commit c663bc2

File tree

11 files changed

+428
-67
lines changed

11 files changed

+428
-67
lines changed

src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ActorServiceProviderPropsWithScopesSpecs.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace Akka.DependencyInjection.Tests
2121

2222
public class ActorServiceProviderPropsWithScopesSpecs : AkkaSpec, IClassFixture<AkkaDiFixture>
2323
{
24-
public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(ServiceProviderSetup.Create(fixture.Provider)
24+
public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(DependencyResolverSetup.Create(fixture.Provider)
2525
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig)), output)
2626
{
2727

@@ -30,7 +30,7 @@ public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutp
3030
[Fact(DisplayName = "DI: actors who receive an IServiceScope through Props should dispose of their dependencies upon termination")]
3131
public void ActorsWithScopedDependenciesShouldDisposeUponStop()
3232
{
33-
var spExtension = ServiceProvider.For(Sys);
33+
var spExtension = DependencyResolver.For(Sys);
3434
var props = spExtension.Props<ScopedActor>();
3535

3636
// create a scoped actor using the props from Akka.DependencyInjection
@@ -60,11 +60,23 @@ public void ActorsWithScopedDependenciesShouldDisposeUponStop()
6060
deps2.Dependencies.All(x => x.Disposed).Should().BeFalse();
6161
}
6262

63+
[Fact(DisplayName = "DI: should be able to start actors with untyped Props")]
64+
public void ShouldStartActorWithUntypedProps()
65+
{
66+
var spExtension = DependencyResolver.For(Sys);
67+
var props = spExtension.Props(typeof(ScopedActor));
68+
69+
// create a scoped actor using the props from Akka.DependencyInjection
70+
var scoped1 = Sys.ActorOf(props, "scoped1");
71+
scoped1.Tell(new FetchDependencies());
72+
var deps1 = ExpectMsg<CurrentDependencies>();
73+
}
74+
6375
[Fact(DisplayName =
6476
"DI: actors who receive an IServiceScope through Props should dispose of their dependencies and recreate upon restart")]
6577
public void ActorsWithScopedDependenciesShouldDisposeAndRecreateUponRestart()
6678
{
67-
var spExtension = ServiceProvider.For(Sys);
79+
var spExtension = DependencyResolver.For(Sys);
6880
var props = spExtension.Props<ScopedActor>();
6981

7082
// create a scoped actor using the props from Akka.DependencyInjection
@@ -95,7 +107,7 @@ public void ActorsWithScopedDependenciesShouldDisposeAndRecreateUponRestart()
95107
"DI: actors who receive a mix of dependencies via IServiceScope should dispose ONLY of their scoped dependencies and recreate upon restart")]
96108
public void ActorsWithMixedDependenciesShouldDisposeAndRecreateScopedUponRestart()
97109
{
98-
var spExtension = ServiceProvider.For(Sys);
110+
var spExtension = DependencyResolver.For(Sys);
99111
var props = spExtension.Props<MixedActor>();
100112

101113
// create a scoped actor using the props from Akka.DependencyInjection
@@ -134,7 +146,7 @@ public void ActorsWithMixedDependenciesShouldDisposeAndRecreateScopedUponRestart
134146
public void ActorsWithNonDiDependenciesShouldStart()
135147
{
136148
// <CreateNonDiActor>
137-
var spExtension = ServiceProvider.For(Sys);
149+
var spExtension = DependencyResolver.For(Sys);
138150
var arg1 = "foo";
139151
var arg2 = "bar";
140152
var props = spExtension.Props<NonDiArgsActor>(arg1, arg2);
@@ -182,7 +194,7 @@ public void ActorsWithNonDiDependenciesShouldStart()
182194
public void ServiceProvider_Props_should_support_copying()
183195
{
184196
// <CreateNonDiActor>
185-
var spExtension = ServiceProvider.For(Sys);
197+
var spExtension = DependencyResolver.For(Sys);
186198
var arg1 = "foo";
187199
var arg2 = "bar";
188200
var props = spExtension.Props<NonDiArgsActor>(arg1, arg2).WithRouter(new RoundRobinPool(10).WithSupervisorStrategy(new OneForOneStrategy(

src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/DelegateInjectionSpecs.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
using System;
1+
//-----------------------------------------------------------------------
2+
// <copyright file="DelegateInjectionSpecs.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
//-----------------------------------------------------------------------
7+
using System;
28
using System.Collections.Generic;
39
using System.Linq;
410
using System.Text;
@@ -73,7 +79,7 @@ public async Task DI_should_be_able_to_retrieve_singleton_using_delegate_from_in
7379
internal class ParentActor : UntypedActor
7480
{
7581
public static Props Props(ActorSystem system) =>
76-
ServiceProvider.For(system).Props<ParentActor>();
82+
DependencyResolver.For(system).Props<ParentActor>();
7783

7884
private readonly IActorRef _echoActor;
7985

@@ -114,7 +120,7 @@ public AkkaService(IServiceProvider serviceProvider)
114120

115121
public Task StartAsync(CancellationToken cancellationToken)
116122
{
117-
var setup = ServiceProviderSetup.Create(_serviceProvider)
123+
var setup = DependencyResolverSetup.Create(_serviceProvider)
118124
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig));
119125

120126
ActorSystem = ActorSystem.Create("TestSystem", setup);

src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ServiceProviderSetupSpecs.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace Akka.DependencyInjection.Tests
1818
{
1919
public class ServiceProviderSetupSpecs : AkkaSpec, IClassFixture<AkkaDiFixture>
2020
{
21-
public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(ServiceProviderSetup.Create(fixture.Provider)
21+
public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(DependencyResolverSetup.Create(fixture.Provider)
2222
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig)), output)
2323
{
2424

@@ -27,29 +27,29 @@ public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output
2727
[Fact(DisplayName = "DI: Should access Microsoft.Extensions.DependencyInjection.IServiceProvider from ServiceProvider ActorSystem extension")]
2828
public void ShouldAccessServiceProviderFromActorSystemExtension()
2929
{
30-
var sp = ServiceProvider.For(Sys);
31-
var dep = sp.Provider.GetService<AkkaDiFixture.ITransientDependency>();
30+
var sp = DependencyResolver.For(Sys);
31+
var dep = sp.Resolver.GetService<AkkaDiFixture.ITransientDependency>();
3232
dep.Should().BeOfType<AkkaDiFixture.Transient>();
3333

34-
var dep2 = sp.Provider.GetService<AkkaDiFixture.ITransientDependency>();
34+
var dep2 = sp.Resolver.GetService<AkkaDiFixture.ITransientDependency>();
3535
dep2.Should().NotBe(dep); // the two transient instances should be different
3636

3737
// scoped services should be the same
38-
var scoped1 = sp.Provider.GetService<AkkaDiFixture.IScopedDependency>();
39-
var scoped2 = sp.Provider.GetService<AkkaDiFixture.IScopedDependency>();
38+
var scoped1 = sp.Resolver.GetService<AkkaDiFixture.IScopedDependency>();
39+
var scoped2 = sp.Resolver.GetService<AkkaDiFixture.IScopedDependency>();
4040

4141
scoped1.Should().Be(scoped2);
4242

4343
// create a new scope
44-
using (var newScope = sp.Provider.CreateScope())
44+
using (var newScope = sp.Resolver.CreateScope())
4545
{
46-
var scoped3 = newScope.ServiceProvider.GetService<AkkaDiFixture.IScopedDependency>();
46+
var scoped3 = newScope.Resolver.GetService<AkkaDiFixture.IScopedDependency>();
4747
scoped1.Should().NotBe(scoped3);
4848
}
4949

5050
// singleton services should be the same
51-
var singleton1 = sp.Provider.GetService<AkkaDiFixture.ISingletonDependency>();
52-
var singleton2 = sp.Provider.GetService<AkkaDiFixture.ISingletonDependency>();
51+
var singleton1 = sp.Resolver.GetService<AkkaDiFixture.ISingletonDependency>();
52+
var singleton2 = sp.Resolver.GetService<AkkaDiFixture.ISingletonDependency>();
5353

5454
singleton1.Should().Be(singleton2);
5555
}
@@ -67,7 +67,7 @@ public void ShouldAccessServiceProviderFromActorSystemExtension()
6767
{
6868
Action getSp = () =>
6969
{
70-
var sp = ServiceProvider.For(Sys);
70+
var sp = DependencyResolver.For(Sys);
7171
};
7272

7373
getSp.Should().Throw<ConfigurationException>();
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="ServiceProvider.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
//-----------------------------------------------------------------------
7+
8+
using System;
9+
using Akka.Actor;
10+
using Akka.Configuration;
11+
using Akka.Event;
12+
using Microsoft.Extensions.DependencyInjection;
13+
14+
namespace Akka.DependencyInjection
15+
{
16+
/// <summary>
17+
/// Provides users with immediate access to the <see cref="IDependencyResolver"/> bound to
18+
/// this <see cref="ActorSystem"/>, if any.
19+
/// </summary>
20+
public sealed class DependencyResolver : IExtension
21+
{
22+
public DependencyResolver(IDependencyResolver resolver)
23+
{
24+
Resolver = resolver;
25+
}
26+
27+
/// <summary>
28+
/// The globally scoped <see cref="IDependencyResolver"/>.
29+
/// </summary>
30+
/// <remarks>
31+
/// Per https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines - please use
32+
/// the appropriate <see cref="IServiceScope"/> for your actors and the dependencies they consume. DI is typically
33+
/// not used for long-lived, stateful objects such as actors.
34+
///
35+
/// Therefore, injecting transient dependencies via constructors is a bad idea in most cases. You'd be far better off
36+
/// creating a local "request scope" each time your actor processes a message that depends on a transient dependency,
37+
/// such as a database connection, and disposing that scope once the operation is complete.
38+
///
39+
/// Actors are not MVC Controllers. Actors can live forever, have the ability to restart, and are often stateful.
40+
/// Be mindful of this as you use this feature or bad things will happen. Akka.NET does not magically manage scopes
41+
/// for you.
42+
/// </remarks>
43+
public IDependencyResolver Resolver { get; }
44+
45+
public static DependencyResolver For(ActorSystem actorSystem)
46+
{
47+
return actorSystem.WithExtension<DependencyResolver, DependencyResolverExtension>();
48+
}
49+
50+
/// <summary>
51+
/// Uses a delegate to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
52+
/// and others are not.
53+
/// </summary>
54+
/// <remarks>
55+
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
56+
/// </remarks>
57+
/// <typeparam name="T">The type of actor to instantiate.</typeparam>
58+
/// <param name="args">Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first.</param>
59+
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
60+
public Props Props<T>(params object[] args) where T : ActorBase
61+
{
62+
return Resolver.Props<T>(args);
63+
}
64+
65+
/// <summary>
66+
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
67+
/// and others are not.
68+
/// </summary>
69+
/// <remarks>
70+
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
71+
/// </remarks>
72+
/// <typeparam name="T">The type of actor to instantiate.</typeparam>
73+
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
74+
public Props Props<T>() where T : ActorBase
75+
{
76+
return Resolver.Props<T>();
77+
}
78+
79+
/// <summary>
80+
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
81+
/// and others are not.
82+
/// </summary>
83+
/// <remarks>
84+
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
85+
/// </remarks>
86+
/// <param name="type">The type of actor to instantiate.</param>
87+
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
88+
public Props Props(Type type)
89+
{
90+
return Resolver.Props(type);
91+
}
92+
93+
/// <summary>
94+
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
95+
/// and others are not.
96+
/// </summary>
97+
/// <remarks>
98+
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
99+
/// </remarks>
100+
/// <param name="type">The type of actor to instantiate.</param>
101+
/// <param name="args">Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first.</param>
102+
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
103+
public Props Props(Type type, params object[] args)
104+
{
105+
return Resolver.Props(type, args);
106+
}
107+
}
108+
109+
/// <summary>
110+
/// INTERNAL API
111+
/// </summary>
112+
public sealed class DependencyResolverExtension : ExtensionIdProvider<DependencyResolver>
113+
{
114+
public override DependencyResolver CreateExtension(ExtendedActorSystem system)
115+
{
116+
var setup = system.Settings.Setup.Get<DependencyResolverSetup>();
117+
if (setup.HasValue) return new DependencyResolver(setup.Value.DependencyResolver);
118+
119+
var exception = new ConfigurationException("Unable to find [DependencyResolverSetup] included in ActorSystem settings." +
120+
" Please specify one before attempting to use dependency injection inside Akka.NET.");
121+
system.EventStream.Publish(new Error(exception, "Akka.DependencyInjection", typeof(DependencyResolverExtension), exception.Message));
122+
throw exception;
123+
}
124+
}
125+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="ServiceProviderSetup.cs" company="Akka.NET Project">
3+
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
4+
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
5+
// </copyright>
6+
//-----------------------------------------------------------------------
7+
8+
using System;
9+
using Akka.Actor;
10+
using Akka.Actor.Setup;
11+
12+
namespace Akka.DependencyInjection
13+
{
14+
/// <summary>
15+
/// Used to help bootstrap an <see cref="ActorSystem"/> with dependency injection (DI)
16+
/// support via a <see cref="IServiceProvider"/> reference.
17+
///
18+
/// The <see cref="IServiceProvider"/> will be used to access previously registered services
19+
/// in the creation of actors and other pieces of infrastructure inside Akka.NET.
20+
///
21+
/// The constructor is internal. Please use <see cref="Create"/> to create a new instance.
22+
/// </summary>
23+
[Obsolete("Used DependencyResolverSetup instead.")]
24+
public class ServiceProviderSetup : Setup
25+
{
26+
internal ServiceProviderSetup(IServiceProvider serviceProvider)
27+
{
28+
ServiceProvider = serviceProvider;
29+
}
30+
31+
public IServiceProvider ServiceProvider { get; }
32+
33+
public static ServiceProviderSetup Create(IServiceProvider provider)
34+
{
35+
if (provider == null)
36+
throw new ArgumentNullException(nameof(provider));
37+
38+
return new ServiceProviderSetup(provider);
39+
}
40+
}
41+
42+
/// <summary>
43+
/// Used to help bootstrap an <see cref="ActorSystem"/> with dependency injection (DI)
44+
/// support via a <see cref="IDependencyResolver"/> reference.
45+
///
46+
/// The <see cref="IDependencyResolver"/> will be used to access previously registered services
47+
/// in the creation of actors and other pieces of infrastructure inside Akka.NET.
48+
///
49+
/// The constructor is internal. Please use <see cref="Create"/> to create a new instance.
50+
/// </summary>
51+
public class DependencyResolverSetup : Setup
52+
{
53+
public IDependencyResolver DependencyResolver { get; }
54+
55+
internal DependencyResolverSetup(IDependencyResolver dependencyResolver)
56+
{
57+
DependencyResolver = dependencyResolver;
58+
}
59+
60+
/// <summary>
61+
/// Creates a new instance of DependencyResolverSetup, passing in <see cref="IServiceProvider"/>
62+
/// here creates an <see cref="IDependencyResolver"/> that resolves dependencies from the specified <see cref="IServiceProvider"/>
63+
/// </summary>
64+
public static DependencyResolverSetup Create(IServiceProvider provider)
65+
{
66+
if (provider == null)
67+
throw new ArgumentNullException(nameof(provider));
68+
69+
return new DependencyResolverSetup(new ServiceProviderDependencyResolver(provider));
70+
}
71+
72+
/// <summary>
73+
/// Creates a new instance of DependencyResolverSetup, an implementation of <see cref="IDependencyResolver"/>
74+
/// can be passed in here to resolve services from test or alternative DI frameworks.
75+
/// </summary>
76+
public static DependencyResolverSetup Create(IDependencyResolver provider)
77+
{
78+
if (provider == null)
79+
throw new ArgumentNullException(nameof(provider));
80+
81+
return new DependencyResolverSetup(provider);
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)