diff --git a/BREAKING_CHANGES_V1.6.md b/BREAKING_CHANGES_V1.6.md index e2fbc50b313..30f95de1c08 100644 --- a/BREAKING_CHANGES_V1.6.md +++ b/BREAKING_CHANGES_V1.6.md @@ -40,6 +40,7 @@ of `Behavior`, `Wire`, `API` (combine with `+`). | Status | PR / Branch | Component | Type | Change | Migration | |--------|-------------|-----------|------|--------|-----------| +| Planned | `aot/m1-c-props-and-loggers` | `Akka` (core / `Akka.Actor.Props`, `Akka.Event.LoggingBus`), `Akka.DependencyInjection` | Behavior + API | Two changes. (1) The whole `Props` actor-type flow now carries one trimming contract, `[DynamicallyAccessedMembers(PublicConstructors \| Interfaces)]`, so the trimmer keeps the constructor `Activator` calls and the `IRequiresMessageQueue<>` interfaces the mailbox requirement check reads off the actor type: the `Type`-taking `Props` constructors, `Props.Type`, `Props.Create(Type, params object[])`, all three `Props.Create` overloads, `Props.CreateBy`, `IIndirectActorProducer.ActorType`, `ActorRefFactoryExtensions.ActorOf`, `ExtendedActorSystem.SystemActorOf` (and the `ActorSystemImpl` override), plus -- in `Akka.DependencyInjection` -- the three `IDependencyResolver.Props` members and their implementations on `DependencyResolver`, `ServiceProvider` and `ServiceProviderDependencyResolver`, and the internal `ServiceProviderActorProducer`. Separately, `ActorSystemWithExtensions.WithExtension(ActorSystem, Type)` and `WithExtension` gain `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]` -- the narrower contract, because those take an `IExtensionId`, not an actor type, and call the parameterless `Activator.CreateInstance`. No runtime behavior changes; the attributes are visible only to trim analysis and change no signature a compiler binds to. (2) `LoggingBus.StartDefaultLoggers` resolves `akka.loggers` from new built-in name tables (all three loggers Akka.dll ships -- `Akka.Event.DefaultLogger`, `Akka.Event.StandardOutLogger` and `Akka.Event.TraceLogger` -- each under three spellings: the bare name core's own `akka.conf` ships, the short assembly-qualified name that `TraceLogger`'s own documentation tells users to write, and the full versioned assembly-qualified name Akka.Hosting writes) before falling back to `Type.GetType`, and that fallback only runs while dynamic type loading is on. With the switch on, which is what every existing application gets, the same config resolves to the same logger as before, for every input. With the switch off, only the names in the table resolve: any other logger throws a `ConfigurationException` naming `akka.loggers`, the value and the switch, where it previously threw `ConfigurationException` "Logger specified in config cannot be found". | None at the default -- the switch is on unless a project file explicitly turns it off. A trimmed or Native AOT application that turns the switch off must leave `akka.loggers` at the built-in loggers. The new attributes need no source change, and nothing warns unless a project sets `EnableTrimAnalyzer` / `IsTrimmable` / `IsAotCompatible` -- no project in this repository does yet. Once one does: passing an unannotated `Type` into any of these members reports `IL2072` when it came from a method return or a field, and `IL2067` when it came from one of the caller's own parameters; an unannotated generic argument reports `IL2091`. Fix either by annotating the `Type` or type parameter it flows from with `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors \| DynamicallyAccessedMemberTypes.Interfaces)]`, or by passing a `typeof(...)`. Because the attribute is part of the contract, an *implementor* has to repeat it verbatim rather than flow it: a class implementing `IIndirectActorProducer.ActorType` or any of the three `IDependencyResolver.Props` members reports `IL2092` / `IL2093` (parameter and return annotation mismatch) or `IL2095` (type-parameter mismatch) until it carries the identical attribute, and so does an override of `ExtendedActorSystem.SystemActorOf`. | | Planned | `aot/m1-b-mailboxes` | `Akka` (core / `Akka.Dispatch.Mailboxes`) | Behavior + API | `Mailboxes` resolves the mailbox names core's own `akka.conf` ships from two built-in tables (`BuiltInMailboxTypes`, `BuiltInMessageQueueSemantics`) instead of `Type.GetType`, so the trimmer and the Native AOT compiler can see which type is loaded. Two observable changes. (1) A built-in `mailbox-type` is constructed directly, so when its constructor rejects its config the `ArgumentException` from `Mailboxes.Lookup` carries that constructor's own exception as `InnerException` instead of the `TargetInvocationException` `Activator.CreateInstance` wrapped it in, and names the configured spelling (e.g. `Akka.Dispatch.BoundedMailbox, Akka`) where it previously named `Type.ToString()`; relatedly, the non-zero-push-timeout warning now runs outside that `try`, so an exception from a custom `IProducesPushTimeoutSemanticsMailbox.PushTimeout` getter propagates as itself rather than wrapped. (2) With the `Akka.DynamicTypeLoading` switch **off**, a key under `akka.actor.mailbox.requirements` that is neither built in nor resolvable now throws a `ConfigurationException` out of `ActorSystem.Create`, where the switch-on path still logs a warning and skips that one binding; an unknown `mailbox-type` or `mailbox-requirement` likewise throws a `ConfigurationException` naming the setting, the value and the switch, in place of the previous `ConfigurationException` / `TypeLoadException`. With the switch on - the default, and what every existing application gets - every other input resolves to the same mailbox as before. Separately, `MailboxType` and the `Type` parameters of `Mailboxes.HasRequiredType`, `Mailboxes.ProducesMessageQueue` and `Mailboxes.GetRequiredType` gain `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]` so that trimming keeps `IProducesMessageQueue<>` / `IRequiresMessageQueue<>` on subclasses. | None at the default -- the switch is on unless a project file explicitly turns it off. Code that caught `TargetInvocationException` around a rejected `mailbox-capacity` / `mailbox-push-timeout-time` should catch the inner exception type instead, and code asserting on the exact `Cannot instantiate MailboxType ...` text should expect the configured spelling for the five built-in types. A trimmed or Native AOT application that turns the switch off must keep `mailbox-type`, `mailbox-requirement` and every key under `akka.actor.mailbox.requirements` at built-in values. The new attributes are visible only to trim analysis -- they change no signature a compiler binds to -- but a caller that has trim analysis enabled and passes an unannotated `Type` into those three methods now gets `IL2072` at its own call site: annotate the `Type` it flows from with `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]`, or pass a `typeof(...)`. | | Planned | `aot/m1-a-feature-switch-and-first-tables` | `Akka` (core / actor system bootstrap) | Behavior | Adds the internal trimming / Native AOT feature switch `Akka.DynamicTypeLoading` (default **on**) and puts built-in lookup tables in front of the first three HOCON type-name sites: `akka.stdout-logger-class` and `akka.logger-formatter` in `Settings`, and `akka.scheduler.implementation` in `ActorSystemImpl.ConfigureScheduler`. Each site now checks a `BuiltIn*` table before falling back to `Type.GetType`. Each table carries two spellings of every name it knows -- the bare `Ns.T` and the `Ns.T, Akka` form, which is what core's own `akka.conf` uses -- and the lookup first runs the configured value through the new internal `Akka.Util.TypeExtensions.StripAssemblyIdentity`, which removes the `Version=`, `Culture=`, `PublicKeyToken=`, `ProcessorArchitecture=`, `Retargetable=` and `ContentType=` components. With the switch on, which is what every existing application gets, the same config resolves to the same type as before, with two narrow differences: (1) a built-in name is constructed directly, so a constructor that rejects its config (e.g. `akka.scheduler.tick-duration` below 10 ms, which `HashedWheelTimerScheduler` refuses) now surfaces its own `ArgumentOutOfRangeException` / `ConfigurationException` instead of the `TargetInvocationException` that `Activator.CreateInstance` wrapped it in; (2) a built-in type name written with a full assembly-qualified name of a different Akka version (what `Akka.Hosting` writes into HOCON, carried across an upgrade) now resolves where it previously threw. `Type.GetType` rejects the version mismatch, so on `dev` such a value fails in both switch states -- measured: `ArgumentException("Could not load type of ...")` for `akka.logger-formatter`/`akka.stdout-logger-class`, and `FileNotFoundException` for `akka.scheduler.implementation`, whose `Type.GetType` call passes `throwOnError: true`. Custom (non-built-in) names are unchanged, since the reflection fallback still receives the raw value. An application that turns the switch off with `` gets only the names in the tables: anything else throws a `ConfigurationException` naming the setting, the value and the switch, where it previously threw `TypeLoadException` (scheduler) or `ArgumentException` (stdout logger, log formatter). | None at the default -- the switch is on unless a project file explicitly turns it off. Code that caught `TargetInvocationException` around `ActorSystem.Create` to read a rejected `akka.scheduler.*` value should catch the inner exception type instead. A trimmed or Native AOT application that turns the switch off must leave these three settings at their built-in values. | | Planned | `fix/aot-provider-constant-type-names` | `Akka` (core / actor system bootstrap) | Behavior | `ActorSystemImpl` now resolves the three built-in actor ref providers from the compile-time constant type names on `ProviderSelection`, with the `Type.GetType` call inlined at each site, so the trimmer and the Native AOT compiler can see which type is loaded and keep it. Two observable consequences: (1) `ProviderSelection.GetProvider` now maps the local provider's type name -- both the bare `"Akka.Actor.LocalActorRefProvider"` that ships in `akka.conf` and the assembly-qualified `"Akka.Actor.LocalActorRefProvider, Akka"` -- onto `ProviderSelection.Local` instead of letting it fall through to `Custom`, so `Settings.ProviderClass` on a default boot now reports `"Akka.Actor.LocalActorRefProvider, Akka"` where it previously reported the bare `"Akka.Actor.LocalActorRefProvider"`. (2) `Settings` only validates `ProviderClass` through `Type.GetType` when the selection is `ProviderSelection.Custom`; the built-in three are validated where they are constructed, and a missing `Akka.Remote` / `Akka.Cluster` reference now throws `ConfigurationException("akka.actor.provider = cluster, but Akka.Cluster is not referenced by this application.")` from `ActorSystemImpl` rather than the generic "not a valid type name" message from `Settings`. Which provider gets constructed for any given config is unchanged. | None for normal use. Code that compares `Settings.ProviderClass` against the bare string `"Akka.Actor.LocalActorRefProvider"` should compare against `ProviderSelection.Local.Instance.Fqn`, or test `Settings.ProviderSelectionType is ProviderSelection.Local`. Code that asserted on the exact text of the misconfiguration exception for `provider = remote` / `provider = cluster` should expect the new message. | diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolver.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolver.cs index ac014f1b94f..749b617b885 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolver.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/DependencyResolver.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Diagnostics.CodeAnalysis; using Akka.Actor; using Akka.Configuration; using Akka.Event; @@ -57,7 +58,7 @@ public static DependencyResolver For(ActorSystem actorSystem) /// 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 + public Props Props<[DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] T>(params object[] args) where T : ActorBase { return Resolver.Props(args); } @@ -71,7 +72,7 @@ public Props Props(params object[] args) where T : ActorBase /// /// The type of actor to instantiate. /// A new instance which uses DI internally. - public Props Props() where T : ActorBase + public Props Props<[DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] T>() where T : ActorBase { return Resolver.Props(); } @@ -85,7 +86,7 @@ public Props Props() where T : ActorBase /// /// The type of actor to instantiate. /// A new instance which uses DI internally. - public Props Props(Type type) + public Props Props([DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] Type type) { return Resolver.Props(type); } @@ -100,7 +101,7 @@ public Props Props(Type type) /// 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) + public Props Props([DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] Type type, params object[] args) { return Resolver.Props(type, args); } diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/IDependencyResolver.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/IDependencyResolver.cs index 1ff0f448e85..d10df610952 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection/IDependencyResolver.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/IDependencyResolver.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Diagnostics.CodeAnalysis; using Akka.Actor; namespace Akka.DependencyInjection @@ -33,7 +34,7 @@ public interface IDependencyResolver /// 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); + Props Props([DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] Type type, params object[] args); /// /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection @@ -44,7 +45,7 @@ public interface IDependencyResolver /// /// The type of actor to instantiate. /// A new instance which uses DI internally. - Props Props(Type type); + Props Props([DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] Type type); /// /// Used to dynamically instantiate an actor where some of the constructor arguments are populated via dependency injection @@ -56,6 +57,6 @@ public interface IDependencyResolver /// 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; + Props Props<[DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] T>(params object[] args) where T : ActorBase; } } diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs index 5238ccc607e..d87437dbd37 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProvider.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Diagnostics.CodeAnalysis; using Akka.Actor; using Akka.Configuration; using Akka.Event; @@ -61,7 +62,7 @@ public static ServiceProvider For(ActorSystem actorSystem) /// 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 + public Props Props<[DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] T>(params object[] args) where T : ActorBase { return Akka.Actor.Props.CreateBy(new ServiceProviderActorProducer(Provider, args)); } @@ -98,7 +99,7 @@ internal class ServiceProviderActorProducer : IIndirectActorProducer private readonly IServiceProvider _provider; private readonly object[] _args; - public ServiceProviderActorProducer(IServiceProvider provider, Type actorType, object[] args) + public ServiceProviderActorProducer(IServiceProvider provider, [DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] Type actorType, object[] args) { _provider = provider; _args = args; @@ -107,9 +108,12 @@ public ServiceProviderActorProducer(IServiceProvider provider, Type actorType, o public ActorBase Produce() { + // ActivatorUtilities.CreateInstance picks a public constructor, which the PublicConstructors half of + // Akka.Actor.Props.ActorTypeMembers keeps; the Interfaces half is what the mailbox requirement check needs. return (ActorBase)ActivatorUtilities.CreateInstance(_provider, ActorType, _args); } + [DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] public Type ActorType { get; } public void Release(ActorBase actor) @@ -124,7 +128,7 @@ public void Release(ActorBase actor) /// Used to create actors via the . /// /// the actor type - internal class ServiceProviderActorProducer : ServiceProviderActorProducer where TActor:ActorBase + internal class ServiceProviderActorProducer<[DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] TActor> : ServiceProviderActorProducer where TActor : ActorBase { public ServiceProviderActorProducer(IServiceProvider provider, object[] args) diff --git a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderDependencyResolver.cs b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderDependencyResolver.cs index be40562131c..42f7ac66eb8 100644 --- a/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderDependencyResolver.cs +++ b/src/contrib/dependencyinjection/Akka.DependencyInjection/ServiceProviderDependencyResolver.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Diagnostics.CodeAnalysis; using Akka.Actor; using Microsoft.Extensions.DependencyInjection; @@ -40,19 +41,19 @@ public object GetService(Type type) return ServiceProvider.GetService(type); } - public Props Props(Type type, params object[] args) + public Props Props([DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] 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) + public Props Props([DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] Type type) { return Props(type, Array.Empty()); } - public Props Props(params object[] args) where T : ActorBase + public Props Props<[DynamicallyAccessedMembers(Akka.Actor.Props.ActorTypeMembers)] T>(params object[] args) where T : ActorBase { return Akka.Actor.Props.CreateBy(new ServiceProviderActorProducer(ServiceProvider, args)); } diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index 20b59a6381b..3c2c126dca8 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -297,7 +297,7 @@ namespace Akka.Actor } public static class ActorRefFactoryExtensions { - public static Akka.Actor.IActorRef ActorOf(this Akka.Actor.IActorRefFactory factory, string name = null) + public static Akka.Actor.IActorRef ActorOf<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TActor>(this Akka.Actor.IActorRefFactory factory, string name = null) where TActor : Akka.Actor.ActorBase, new () { } public static Akka.Actor.ActorSelection ActorSelection(this Akka.Actor.IActorRefFactory factory, Akka.Actor.IActorRef anchorRef, string actorPath) { } } @@ -422,9 +422,9 @@ namespace Akka.Actor { public static T WithExtension(this Akka.Actor.ActorSystem system) where T : class, Akka.Actor.IExtension { } - public static T WithExtension(this Akka.Actor.ActorSystem system, System.Type extensionId) + public static T WithExtension(this Akka.Actor.ActorSystem system, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] System.Type extensionId) where T : class, Akka.Actor.IExtension { } - public static T WithExtension(this Akka.Actor.ActorSystem system) + public static T WithExtension(this Akka.Actor.ActorSystem system) where T : class, Akka.Actor.IExtension where TI : Akka.Actor.IExtensionId { } } @@ -759,7 +759,7 @@ namespace Akka.Actor public abstract void Abort(); public abstract string PrintTree(); public abstract Akka.Actor.IActorRef SystemActorOf(Akka.Actor.Props props, string name = null); - public abstract Akka.Actor.IActorRef SystemActorOf(string name = null) + public abstract Akka.Actor.IActorRef SystemActorOf<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TActor>(string name = null) where TActor : Akka.Actor.ActorBase, new (); } public abstract class ExtensionIdProvider : Akka.Actor.IExtensionId, Akka.Actor.IExtensionId @@ -1124,6 +1124,7 @@ namespace Akka.Actor } public interface IIndirectActorProducer { + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type ActorType { get; } Akka.Actor.ActorBase Produce(); void Release(Akka.Actor.ActorBase actor); @@ -1483,12 +1484,12 @@ namespace Akka.Actor public static readonly Akka.Actor.Props None; protected Props() { } protected Props(Akka.Actor.Props copy) { } - public Props(System.Type type) { } - public Props(System.Type type, object[] args) { } - public Props(Akka.Actor.Deploy deploy, System.Type type, System.Collections.Generic.IEnumerable args) { } - public Props(Akka.Actor.Deploy deploy, System.Type type, params object[] args) { } - public Props(System.Type type, Akka.Actor.SupervisorStrategy supervisorStrategy, System.Collections.Generic.IEnumerable args) { } - public Props(System.Type type, Akka.Actor.SupervisorStrategy supervisorStrategy, params object[] args) { } + public Props([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type type) { } + public Props([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type type, object[] args) { } + public Props(Akka.Actor.Deploy deploy, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type type, System.Collections.Generic.IEnumerable args) { } + public Props(Akka.Actor.Deploy deploy, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type type, params object[] args) { } + public Props([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type type, Akka.Actor.SupervisorStrategy supervisorStrategy, System.Collections.Generic.IEnumerable args) { } + public Props([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type type, Akka.Actor.SupervisorStrategy supervisorStrategy, params object[] args) { } public object[] Arguments { get; } public Akka.Actor.Deploy Deploy { get; protected set; } [Newtonsoft.Json.JsonIgnore] @@ -1499,6 +1500,7 @@ namespace Akka.Actor public Akka.Routing.RouterConfig RouterConfig { get; } public Akka.Actor.SupervisorStrategy SupervisorStrategy { get; protected set; } [Newtonsoft.Json.JsonIgnore] + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type Type { get; } public string TypeName { get; } public static Akka.Actor.Props Empty { get; } @@ -1514,17 +1516,17 @@ namespace Akka.Actor public Akka.Actor.Props WithRouter(Akka.Routing.RouterConfig routerConfig) { } public Akka.Actor.Props WithStashCapacity(int stashCapacity) { } public Akka.Actor.Props WithSupervisorStrategy(Akka.Actor.SupervisorStrategy supervisorStrategy) { } - public static Akka.Actor.Props Create(System.Type type, params object[] args) { } - public static Akka.Actor.Props Create(Akka.Actor.SupervisorStrategy supervisorStrategy) + public static Akka.Actor.Props Create([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type type, params object[] args) { } + public static Akka.Actor.Props Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TActor>(Akka.Actor.SupervisorStrategy supervisorStrategy) where TActor : Akka.Actor.ActorBase, new () { } - public static Akka.Actor.Props Create(params object[] args) + public static Akka.Actor.Props Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TActor>(params object[] args) where TActor : Akka.Actor.ActorBase { } - public static Akka.Actor.Props Create(System.Linq.Expressions.Expression> factory, Akka.Actor.SupervisorStrategy supervisorStrategy = null) + public static Akka.Actor.Props Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TActor>(System.Linq.Expressions.Expression> factory, Akka.Actor.SupervisorStrategy supervisorStrategy = null) where TActor : Akka.Actor.ActorBase { } public static Akka.Actor.Props CreateBy(Akka.Actor.IIndirectActorProducer producer, params object[] args) { } [System.Obsolete("Do not use this method. Call CreateBy(IIndirectActorProducer, params object[] arg" + "s) instead")] - public static Akka.Actor.Props CreateBy(params object[] args) + public static Akka.Actor.Props CreateBy<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TProducer>(params object[] args) where TProducer : class, Akka.Actor.IIndirectActorProducer { } public class PropsSurrogate : Akka.Util.ISurrogate { @@ -2040,7 +2042,7 @@ namespace Akka.Actor.Internal public void Start() { } public override void Stop(Akka.Actor.IActorRef actor) { } public override Akka.Actor.IActorRef SystemActorOf(Akka.Actor.Props props, string name = null) { } - public override Akka.Actor.IActorRef SystemActorOf(string name = null) + public override Akka.Actor.IActorRef SystemActorOf<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TActor>(string name = null) where TActor : Akka.Actor.ActorBase, new () { } public override System.Threading.Tasks.Task Terminate() { } public override string ToString() { } diff --git a/src/core/Akka.Tests/Event/BuiltInLoggersSpec.cs b/src/core/Akka.Tests/Event/BuiltInLoggersSpec.cs new file mode 100644 index 00000000000..424e5bef820 --- /dev/null +++ b/src/core/Akka.Tests/Event/BuiltInLoggersSpec.cs @@ -0,0 +1,200 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2025 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Configuration; +using Akka.Event; +using Akka.Tests.Util; +using FluentAssertions; +using Xunit; + +namespace Akka.Tests.Event +{ + /// + /// A logger that lives in Akka.Tests - so it is not in LoggingBus.BuiltInDefaultLoggerNames or either of + /// its siblings, and can only be reached by name - and which reports that it was constructed and initialized. + /// + public sealed class CountingTestLogger : DefaultLogger + { + /// + /// Completed by the logger actor once it has answered . A test that starts a + /// system using this logger replaces it first, so a stale result from an earlier system cannot satisfy it. + /// + public static TaskCompletionSource Initialized { get; set; } = NewCompletionSource(); + + public static TaskCompletionSource NewCompletionSource() + => new(TaskCreationOptions.RunContinuationsAsynchronously); + + protected override bool Receive(object message) + { + var handled = base.Receive(message); + if (message is InitializeLogger) + Initialized.TrySetResult(true); + return handled; + } + } + + /// + /// Covers the akka.loggers resolution in : the three loggers Akka.dll ships + /// resolve from the built-in name tables under every spelling HOCON uses, and anything else needs dynamic + /// type loading. + /// + [Collection(DynamicTypeLoadingCollection.Name)] + public class BuiltInLoggersSpec + { + private const string CustomLoggerTypeName = "Akka.Tests.Event.CountingTestLogger"; + + private const string CustomLoggerConfig = + "akka.loggers = [\"" + CustomLoggerTypeName + ", Akka.Tests\"]"; + + /// + /// Every spelling that has to reach : the two keys + /// LoggingBus.BuiltInDefaultLoggerNames carries - the bare name core's own akka.conf ships and the + /// "Ns.T, Akka" form - plus the two assembly-qualified names that only match because the lookup + /// strips assembly identity first. ForeignVersion is the load-bearing one: it names a version this + /// build is not, which is what Akka.Hosting HOCON written against another Akka version looks like. + /// + public static IEnumerable DefaultLoggerSpellings() + { + yield return new object[] { "Akka.Event.DefaultLogger" }; + yield return new object[] { "Akka.Event.DefaultLogger, Akka" }; + yield return new object[] { typeof(DefaultLogger).AssemblyQualifiedName! }; + yield return new object[] { ForeignVersion("Akka.Event.DefaultLogger") }; + } + + /// + /// LoggingBus.BuiltInTraceLoggerNames. 's own documentation tells users to + /// write the "Ns.T, Akka" spelling, so all three have to resolve. + /// + public static IEnumerable TraceLoggerSpellings() + { + yield return new object[] { "Akka.Event.TraceLogger" }; + yield return new object[] { "Akka.Event.TraceLogger, Akka" }; + yield return new object[] { typeof(TraceLogger).AssemblyQualifiedName! }; + yield return new object[] { ForeignVersion("Akka.Event.TraceLogger") }; + } + + /// + /// LoggingBus.BuiltInStandardOutLoggerNames. is a + /// , so StartDefaultLoggers takes the skip branch and starts no actor - + /// but the name still has to resolve, which is what LoggerSpec relies on. + /// + public static IEnumerable StandardOutLoggerSpellings() + { + yield return new object[] { "Akka.Event.StandardOutLogger" }; + yield return new object[] { "Akka.Event.StandardOutLogger, Akka" }; + yield return new object[] { typeof(StandardOutLogger).AssemblyQualifiedName! }; + yield return new object[] { ForeignVersion("Akka.Event.StandardOutLogger") }; + } + + /// + /// An assembly-qualified name for that pins a version, culture and public key + /// token this build does not have - so it can only resolve if the lookup strips assembly identity rather + /// than comparing against typeof(T).AssemblyQualifiedName. + /// + private static string ForeignVersion(string typeName) + => $"{typeName}, Akka, Version=99.0.0.0, Culture=neutral, PublicKeyToken=1234567890abcdef"; + + [Theory(DisplayName = "LoggingBus should start the built-in default logger when dynamic type loading is off")] + [MemberData(nameof(DefaultLoggerSpellings))] + public async Task Should_start_the_built_in_default_logger_When_dynamic_type_loading_is_disabled(string spelling) + => await AssertLoggerStarts(spelling, nameof(DefaultLogger)); + + [Theory(DisplayName = "LoggingBus should start the built-in trace logger when dynamic type loading is off")] + [MemberData(nameof(TraceLoggerSpellings))] + public async Task Should_start_the_built_in_trace_logger_When_dynamic_type_loading_is_disabled(string spelling) + => await AssertLoggerStarts(spelling, nameof(TraceLogger)); + + [Theory(DisplayName = "LoggingBus should accept the built-in standard out logger when dynamic type loading is off")] + [MemberData(nameof(StandardOutLoggerSpellings))] + public async Task Should_accept_the_built_in_standard_out_logger_When_dynamic_type_loading_is_disabled(string spelling) + { + await AkkaFeaturesSpec.WithDynamicTypeLoading(false, async () => + { + var config = ConfigurationFactory.ParseString($"akka.loggers = [\"{spelling}\"]"); + + // a MinimalLogger is never started as an actor, so the check is that the name resolved: + // with the switch off an unresolved akka.loggers entry makes ActorSystem.Create throw + var system = ActorSystem.Create("built-in-stdout-logger-off", config); + await system.Terminate(); + }); + } + + /// + /// The switch-ON regression guard: a logger that is not built in must keep resolving by name exactly as it + /// does today, which is what every application that names its own logger depends on. + /// + [Fact(DisplayName = "LoggingBus should resolve a logger named in HOCON when dynamic type loading is on")] + public async Task Should_resolve_a_custom_logger_When_dynamic_type_loading_is_enabled() + { + await AkkaFeaturesSpec.WithDynamicTypeLoading(true, async () => + { + CountingTestLogger.Initialized = CountingTestLogger.NewCompletionSource(); + + var system = ActorSystem.Create("custom-logger-on", ConfigurationFactory.ParseString(CustomLoggerConfig)); + try + { + await CountingTestLogger.Initialized.Task.WaitAsync(TimeSpan.FromSeconds(10)); + } + finally + { + await system.Terminate(); + } + }); + } + + [Fact(DisplayName = "LoggingBus should reject a logger named in HOCON that is not built in when dynamic type loading is off")] + public async Task Should_throw_ConfigurationException_When_the_logger_is_not_built_in_and_dynamic_type_loading_is_disabled() + { + await AkkaFeaturesSpec.WithDynamicTypeLoading(false, () => + { + var config = ConfigurationFactory.ParseString(CustomLoggerConfig); + + var exception = Assert.Throws( + () => ActorSystem.Create("custom-logger-off", config)); + + exception.Message.Should().Contain("akka.loggers"); + exception.Message.Should().Contain(CustomLoggerTypeName); + exception.Message.Should().Contain(AkkaFeaturesSpec.SwitchName); + return Task.CompletedTask; + }); + } + + /// + /// Boots a system on with the switch off and resolves the logger actor the + /// logging bus started, which is what proves the name reached Props.Create and an actor came out. + /// + /// + /// LoggingBus.CreateLoggerName numbers loggers from a process-global counter, so the name cannot be + /// predicted - hence the wildcard selection rather than a hard-coded log1-. + /// + private static async Task AssertLoggerStarts(string spelling, string loggerTypeName) + { + await AkkaFeaturesSpec.WithDynamicTypeLoading(false, async () => + { + var config = ConfigurationFactory.ParseString($"akka.loggers = [\"{spelling}\"]"); + + var system = ActorSystem.Create("built-in-logger-off", config); + try + { + var logger = await system.ActorSelection($"/system/log*-{loggerTypeName}") + .ResolveOne(TimeSpan.FromSeconds(3)); + + logger.Path.Name.Should().EndWith($"-{loggerTypeName}"); + } + finally + { + await system.Terminate(); + } + }); + } + } +} diff --git a/src/core/Akka/Actor/ActorRefFactoryExtensions.cs b/src/core/Akka/Actor/ActorRefFactoryExtensions.cs index 5e06a0120c9..af5f4861d7c 100644 --- a/src/core/Akka/Actor/ActorRefFactoryExtensions.cs +++ b/src/core/Akka/Actor/ActorRefFactoryExtensions.cs @@ -5,6 +5,8 @@ // //----------------------------------------------------------------------- +using System.Diagnostics.CodeAnalysis; + namespace Akka.Actor { /// @@ -19,7 +21,7 @@ public static class ActorRefFactoryExtensions /// TBD /// TBD /// TBD - public static IActorRef ActorOf(this IActorRefFactory factory, string name = null) + public static IActorRef ActorOf<[DynamicallyAccessedMembers(Props.ActorTypeMembers)] TActor>(this IActorRefFactory factory, string name = null) where TActor : ActorBase, new() { return factory.ActorOf(Props.Create(), name: name); diff --git a/src/core/Akka/Actor/ExtendedActorSystem.cs b/src/core/Akka/Actor/ExtendedActorSystem.cs index b45c6955ac8..53b75e51f7c 100644 --- a/src/core/Akka/Actor/ExtendedActorSystem.cs +++ b/src/core/Akka/Actor/ExtendedActorSystem.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Diagnostics.CodeAnalysis; namespace Akka.Actor { @@ -63,7 +64,7 @@ public abstract class ExtendedActorSystem : ActorSystem /// The type of actor to create /// The name of the actor to create. The default value is . /// A reference to the newly created actor - public abstract IActorRef SystemActorOf(string name = null) where TActor : ActorBase, new(); + public abstract IActorRef SystemActorOf<[DynamicallyAccessedMembers(Props.ActorTypeMembers)] TActor>(string name = null) where TActor : ActorBase, new(); /// /// Aggressively terminates an without waiting for the normal shutdown process to run as-is. diff --git a/src/core/Akka/Actor/Extensions.cs b/src/core/Akka/Actor/Extensions.cs index eead296d47d..97c7582ac60 100644 --- a/src/core/Akka/Actor/Extensions.cs +++ b/src/core/Akka/Actor/Extensions.cs @@ -6,6 +6,7 @@ //----------------------------------------------------------------------- using System; +using System.Diagnostics.CodeAnalysis; namespace Akka.Actor { @@ -107,7 +108,9 @@ public static T WithExtension(this ActorSystem system) where T : class, IExte /// The actor system from which to retrieve the extension or to register with if it does not exist. /// The type of the extension to register if it does not exist in the given actor system. /// The extension retrieved from the given actor system. - public static T WithExtension(this ActorSystem system, Type extensionId) where T : class, IExtension + public static T WithExtension(this ActorSystem system, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type extensionId) + where T : class, IExtension { if (system.HasExtension()) return system.GetExtension(); @@ -126,8 +129,9 @@ public static T WithExtension(this ActorSystem system, Type extensionId) wher /// The type associated with the extension to retrieve if it does not exist within the system. /// The actor system from which to retrieve the extension or to register with if it does not exist. /// The extension retrieved from the given actor system. - public static T WithExtension(this ActorSystem system) where T : class, IExtension - where TI: IExtensionId + public static T WithExtension(this ActorSystem system) + where T : class, IExtension + where TI : IExtensionId { if (system.HasExtension()) return system.GetExtension(); diff --git a/src/core/Akka/Actor/Internal/ActorSystemImpl.cs b/src/core/Akka/Actor/Internal/ActorSystemImpl.cs index 1b903a6e857..c3b7d13f075 100644 --- a/src/core/Akka/Actor/Internal/ActorSystemImpl.cs +++ b/src/core/Akka/Actor/Internal/ActorSystemImpl.cs @@ -186,7 +186,7 @@ public override IActorRef SystemActorOf(Props props, string name = null) /// This exception is thrown when deployment, dispatcher or mailbox configuration is incorrect. /// /// A reference to the underlying actor. - public override IActorRef SystemActorOf(string name = null) + public override IActorRef SystemActorOf<[DynamicallyAccessedMembers(Props.ActorTypeMembers)] TActor>(string name = null) { return _provider.SystemGuardian.Cell.AttachChild(Props.Create(), true, name); } diff --git a/src/core/Akka/Actor/Props.cs b/src/core/Akka/Actor/Props.cs index 0ecb3dbabf8..39419443a9f 100644 --- a/src/core/Akka/Actor/Props.cs +++ b/src/core/Akka/Actor/Props.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using Akka.Configuration; @@ -34,6 +35,13 @@ namespace Akka.Actor /// public class Props : IEquatable, ISurrogated { + /// + /// What the trimmer has to keep on an actor type reached through : the public + /// constructors needs, and the interface list the mailbox requirement check walks. + /// + internal const DynamicallyAccessedMemberTypes ActorTypeMembers = + DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces; + private const string NullActorTypeExceptionText = "Props must be instantiated with an actor type."; private static readonly Deploy DefaultDeploy = new(); @@ -46,7 +54,10 @@ public class Props : IEquatable, ISurrogated /// /// public static readonly Props None = null; + [DynamicallyAccessedMembers(ActorTypeMembers)] private Type _inputType; + + [DynamicallyAccessedMembers(ActorTypeMembers)] private Type _outputType; private readonly IIndirectActorProducer _producer; @@ -79,7 +90,7 @@ protected Props(Props copy) /// /// This exception is thrown if is not instantiated with an actor type. /// - public Props(Type type, object[] args) + public Props([DynamicallyAccessedMembers(ActorTypeMembers)] Type type, object[] args) : this(DefaultDeploy, type, args) { if (type == null) @@ -96,7 +107,7 @@ public Props(Type type, object[] args) /// /// This exception is thrown if is not instantiated with an actor type. /// - public Props(Type type) + public Props([DynamicallyAccessedMembers(ActorTypeMembers)] Type type) : this(DefaultDeploy, type, NoArgs) { if (type == null) @@ -112,7 +123,7 @@ public Props(Type type) /// /// This exception is thrown if is not instantiated with an actor type. /// - public Props(Type type, SupervisorStrategy supervisorStrategy, IEnumerable args) + public Props([DynamicallyAccessedMembers(ActorTypeMembers)] Type type, SupervisorStrategy supervisorStrategy, IEnumerable args) : this(DefaultDeploy, type, args.ToArray()) { if (type == null) @@ -130,7 +141,7 @@ public Props(Type type, SupervisorStrategy supervisorStrategy, IEnumerable /// This exception is thrown if is not instantiated with an actor type. /// - public Props(Type type, SupervisorStrategy supervisorStrategy, params object[] args) + public Props([DynamicallyAccessedMembers(ActorTypeMembers)] Type type, SupervisorStrategy supervisorStrategy, params object[] args) : this(DefaultDeploy, type, args) { if (type == null) @@ -148,7 +159,7 @@ public Props(Type type, SupervisorStrategy supervisorStrategy, params object[] a /// /// This exception is thrown if is not instantiated with an actor type. /// - public Props(Deploy deploy, Type type, IEnumerable args) + public Props(Deploy deploy, [DynamicallyAccessedMembers(ActorTypeMembers)] Type type, IEnumerable args) : this(deploy, type, args.ToArray()) { if (type == null) @@ -162,7 +173,7 @@ public Props(Deploy deploy, Type type, IEnumerable args) /// The type of the actor to create. /// The arguments needed to create the actor. /// This exception is thrown if is an unknown actor producer. - public Props(Deploy deploy, Type type, params object[] args) + public Props(Deploy deploy, [DynamicallyAccessedMembers(ActorTypeMembers)] Type type, params object[] args) #pragma warning disable CS0618 // Type or member is obsolete : this(CreateProducer(type, args), deploy, args) // have to preserve the "CreateProducer" call here to preserve backwards compat with Akka.DI.Core #pragma warning restore CS0618 // Type or member is obsolete @@ -191,6 +202,7 @@ internal Props(IIndirectActorProducer producer, Deploy deploy, params object[] a /// The type of the actor that is created. /// [JsonIgnore] + [DynamicallyAccessedMembers(ActorTypeMembers)] public Type Type { get @@ -349,8 +361,13 @@ public override int GetHashCode() /// The lambda expression used to create the actor. /// Optional: The supervisor strategy used to manage the actor. /// The newly created . + /// + /// is annotated so trimming keeps the public constructors + /// needs and the interfaces the + /// mailbox requirement check reads off the actor type. + /// /// The create function must be a 'new T (args)' expression - public static Props Create(Expression> factory, + public static Props Create<[DynamicallyAccessedMembers(ActorTypeMembers)] TActor>(Expression> factory, SupervisorStrategy supervisorStrategy = null) where TActor : ActorBase { if (factory.Body is UnaryExpression) @@ -371,7 +388,12 @@ public static Props Create(Expression> factory, /// The type of the actor to create. /// The arguments needed to create the actor. /// The newly created . - public static Props Create(params object[] args) where TActor : ActorBase + /// + /// is annotated so trimming keeps the public constructors + /// needs and the interfaces the + /// mailbox requirement check reads off the actor type. + /// + public static Props Create<[DynamicallyAccessedMembers(ActorTypeMembers)] TActor>(params object[] args) where TActor : ActorBase { return new Props(new ActivatorProducer(typeof(TActor), args), DefaultDeploy, args); } @@ -383,7 +405,7 @@ public static Props Create(params object[] args) where TActor : ActorBas /// The arguments needed to create the actor. /// The newly created . [Obsolete("Do not use this method. Call CreateBy(IIndirectActorProducer, params object[] args) instead")] - public static Props CreateBy(params object[] args) where TProducer : class, IIndirectActorProducer + public static Props CreateBy<[DynamicallyAccessedMembers(ActorTypeMembers)] TProducer>(params object[] args) where TProducer : class, IIndirectActorProducer { return new Props(typeof(TProducer), args); } @@ -405,7 +427,12 @@ public static Props CreateBy(IIndirectActorProducer producer, params object[] ar /// The type of the actor to create. /// The supervisor strategy used to manage the actor. /// The newly created . - public static Props Create(SupervisorStrategy supervisorStrategy) where TActor : ActorBase, new() + /// + /// is annotated so trimming keeps the public constructors + /// needs and the interfaces the + /// mailbox requirement check reads off the actor type. + /// + public static Props Create<[DynamicallyAccessedMembers(ActorTypeMembers)] TActor>(SupervisorStrategy supervisorStrategy) where TActor : ActorBase, new() { return new Props(new ActivatorProducer(typeof(TActor), NoArgs), DefaultDeploy, NoArgs){ SupervisorStrategy = supervisorStrategy }; } @@ -417,7 +444,7 @@ public static Props CreateBy(IIndirectActorProducer producer, params object[] ar /// The arguments needed to create the actor. /// The newly created . /// Props must be instantiated with an actor type. - public static Props Create(Type type, params object[] args) + public static Props Create([DynamicallyAccessedMembers(ActorTypeMembers)] Type type, params object[] args) { if (type == null) throw new ArgumentNullException(nameof(type), NullActorTypeExceptionText); @@ -580,7 +607,7 @@ protected virtual Props Copy() } [Obsolete("we should not be calling this method. Pass in an explicit IIndirectActorProducer reference instead.")] - private static IIndirectActorProducer CreateProducer(Type type, object[] args) + private static IIndirectActorProducer CreateProducer([DynamicallyAccessedMembers(ActorTypeMembers)] Type type, object[] args) { if (type == null) return DefaultProducer.Instance; @@ -667,6 +694,7 @@ public ActorBase Produce() throw new InvalidOperationException("No actor producer specified!"); } + [DynamicallyAccessedMembers(ActorTypeMembers)] public Type ActorType => typeof(ActorBase); @@ -680,7 +708,7 @@ private class ActivatorProducer : IIndirectActorProducer { private readonly object[] _args; - public ActivatorProducer(Type actorType, object[] args) + public ActivatorProducer([DynamicallyAccessedMembers(ActorTypeMembers)] Type actorType, object[] args) { ActorType = actorType; _args = args; @@ -691,6 +719,7 @@ public ActorBase Produce() return Activator.CreateInstance(ActorType, _args).AsInstanceOf(); } + [DynamicallyAccessedMembers(ActorTypeMembers)] public Type ActorType { get; } @@ -700,7 +729,7 @@ public void Release(ActorBase actor) } } - private class FactoryConsumer : IIndirectActorProducer where TActor : ActorBase + private class FactoryConsumer<[DynamicallyAccessedMembers(ActorTypeMembers)] TActor> : IIndirectActorProducer where TActor : ActorBase { private readonly Func _factory; @@ -714,6 +743,7 @@ public ActorBase Produce() return _factory.Invoke(); } + [DynamicallyAccessedMembers(ActorTypeMembers)] public Type ActorType => typeof(TActor); @@ -750,7 +780,7 @@ public override ActorBase NewActor() /// /// /// The type of the actor to create. - internal class DynamicProps : Props where TActor : ActorBase + internal class DynamicProps<[DynamicallyAccessedMembers(Props.ActorTypeMembers)] TActor> : Props where TActor : ActorBase { private readonly Func invoker; @@ -808,6 +838,13 @@ public interface IIndirectActorProducer /// The returned type is not used to produce the actor. /// /// The type of the actor created. + /// + /// The property is annotated so trimming keeps the public constructors needs + /// and the interfaces the mailbox requirement check reads off + /// the actor type; an implementation returning a from elsewhere has to carry the + /// same annotation on whatever it reads that type from. + /// + [DynamicallyAccessedMembers(Props.ActorTypeMembers)] Type ActorType { get; } /// diff --git a/src/core/Akka/Event/LoggingBus.cs b/src/core/Akka/Event/LoggingBus.cs index c9e8eaaf2b6..7a394a1d0bc 100644 --- a/src/core/Akka/Event/LoggingBus.cs +++ b/src/core/Akka/Event/LoggingBus.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Threading; @@ -14,6 +15,7 @@ using Akka.Actor; using Akka.Actor.Internal; using Akka.Configuration; +using Akka.Util; namespace Akka.Event { @@ -36,7 +38,40 @@ public LoggerStartInfo(string loggerName, Type loggerType, IActorRef actorRef) public override string ToString() => _name; } - + + // The akka.loggers values that name one of the three loggers Akka.dll ships. GetBuiltInLoggerType + // matches these and hands back the type from a constant typeof, so the trimmer and the Native AOT + // compiler can see it without looking through Type.GetType. + // + // Two spellings per logger, both deliberate: akka.conf ships the bare "Akka.Event.DefaultLogger", and + // HOCON in the wild also carries the "Ns.T, Akka" form - which is what TraceLogger's own documentation + // tells users to write. The lookup runs the configured value through + // TypeExtensions.StripAssemblyIdentity first, so a full AssemblyQualifiedName - which Akka.Hosting + // writes into HOCON - matches the second key whatever version, culture or public key token it names. + // A value that still misses these tables falls through to the reflection path, which is unavailable + // (and therefore throws) once dynamic type loading is switched off. Do not remove a spelling, and do + // not add a versioned third key. + private static readonly string[] BuiltInDefaultLoggerNames = + new[] + { + "Akka.Event.DefaultLogger", + "Akka.Event.DefaultLogger, Akka" + }; + + private static readonly string[] BuiltInStandardOutLoggerNames = + new[] + { + "Akka.Event.StandardOutLogger", + "Akka.Event.StandardOutLogger, Akka" + }; + + private static readonly string[] BuiltInTraceLoggerNames = + new[] + { + "Akka.Event.TraceLogger", + "Akka.Event.TraceLogger, Akka" + }; + private static readonly LogLevel[] AllLogLevels = Enum.GetValues(typeof(LogLevel)).Cast().ToArray(); private static int _loggerId; @@ -114,10 +149,16 @@ internal void StartDefaultLoggers(ActorSystemImpl system) var taskInfos = new Dictionary(); foreach (var strLoggerType in loggerTypes) { - var loggerType = Type.GetType(strLoggerType); + var loggerType = GetBuiltInLoggerType(strLoggerType); if (loggerType == null) { - throw new ConfigurationException($@"Logger specified in config cannot be found: ""{strLoggerType}"""); + if (!AkkaFeatures.IsDynamicTypeLoadingSupported) + throw new ConfigurationException(AkkaFeatures.NotBuiltIn( + "akka.loggers", strLoggerType, "one of the built-in loggers")); + + loggerType = ResolveLoggerType(strLoggerType); + if (loggerType == null) + throw new ConfigurationException($@"Logger specified in config cannot be found: ""{strLoggerType}"""); } if (typeof(MinimalLogger).IsAssignableFrom(loggerType)) @@ -198,7 +239,40 @@ private void RemoveLogger(IActorRef logger) } } - private (Task task, string name) AddLogger(ActorSystemImpl system, Type loggerType, string loggingBusName) + // Matching the name and returning the type happen inside one method with an annotated return on purpose. + // A Dictionary would read better, but it cannot carry DynamicallyAccessedMembers on its + // values: the type coming back out of TryGetValue arrives unannotated, and the trimmer then drops the + // constructor Props needs - IL2067 here, MissingMethodException on the first log event at run time. + // Array.IndexOf over string[] compares ordinally, which is what these names want. + [return: DynamicallyAccessedMembers(Props.ActorTypeMembers)] + private static Type GetBuiltInLoggerType(string loggerTypeName) + { + // Util. qualifies this: System.Reflection.TypeExtensions is in scope here too. + var name = Util.TypeExtensions.StripAssemblyIdentity(loggerTypeName); + + if (Array.IndexOf(BuiltInDefaultLoggerNames, name) >= 0) + return typeof(DefaultLogger); + + if (Array.IndexOf(BuiltInStandardOutLoggerNames, name) >= 0) + return typeof(StandardOutLogger); + + if (Array.IndexOf(BuiltInTraceLoggerNames, name) >= 0) + return typeof(TraceLogger); + + return null; + } + + [RequiresUnreferencedCode("Loads the [akka.loggers] type by name. The trimmer cannot tell which type that is, so it may have been trimmed away.")] + [return: DynamicallyAccessedMembers(Props.ActorTypeMembers)] + private static Type ResolveLoggerType(string loggerTypeName) + { + return Type.GetType(loggerTypeName); + } + + private (Task task, string name) AddLogger( + ActorSystemImpl system, + [DynamicallyAccessedMembers(Props.ActorTypeMembers)] Type loggerType, + string loggingBusName) { var loggerName = CreateLoggerName(loggerType); var fullLoggerName = $"{loggerName} [{loggerType.FullName}]";