diff --git a/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs index 02c72881c98..b9885c1290c 100644 --- a/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs +++ b/src/benchmark/Akka.Benchmarks/Actor/ActorPathBenchmarks.cs @@ -16,12 +16,16 @@ public class ActorPathBenchmarks { private ActorPath x; private ActorPath y; + private ActorPath _childPath; + private Address _sysAdr = new Address("akka.tcp", "system", "127.0.0.1", 1337); + private Address _otherAdr = new Address("akka.tcp", "system", "127.0.0.1", 1338); [GlobalSetup] public void Setup() { - x = new RootActorPath(new Address("akka.tcp", "system", "127.0.0.1", 1337), "user"); - y = new RootActorPath(new Address("akka.tcp", "system", "127.0.0.1", 1337), "system"); + x = new RootActorPath(_sysAdr, "user"); + y = new RootActorPath(_sysAdr, "system"); + _childPath = x / "parent" / "child"; } [Benchmark] @@ -45,7 +49,19 @@ public bool ActorPath_Equals() [Benchmark] public string ActorPath_ToString() { - return x.ToString(); + return _childPath.ToString(); + } + + [Benchmark] + public string ActorPath_ToSerializationFormat() + { + return _childPath.ToSerializationFormat(); + } + + [Benchmark] + public string ActorPath_ToSerializationFormatWithAddress() + { + return _childPath.ToSerializationFormatWithAddress(_otherAdr); } } } diff --git a/src/benchmark/Akka.Benchmarks/Actor/NameAndUidBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Actor/NameAndUidBenchmarks.cs new file mode 100644 index 00000000000..c616f5527b4 --- /dev/null +++ b/src/benchmark/Akka.Benchmarks/Actor/NameAndUidBenchmarks.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Text; +using Akka.Actor; +using Akka.Benchmarks.Configurations; +using BenchmarkDotNet.Attributes; + +namespace Akka.Benchmarks.Actor +{ + [Config(typeof(MicroBenchmarkConfig))] + public class NameAndUidBenchmarks + { + public const string ActorPath = "foo#11241311"; + + [Benchmark] + public NameAndUid ActorCell_SplitNameAndUid() + { + return ActorCell.SplitNameAndUid(ActorPath); + } + + [Benchmark] + public (string name, int uid) ActorCell_GetNameAndUid() + { + return ActorCell.GetNameAndUid(ActorPath); + } + } +} diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ActorServiceProviderPropsWithScopesSpecs.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ActorServiceProviderPropsWithScopesSpecs.cs index ef5d2fe0c39..4c2c84c5f54 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ActorServiceProviderPropsWithScopesSpecs.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ActorServiceProviderPropsWithScopesSpecs.cs @@ -21,7 +21,7 @@ namespace Akka.DependencyInjection.Tests public class ActorServiceProviderPropsWithScopesSpecs : AkkaSpec, IClassFixture { - 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) { @@ -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(); // create a scoped actor using the props from Akka.DependencyInjection @@ -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(); + } + [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(); // create a scoped actor using the props from Akka.DependencyInjection @@ -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(); // create a scoped actor using the props from Akka.DependencyInjection @@ -134,7 +146,7 @@ public void ActorsWithMixedDependenciesShouldDisposeAndRecreateScopedUponRestart public void ActorsWithNonDiDependenciesShouldStart() { // - var spExtension = ServiceProvider.For(Sys); + var spExtension = DependencyResolver.For(Sys); var arg1 = "foo"; var arg2 = "bar"; var props = spExtension.Props(arg1, arg2); @@ -182,7 +194,7 @@ public void ActorsWithNonDiDependenciesShouldStart() public void ServiceProvider_Props_should_support_copying() { // - var spExtension = ServiceProvider.For(Sys); + var spExtension = DependencyResolver.For(Sys); var arg1 = "foo"; var arg2 = "bar"; var props = spExtension.Props(arg1, arg2).WithRouter(new RoundRobinPool(10).WithSupervisorStrategy(new OneForOneStrategy( diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/DelegateInjectionSpecs.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/DelegateInjectionSpecs.cs index bc0a64388ad..1e2342644d6 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/DelegateInjectionSpecs.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/DelegateInjectionSpecs.cs @@ -1,4 +1,10 @@ -using System; +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -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(); + DependencyResolver.For(system).Props(); private readonly IActorRef _echoActor; @@ -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); diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ServiceProviderSetupSpecs.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ServiceProviderSetupSpecs.cs index e14e8e8ee4b..d44085f69f6 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ServiceProviderSetupSpecs.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection.Tests/ServiceProviderSetupSpecs.cs @@ -18,7 +18,7 @@ namespace Akka.DependencyInjection.Tests { public class ServiceProviderSetupSpecs : AkkaSpec, IClassFixture { - 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) { @@ -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(); + var sp = DependencyResolver.For(Sys); + var dep = sp.Resolver.GetService(); dep.Should().BeOfType(); - var dep2 = sp.Provider.GetService(); + var dep2 = sp.Resolver.GetService(); dep2.Should().NotBe(dep); // the two transient instances should be different // scoped services should be the same - var scoped1 = sp.Provider.GetService(); - var scoped2 = sp.Provider.GetService(); + var scoped1 = sp.Resolver.GetService(); + var scoped2 = sp.Resolver.GetService(); 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(); + var scoped3 = newScope.Resolver.GetService(); scoped1.Should().NotBe(scoped3); } // singleton services should be the same - var singleton1 = sp.Provider.GetService(); - var singleton2 = sp.Provider.GetService(); + var singleton1 = sp.Resolver.GetService(); + var singleton2 = sp.Resolver.GetService(); singleton1.Should().Be(singleton2); } @@ -67,7 +67,7 @@ public void ShouldAccessServiceProviderFromActorSystemExtension() { Action getSp = () => { - var sp = ServiceProvider.For(Sys); + var sp = DependencyResolver.For(Sys); }; getSp.Should().Throw(); diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolver.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolver.cs new file mode 100644 index 00000000000..4917a1b8ed5 --- /dev/null +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolver.cs @@ -0,0 +1,125 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Configuration; +using Akka.Event; +using Microsoft.Extensions.DependencyInjection; + +namespace Akka.DependencyInjection +{ + /// + /// Provides users with immediate access to the bound to + /// this , if any. + /// + public sealed class DependencyResolver : IExtension + { + public DependencyResolver(IDependencyResolver resolver) + { + Resolver = resolver; + } + + /// + /// The globally scoped . + /// + /// + /// Per https://docs.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines - please use + /// the appropriate 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. + /// + public IDependencyResolver Resolver { get; } + + public static DependencyResolver For(ActorSystem actorSystem) + { + return actorSystem.WithExtension(); + } + + /// + /// Uses a delegate to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection + /// and others are not. + /// + /// + /// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU. + /// + /// The type of actor to instantiate. + /// Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first. + /// A new instance which uses DI internally. + public Props Props(params object[] args) where T : ActorBase + { + return Resolver.Props(args); + } + + /// + /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection + /// and others are not. + /// + /// + /// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU. + /// + /// The type of actor to instantiate. + /// A new instance which uses DI internally. + public Props Props() where T : ActorBase + { + return Resolver.Props(); + } + + /// + /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection + /// and others are not. + /// + /// + /// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU. + /// + /// The type of actor to instantiate. + /// A new instance which uses DI internally. + public Props Props(Type type) + { + return Resolver.Props(type); + } + + /// + /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection + /// and others are not. + /// + /// + /// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU. + /// + /// The type of actor to instantiate. + /// Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first. + /// A new instance which uses DI internally. + public Props Props(Type type, params object[] args) + { + return Resolver.Props(type, args); + } + } + + /// + /// INTERNAL API + /// + public sealed class DependencyResolverExtension : ExtensionIdProvider + { + public override DependencyResolver CreateExtension(ExtendedActorSystem system) + { + var setup = system.Settings.Setup.Get(); + 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; + } + } +} diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolverSetup.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolverSetup.cs new file mode 100644 index 00000000000..3df580095a3 --- /dev/null +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolverSetup.cs @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Actor.Setup; + +namespace Akka.DependencyInjection +{ + /// + /// Used to help bootstrap an with dependency injection (DI) + /// support via a reference. + /// + /// The 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 to create a new instance. + /// + [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); + } + } + + /// + /// Used to help bootstrap an with dependency injection (DI) + /// support via a reference. + /// + /// The 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 to create a new instance. + /// + public class DependencyResolverSetup : Setup + { + public IDependencyResolver DependencyResolver { get; } + + internal DependencyResolverSetup(IDependencyResolver dependencyResolver) + { + DependencyResolver = dependencyResolver; + } + + /// + /// Creates a new instance of DependencyResolverSetup, passing in + /// here creates an that resolves dependencies from the specified + /// + public static DependencyResolverSetup Create(IServiceProvider provider) + { + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + + return new DependencyResolverSetup(new ServiceProviderDependencyResolver(provider)); + } + + /// + /// Creates a new instance of DependencyResolverSetup, an implementation of + /// can be passed in here to resolve services from test or alternative DI frameworks. + /// + public static DependencyResolverSetup Create(IDependencyResolver provider) + { + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + + return new DependencyResolverSetup(provider); + } + } +} diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/IDependencyResolver.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/IDependencyResolver.cs new file mode 100644 index 00000000000..d221f6480db --- /dev/null +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/IDependencyResolver.cs @@ -0,0 +1,61 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2021 Lightbend Inc. +// // Copyright (C) 2013-2021 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System; +using Akka.Actor; + +namespace Akka.DependencyInjection +{ + /// + /// Interface abstraction for working with DI providers + /// in Akka.NET without being bound to any specific implementation. + /// + /// + /// See for a reference implementation. + /// + public interface IDependencyResolver + { + IResolverScope CreateScope(); + object GetService(); + object GetService(Type type); + + /// + /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection + /// and others are not. + /// + /// + /// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU. + /// + /// The type of actor to instantiate. + /// Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first. + /// A new instance which uses DI internally. + Props Props(Type type, params object[] args); + + /// + /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection + /// and others are not. + /// + /// + /// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU. + /// + /// The type of actor to instantiate. + /// A new instance which uses DI internally. + Props Props(Type type); + + /// + /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection + /// and others are not. + /// + /// + /// YOU ARE RESPONSIBLE FOR MANAGING THE LIFECYCLE OF YOUR OWN DEPENDENCIES. AKKA.NET WILL NOT ATTEMPT TO DO IT FOR YOU. + /// + /// The type of actor to instantiate. + /// Optional. Any constructor arguments that will be passed into the actor's constructor directly without being resolved by DI first. + /// A new instance which uses DI internally. + Props Props(params object[] args) where T : ActorBase; + } +} \ No newline at end of file diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs index 883730d0b27..aa652707737 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs @@ -17,6 +17,10 @@ namespace Akka.DependencyInjection /// Provides users with immediate access to the bound to /// this , if any. /// + /// + /// [OBSOLETE] Switch to the instead. + /// + [Obsolete("Replaced by the Akka.DependencyInjection.DependencyResolver in Akka.NET v1.4.20. Please switch to that.")] public sealed class ServiceProvider : IExtension { public ServiceProvider(IServiceProvider provider) @@ -66,6 +70,7 @@ public static ServiceProvider For(ActorSystem actorSystem) /// /// INTERNAL API /// + [Obsolete("Use the DependencyResolverExtensions instead.")] public sealed class ServiceProviderExtension : ExtensionIdProvider { public override ServiceProvider CreateExtension(ExtendedActorSystem system) @@ -88,17 +93,16 @@ public override ServiceProvider CreateExtension(ExtendedActorSystem system) /// /// Used to create actors via the . /// - /// the actor type - internal sealed class ServiceProviderActorProducer : IIndirectActorProducer where TActor:ActorBase + internal class ServiceProviderActorProducer : IIndirectActorProducer { private readonly IServiceProvider _provider; private readonly object[] _args; - public ServiceProviderActorProducer(IServiceProvider provider, object[] args) + public ServiceProviderActorProducer(IServiceProvider provider, Type actorType, object[] args) { _provider = provider; _args = args; - ActorType = typeof(TActor); + ActorType = actorType; } public ActorBase Produce() @@ -113,4 +117,19 @@ public void Release(ActorBase actor) // no-op } } + + /// + /// INTERNAL API + /// + /// Used to create actors via the . + /// + /// the actor type + internal class ServiceProviderActorProducer : ServiceProviderActorProducer where TActor:ActorBase + { + + public ServiceProviderActorProducer(IServiceProvider provider, object[] args) + : base(provider, typeof(TActor), args) + { + } + } } diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderDependencyResolver.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderDependencyResolver.cs new file mode 100644 index 00000000000..8fdab7c3ada --- /dev/null +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderDependencyResolver.cs @@ -0,0 +1,62 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2021 Lightbend Inc. +// // Copyright (C) 2013-2021 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Microsoft.Extensions.DependencyInjection; + +namespace Akka.DependencyInjection +{ + /// + /// INTERNAL API. + /// + /// implementation backed by + /// + public class ServiceProviderDependencyResolver : IDependencyResolver + { + public IServiceProvider ServiceProvider { get; } + + public ServiceProviderDependencyResolver(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public IResolverScope CreateScope() + { + return new ServiceProviderScope(ServiceProvider.CreateScope()); + } + + public object GetService() + { + return ServiceProvider.GetService(); + } + + public object GetService(Type type) + { + return ServiceProvider.GetService(type); + } + + public Props Props(Type type, params object[] args) + { + if(typeof(ActorBase).IsAssignableFrom(type)) + return Akka.Actor.Props.CreateBy(new ServiceProviderActorProducer(ServiceProvider, type, args)); + throw new ArgumentException(nameof(type), $"[{type}] does not implement Akka.Actor.ActorBase."); + } + + public Props Props(Type type) + { + return Props(type, Array.Empty()); + } + + public Props Props(params object[] args) where T : ActorBase + { + return Akka.Actor.Props.CreateBy(new ServiceProviderActorProducer(ServiceProvider, args)); + } + } + + +} diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderScope.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderScope.cs new file mode 100644 index 00000000000..ce3f1911dcc --- /dev/null +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderScope.cs @@ -0,0 +1,33 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2021 Lightbend Inc. +// // Copyright (C) 2013-2021 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Akka.DependencyInjection +{ + public interface IResolverScope : IDisposable + { + IDependencyResolver Resolver { get; } + } + + public class ServiceProviderScope : IResolverScope + { + private readonly IServiceScope _scope; + public IDependencyResolver Resolver { get; } + public ServiceProviderScope(IServiceScope scope) + { + _scope = scope; + Resolver = new ServiceProviderDependencyResolver(scope.ServiceProvider); + } + + public void Dispose() + { + _scope?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderSetup.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderSetup.cs deleted file mode 100644 index 835d1ad98f3..00000000000 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderSetup.cs +++ /dev/null @@ -1,40 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2021 Lightbend Inc. -// Copyright (C) 2013-2021 .NET Foundation -// -//----------------------------------------------------------------------- - -using System; -using Akka.Actor; -using Akka.Actor.Setup; - -namespace Akka.DependencyInjection -{ - /// - /// Used to help bootstrap an with dependency injection (DI) - /// support via a reference. - /// - /// The 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 to create a new instance. - /// - 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); - } - } -} diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index 8aee29b1c71..cde266ae92e 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -115,6 +115,7 @@ namespace Akka.Actor protected bool SetChildrenTerminationReason(Akka.Actor.Internal.SuspendReason reason) { } public void SetReceiveTimeout(System.Nullable timeout = null) { } protected void SetTerminated() { } + [System.ObsoleteAttribute("Not used. Will be removed in Akka.NET v1.5.")] public static Akka.Actor.NameAndUid SplitNameAndUid(string name) { } public virtual void Start() { } protected void Stash(Akka.Dispatch.SysMsg.SystemMessage msg) { } @@ -1325,6 +1326,7 @@ namespace Akka.Actor public override void Suspend() { } protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { } } + [System.ObsoleteAttribute("Not used. Will be removed in Akka.NET v1.5.")] public class NameAndUid { public NameAndUid(string name, int uid) { } diff --git a/src/core/Akka.Remote/RemoteSystemDaemon.cs b/src/core/Akka.Remote/RemoteSystemDaemon.cs index 7393d0aa5b6..1a820b62fb3 100644 --- a/src/core/Akka.Remote/RemoteSystemDaemon.cs +++ b/src/core/Akka.Remote/RemoteSystemDaemon.cs @@ -308,10 +308,10 @@ public override IActorRef GetChild(IEnumerable name) var n = 0; while (true) { - var nameAndUid = ActorCell.SplitNameAndUid(path); - if (TryGetChild(nameAndUid.Name, out var child)) + var (s, uid) = ActorCell.GetNameAndUid(path); + if (TryGetChild(s, out var child)) { - if (nameAndUid.Uid != ActorCell.UndefinedUid && nameAndUid.Uid != child.Path.Uid) + if (uid != ActorCell.UndefinedUid && uid != child.Path.Uid) return Nobody.Instance; return n == 0 ? child : child.GetChild(name.TakeRight(n)); } diff --git a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs index f5b0e9b5111..e2a99fde2f3 100644 --- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs +++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs @@ -25,7 +25,7 @@ internal static class FastHash /// A 32-bit pseudo-random hash value. public static int OfString(string s) { - var chars = s.ToCharArray(); + var chars = s.AsSpan(); var s0 = 391408L; // seed value 1, DON'T CHANGE var s1 = 601258L; // seed value 2, DON'T CHANGE unchecked diff --git a/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs b/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs index 89a863597a3..49f6152c646 100644 --- a/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs +++ b/src/core/Akka.Tests/Actor/ActorSelectionSpec.cs @@ -76,12 +76,10 @@ private IActorRef AskNode(IActorRef node, IQuery query) { var result = node.Ask(query).Result; - var actorRef = result as IActorRef; - if (actorRef != null) + if (result is IActorRef actorRef) return actorRef; - var selection = result as ActorSelection; - return selection != null ? Identify(selection) : null; + return result is ActorSelection selection ? Identify(selection) : null; } [Fact] diff --git a/src/core/Akka/Actor/ActorCell.Children.cs b/src/core/Akka/Actor/ActorCell.Children.cs index cc38364269d..9c69a74df31 100644 --- a/src/core/Akka/Actor/ActorCell.Children.cs +++ b/src/core/Akka/Actor/ActorCell.Children.cs @@ -387,17 +387,16 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) } else { - var nameAndUid = SplitNameAndUid(name); - if (TryGetChildRestartStatsByName(nameAndUid.Name, out var stats)) + var (s, uid) = GetNameAndUid(name); + if (TryGetChildRestartStatsByName(s, out var stats)) { - var uid = nameAndUid.Uid; if (uid == ActorCell.UndefinedUid || uid == stats.Uid) { child = stats.Child; return true; } } - else if (TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) + else if (TryGetFunctionRef(s, uid, out var functionRef)) { child = functionRef; return true; diff --git a/src/core/Akka/Actor/ActorCell.cs b/src/core/Akka/Actor/ActorCell.cs index 13b5a00e6b7..0894b4707e5 100644 --- a/src/core/Akka/Actor/ActorCell.cs +++ b/src/core/Akka/Actor/ActorCell.cs @@ -479,10 +479,11 @@ protected void SetActorFields(ActorBase actor) actor?.Unclear(); } /// - /// TBD + /// INTERNAL API /// /// TBD /// TBD + [Obsolete("Not used. Will be removed in Akka.NET v1.5.")] public static NameAndUid SplitNameAndUid(string name) { var i = name.IndexOf('#'); @@ -491,6 +492,19 @@ public static NameAndUid SplitNameAndUid(string name) : new NameAndUid(name.Substring(0, i), Int32.Parse(name.Substring(i + 1))); } + /// + /// INTERNAL API + /// + /// The full name of the actor, including the UID if known + /// A new (string name, int uid) instance. + internal static (string name, int uid) GetNameAndUid(string name) + { + var i = name.IndexOf('#'); + return i < 0 + ? (name, UndefinedUid) + : (name.Substring(0, i), SpanHacks.Parse(name.AsSpan(i + 1))); + } + /// /// TBD /// diff --git a/src/core/Akka/Actor/ActorPath.cs b/src/core/Akka/Actor/ActorPath.cs index db2a0488988..9c3fc0dfbfd 100644 --- a/src/core/Akka/Actor/ActorPath.cs +++ b/src/core/Akka/Actor/ActorPath.cs @@ -276,8 +276,8 @@ public bool Equals(ActorPath other) /// A newly created public static ActorPath operator /(ActorPath path, string name) { - var nameAndUid = ActorCell.SplitNameAndUid(name); - return new ChildActorPath(path, nameAndUid.Name, nameAndUid.Uid); + var (s, uid) = ActorCell.GetNameAndUid(name); + return new ChildActorPath(path, s, uid); } /// @@ -327,74 +327,173 @@ public static bool TryParse(string path, out ActorPath actorPath) { actorPath = null; + if (!TryParseAddress(path, out var address, out var absoluteUri)) return false; + var spanified = absoluteUri; - Address address; - Uri uri; - if (!TryParseAddress(path, out address, out uri)) return false; - var pathElements = uri.AbsolutePath.Split('/'); - actorPath = new RootActorPath(address) / pathElements.Skip(1); - if (uri.Fragment.StartsWith("#")) + // check for Uri fragment here + var fragment = ReadOnlySpan.Empty; + var fragLoc = spanified.IndexOf('#'); + if (fragLoc > -1) { - var uid = int.Parse(uri.Fragment.Substring(1)); + fragment = spanified.Slice(fragLoc + 1); + spanified = spanified.Slice(0, fragLoc); + } + var nextSlash = 0; + + actorPath = new RootActorPath(address); + + do + { + nextSlash = spanified.IndexOf('/'); + if (nextSlash > 0) + { + actorPath /= spanified.Slice(0, nextSlash).ToString(); + } + else if (nextSlash < 0 && spanified.Length > 0) + { + actorPath /= spanified.ToString(); + } + + spanified = spanified.Slice(nextSlash + 1); + } while (nextSlash >= 0); + + if (!fragment.IsEmpty) + { + var uid = SpanHacks.Parse(fragment); actorPath = actorPath.WithUid(uid); } return true; } /// - /// TBD + /// Attempts to parse an from a stringified . /// - /// TBD - /// TBD - /// TBD + /// The string representation of the . + /// If true, the parsed . Otherwise null. + /// true if the could be parsed, false otherwise. public static bool TryParseAddress(string path, out Address address) { - Uri uri; - return TryParseAddress(path, out address, out uri); + return TryParseAddress(path, out address, out var _); } - private static bool TryParseAddress(string path, out Address address, out Uri uri) + /// + /// Attempts to parse an from a stringified . + /// + /// The string representation of the . + /// If true, the parsed . Otherwise null. + /// A containing the path following the address. + /// true if the could be parsed, false otherwise. + private static bool TryParseAddress(string path, out Address address, out ReadOnlySpan absoluteUri) { - //This code corresponds to AddressFromURIString.unapply address = null; - if (!Uri.TryCreate(path, UriKind.Absolute, out uri)) + + var spanified = path.AsSpan(); + absoluteUri = spanified; + + var firstColonPos = spanified.IndexOf(':'); + + if (firstColonPos == -1) // not an absolute Uri return false; - var protocol = uri.Scheme; //Typically "akka" - if (!protocol.StartsWith("akka", StringComparison.OrdinalIgnoreCase)) - { - // Protocol must start with 'akka.* + + var fullScheme = SpanHacks.ToLowerInvariant(spanified.Slice(0, firstColonPos)); + if (!fullScheme.StartsWith("akka")) return false; + + spanified = spanified.Slice(firstColonPos + 1); + if (spanified.Length < 2 || !(spanified[0] == '/' && spanified[1] == '/')) + return false; + + spanified = spanified.Slice(2); // move past the double // + var firstAtPos = spanified.IndexOf('@'); + var sysName = string.Empty; + + if (firstAtPos == -1) + { // dealing with an absolute local Uri + var nextSlash = spanified.IndexOf('/'); + + if (nextSlash == -1) + { + sysName = spanified.ToString(); + absoluteUri = "/".AsSpan(); // RELY ON THE JIT + } + else + { + sysName = spanified.Slice(0, nextSlash).ToString(); + absoluteUri = spanified.Slice(nextSlash); + } + + address = new Address(fullScheme, sysName); + return true; } + // dealing with a remote Uri + sysName = spanified.Slice(0, firstAtPos).ToString(); + spanified = spanified.Slice(firstAtPos + 1); + + /* + * Need to check for: + * - IPV4 / hostnames + * - IPV6 (must be surrounded by '[]') according to spec. + */ + var host = string.Empty; + + // check for IPV6 first + var openBracket = spanified.IndexOf('['); + var closeBracket = spanified.IndexOf(']'); + if (openBracket > -1 && closeBracket > openBracket) + { + // found an IPV6 address + host = spanified.Slice(openBracket, closeBracket - openBracket + 1).ToString(); + spanified = spanified.Slice(closeBracket + 1); // advance past the address + + // need to check for trailing colon + var secondColonPos = spanified.IndexOf(':'); + if (secondColonPos == -1) + return false; - string systemName; - string host = null; - int? port = null; - if (IsNullOrEmpty(uri.UserInfo)) + spanified = spanified.Slice(secondColonPos + 1); + } + else { - // protocol://SystemName/Path1/Path2 - if (uri.Port > 0) - { - //port may not be specified for these types of paths + var secondColonPos = spanified.IndexOf(':'); + if (secondColonPos == -1) return false; - } - //System name is in the "host" position. According to rfc3986 host is case - //insensitive, but should be produced as lowercase, so if we use uri.Host - //we'll get it in lower case. - //So we'll extract it ourselves using the original path. - //We skip the protocol and "://" - var systemNameLength = uri.Host.Length; - systemName = path.Substring(protocol.Length + 3, systemNameLength); + + host = spanified.Slice(0, secondColonPos).ToString(); + + // move past the host + spanified = spanified.Slice(secondColonPos + 1); + } + + var actorPathSlash = spanified.IndexOf('/'); + ReadOnlySpan strPort; + if (actorPathSlash == -1) + { + strPort = spanified; } else { - // protocol://SystemName@Host:port/Path1/Path2 - systemName = uri.UserInfo; - host = uri.Host; - port = uri.Port; + strPort = spanified.Slice(0, actorPathSlash); } - address = new Address(protocol, systemName, host, port); - return true; + + if (SpanHacks.TryParse(strPort, out var port)) + { + address = new Address(fullScheme, sysName, host, port); + + // need to compute the absolute path after the Address + if (actorPathSlash == -1) + { + absoluteUri = "/".AsSpan(); + } + else + { + absoluteUri = spanified.Slice(actorPathSlash); + } + + return true; + } + + return false; } diff --git a/src/core/Akka/Actor/NameAndUid.cs b/src/core/Akka/Actor/NameAndUid.cs index 104a45d4a77..fdef5823b4f 100644 --- a/src/core/Akka/Actor/NameAndUid.cs +++ b/src/core/Akka/Actor/NameAndUid.cs @@ -5,11 +5,14 @@ // //----------------------------------------------------------------------- +using System; + namespace Akka.Actor { /// - /// TBD + /// INTERNAL API /// + [Obsolete("Not used. Will be removed in Akka.NET v1.5.")] public class NameAndUid { private readonly string _name; diff --git a/src/core/Akka/Actor/RepointableActorRef.cs b/src/core/Akka/Actor/RepointableActorRef.cs index c0e3773a8d9..6933f23da21 100644 --- a/src/core/Akka/Actor/RepointableActorRef.cs +++ b/src/core/Akka/Actor/RepointableActorRef.cs @@ -292,12 +292,10 @@ public override IActorRef GetChild(IEnumerable name) case "": return ActorRefs.Nobody; default: - var nameAndUid = ActorCell.SplitNameAndUid(next); - if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out var stats)) + var (s, uid) = ActorCell.GetNameAndUid(next); + if (Lookup.TryGetChildStatsByName(s, out var stats)) { - var crs = stats as ChildRestartStats; - var uid = nameAndUid.Uid; - if (crs != null && (uid == ActorCell.UndefinedUid || uid == crs.Uid)) + if (stats is ChildRestartStats crs && (uid == ActorCell.UndefinedUid || uid == crs.Uid)) { if (name.Skip(1).Any()) return crs.Child.GetChild(name.Skip(1)); @@ -305,7 +303,7 @@ public override IActorRef GetChild(IEnumerable name) return crs.Child; } } - else if (Lookup is ActorCell cell && cell.TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) + else if (Lookup is ActorCell cell && cell.TryGetFunctionRef(s, uid, out var functionRef)) { return functionRef; } diff --git a/src/core/Akka/Util/SpanHacks.cs b/src/core/Akka/Util/SpanHacks.cs new file mode 100644 index 00000000000..08464610126 --- /dev/null +++ b/src/core/Akka/Util/SpanHacks.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Akka.Util +{ + /// + /// INTERNAL API. + /// + /// polyfills that should be deleted once we drop .NET Standard 2.0 support. + /// + internal static class SpanHacks + { + public static bool IsNumeric(char x) + { + return (x >= '0' && x <= '9'); + } + + /// + /// Parses an integer from a string. + /// + /// + /// PERFORMS NO INPUT VALIDATION. + /// + /// The span of input characters. + /// An . + public static int Parse(ReadOnlySpan str) + { + if (TryParse(str, out var i)) + return i; + throw new FormatException($"[{str.ToString()}] is now a valid numeric format"); + } + + /// + /// Parses an integer from a string. + /// + /// + /// PERFORMS NO INPUT VALIDATION. + /// + /// The span of input characters. + /// The parsed integer, if any. + /// An . + public static bool TryParse(ReadOnlySpan str, out int returnValue) + { + var pos = 0; + returnValue = 0; + var sign = 1; + if (str[0] == '-') + { + sign = -1; + pos++; + } + + for (; pos < str.Length; pos++) + { + if (!IsNumeric(str[pos])) + return false; + returnValue = returnValue * 10 + str[pos] - '0'; + } + + returnValue = sign * returnValue; + + return true; + } + + /// + /// Performs without having to + /// allocate a new first. + /// + /// The set of characters to be lower-cased + /// A new string. + public static string ToLowerInvariant(ReadOnlySpan input) + { + Span output = stackalloc char[input.Length]; + for (var i = 0; i < input.Length; i++) + { + output[i] = char.ToLowerInvariant(input[i]); + } + return output.ToString(); + } + } +} diff --git a/src/examples/AspNetCore/Samples.Akka.AspNetCore/Actors/AkkaService.cs b/src/examples/AspNetCore/Samples.Akka.AspNetCore/Actors/AkkaService.cs index 98bb914016e..efb40de587d 100644 --- a/src/examples/AspNetCore/Samples.Akka.AspNetCore/Actors/AkkaService.cs +++ b/src/examples/AspNetCore/Samples.Akka.AspNetCore/Actors/AkkaService.cs @@ -18,7 +18,6 @@ using Microsoft.Extensions.Hosting; using Samples.Akka.AspNetCore.Messages; using Samples.Akka.AspNetCore.Services; -using ServiceProvider = Akka.DependencyInjection.ServiceProvider; namespace Samples.Akka.AspNetCore.Actors { @@ -41,14 +40,14 @@ public async Task StartAsync(CancellationToken cancellationToken) { var hocon = ConfigurationFactory.ParseString(await File.ReadAllTextAsync("app.conf", cancellationToken)); var bootstrap = BootstrapSetup.Create().WithConfig(hocon); - var di = ServiceProviderSetup.Create(_sp); + var di = DependencyResolverSetup.Create(_sp); var actorSystemSetup = bootstrap.And(di); _actorSystem = ActorSystem.Create("AspNetDemo", actorSystemSetup); // // // props created via IServiceProvider dependency injection - var hasherProps = ServiceProvider.For(_actorSystem).Props(); + var hasherProps = DependencyResolver.For(_actorSystem).Props(); RouterActor = _actorSystem.ActorOf(hasherProps.WithRouter(FromConfig.Instance), "hasher"); //