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 committed May 26, 2021
1 parent 99afc0e commit c663bc2
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 67 deletions.
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
@@ -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
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
@@ -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;
}
}
}
@@ -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);
}
}
}

0 comments on commit c663bc2

Please sign in to comment.