Skip to content

Commit

Permalink
Abstraction of ServiceProvider, Improving Akka.DependencyInjection (#…
Browse files Browse the repository at this point in the history
…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 <aaron@petabridge.com>
  • Loading branch information
SamEmber and Aaronontheweb authored May 26, 2021
1 parent 99afc0e commit c663bc2
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Akka.DependencyInjection.Tests

public class ActorServiceProviderPropsWithScopesSpecs : AkkaSpec, IClassFixture<AkkaDiFixture>
{
public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(ServiceProviderSetup.Create(fixture.Provider)
public ActorServiceProviderPropsWithScopesSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(DependencyResolverSetup.Create(fixture.Provider)
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig)), output)
{

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

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

[Fact(DisplayName = "DI: should be able to start actors with untyped Props")]
public void ShouldStartActorWithUntypedProps()
{
var spExtension = DependencyResolver.For(Sys);
var props = spExtension.Props(typeof(ScopedActor));

// create a scoped actor using the props from Akka.DependencyInjection
var scoped1 = Sys.ActorOf(props, "scoped1");
scoped1.Tell(new FetchDependencies());
var deps1 = ExpectMsg<CurrentDependencies>();
}

[Fact(DisplayName =
"DI: actors who receive an IServiceScope through Props should dispose of their dependencies and recreate upon restart")]
public void ActorsWithScopedDependenciesShouldDisposeAndRecreateUponRestart()
{
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var props = spExtension.Props<ScopedActor>();

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

// create a scoped actor using the props from Akka.DependencyInjection
Expand Down Expand Up @@ -134,7 +146,7 @@ public void ActorsWithMixedDependenciesShouldDisposeAndRecreateScopedUponRestart
public void ActorsWithNonDiDependenciesShouldStart()
{
// <CreateNonDiActor>
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var arg1 = "foo";
var arg2 = "bar";
var props = spExtension.Props<NonDiArgsActor>(arg1, arg2);
Expand Down Expand Up @@ -182,7 +194,7 @@ public void ActorsWithNonDiDependenciesShouldStart()
public void ServiceProvider_Props_should_support_copying()
{
// <CreateNonDiActor>
var spExtension = ServiceProvider.For(Sys);
var spExtension = DependencyResolver.For(Sys);
var arg1 = "foo";
var arg2 = "bar";
var props = spExtension.Props<NonDiArgsActor>(arg1, arg2).WithRouter(new RoundRobinPool(10).WithSupervisorStrategy(new OneForOneStrategy(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
using System;
//-----------------------------------------------------------------------
// <copyright file="DelegateInjectionSpecs.cs" company="Akka.NET Project">
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -73,7 +79,7 @@ public async Task DI_should_be_able_to_retrieve_singleton_using_delegate_from_in
internal class ParentActor : UntypedActor
{
public static Props Props(ActorSystem system) =>
ServiceProvider.For(system).Props<ParentActor>();
DependencyResolver.For(system).Props<ParentActor>();

private readonly IActorRef _echoActor;

Expand Down Expand Up @@ -114,7 +120,7 @@ public AkkaService(IServiceProvider serviceProvider)

public Task StartAsync(CancellationToken cancellationToken)
{
var setup = ServiceProviderSetup.Create(_serviceProvider)
var setup = DependencyResolverSetup.Create(_serviceProvider)
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig));

ActorSystem = ActorSystem.Create("TestSystem", setup);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Akka.DependencyInjection.Tests
{
public class ServiceProviderSetupSpecs : AkkaSpec, IClassFixture<AkkaDiFixture>
{
public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(ServiceProviderSetup.Create(fixture.Provider)
public ServiceProviderSetupSpecs(AkkaDiFixture fixture, ITestOutputHelper output) : base(DependencyResolverSetup.Create(fixture.Provider)
.And(BootstrapSetup.Create().WithConfig(TestKitBase.DefaultConfig)), output)
{

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

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

// scoped services should be the same
var scoped1 = sp.Provider.GetService<AkkaDiFixture.IScopedDependency>();
var scoped2 = sp.Provider.GetService<AkkaDiFixture.IScopedDependency>();
var scoped1 = sp.Resolver.GetService<AkkaDiFixture.IScopedDependency>();
var scoped2 = sp.Resolver.GetService<AkkaDiFixture.IScopedDependency>();

scoped1.Should().Be(scoped2);

// create a new scope
using (var newScope = sp.Provider.CreateScope())
using (var newScope = sp.Resolver.CreateScope())
{
var scoped3 = newScope.ServiceProvider.GetService<AkkaDiFixture.IScopedDependency>();
var scoped3 = newScope.Resolver.GetService<AkkaDiFixture.IScopedDependency>();
scoped1.Should().NotBe(scoped3);
}

// singleton services should be the same
var singleton1 = sp.Provider.GetService<AkkaDiFixture.ISingletonDependency>();
var singleton2 = sp.Provider.GetService<AkkaDiFixture.ISingletonDependency>();
var singleton1 = sp.Resolver.GetService<AkkaDiFixture.ISingletonDependency>();
var singleton2 = sp.Resolver.GetService<AkkaDiFixture.ISingletonDependency>();

singleton1.Should().Be(singleton2);
}
Expand All @@ -67,7 +67,7 @@ public void ShouldAccessServiceProviderFromActorSystemExtension()
{
Action getSp = () =>
{
var sp = ServiceProvider.For(Sys);
var sp = DependencyResolver.For(Sys);
};

getSp.Should().Throw<ConfigurationException>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//-----------------------------------------------------------------------
// <copyright file="ServiceProvider.cs" company="Akka.NET Project">
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using Akka.Actor;
using Akka.Configuration;
using Akka.Event;
using Microsoft.Extensions.DependencyInjection;

namespace Akka.DependencyInjection
{
/// <summary>
/// Provides users with immediate access to the <see cref="IDependencyResolver"/> bound to
/// this <see cref="ActorSystem"/>, if any.
/// </summary>
public sealed class DependencyResolver : IExtension
{
public DependencyResolver(IDependencyResolver resolver)
{
Resolver = resolver;
}

/// <summary>
/// The globally scoped <see cref="IDependencyResolver"/>.
/// </summary>
/// <remarks>
/// Per https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines - please use
/// the appropriate <see cref="IServiceScope"/> for your actors and the dependencies they consume. DI is typically
/// not used for long-lived, stateful objects such as actors.
///
/// Therefore, injecting transient dependencies via constructors is a bad idea in most cases. You'd be far better off
/// creating a local "request scope" each time your actor processes a message that depends on a transient dependency,
/// such as a database connection, and disposing that scope once the operation is complete.
///
/// Actors are not MVC Controllers. Actors can live forever, have the ability to restart, and are often stateful.
/// Be mindful of this as you use this feature or bad things will happen. Akka.NET does not magically manage scopes
/// for you.
/// </remarks>
public IDependencyResolver Resolver { get; }

public static DependencyResolver For(ActorSystem actorSystem)
{
return actorSystem.WithExtension<DependencyResolver, DependencyResolverExtension>();
}

/// <summary>
/// Uses a delegate to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <typeparam name="T">The type of actor to instantiate.</typeparam>
/// <param name="args">Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first.</param>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props<T>(params object[] args) where T : ActorBase
{
return Resolver.Props<T>(args);
}

/// <summary>
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <typeparam name="T">The type of actor to instantiate.</typeparam>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props<T>() where T : ActorBase
{
return Resolver.Props<T>();
}

/// <summary>
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <param name="type">The type of actor to instantiate.</param>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props(Type type)
{
return Resolver.Props(type);
}

/// <summary>
/// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection
/// and others are not.
/// </summary>
/// <remarks>
/// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU.
/// </remarks>
/// <param name="type">The type of actor to instantiate.</param>
/// <param name="args">Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first.</param>
/// <returns>A new <see cref="Akka.Actor.Props"/> instance which uses DI internally.</returns>
public Props Props(Type type, params object[] args)
{
return Resolver.Props(type, args);
}
}

/// <summary>
/// INTERNAL API
/// </summary>
public sealed class DependencyResolverExtension : ExtensionIdProvider<DependencyResolver>
{
public override DependencyResolver CreateExtension(ExtendedActorSystem system)
{
var setup = system.Settings.Setup.Get<DependencyResolverSetup>();
if (setup.HasValue) return new DependencyResolver(setup.Value.DependencyResolver);

var exception = new ConfigurationException("Unable to find [DependencyResolverSetup] included in ActorSystem settings." +
" Please specify one before attempting to use dependency injection inside Akka.NET.");
system.EventStream.Publish(new Error(exception, "Akka.DependencyInjection", typeof(DependencyResolverExtension), exception.Message));
throw exception;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//-----------------------------------------------------------------------
// <copyright file="ServiceProviderSetup.cs" company="Akka.NET Project">
// Copyright (C) 2009-2021 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2021 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using System;
using Akka.Actor;
using Akka.Actor.Setup;

namespace Akka.DependencyInjection
{
/// <summary>
/// Used to help bootstrap an <see cref="ActorSystem"/> with dependency injection (DI)
/// support via a <see cref="IServiceProvider"/> reference.
///
/// The <see cref="IServiceProvider"/> will be used to access previously registered services
/// in the creation of actors and other pieces of infrastructure inside Akka.NET.
///
/// The constructor is internal. Please use <see cref="Create"/> to create a new instance.
/// </summary>
[Obsolete("Used DependencyResolverSetup instead.")]
public class ServiceProviderSetup : Setup
{
internal ServiceProviderSetup(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}

public IServiceProvider ServiceProvider { get; }

public static ServiceProviderSetup Create(IServiceProvider provider)
{
if (provider == null)
throw new ArgumentNullException(nameof(provider));

return new ServiceProviderSetup(provider);
}
}

/// <summary>
/// Used to help bootstrap an <see cref="ActorSystem"/> with dependency injection (DI)
/// support via a <see cref="IDependencyResolver"/> reference.
///
/// The <see cref="IDependencyResolver"/> will be used to access previously registered services
/// in the creation of actors and other pieces of infrastructure inside Akka.NET.
///
/// The constructor is internal. Please use <see cref="Create"/> to create a new instance.
/// </summary>
public class DependencyResolverSetup : Setup
{
public IDependencyResolver DependencyResolver { get; }

internal DependencyResolverSetup(IDependencyResolver dependencyResolver)
{
DependencyResolver = dependencyResolver;
}

/// <summary>
/// Creates a new instance of DependencyResolverSetup, passing in <see cref="IServiceProvider"/>
/// here creates an <see cref="IDependencyResolver"/> that resolves dependencies from the specified <see cref="IServiceProvider"/>
/// </summary>
public static DependencyResolverSetup Create(IServiceProvider provider)
{
if (provider == null)
throw new ArgumentNullException(nameof(provider));

return new DependencyResolverSetup(new ServiceProviderDependencyResolver(provider));
}

/// <summary>
/// Creates a new instance of DependencyResolverSetup, an implementation of <see cref="IDependencyResolver"/>
/// can be passed in here to resolve services from test or alternative DI frameworks.
/// </summary>
public static DependencyResolverSetup Create(IDependencyResolver provider)
{
if (provider == null)
throw new ArgumentNullException(nameof(provider));

return new DependencyResolverSetup(provider);
}
}
}
Loading

0 comments on commit c663bc2

Please sign in to comment.