diff --git a/all.sln b/all.sln
index 87dd937bc..05f8cd5e6 100644
--- a/all.sln
+++ b/all.sln
@@ -254,6 +254,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Crypto
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Actors", "test\Dapr.IntegrationTest.Actors\Dapr.IntegrationTest.Actors.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.IntegrationTest.Actors.Generators", "test\Dapr.IntegrationTest.Actors.Generators\Dapr.IntegrationTest.Actors.Generators.csproj", "{C6948155-C70C-4C03-A733-0624402AEC97}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowRetryPolicy", "examples\Workflow\WorkflowRetryPolicy\WorkflowRetryPolicy.csproj", "{6C77A2C4-0A96-4B90-AFEA-16E86A906259}"
EndProject
Global
@@ -1490,6 +1492,18 @@ Global
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Debug|x64.Build.0 = Debug|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Debug|x86.Build.0 = Debug|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Release|x64.ActiveCfg = Release|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Release|x64.Build.0 = Release|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Release|x86.ActiveCfg = Release|Any CPU
+ {C6948155-C70C-4C03-A733-0624402AEC97}.Release|x86.Build.0 = Release|Any CPU
{6C77A2C4-0A96-4B90-AFEA-16E86A906259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6C77A2C4-0A96-4B90-AFEA-16E86A906259}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C77A2C4-0A96-4B90-AFEA-16E86A906259}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1625,6 +1639,7 @@ Global
{01A20A89-53A1-4D5B-B563-89E157718474} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{7B14879F-156B-417E-ACA3-0B5A69CC2F39} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {8462B106-175A-423A-BA94-BE0D39D0BD8E}
+ {C6948155-C70C-4C03-A733-0624402AEC97} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{6C77A2C4-0A96-4B90-AFEA-16E86A906259} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/Actors/IClientActor.cs b/test/Dapr.IntegrationTest.Actors.Generators/Actors/IClientActor.cs
new file mode 100644
index 000000000..9e9e04a64
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/Actors/IClientActor.cs
@@ -0,0 +1,63 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.Actors.Generators;
+
+namespace Dapr.IntegrationTest.Actors.Generators.Actors;
+
+///
+/// State record used by the client-side generated actor client.
+///
+public record ClientState(string Value);
+
+///
+/// Actor interface decorated with to trigger
+/// source generation of a strongly-typed client (ClientActorClient).
+/// The methods use to map to the actual server-side
+/// actor method names.
+///
+[GenerateActorClient]
+public interface IClientActor
+{
+ ///
+ /// Gets the current state from the remote actor.
+ ///
+ [ActorMethod(Name = "GetState")]
+ Task GetStateAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Sets the state on the remote actor.
+ ///
+ [ActorMethod(Name = "SetState")]
+ Task SetStateAsync(ClientState state, CancellationToken cancellationToken = default);
+
+ ///
+ /// Calls the SayHello method on the remote actor.
+ ///
+ [ActorMethod(Name = "SayHello")]
+ Task SayHelloAsync(string name, CancellationToken cancellationToken = default);
+
+ ///
+ /// Calls the IncrementCallCount method on the remote actor.
+ ///
+ [ActorMethod(Name = "IncrementCallCount")]
+ Task IncrementCallCountAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Gets the call count from the remote actor.
+ ///
+ [ActorMethod(Name = "GetCallCount")]
+ Task GetCallCountAsync(CancellationToken cancellationToken = default);
+}
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/Actors/IPingActor.cs b/test/Dapr.IntegrationTest.Actors.Generators/Actors/IPingActor.cs
new file mode 100644
index 000000000..f912943f1
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/Actors/IPingActor.cs
@@ -0,0 +1,33 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.Actors;
+
+namespace Dapr.IntegrationTest.Actors.Generators;
+
+///
+/// Minimal actor interface used as a readiness probe.
+///
+public interface IPingActor : IActor
+{
+ ///
+ /// Pings the actor to verify that the runtime is available.
+ ///
+ ///
+ /// A token that cancels the underlying HTTP request, allowing the caller to impose a
+ /// per-attempt timeout instead of waiting for the HttpClient default (100 s).
+ ///
+ Task Ping(CancellationToken cancellationToken = default);
+}
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/Actors/IRemoteActor.cs b/test/Dapr.IntegrationTest.Actors.Generators/Actors/IRemoteActor.cs
new file mode 100644
index 000000000..3e8c968a9
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/Actors/IRemoteActor.cs
@@ -0,0 +1,56 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Dapr.IntegrationTest.Actors.Generators.Actors;
+
+///
+/// State record used by the remote actor for get/set operations.
+///
+public record RemoteState(string Value);
+
+///
+/// Actor interface for the server-side actor that manages state.
+/// Extends so the runtime readiness check can be performed.
+///
+public interface IRemoteActor : IPingActor
+{
+ ///
+ /// Returns the current state of the actor.
+ ///
+ Task GetState();
+
+ ///
+ /// Sets the state of the actor.
+ ///
+ /// The new state to set.
+ Task SetState(RemoteState state);
+
+ ///
+ /// Returns a greeting message for the specified name.
+ ///
+ /// The name to greet.
+ Task SayHello(string name);
+
+ ///
+ /// A fire-and-forget method with no parameters and no return value.
+ ///
+ Task IncrementCallCount();
+
+ ///
+ /// Returns the current call count.
+ ///
+ Task GetCallCount();
+}
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/Actors/RemoteActor.cs b/test/Dapr.IntegrationTest.Actors.Generators/Actors/RemoteActor.cs
new file mode 100644
index 000000000..89e69b6c4
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/Actors/RemoteActor.cs
@@ -0,0 +1,90 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.Actors.Runtime;
+using Microsoft.Extensions.Logging;
+
+namespace Dapr.IntegrationTest.Actors.Generators.Actors;
+
+///
+/// Implementation of that manages state in memory.
+///
+internal sealed partial class RemoteActor(ActorHost host, ILogger logger) : Actor(host), IRemoteActor
+{
+ private RemoteState currentState = new("default");
+
+ private int callCount;
+
+ ///
+ public Task GetState()
+ {
+ logger.LogGetState();
+ return Task.FromResult(this.currentState);
+ }
+
+ ///
+ public Task SetState(RemoteState state)
+ {
+ logger.LogSetState();
+ this.currentState = state;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task SayHello(string name)
+ {
+ logger.LogSayHello(name);
+ return Task.FromResult($"Hello, {name}!");
+ }
+
+ ///
+ public Task IncrementCallCount()
+ {
+ logger.LogIncrementCallCount();
+ this.callCount++;
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task GetCallCount()
+ {
+ logger.LogGetCallCount();
+ return Task.FromResult(this.callCount);
+ }
+
+ ///
+ public Task Ping(CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+}
+
+internal static partial class RemoteActorLogMessages
+{
+ [LoggerMessage(LogLevel.Information, "GetState called.")]
+ public static partial void LogGetState(this ILogger logger);
+
+ [LoggerMessage(LogLevel.Information, "SetState called.")]
+ public static partial void LogSetState(this ILogger logger);
+
+ [LoggerMessage(LogLevel.Information, "SayHello called with name: {Name}")]
+ public static partial void LogSayHello(this ILogger logger, string name);
+
+ [LoggerMessage(LogLevel.Information, "IncrementCallCount called.")]
+ public static partial void LogIncrementCallCount(this ILogger logger);
+
+ [LoggerMessage(LogLevel.Information, "GetCallCount called.")]
+ public static partial void LogGetCallCount(this ILogger logger);
+}
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/Dapr.IntegrationTest.Actors.Generators.csproj b/test/Dapr.IntegrationTest.Actors.Generators/Dapr.IntegrationTest.Actors.Generators.csproj
new file mode 100644
index 000000000..53de431f7
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/Dapr.IntegrationTest.Actors.Generators.csproj
@@ -0,0 +1,30 @@
+
+
+
+ enable
+ enable
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/GeneratedClientTests.cs b/test/Dapr.IntegrationTest.Actors.Generators/GeneratedClientTests.cs
new file mode 100644
index 000000000..2ee2deab9
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/GeneratedClientTests.cs
@@ -0,0 +1,313 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.Actors;
+using Dapr.Actors.Client;
+using Dapr.IntegrationTest.Actors.Generators.Actors;
+using Dapr.IntegrationTest.Actors.Generators.Infrastructure;
+using Dapr.Testcontainers.Common;
+using Dapr.Testcontainers.Harnesses;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.IntegrationTest.Actors.Generators;
+
+///
+/// Integration tests that verify the source-generated actor client (ClientActorClient)
+/// can correctly invoke actor methods on a real Dapr sidecar and actor runtime.
+/// These tests replicate and extend the coverage of the E2E test suite in
+/// Dapr.E2E.Test.Actors.Generators.
+///
+public sealed class GeneratedClientTests
+{
+ ///
+ /// Verifies that the generated client can retrieve the default state from the remote actor.
+ /// This is equivalent to the first half of the E2E TestGeneratedClientAsync test.
+ ///
+ [Fact]
+ public async Task GeneratedClient_CanGetDefaultState()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ var actorId = ActorId.CreateRandom();
+ const string actorType = "RemoteActor";
+
+ // Use the strongly-typed proxy to wait for the runtime to be ready.
+ var pingProxy = proxyFactory.CreateActorProxy(actorId, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ // Create the generated client through the weakly-typed proxy.
+ var actorProxy = proxyFactory.Create(actorId, actorType);
+ var client = new ClientActorClient(actorProxy);
+
+ var state = await client.GetStateAsync(cts.Token);
+
+ Assert.NotNull(state);
+ Assert.Equal("default", state.Value);
+ }
+
+ ///
+ /// Verifies that the generated client can set and then retrieve state from the remote actor.
+ /// This is equivalent to the E2E TestGeneratedClientAsync test.
+ ///
+ [Fact]
+ public async Task GeneratedClient_CanSetAndGetState()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ var actorId = ActorId.CreateRandom();
+ const string actorType = "RemoteActor";
+
+ var pingProxy = proxyFactory.CreateActorProxy(actorId, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ var actorProxy = proxyFactory.Create(actorId, actorType);
+ var client = new ClientActorClient(actorProxy);
+
+ await client.SetStateAsync(new ClientState("updated state"), cts.Token);
+
+ var state = await client.GetStateAsync(cts.Token);
+ Assert.Equal("updated state", state.Value);
+ }
+
+ ///
+ /// Verifies that the generated client correctly maps method names via
+ /// when the server-side method has a different name from the client-side async method.
+ ///
+ [Fact]
+ public async Task GeneratedClient_RespectsActorMethodNameMapping()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ var actorId = ActorId.CreateRandom();
+ const string actorType = "RemoteActor";
+
+ var pingProxy = proxyFactory.CreateActorProxy(actorId, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ var actorProxy = proxyFactory.Create(actorId, actorType);
+ var client = new ClientActorClient(actorProxy);
+
+ // SayHelloAsync on the client maps to SayHello on the server via [ActorMethod(Name = "SayHello")]
+ var result = await client.SayHelloAsync("World", cts.Token);
+ Assert.Equal("Hello, World!", result);
+ }
+
+ ///
+ /// Verifies that the generated client can invoke a void (fire-and-forget) method
+ /// with no parameters via the IncrementCallCountAsync method.
+ ///
+ [Fact]
+ public async Task GeneratedClient_CanInvokeVoidMethodWithNoParameters()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ var actorId = ActorId.CreateRandom();
+ const string actorType = "RemoteActor";
+
+ var pingProxy = proxyFactory.CreateActorProxy(actorId, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ var actorProxy = proxyFactory.Create(actorId, actorType);
+ var client = new ClientActorClient(actorProxy);
+
+ // Call the void method multiple times.
+ await client.IncrementCallCountAsync(cts.Token);
+ await client.IncrementCallCountAsync(cts.Token);
+ await client.IncrementCallCountAsync(cts.Token);
+
+ // Verify the side-effect via GetCallCount.
+ var count = await client.GetCallCountAsync(cts.Token);
+ Assert.Equal(3, count);
+ }
+
+ ///
+ /// Verifies that multiple state updates through the generated client are correctly
+ /// reflected, confirming that the last write wins.
+ ///
+ [Fact]
+ public async Task GeneratedClient_MultipleStateUpdatesReflectLastWrite()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ var actorId = ActorId.CreateRandom();
+ const string actorType = "RemoteActor";
+
+ var pingProxy = proxyFactory.CreateActorProxy(actorId, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ var actorProxy = proxyFactory.Create(actorId, actorType);
+ var client = new ClientActorClient(actorProxy);
+
+ await client.SetStateAsync(new ClientState("first"), cts.Token);
+ await client.SetStateAsync(new ClientState("second"), cts.Token);
+ await client.SetStateAsync(new ClientState("third"), cts.Token);
+
+ var state = await client.GetStateAsync(cts.Token);
+ Assert.Equal("third", state.Value);
+ }
+
+ ///
+ /// Verifies that two generated clients pointing at different actor IDs maintain
+ /// independent state — a write to one does not affect the other.
+ ///
+ [Fact]
+ public async Task GeneratedClient_DifferentActorIdsHaveIndependentState()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ const string actorType = "RemoteActor";
+
+ var actorId1 = ActorId.CreateRandom();
+ var actorId2 = ActorId.CreateRandom();
+
+ var pingProxy = proxyFactory.CreateActorProxy(actorId1, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ var client1 = new ClientActorClient(proxyFactory.Create(actorId1, actorType));
+ var client2 = new ClientActorClient(proxyFactory.Create(actorId2, actorType));
+
+ await client1.SetStateAsync(new ClientState("state-for-actor-1"), cts.Token);
+ await client2.SetStateAsync(new ClientState("state-for-actor-2"), cts.Token);
+
+ var state1 = await client1.GetStateAsync(cts.Token);
+ var state2 = await client2.GetStateAsync(cts.Token);
+
+ Assert.Equal("state-for-actor-1", state1.Value);
+ Assert.Equal("state-for-actor-2", state2.Value);
+ }
+
+ ///
+ /// Verifies that the generated client correctly handles cancellation tokens by
+ /// passing them through to the actor proxy without errors under normal operation.
+ ///
+ [Fact]
+ public async Task GeneratedClient_SupportsCancellationToken()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ var actorId = ActorId.CreateRandom();
+ const string actorType = "RemoteActor";
+
+ var pingProxy = proxyFactory.CreateActorProxy(actorId, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ var actorProxy = proxyFactory.Create(actorId, actorType);
+ var client = new ClientActorClient(actorProxy);
+
+ // Create a separate cancellation token to ensure it's properly threaded through.
+ using var methodCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
+
+ await client.SetStateAsync(new ClientState("cancellation-test"), methodCts.Token);
+ var state = await client.GetStateAsync(methodCts.Token);
+
+ Assert.Equal("cancellation-test", state.Value);
+ }
+
+ ///
+ /// Verifies that the generated client works with JSON serialization enabled,
+ /// confirming that complex state objects are correctly serialized and deserialized.
+ ///
+ [Fact]
+ public async Task GeneratedClient_WorksWithJsonSerialization()
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(120));
+ await using var testApp = await CreateTestAppAsync(useJsonSerialization: true, cts.Token);
+
+ using var scope = testApp.CreateScope();
+ var proxyFactory = scope.ServiceProvider.GetRequiredService();
+
+ var actorId = ActorId.CreateRandom();
+ const string actorType = "RemoteActor";
+
+ var pingProxy = proxyFactory.CreateActorProxy(actorId, actorType);
+ await ActorRuntimeHelper.WaitForActorRuntimeAsync(pingProxy, cts.Token);
+
+ var actorProxy = proxyFactory.Create(actorId, actorType);
+ var client = new ClientActorClient(actorProxy);
+
+ // Set a state with special characters to verify JSON handling.
+ var specialState = new ClientState("value with \"quotes\" and special chars: <>&");
+ await client.SetStateAsync(specialState, cts.Token);
+
+ var retrieved = await client.GetStateAsync(cts.Token);
+ Assert.Equal(specialState.Value, retrieved.Value);
+ }
+
+ // ------------------------------------------------------------------
+ // Test infrastructure helpers
+ // ------------------------------------------------------------------
+
+ private static async Task CreateTestAppAsync(
+ bool useJsonSerialization,
+ CancellationToken cancellationToken)
+ {
+ var componentsDir = TestDirectoryManager.CreateTestDirectory("actor-generators-components");
+
+ var environment = await DaprTestEnvironment.CreateWithPooledNetworkAsync(
+ needsActorState: true,
+ cancellationToken: cancellationToken);
+ await environment.StartAsync(cancellationToken);
+
+ var harness = new DaprHarnessBuilder(componentsDir)
+ .WithEnvironment(environment)
+ .BuildActors();
+
+ var testApp = await DaprHarnessBuilder.ForHarness(harness)
+ .ConfigureServices(builder =>
+ {
+ builder.Services.AddActors(options =>
+ {
+ options.UseJsonSerialization = useJsonSerialization;
+ options.Actors.RegisterActor();
+ });
+ })
+ .ConfigureApp(app =>
+ {
+ app.MapActorsHandlers();
+ })
+ .BuildAndStartAsync();
+ return new ActorTestContext(environment, testApp);
+ }
+}
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/Infrastructure/ActorRuntimeHelper.cs b/test/Dapr.IntegrationTest.Actors.Generators/Infrastructure/ActorRuntimeHelper.cs
new file mode 100644
index 000000000..c74b34c06
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/Infrastructure/ActorRuntimeHelper.cs
@@ -0,0 +1,80 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Dapr.IntegrationTest.Actors.Generators.Infrastructure;
+
+///
+/// Provides helpers for waiting until the Dapr actor runtime is ready to process requests.
+///
+public static class ActorRuntimeHelper
+{
+ // Per-attempt timeout for each Ping call. Keeping this well below the HttpClient
+ // default (100 s) prevents a hung placement-registration request from stalling
+ // the poll loop for a full 100 seconds before the next retry.
+ private static readonly TimeSpan PingAttemptTimeout = TimeSpan.FromSeconds(5);
+
+ ///
+ /// Polls until a call succeeds,
+ /// indicating that the actor runtime has registered the actor type with the placement service.
+ ///
+ ///
+ /// The actor proxy to use for health probing.
+ ///
+ ///
+ /// A token that cancels the polling loop.
+ ///
+ ///
+ /// Thrown when is cancelled before the runtime is ready.
+ ///
+ public static async Task WaitForActorRuntimeAsync(IPingActor pingActor, CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ // Use a short per-attempt timeout so a hung request (e.g. while the Dapr
+ // placement service is still registering the actor type) does not stall the
+ // poll loop for the full HttpClient default of 100 seconds.
+ using var attemptCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ attemptCts.CancelAfter(PingAttemptTimeout);
+
+ await pingActor.Ping(attemptCts.Token);
+ return;
+ }
+ catch (DaprApiException)
+ {
+ // The actor runtime returned an error response – placement may not have
+ // registered the actor type yet. Retry after a short pause.
+ }
+ catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
+ {
+ // The per-attempt timeout fired (not the outer cancellation token). The
+ // sidecar accepted the TCP connection but did not respond in time – this
+ // happens while Dapr is still acquiring a placement token. Retry.
+ }
+ catch (HttpRequestException)
+ {
+ // Connection-level error – the sidecar may still be starting up.
+ }
+
+ await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
+ }
+ }
+}
diff --git a/test/Dapr.IntegrationTest.Actors.Generators/Infrastructure/ActorTestContext.cs b/test/Dapr.IntegrationTest.Actors.Generators/Infrastructure/ActorTestContext.cs
new file mode 100644
index 000000000..142d262b0
--- /dev/null
+++ b/test/Dapr.IntegrationTest.Actors.Generators/Infrastructure/ActorTestContext.cs
@@ -0,0 +1,55 @@
+// ------------------------------------------------------------------------
+// Copyright 2026 The Dapr Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ------------------------------------------------------------------------
+
+using System;
+using System.Threading.Tasks;
+using Dapr.Testcontainers.Common.Testing;
+using Dapr.Testcontainers.Harnesses;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.IntegrationTest.Actors.Generators.Infrastructure;
+
+///
+/// Combines a with its owning
+/// so that both are disposed together
+/// when the test ends. The must outlive
+/// the application (placement / scheduler must stay up while the test runs).
+///
+public sealed class ActorTestContext : IAsyncDisposable
+{
+ private readonly DaprTestEnvironment _environment;
+ private readonly DaprTestApplication _app;
+
+ ///
+ /// Initializes a new .
+ ///
+ internal ActorTestContext(DaprTestEnvironment environment, DaprTestApplication app)
+ {
+ _environment = environment;
+ _app = app;
+ }
+
+ ///
+ /// Creates a DI service scope from the running test application.
+ ///
+ public IServiceScope CreateScope() => _app.CreateScope();
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ // Dispose the application (and its harness) before shutting down the environment
+ // so the Dapr sidecar can drain cleanly before placement/scheduler stop.
+ await _app.DisposeAsync();
+ await _environment.DisposeAsync();
+ }
+}