diff --git a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs
index ab09a5d99d0..4d14d65ce1c 100644
--- a/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs
+++ b/src/Orleans.Runtime/Activation/ActivationDataActivatorProvider.cs
@@ -76,7 +76,7 @@ public ActivationDataActivator(
_startActivation = state => ((ActivationData)state!).Start(_grainActivator);
}
- public IGrainContext CreateContext(GrainAddress activationAddress)
+ public IGrainContext CreateContext(GrainAddress activationAddress, IConfigureGrainContext[] configureActions)
{
var context = new ActivationData(
activationAddress,
@@ -84,6 +84,11 @@ public IGrainContext CreateContext(GrainAddress activationAddress)
_serviceProvider,
_sharedComponents);
+ foreach (var configure in configureActions)
+ {
+ configure.Configure(context);
+ }
+
using var ecSuppressor = ExecutionContext.SuppressFlow();
_ = Task.Factory.StartNew(
_startActivation,
@@ -98,5 +103,6 @@ public IGrainContext CreateContext(GrainAddress activationAddress)
internal class StatelessWorkerActivator(StatelessWorkerGrainTypeSharedContext sharedContext, IGrainContextActivator innerActivator) : IGrainContextActivator
{
- public IGrainContext CreateContext(GrainAddress address) => new StatelessWorkerGrainContext(address, sharedContext, innerActivator);
+ public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions)
+ => new StatelessWorkerGrainContext(address, sharedContext, innerActivator, configureActions);
}
diff --git a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs
index e7dd84ba8a3..ab496fbccb9 100644
--- a/src/Orleans.Runtime/Activation/IGrainContextActivator.cs
+++ b/src/Orleans.Runtime/Activation/IGrainContextActivator.cs
@@ -64,13 +64,7 @@ public IGrainContext CreateInstance(GrainAddress address)
activator = this.CreateActivator(grainId.Type);
}
- var result = activator.Activator.CreateContext(address);
- foreach (var configure in activator.ConfigureActions)
- {
- configure.Configure(result);
- }
-
- return result;
+ return activator.Activator.CreateContext(address, activator.ConfigureActions);
}
private (IGrainContextActivator, IConfigureGrainContext[]) CreateActivator(GrainType grainType)
@@ -135,8 +129,9 @@ public interface IGrainContextActivator
/// Creates a grain context for the given grain address.
///
/// The grain address.
+ /// The actions which must be used to configure the context before grain construction begins.
/// The newly created grain context.
- public IGrainContext CreateContext(GrainAddress address);
+ public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions);
}
///
diff --git a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs
index a51de49ae0d..b244328caf0 100644
--- a/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs
+++ b/src/Orleans.Runtime/Catalog/StatelessWorkerGrainContext.cs
@@ -43,12 +43,18 @@ internal partial class StatelessWorkerGrainContext : IGrainContext, IAsyncDispos
public StatelessWorkerGrainContext(
GrainAddress address,
StatelessWorkerGrainTypeSharedContext sharedContext,
- IGrainContextActivator innerActivator)
+ IGrainContextActivator innerActivator,
+ IConfigureGrainContext[] configureActions)
{
Address = address;
_shared = sharedContext;
_innerActivator = innerActivator;
+ foreach (var configure in configureActions)
+ {
+ configure.Configure(this);
+ }
+
if (_shared.RemoveIdleWorkers)
{
_inspectionTimer = new Timer(
@@ -320,7 +326,7 @@ private ActivationData CreateWorker(object? message)
{
Debug.Assert(!_terminated, "CreateWorker must not be called on a terminated stateless worker context.");
var address = GrainAddress.GetAddress(Address.SiloAddress, Address.GrainId, ActivationId.NewId());
- var newWorker = (ActivationData)_innerActivator.CreateContext(address);
+ var newWorker = (ActivationData)_innerActivator.CreateContext(address, []);
// Observe the create/destroy lifecycle of the activation
newWorker.SetComponent(this);
diff --git a/src/api/Orleans.Runtime/Orleans.Runtime.cs b/src/api/Orleans.Runtime/Orleans.Runtime.cs
index 730989de04e..ece190a18f2 100644
--- a/src/api/Orleans.Runtime/Orleans.Runtime.cs
+++ b/src/api/Orleans.Runtime/Orleans.Runtime.cs
@@ -749,7 +749,7 @@ public partial interface IGrainActivator
public partial interface IGrainContextActivator
{
- IGrainContext CreateContext(GrainAddress address);
+ IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions);
}
public partial interface IGrainContextActivatorProvider
diff --git a/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs
new file mode 100644
index 00000000000..5ace698222b
--- /dev/null
+++ b/test/Orleans.Core.Tests/Runtime/GrainContextActivatorTests.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using NSubstitute;
+using Orleans.Metadata;
+using Orleans.Runtime;
+using TestExtensions;
+using Xunit;
+
+namespace UnitTests.Runtime;
+
+public class GrainContextActivatorTests
+{
+ [Fact, TestCategory("BVT")]
+ public void CreateInstance_ConfiguresContextBeforeStartingActivation()
+ {
+ var events = new List();
+ var context = Substitute.For();
+ var contextActivator = new TestGrainContextActivator(context, events);
+ var activator = new GrainContextActivator(
+ [new TestGrainContextActivatorProvider(contextActivator)],
+ [new TestConfigureGrainContextProvider(events)],
+ new GrainPropertiesResolver(Substitute.For()));
+ var address = new GrainAddress { GrainId = GrainId.Create("test", "grain") };
+
+ Assert.Same(context, activator.CreateInstance(address));
+ Assert.Equal(["configure", "activate"], events);
+ }
+
+ private sealed class TestGrainContextActivatorProvider(IGrainContextActivator activator) : IGrainContextActivatorProvider
+ {
+ public bool TryGet(GrainType grainType, [NotNullWhen(true)] out IGrainContextActivator? result)
+ {
+ result = activator;
+ return true;
+ }
+ }
+
+ private sealed class TestConfigureGrainContextProvider(List events) : IConfigureGrainContextProvider
+ {
+ public bool TryGetConfigurator(
+ GrainType grainType,
+ GrainProperties properties,
+ [NotNullWhen(true)] out IConfigureGrainContext? configurator)
+ {
+ configurator = new TestConfigureGrainContext(events);
+ return true;
+ }
+ }
+
+ private sealed class TestConfigureGrainContext(List events) : IConfigureGrainContext
+ {
+ public void Configure(IGrainContext context) => events.Add("configure");
+ }
+
+ private sealed class TestGrainContextActivator(IGrainContext context, List events) : IGrainContextActivator
+ {
+ public IGrainContext CreateContext(GrainAddress address, IConfigureGrainContext[] configureActions)
+ {
+ foreach (var configure in configureActions)
+ {
+ configure.Configure(context);
+ }
+
+ events.Add("activate");
+ return context;
+ }
+ }
+}
diff --git a/test/Orleans.Runtime.Tests/GrainActivatorTests.cs b/test/Orleans.Runtime.Tests/GrainActivatorTests.cs
index 2bf31339b4d..167363cecb5 100644
--- a/test/Orleans.Runtime.Tests/GrainActivatorTests.cs
+++ b/test/Orleans.Runtime.Tests/GrainActivatorTests.cs
@@ -1,12 +1,13 @@
+using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
+using Orleans.Metadata;
using Orleans.Runtime;
using Orleans.TestingHost;
using TestExtensions;
using UnitTests.GrainInterfaces;
using UnitTests.Grains;
using Xunit;
-using Orleans.Metadata;
namespace UnitTests.General
{
@@ -48,6 +49,7 @@ public void Configure(ISiloBuilder hostBuilder)
// Register our custom grain activator as a grain type component configurator
// This allows it to selectively apply to specific grain types
services.AddSingleton();
+ services.AddSingleton();
});
}
}
@@ -96,6 +98,20 @@ public async Task CanUseCustomGrainActivatorToReleaseGrains()
Assert.Equal(initialReleasedInstances + 1, finalReleasedInstances);
}
+ [Fact, TestCategory("BVT")]
+ public async Task GrainContextIsConfiguredBeforeGrainConstruction()
+ {
+ var state = ActivationOrderingState.Instance;
+ state.Arm();
+ var grain = this.fixture.GrainFactory.GetGrain(
+ GetRandomGrainId(),
+ grainClassNamePrefix: "UnitTests.Grains.ExplicitlyRegistered");
+
+ await grain.GetStringValue();
+
+ Assert.True(state.WasConfiguredAtConstruction);
+ }
+
///
/// Custom grain activator that bypasses dependency injection entirely.
/// Implements both IGrainActivator (for creation/disposal) and IConfigureGrainTypeComponents
@@ -119,7 +135,7 @@ public void Configure(GrainType grainType, GrainProperties properties, GrainType
{
// Selectively register this activator only for ExplicitlyRegisteredSimpleDIGrain types
// Other grain types will continue using the default DI-based activator
- if (_grainClassMap.TryGetGrainClass(grainType, out var grainClass) && grainClass.IsAssignableFrom(typeof(ExplicitlyRegisteredSimpleDIGrain)))
+ if (_grainClassMap.TryGetGrainClass(grainType, out var grainClass) && grainClass == typeof(ExplicitlyRegisteredSimpleDIGrain))
{
shared.SetComponent(this);
}
@@ -127,6 +143,8 @@ public void Configure(GrainType grainType, GrainProperties properties, GrainType
public object CreateInstance(IGrainContext context)
{
+ ActivationOrderingState.Instance.ObserveConstruction(context);
+
// Custom instantiation logic - creates grain with hardcoded dependencies
// In real scenarios, this could get objects from a pool, perform complex
// initialization, or integrate with external systems
@@ -141,5 +159,67 @@ public ValueTask DisposeInstance(IGrainContext context, object instance)
return default;
}
}
+
+ private sealed class ActivationOrderingConfiguratorProvider(GrainClassMap grainClassMap) : IConfigureGrainContextProvider
+ {
+ public bool TryGetConfigurator(
+ GrainType grainType,
+ GrainProperties properties,
+ [NotNullWhen(true)] out IConfigureGrainContext? configurator)
+ {
+ if (grainClassMap.TryGetGrainClass(grainType, out var grainClass)
+ && grainClass == typeof(ExplicitlyRegisteredSimpleDIGrain))
+ {
+ configurator = ActivationOrderingState.Instance;
+ return true;
+ }
+
+ configurator = null;
+ return false;
+ }
+ }
+
+ private sealed class ActivationOrderingState : IConfigureGrainContext
+ {
+ private int _armed;
+ private int _wasConfiguredAtConstruction;
+
+ public static ActivationOrderingState Instance { get; } = new();
+
+ public bool WasConfiguredAtConstruction => Volatile.Read(ref _wasConfiguredAtConstruction) != 0;
+
+ public void Arm()
+ {
+ Volatile.Write(ref _wasConfiguredAtConstruction, 0);
+ Volatile.Write(ref _armed, 1);
+ }
+
+ public void Configure(IGrainContext context)
+ {
+ if (Volatile.Read(ref _armed) == 0)
+ {
+ return;
+ }
+
+ context.SetComponent(ConfiguredContextMarker.Instance);
+ }
+
+ public void ObserveConstruction(IGrainContext context)
+ {
+ if (Interlocked.Exchange(ref _armed, 0) == 0)
+ {
+ return;
+ }
+
+ Volatile.Write(
+ ref _wasConfiguredAtConstruction,
+ context.GetComponent() is not null ? 1 : 0);
+ }
+ }
+
+ private sealed class ConfiguredContextMarker
+ {
+ public static ConfiguredContextMarker Instance { get; } = new();
+ }
}
}