diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6db58e8e9..79da9a732 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -52,6 +52,7 @@
+
diff --git a/all.sln b/all.sln
index f000a22c0..0da90b211 100644
--- a/all.sln
+++ b/all.sln
@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32929.385
MinimumVisualStudioVersion = 10.0.40219.1
@@ -205,6 +205,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DistributedLock", "Distribu
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedLock", "examples\DistributedLock\DistributedLock\DistributedLock.csproj", "{F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.TestContainers", "src\Dapr.TestContainers\Dapr.TestContainers.csproj", "{D14893E1-EF21-4EB4-9DAD-82B5127832CB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.PubSub", "test\Dapr.E2E.Test.PubSub\Dapr.E2E.Test.PubSub.csproj", "{4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.Workflow", "test\Dapr.E2E.Test.Workflow\Dapr.E2E.Test.Workflow.csproj", "{3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.Jobs", "test\Dapr.E2E.Test.Jobs\Dapr.E2E.Test.Jobs.csproj", "{775302E3-69CC-4FBD-98CC-0F889A5D4C63}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -541,6 +549,22 @@ Global
{F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D14893E1-EF21-4EB4-9DAD-82B5127832CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {775302E3-69CC-4FBD-98CC-0F889A5D4C63}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -640,6 +664,10 @@ Global
{9BD12D26-AD9B-4C76-A97F-7A89B7276ABE} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{11D2CA0F-6D38-4DC7-AE06-C1DAE7FC1C20} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
{F2DFB0FE-DF35-4D94-9CC9-43212B1D6F93} = {11D2CA0F-6D38-4DC7-AE06-C1DAE7FC1C20}
+ {D14893E1-EF21-4EB4-9DAD-82B5127832CB} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {4997B2D1-49AF-40B7-8D84-4BD1E94D9B2E} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {3CE069E8-9AAB-4DE4-90AC-077C1DB4EAEB} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {775302E3-69CC-4FBD-98CC-0F889A5D4C63} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
diff --git a/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs
new file mode 100644
index 000000000..70546a84f
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/DaprHarnessBuilder.cs
@@ -0,0 +1,83 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.E2E.Test.Common;
+using Dapr.TestContainers.Common.Options;
+using Dapr.TestContainers.Harnesses;
+
+namespace Dapr.TestContainers.Common;
+
+///
+/// Builds the Dapr harnesses for different building blocks.
+///
+/// The Dapr runtime options.
+/// The test app to run.
+public sealed class DaprHarnessBuilder(DaprRuntimeOptions options, Func? startApp = null)
+{
+ ///
+ /// Builds a workflow harness.
+ ///
+ /// The path to the Dapr resources.
+ public WorkflowHarness BuildWorkflow(string componentsDir) => new(componentsDir, startApp, options);
+
+ ///
+ /// Builds a distributed lock harness.
+ ///
+ /// The path to the Dapr resources.
+ public DistributedLockHarness BuildDistributedLock(string componentsDir) => new(componentsDir, startApp, options);
+
+ ///
+ /// Builds a conversation harness.
+ ///
+ /// The path to the Dapr resources.
+ public ConversationHarness BuildConversation(string componentsDir) => new(componentsDir, startApp, options);
+
+ ///
+ /// Builds a cryptography harness.
+ ///
+ /// The path to the Dapr resources.
+ /// The path to the cryptography keys.
+ public CryptographyHarness BuildCryptography(string componentsDir, string keysDir) =>
+ new(componentsDir, startApp, keysDir, options);
+
+ ///
+ /// Builds a jobs harness.
+ ///
+ /// The path to the Dapr resources.
+ public JobsHarness BuildJobs(string componentsDir) => new(componentsDir, startApp, options);
+
+ ///
+ /// Builds a PubSub harness.
+ ///
+ /// The path to the Dapr resources.
+ public PubSubHarness BuildPubSub(string componentsDir) => new(componentsDir, startApp, options);
+
+ ///
+ /// Builds a state management harness.
+ ///
+ /// The path to the Dapr resources.
+ public StateManagementHarness BuildStateManagement(string componentsDir) => new(componentsDir, startApp, options);
+
+ ///
+ /// Builds an actor harness.
+ ///
+ /// The path to the Dapr resources.
+ public ActorHarness BuildActors(string componentsDir) => new(componentsDir, startApp, options);
+
+ ///
+ /// Creates a test application builder for the specified harness.
+ ///
+ public static DaprTestApplicationBuilder ForHarness(BaseHarness harness) => new(harness);
+}
diff --git a/src/Dapr.TestContainers/Common/HostPortPair.cs b/src/Dapr.TestContainers/Common/HostPortPair.cs
new file mode 100644
index 000000000..a96fe2464
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/HostPortPair.cs
@@ -0,0 +1,25 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.
+// ------------------------------------------------------------------------
+
+namespace Dapr.TestContainers.Common;
+
+///
+/// A hostname and port value pair.
+///
+public sealed record HostPortPair(string Hostname, int Port)
+{
+ ///
+ /// Provides a string version of the record.
+ ///
+ public override string ToString() => $"{Hostname}:{Port}";
+}
diff --git a/src/Dapr.TestContainers/Common/IAsyncContainerFixture.cs b/src/Dapr.TestContainers/Common/IAsyncContainerFixture.cs
new file mode 100644
index 000000000..a3adb135b
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/IAsyncContainerFixture.cs
@@ -0,0 +1,27 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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;
+
+namespace Dapr.TestContainers.Common;
+
+///
+/// Represents a harness that's created for test purposes.
+///
+public interface IAsyncContainerFixture : IAsyncDisposable
+{
+ ///
+ Task InitializeAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/Dapr.TestContainers/Common/IAsyncStartable.cs b/src/Dapr.TestContainers/Common/IAsyncStartable.cs
new file mode 100644
index 000000000..f44635aa7
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/IAsyncStartable.cs
@@ -0,0 +1,36 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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;
+
+namespace Dapr.TestContainers.Common;
+
+///
+/// Represents a resource that can be started and stopped.
+///
+public interface IAsyncStartable : IAsyncDisposable
+{
+ ///
+ /// Starts the resource.
+ ///
+ /// Cancellation token/
+ Task StartAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Stops the resource.
+ ///
+ /// Cancellation token.
+ Task StopAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/Dapr.TestContainers/Common/Options/DaprLogLevel.cs b/src/Dapr.TestContainers/Common/Options/DaprLogLevel.cs
new file mode 100644
index 000000000..77972bf30
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/Options/DaprLogLevel.cs
@@ -0,0 +1,45 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.
+// ------------------------------------------------------------------------
+
+namespace Dapr.TestContainers.Common.Options;
+
+///
+/// A configurable log level.
+///
+public enum DaprLogLevel
+{
+ ///
+ /// Represents a "debug" log level.
+ ///
+ Debug,
+ ///
+ /// Represents an "info" log level.
+ ///
+ Info,
+ ///
+ /// Represents a "warn" log level.
+ ///
+ Warn,
+ ///
+ /// Represents an "error" log level.
+ ///
+ Error,
+ ///
+ /// Represents a "fatal" log level.
+ ///
+ Fatal,
+ ///
+ /// Represents a "panic" log level.
+ ///
+ Panic
+}
diff --git a/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs
new file mode 100644
index 000000000..d5c60625a
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/Options/DaprRuntimeOptions.cs
@@ -0,0 +1,71 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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;
+
+namespace Dapr.TestContainers.Common.Options;
+
+///
+/// The various options used to spin up the Dapr containers.
+///
+/// The version of the Dapr images to use.
+public sealed record DaprRuntimeOptions(string Version = "latest")
+{
+ ///
+ /// The application's port.
+ ///
+ public int AppPort { get; set; } = 8080;
+
+ ///
+ /// The ID of the test application.
+ ///
+ public string AppId { get; private set; } = $"test-app-{Guid.NewGuid():N}";
+
+ ///
+ /// The level of Dapr logs to show.
+ ///
+ public DaprLogLevel LogLevel { get; private set; } = DaprLogLevel.Info;
+
+ ///
+ /// The image tag for the Dapr runtime.
+ ///
+ public string RuntimeImageTag => $"daprio/daprd:{Version}";
+ ///
+ /// The image tag for the Dapr placement service.
+ ///
+ public string PlacementImageTag => $"daprio/placement:{Version}";
+ ///
+ /// The image tag for the Dapr scheduler service.
+ ///
+ public string SchedulerImageTag => $"daprio/scheduler:{Version}";
+
+ ///
+ /// Sets the Dapr log level.
+ ///
+ /// The log level to specify.
+ public DaprRuntimeOptions WithLogLevel(DaprLogLevel logLevel)
+ {
+ LogLevel = logLevel;
+ return this;
+ }
+
+ ///
+ /// Sets the Dapr App ID.
+ ///
+ /// The App ID to use for the test application.
+ public DaprRuntimeOptions WithAppId(string appId)
+ {
+ AppId = appId;
+ return this;
+ }
+}
diff --git a/src/Dapr.TestContainers/Common/PortUtilities.cs b/src/Dapr.TestContainers/Common/PortUtilities.cs
new file mode 100644
index 000000000..1d3387f89
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/PortUtilities.cs
@@ -0,0 +1,34 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Net;
+using System.Net.Sockets;
+
+namespace Dapr.TestContainers.Common;
+
+///
+/// Provides port-related utilities.
+///
+internal static class PortUtilities
+{
+ ///
+ /// Finds a port that's available to use.
+ ///
+ /// The available port number.
+ public static int GetAvailablePort()
+ {
+ using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ socket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
+ return ((IPEndPoint)socket.LocalEndPoint!).Port;
+ }
+}
diff --git a/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs b/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs
new file mode 100644
index 000000000..c503e34df
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/Testing/DaprTestApplication.cs
@@ -0,0 +1,61 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Harnesses;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Dapr.TestContainers.Common.Testing;
+
+///
+/// Represents a running Dapr test application.
+///
+public sealed class DaprTestApplication : IAsyncDisposable
+{
+ private readonly BaseHarness _harness;
+ private readonly WebApplication? _app;
+
+ internal DaprTestApplication(BaseHarness harness, WebApplication? app)
+ {
+ _harness = harness;
+ _app = app;
+ }
+
+ ///
+ /// Gets a service from the application's DI container.
+ ///
+ public T GetRequiredService() where T : notnull
+ {
+ if (_app is null)
+ throw new InvalidOperationException("No app configured");
+
+ return _app.Services.GetRequiredService();
+ }
+
+ ///
+ /// Creates a service scope.
+ ///
+ public IServiceScope CreateScope() =>
+ _app?.Services.CreateScope() ?? throw new InvalidOperationException("No app configured");
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_app is not null)
+ await _app.DisposeAsync();
+
+ await _harness.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs
new file mode 100644
index 000000000..cba540d05
--- /dev/null
+++ b/src/Dapr.TestContainers/Common/Testing/DaprTestApplicationBuilder.cs
@@ -0,0 +1,90 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Generic;
+using System.Threading.Tasks;
+using Dapr.TestContainers.Common.Testing;
+using Dapr.TestContainers.Harnesses;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Dapr.E2E.Test.Common;
+
+///
+/// Fluent builder for creating test applications with Dapr harnesses.
+///
+public sealed class DaprTestApplicationBuilder(BaseHarness harness)
+{
+ private Action? _configureServices;
+ private Action? _configureApp;
+
+ ///
+ /// Configures services for the test application.
+ ///
+ public DaprTestApplicationBuilder ConfigureServices(Action configure)
+ {
+ _configureServices = configure;
+ return this;
+ }
+
+ ///
+ /// Configures the test application pipeline.
+ ///
+ public DaprTestApplicationBuilder ConfigureApp(Action configure)
+ {
+ _configureApp = configure;
+ return this;
+ }
+
+ ///
+ /// Builds and starts the test application and harness.
+ ///
+ ///
+ public async Task BuildAndStartAsync()
+ {
+ await harness.InitializeAsync();
+
+ WebApplication? app = null;
+ if (_configureServices is not null || _configureApp is not null)
+ {
+ var builder = WebApplication.CreateBuilder();
+
+ // Configure Dapr endpoints via in-memory configuration instead of environment variables
+ builder.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ { "DAPR_HTTP_ENDPOINT", $"http://127.0.0.1:{harness.DaprHttpPort}" },
+ { "DAPR_GRPC_ENDPOINT", $"http://127.0.0.1:{harness.DaprGrpcPort}" }
+ });
+
+ builder.Logging.ClearProviders();
+ builder.Logging.AddSimpleConsole();
+ builder.WebHost.UseUrls($"http://0.0.0.0:{harness.AppPort}");
+
+
+
+ _configureServices?.Invoke(builder);
+
+ app = builder.Build();
+
+ _configureApp?.Invoke(app);
+
+ await app.StartAsync();
+ }
+
+ return new DaprTestApplication(harness, app);
+ }
+}
diff --git a/src/Dapr.TestContainers/Configuration/ConfigurationSettings.cs b/src/Dapr.TestContainers/Configuration/ConfigurationSettings.cs
new file mode 100644
index 000000000..6b333bebd
--- /dev/null
+++ b/src/Dapr.TestContainers/Configuration/ConfigurationSettings.cs
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.
+// ------------------------------------------------------------------------
+
+namespace Dapr.TestContainers.Configuration;
+
+///
+/// Base Dapr configuration settings.
+///
+public abstract class ConfigurationSettings;
diff --git a/src/Dapr.TestContainers/Configuration/OtelTracingConfigurationSettings.cs b/src/Dapr.TestContainers/Configuration/OtelTracingConfigurationSettings.cs
new file mode 100644
index 000000000..856ffaec0
--- /dev/null
+++ b/src/Dapr.TestContainers/Configuration/OtelTracingConfigurationSettings.cs
@@ -0,0 +1,36 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.
+// ------------------------------------------------------------------------
+
+namespace Dapr.TestContainers.Configuration;
+
+///
+/// Configuration settings for OTEL tracing.
+///
+/// The tracing endpoint address.
+/// Indicates whether the endpoint is secure.
+/// The tracing protocol.
+public sealed class OtelTracingConfigurationSettings(string endpointAddress, bool isSecure, string protocol) : ConfigurationSettings
+{
+ ///
+ /// The endpoint address.
+ ///
+ public string EndpointAddress => endpointAddress;
+ ///
+ /// Whether the endpoint address is secure.
+ ///
+ public bool IsSecure => isSecure;
+ ///
+ /// The collection protocol.
+ ///
+ public string Protocol => protocol;
+}
diff --git a/src/Dapr.TestContainers/Configuration/SecretScope.cs b/src/Dapr.TestContainers/Configuration/SecretScope.cs
new file mode 100644
index 000000000..ecfe9701f
--- /dev/null
+++ b/src/Dapr.TestContainers/Configuration/SecretScope.cs
@@ -0,0 +1,80 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Dapr.TestContainers.Configuration;
+
+///
+/// Builds out a collection of secret scopes to the Dapr configuration.
+///
+/// The secret scopes to emit.
+public sealed class SecretScopeConfigurationBuilder(IReadOnlyCollection scopes)
+{
+ ///
+ /// Builds the secret scope section of the Dapr configuration file.
+ ///
+ ///
+ public string Build()
+ {
+ var sb = new StringBuilder("secrets:");
+ sb.AppendLine(" scopes:");
+ foreach (var scope in scopes)
+ {
+ sb.AppendLine($" - storeName: {scope.StoreName}");
+ sb.AppendLine($" defaultAccess: {scope.DefaultAccess.ToString().ToLowerInvariant()}");
+ if (scope.AllowedSecrets.Count > 0)
+ {
+ sb.AppendLine(
+ $" allowedSecrets: [{string.Join(", ", scope.AllowedSecrets.Select(s => $"\"{s}\""))}]");
+ }
+ if (scope.DeniedSecrets.Count > 0)
+ {
+ sb.AppendLine(
+ $" deniedSecrets: [{string.Join(", ", scope.DeniedSecrets.Select(s => $"\"{s}\""))}]");
+ }
+ }
+
+ return sb.ToString();
+ }
+}
+
+///
+/// Used to scope a named secret store component to one or more secrets for an application.
+///
+/// The name of the secret store.
+/// The default access to the secrets in the store.
+/// The list of secret keys that can be accessed.
+/// The list of secret keys that cannot be accessed.
+public sealed record SecretScope(
+ string StoreName,
+ SecretStoreAccess DefaultAccess,
+ IReadOnlyCollection AllowedSecrets,
+ IReadOnlyCollection DeniedSecrets);
+
+///
+/// The secret store access modifier.
+///
+public enum SecretStoreAccess
+{
+ ///
+ /// Indicates that secret access should be allowed by default.
+ ///
+ Allow,
+ ///
+ /// Indicates that secret access should be denied by default.
+ ///
+ Deny
+}
diff --git a/src/Dapr.TestContainers/Configuration/ZipkinTracingConfigurationSettings.cs b/src/Dapr.TestContainers/Configuration/ZipkinTracingConfigurationSettings.cs
new file mode 100644
index 000000000..a21c9763d
--- /dev/null
+++ b/src/Dapr.TestContainers/Configuration/ZipkinTracingConfigurationSettings.cs
@@ -0,0 +1,26 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.
+// ------------------------------------------------------------------------
+
+namespace Dapr.TestContainers.Configuration;
+
+///
+/// Configuration settings for Zipkin tracing.
+///
+/// The tracing endpoint address.
+public sealed class ZipkinTracingConfigurationSettings(string endpointAddress) : ConfigurationSettings
+{
+ ///
+ /// The endpoint address.
+ ///
+ public string EndpointAddress => endpointAddress;
+}
diff --git a/src/Dapr.TestContainers/Constants.cs b/src/Dapr.TestContainers/Constants.cs
new file mode 100644
index 000000000..badd283a1
--- /dev/null
+++ b/src/Dapr.TestContainers/Constants.cs
@@ -0,0 +1,47 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.
+// ------------------------------------------------------------------------
+
+namespace Dapr.TestContainers;
+
+///
+/// Various constants used throughout the project.
+///
+public static class Constants
+{
+ ///
+ /// Various names used for the Dapr components.
+ ///
+ public static class DaprComponentNames
+ {
+ ///
+ /// The name of the State Management component.
+ ///
+ public const string StateManagementComponentName = "statestore";
+ ///
+ /// The name of the PubSub component.
+ ///
+ public const string PubSubComponentName = "pubsub";
+ ///
+ /// The name of the Conversation component.
+ ///
+ public const string ConversationComponentName = "conversation";
+ ///
+ /// The name of the Cryptography component.
+ ///
+ public const string CryptographyComponentName = "cryptography";
+ ///
+ /// The name of the Distributed Lock component.
+ ///
+ public const string DistributedLockComponentName = "distributed-lock";
+ }
+}
diff --git a/src/Dapr.TestContainers/Containers/Components/Component.cs b/src/Dapr.TestContainers/Containers/Components/Component.cs
new file mode 100644
index 000000000..51dc29011
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/Components/Component.cs
@@ -0,0 +1,44 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Generic;
+using System.Linq;
+
+namespace Dapr.TestContainers.Containers.Components;
+
+///
+/// Represents a Dapr component.
+///
+/// The name of the component.
+/// The type of the component.
+/// The component's version.
+/// Metadata associated with the component.
+public abstract class Component(string name, string type, string version, Dictionary metadata)
+{
+ ///
+ /// The name of the component.
+ ///
+ public string Name => name;
+ ///
+ /// The type of the component.
+ ///
+ public string Type => type;
+ ///
+ /// The version of the component.
+ ///
+ public string Version => version;
+ ///
+ /// The metadata attached to the component.
+ ///
+ public List Metadata => metadata.Select(kv => new MetadataEntry(kv.Key, kv.Value)).ToList();
+}
diff --git a/src/Dapr.TestContainers/Containers/Components/MetadataEntry.cs b/src/Dapr.TestContainers/Containers/Components/MetadataEntry.cs
new file mode 100644
index 000000000..fcc3c3162
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/Components/MetadataEntry.cs
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.
+// ------------------------------------------------------------------------
+
+namespace Dapr.TestContainers.Containers.Components;
+
+///
+/// Provides the name and value for a metadata record in a component.
+///
+public sealed record MetadataEntry(string Name, string Value);
diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs
new file mode 100644
index 000000000..15447fce4
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/Dapr/DaprPlacementContainer.cs
@@ -0,0 +1,80 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common;
+using Dapr.TestContainers.Common.Options;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+
+namespace Dapr.TestContainers.Containers.Dapr;
+
+///
+/// The container for the Dapr placement service.
+///
+public sealed class DaprPlacementContainer : IAsyncStartable
+{
+ private readonly IContainer _container;
+
+ private string _containerName = $"placement-{Guid.NewGuid():N}";
+
+ ///
+ /// The internal network alias/name of the container.
+ ///
+ public string NetworkAlias => _containerName;
+ ///
+ /// The container hostname.
+ ///
+ public string Host => _container.Hostname;
+ ///
+ /// The container's external port.
+ ///
+ public int ExternalPort { get; private set; }
+ ///
+ /// THe contains' internal port.
+ ///
+ public const int InternalPort = 50006;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The Dapr runtime options.
+ /// The shared Docker network to connect to.
+ public DaprPlacementContainer(DaprRuntimeOptions options, INetwork network)
+ {
+ //Placement service runs via port 50006
+ _container = new ContainerBuilder()
+ .WithImage(options.PlacementImageTag)
+ .WithName(_containerName)
+ .WithNetwork(network)
+ .WithCommand("./placement", "-port", InternalPort.ToString())
+ .WithPortBinding(InternalPort, assignRandomHostPort: true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("placement server leadership acquired"))
+ .Build();
+ }
+
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ await _container.StartAsync(cancellationToken);
+ ExternalPort = _container.GetMappedPublicPort(InternalPort);
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken);
+ ///
+ public ValueTask DisposeAsync() => _container.DisposeAsync();
+}
diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs
new file mode 100644
index 000000000..99c58fc5d
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/Dapr/DaprSchedulerContainer.cs
@@ -0,0 +1,107 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.TestContainers.Common;
+using Dapr.TestContainers.Common.Options;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Configurations;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+
+namespace Dapr.TestContainers.Containers.Dapr;
+
+///
+/// The container for the Dapr scheduler service.
+///
+public sealed class DaprSchedulerContainer : IAsyncStartable
+{
+ private readonly IContainer _container;
+ // Contains the data directory used by this instance of the Dapr scheduler service
+ private string _hostDataDir = Path.Combine(Path.GetTempPath(), $"dapr-scheduler-{Guid.NewGuid():N}");
+ private string _containerName = $"scheduler-{Guid.NewGuid():N}";
+
+ ///
+ /// The internal network alias/name of the container.
+ ///
+ public string NetworkAlias => _containerName;
+ ///
+ /// The container's hostname.
+ ///
+ public string Host => _container.Hostname;
+ ///
+ /// The container's external port.
+ ///
+ public int ExternalPort { get; private set; }
+ ///
+ /// The container's internal port.
+ ///
+ public const int InternalPort = 51005;
+
+ ///
+ /// Creates a new instance of a .
+ ///
+ public DaprSchedulerContainer(DaprRuntimeOptions options, INetwork network)
+ {
+ // Scheduler service runs via port 51005
+ const string containerDataDir = "/tmp/dapr-scheduler";
+ string[] cmd =
+ [
+ "./scheduler",
+ "--port", InternalPort.ToString(),
+ "--etcd-data-dir", containerDataDir
+ ];
+
+ //Create a unique temp directory on the host for the test run based on this instance's data directory
+ Directory.CreateDirectory(_hostDataDir);
+
+ _container = new ContainerBuilder()
+ .WithImage(options.SchedulerImageTag)
+ .WithName(_containerName)
+ .WithNetwork(network)
+ .WithCommand(cmd.ToArray())
+ .WithPortBinding(InternalPort, assignRandomHostPort: true)
+ // Mount an anonymous volume to /data to ensure the scheduler has write permissions
+ .WithBindMount(_hostDataDir, containerDataDir, AccessMode.ReadWrite)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("api is ready"))
+ .Build();
+ }
+
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ await _container.StartAsync(cancellationToken);
+ ExternalPort = _container.GetMappedPublicPort(InternalPort);
+
+ // Empty dirs with 0777 inside container:
+ await _container.ExecAsync(["sh", "-c", "mkdir -p ./default-dapr-scheduler-server-0/dapr-0.1 && chmod 0777 ./default-dapr-scheduler-server-0/dapr-0.1"
+ ], cancellationToken);
+ await _container.ExecAsync(["sh", "-c", "mkdir -p ./dapr-scheduler-existing-cluster && chmod 0777 ./dapr-scheduler-existing-cluster"
+ ], cancellationToken);
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken);
+ ///
+ public ValueTask DisposeAsync()
+ {
+ // Remove the data directory if it exists
+ if (Directory.Exists(_hostDataDir))
+ Directory.Delete(_hostDataDir, true);
+ return _container.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs
new file mode 100644
index 000000000..3ad4a4faa
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/Dapr/DaprdContainer.cs
@@ -0,0 +1,134 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.TestContainers.Common;
+using Dapr.TestContainers.Common.Options;
+using DotNet.Testcontainers;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Configurations;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+
+namespace Dapr.TestContainers.Containers.Dapr;
+
+///
+/// The container for the Dapr runtime.
+///
+public sealed class DaprdContainer : IAsyncStartable
+{
+ private const int InternalHttpPort = 3500;
+ private const int InternalGrpcPort = 50001;
+ private readonly IContainer _container;
+
+ private string _containerName = $"dapr-{Guid.NewGuid():N}";
+
+ ///
+ /// The internal network alias/name of the container.
+ ///
+ public string NetworkAlias => _containerName;
+ ///
+ /// The HTTP port of the Dapr runtime.
+ ///
+ public int HttpPort { get; private set; }
+ ///
+ /// The gRPC port of the Dapr runtime.
+ ///
+ public int GrpcPort { get; private set; }
+
+ ///
+ /// The hostname to locate the Dapr runtime on in the shared Docker network.
+ ///
+ public const string ContainerHostAlias = "host.docker.internal";
+
+ ///
+ /// Used to initialize a new instance of a ..
+ ///
+ /// The ID of the app to initialize daprd with.
+ /// The path to the Dapr resources directory.
+ /// The Dapr runtime options.
+ /// The shared Docker network to connect to.
+ /// The hostname and port of the Placement service.
+ /// The hostname and port of the Scheduler service.
+ public DaprdContainer(string appId, string componentsHostFolder, DaprRuntimeOptions options, INetwork network, HostPortPair? placementHostAndPort = null, HostPortPair? schedulerHostAndPort = null)
+ {
+ const string componentsPath = "/components";
+ var cmd =
+ new List
+ {
+ "/daprd",
+ "-app-id", appId,
+ "-app-port", options.AppPort.ToString(),
+ "-app-channel-address", "host.docker.internal",
+ "-dapr-http-port", InternalHttpPort.ToString(),
+ "-dapr-grpc-port", InternalGrpcPort.ToString(),
+ "-log-level", options.LogLevel.ToString().ToLowerInvariant(),
+ "-resources-path", componentsPath
+ };
+
+ if (placementHostAndPort is not null)
+ {
+ cmd.Add("-placement-host-address");
+ cmd.Add(placementHostAndPort.ToString());
+ }
+ else
+ {
+ // Explicitly disable placement if not provided to speed up startup
+ cmd.Add("-placement-host-address");
+ cmd.Add("");
+ }
+
+ if (schedulerHostAndPort is not null)
+ {
+ cmd.Add("-scheduler-host-address");
+ cmd.Add(schedulerHostAndPort.ToString());
+ }
+ else
+ {
+ // Explicitly disable scheduler if not provider
+ cmd.Add("-scheduler-host-address");
+ cmd.Add("");
+ }
+
+ _container = new ContainerBuilder()
+ .WithImage(options.RuntimeImageTag)
+ .WithName(_containerName)
+ .WithLogger(ConsoleLogger.Instance)
+ .WithCommand(cmd.ToArray())
+ .WithNetwork(network)
+ .WithExtraHost(ContainerHostAlias, "host-gateway")
+ .WithPortBinding(InternalHttpPort, assignRandomHostPort: true)
+ .WithPortBinding(InternalGrpcPort, assignRandomHostPort: true)
+ .WithBindMount(componentsHostFolder, componentsPath, AccessMode.ReadOnly)
+ .WithWaitStrategy(Wait.ForUnixContainer()
+ .UntilMessageIsLogged("Internal gRPC server is running"))
+ //.UntilMessageIsLogged(@"^dapr initialized. Status: Running. Init Elapsed "))
+ .Build();
+ }
+
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ await _container.StartAsync(cancellationToken);
+ HttpPort = _container.GetMappedPublicPort(InternalHttpPort);
+ GrpcPort = _container.GetMappedPublicPort(InternalGrpcPort);
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken);
+ ///
+ public ValueTask DisposeAsync() => _container.DisposeAsync();
+}
diff --git a/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs b/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs
new file mode 100644
index 000000000..71dc85941
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/LocalStorageCryptographyContainer.cs
@@ -0,0 +1,61 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.IO;
+
+namespace Dapr.TestContainers.Containers;
+
+///
+/// This is an odd one because it's not actually a container - just the entry point for the YAML file builder.
+///
+public sealed class LocalStorageCryptographyContainer
+{
+ ///
+ /// Builds out the YAML components for the local storage cryptography implementation.
+ ///
+ public static class Yaml
+ {
+ ///
+ /// Writes a
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string WriteCryptoYamlToFolder(string folderPath, string keyPath, string fileName = "local-crypto.yaml")
+ {
+ var yaml = GetLocalStorageYaml(keyPath);
+ return WriteToFolder(folderPath, fileName, yaml);
+ }
+
+ private static string WriteToFolder(string folderPath, string fileName, string yaml)
+ {
+ Directory.CreateDirectory(folderPath);
+ var fullPath = Path.Combine(folderPath, fileName);
+ File.WriteAllText(fullPath, yaml);
+ return fullPath;
+ }
+
+ private static string GetLocalStorageYaml(string keyPath) =>
+ $@"apiVersion: dapr.io/v1alpha
+kind: Component
+metadata:
+ name: {Constants.DaprComponentNames.CryptographyComponentName}
+spec:
+ type: crypto.dapr.localstorage
+ version: v1
+ metadata:
+ - name: path
+ value: {keyPath}";
+ }
+}
diff --git a/src/Dapr.TestContainers/Containers/OllamaContainer.cs b/src/Dapr.TestContainers/Containers/OllamaContainer.cs
new file mode 100644
index 000000000..f2dd4006a
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/OllamaContainer.cs
@@ -0,0 +1,115 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.TestContainers.Common;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+
+namespace Dapr.TestContainers.Containers;
+
+///
+/// Provides an Ollama container.
+///
+public sealed class OllamaContainer : IAsyncStartable
+{
+ private const int InternalPort = 11434;
+ private string _containerName = $"ollama-{Guid.NewGuid():N}";
+
+ private readonly IContainer _container;
+
+ ///
+ /// Provides an Ollama container.
+ ///
+ public OllamaContainer(INetwork network)
+ {
+ _container = new ContainerBuilder()
+ .WithImage("ollama/ollama")
+ .WithName(_containerName)
+ .WithNetwork(network)
+ .WithPortBinding(InternalPort, assignRandomHostPort: true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort))
+ .Build();
+ }
+
+ ///
+ /// The internal container port used by Ollama.
+ ///
+ public const int ContainerPort = InternalPort;
+ ///
+ /// The internal network alias/name of the container.
+ ///
+ public string NetworkAlias => _containerName;
+ ///
+ /// The hostname of the container.
+ ///
+ public string Host => _container.Hostname;
+ ///
+ /// The port of the container.
+ ///
+ public int Port { get; private set; }
+
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ await _container.StartAsync(cancellationToken);
+ Port = _container.GetMappedPublicPort(InternalPort);
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken);
+ ///
+ public ValueTask DisposeAsync() => _container.DisposeAsync();
+
+ ///
+ /// Builds out the YAML components for Ollama.
+ ///
+ public static class Yaml
+ {
+ ///
+ /// Writes the component YAML.
+ ///
+ public static string WriteConversationYamlToFolder(string folderPath, string fileName = "ollama-conversation.yaml", string model = "smollm:135m", string cacheTtl = "10m", string endpoint = "http://localhost:11434/v1")
+ {
+ var yaml = GetConversationYaml(model, cacheTtl, endpoint);
+ return WriteToFolder(folderPath, fileName, yaml);
+ }
+
+ private static string WriteToFolder(string folderPath, string fileName, string yaml)
+ {
+ Directory.CreateDirectory(folderPath);
+ var fullPath = Path.Combine(folderPath, fileName);
+ File.WriteAllText(fullPath, yaml);
+ return fullPath;
+ }
+
+ private static string GetConversationYaml(string model, string cacheTtl, string endpoint) =>
+ $@"apiVersion: dapr.io/v1alpha
+kind: Component
+metadata:
+ name: {Constants.DaprComponentNames.ConversationComponentName}
+spec:
+ type: conversation.ollama
+ metadata:
+ - name: model
+ value: {model}
+ - name: cacheTTL
+ value: {cacheTtl}
+ - name: endpoint
+ value: {endpoint}";
+ }
+}
diff --git a/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs
new file mode 100644
index 000000000..e5c5eda94
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/RabbitMqContainer.cs
@@ -0,0 +1,119 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.TestContainers.Common;
+using DotNet.Testcontainers;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+
+namespace Dapr.TestContainers.Containers;
+
+///
+/// Provides a RabbitMQ container.
+///
+public sealed class RabbitMqContainer : IAsyncStartable
+{
+ private const int InternalPort = 5672;
+
+ private readonly IContainer _container;
+ private string _containerName = $"rabbitmq-{Guid.NewGuid():N}";
+
+ ///
+ /// Provides a RabbitMQ container.
+ ///
+ public RabbitMqContainer(INetwork network)
+ {
+ _container = new ContainerBuilder()
+ .WithImage("rabbitmq:alpine")
+ .WithName(_containerName)
+ .WithNetwork(network)
+ .WithLogger(ConsoleLogger.Instance)
+ .WithPortBinding(InternalPort, assignRandomHostPort: true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort))
+ .Build();
+ }
+
+ ///
+ /// The internal container port used by RabbitMQ.
+ ///
+ public const int ContainerPort = InternalPort;
+ ///
+ /// The internal network alias/name of the container.
+ ///
+ public string NetworkAlias => _containerName;
+ ///
+ /// The hostname of the container.
+ ///
+ public string Host => _container.Hostname;
+ ///
+ /// The port of the container.
+ ///
+ public int Port { get; private set; }
+
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ await _container.StartAsync(cancellationToken);
+ Port = _container.GetMappedPublicPort(InternalPort);
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken);
+ ///
+ public ValueTask DisposeAsync() => _container.DisposeAsync();
+
+ ///
+ /// Builds out the YAML components for RabbitMQ.
+ ///
+ public static class Yaml
+ {
+ ///
+ /// Writes a PubSub YAML component.
+ ///
+ public static string WritePubSubYamlToFolder(string folderPath, string fileName = "rabbitmq-pubsub.yaml", string rabbitmqHost = "localhost:5672")
+ {
+ var yaml = GetPubSubYaml(rabbitmqHost);
+ return WriteToFolder(folderPath, fileName, yaml);
+ }
+
+ private static string WriteToFolder(string folderPath, string fileName, string yaml)
+ {
+ Directory.CreateDirectory(folderPath);
+ var fullPath = Path.Combine(folderPath, fileName);
+ File.WriteAllText(fullPath, yaml);
+ return fullPath;
+ }
+
+ private static string GetPubSubYaml(string rabbitmqHost) =>
+ $@"apiVersion: dapr.io/v1alpha
+kind: Component
+metadata:
+ name: {Constants.DaprComponentNames.PubSubComponentName}
+spec:
+ type: pubsub.rabbitmq
+ metadata:
+ - name: protocol
+ value: amqp
+ - name: hostname
+ value: {rabbitmqHost}
+ - name: username
+ value: default
+ - name: password
+ value: default";
+ }
+}
diff --git a/src/Dapr.TestContainers/Containers/RedisContainer.cs b/src/Dapr.TestContainers/Containers/RedisContainer.cs
new file mode 100644
index 000000000..d6daec0b9
--- /dev/null
+++ b/src/Dapr.TestContainers/Containers/RedisContainer.cs
@@ -0,0 +1,153 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.TestContainers.Common;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+
+namespace Dapr.TestContainers.Containers;
+
+///
+/// Provides a Redis container.
+///
+public sealed class RedisContainer : IAsyncStartable
+{
+ private const int InternalPort = 6379;
+ private readonly string _containerName = $"redis-{Guid.NewGuid():N}";
+
+ private readonly IContainer _container;
+
+ ///
+ /// Provides a Redis container.
+ ///
+ public RedisContainer(INetwork network)
+ {
+ _container = new ContainerBuilder()
+ .WithImage("redis:alpine")
+ .WithName(_containerName)
+ .WithNetwork(network)
+ .WithPortBinding(InternalPort, assignRandomHostPort: true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(InternalPort))
+ .Build();
+ }
+
+ ///
+ /// The internal container port used by Redis.
+ ///
+ public const int ContainerPort = InternalPort;
+ ///
+ /// The internal network alias/name of the container.
+ ///
+ public string NetworkAlias => _containerName;
+ ///
+ /// The hostname of the container.
+ ///
+ public string Host => _container.Hostname;
+ ///
+ /// The port of the container.
+ ///
+ public int Port { get; private set; }
+
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ await _container.StartAsync(cancellationToken);
+ Port = _container.GetMappedPublicPort(InternalPort);
+ }
+ ///
+ public Task StopAsync(CancellationToken cancellationToken = default) => _container.StopAsync(cancellationToken);
+ ///
+ public ValueTask DisposeAsync() => _container.DisposeAsync();
+
+ ///
+ /// Builds out each of the YAML components for Redis
+ ///
+ public static class Yaml
+ {
+ ///
+ /// Writes a state store YAML component.
+ ///
+ public static string WriteStateStoreYamlToFolder(string folderPath, string fileName = "redis-state.yaml",string redisHost = "localhost:6379",
+ string? passwordSecretName = null)
+ {
+ var yaml = GetRedisStateStoreYaml(redisHost, passwordSecretName);
+ return WriteToFolder(folderPath, fileName, yaml);
+ }
+
+ ///
+ /// Writes a distributed lock YAML component.
+ ///
+ public static string WriteDistributedLockYamlToFolder(string folderPath, string fileName = "redis-lock.yaml",
+ string redisHost = "localhost:6379", string? passwordSecretName = null)
+ {
+ var yaml = GetDistributedLockYaml(redisHost, passwordSecretName);
+ return WriteToFolder(folderPath, fileName, yaml);
+ }
+
+ private static string WriteToFolder(string folderPath, string fileName, string yaml)
+ {
+ Directory.CreateDirectory(folderPath);
+ var fullPath = Path.Combine(folderPath, fileName);
+ File.WriteAllText(fullPath, yaml);
+ return fullPath;
+ }
+
+ private static string BuildSecretBlock(string? passwordSecretName) =>
+ passwordSecretName is null
+ ? string.Empty
+ : $" - name: redisPassword\n secretKeyRef:\n name: {passwordSecretName}\n key: redis-password\n";
+
+ private static string GetDistributedLockYaml(string redisHost, string? passwordSecretName)
+ {
+ var secretBlock = BuildSecretBlock(passwordSecretName);
+ return
+ $@"apiVersion: dapr.io/v1alpha1
+kind: Component
+metadata:
+ name: {Constants.DaprComponentNames.DistributedLockComponentName}
+ namespace: default
+spec:
+ type: lock.redis
+ version: v1
+ metadata:
+ - name: redisHost
+ value: {redisHost}
+{secretBlock}";
+ }
+
+ private static string GetRedisStateStoreYaml(string redisHost, string? passwordSecretName)
+ {
+ var secretBlock = BuildSecretBlock(passwordSecretName);
+ return
+ $@"apiVersion: dapr.io/v1alpha1
+kind: Component
+metadata:
+ name: {Constants.DaprComponentNames.StateManagementComponentName}
+ namespace: default
+spec:
+ type: state.redis
+ version: v1
+ metadata:
+ - name: redisHost
+ value: {redisHost}
+ - name: actorStateStore
+ value: ""true""
+{secretBlock}";
+ }
+ }
+}
diff --git a/src/Dapr.TestContainers/Dapr.TestContainers.csproj b/src/Dapr.TestContainers/Dapr.TestContainers.csproj
new file mode 100644
index 000000000..96485ea10
--- /dev/null
+++ b/src/Dapr.TestContainers/Dapr.TestContainers.csproj
@@ -0,0 +1,15 @@
+
+
+
+ enable
+ Dapr.TestContainers
+ Dapr TestContainers implementation
+ TestContainers for validating Dapr applications
+
+
+
+
+
+
+
+
diff --git a/src/Dapr.TestContainers/Harnesses/ActorHarness.cs b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs
new file mode 100644
index 000000000..19cd7a379
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/ActorHarness.cs
@@ -0,0 +1,69 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers;
+using Dapr.TestContainers.Containers.Dapr;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for Dapr's actor building block.
+///
+public sealed class ActorHarness : BaseHarness
+{
+ private readonly RedisContainer _redis;
+ private readonly DaprPlacementContainer _placement;
+ private readonly DaprSchedulerContainer _schedueler;
+ private readonly string componentsDir;
+
+ ///
+ /// Provides an implementation harness for Dapr's actor building block.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The dapr runtime options.
+ public ActorHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options)
+ {
+ this.componentsDir = componentsDir;
+ _placement = new DaprPlacementContainer(options, Network);
+ _schedueler = new DaprSchedulerContainer(options, Network);
+ _redis = new(Network);
+ }
+
+ ///
+ protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Start infrastructure
+ await _redis.StartAsync(cancellationToken);
+ await _placement.StartAsync(cancellationToken);
+ await _schedueler.StartAsync(cancellationToken);
+
+ // Emit component YAMLs pointing to Redis
+ RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");
+
+ DaprPlacementExternalPort = _placement.ExternalPort;
+ DaprSchedulerExternalPort = _schedueler.ExternalPort;
+ }
+
+ ///
+ protected override async ValueTask OnDisposeAsync()
+ {
+ await _redis.DisposeAsync();
+ await _placement.DisposeAsync();
+ await _schedueler.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/BaseHarness.cs b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs
new file mode 100644
index 000000000..b19589b90
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/BaseHarness.cs
@@ -0,0 +1,180 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapr.TestContainers.Common;
+using Dapr.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers.Dapr;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Networks;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides a base harness for building Dapr building block harnesses.
+///
+public abstract class BaseHarness(string componentsDirectory, Func? startApp, DaprRuntimeOptions options) : IAsyncContainerFixture
+{
+ ///
+ /// The Daprd container exposed by the harness.
+ ///
+ private protected DaprdContainer? _daprd;
+
+ private readonly TaskCompletionSource _sidecarPortsReady = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ ///
+ /// A shared Docker network that's safer for CI environments - each harness instance gets its own network for isolation.
+ ///
+ protected readonly INetwork Network = new NetworkBuilder().Build();
+
+ ///
+ /// Gets the port that the Dapr sidecar is configured to talk to - this is the port the test application should use.
+ ///
+ public int AppPort { get; } = PortUtilities.GetAvailablePort();
+
+ ///
+ /// The HTTP port used by the Daprd container.
+ ///
+ public int DaprHttpPort => _daprd?.HttpPort ?? 0;
+
+ ///
+ /// The HTTP endpoint used by the Daprd container.
+ ///
+ public string DaprHttpEndpoint => $"http://{DaprdContainer.ContainerHostAlias}:{DaprHttpPort}";
+
+ ///
+ /// The gRPC port used by the Daprd container.
+ ///
+ public int DaprGrpcPort => _daprd?.GrpcPort ?? 0;
+
+ ///
+ /// The gRPC endpoint used by the Daprd container.
+ ///
+ public string DaprGrpcEndpoint => $"http://{DaprdContainer.ContainerHostAlias}:{DaprGrpcPort}";
+
+ ///
+ /// The Dapr components directory.
+ ///
+ protected string ComponentsDirectory => componentsDirectory;
+
+ ///
+ /// The port of the Dapr placement service, if started.
+ ///
+ protected int? DaprPlacementExternalPort { get; set; }
+
+ ///
+ /// The network alias of the placement container, if started.
+ ///
+ protected string? DaprPlacementAlias { get; set; }
+
+ ///
+ /// The port of the Dapr scheduler service, if started.
+ ///
+ protected int? DaprSchedulerExternalPort { get; set; }
+
+ ///
+ /// The network alias of the scheduler container, if started.
+ ///
+ protected string? DaprSchedulerAlias { get; set; }
+
+ ///
+ /// The specific container startup logic for the harness.
+ ///
+ /// Cancellation token.
+ protected abstract Task OnInitializeAsync(CancellationToken cancellationToken);
+
+ ///
+ /// Initializes and runs the test app with the harness.
+ ///
+ /// Cancellation token.
+ ///
+ public async Task InitializeAsync(CancellationToken cancellationToken = default)
+ {
+ // Run the actual container orchestration defined in the subclass to set up any pre-requisite containers before loading daprd and the start app, if specified
+ await OnInitializeAsync(cancellationToken);
+
+ // Configure and start daprd; point at placement & scheduler
+ _daprd = new DaprdContainer(
+ appId: options.AppId,
+ componentsHostFolder: ComponentsDirectory,
+ options: options with {AppPort = this.AppPort},
+ Network,
+ DaprPlacementExternalPort is null || DaprPlacementAlias is null ? null : new HostPortPair(DaprPlacementAlias, DaprPlacementContainer.InternalPort),
+ DaprSchedulerExternalPort is null || DaprSchedulerAlias is null ? null : new HostPortPair(DaprSchedulerAlias, DaprSchedulerContainer.InternalPort));
+
+ var daprdTask = Task.Run(async () =>
+ {
+ await _daprd!.StartAsync(cancellationToken);
+
+ _sidecarPortsReady.TrySetResult();
+ }, cancellationToken);
+
+ Task? appTask = null;
+ if (startApp is not null)
+ {
+ appTask = Task.Run(async () =>
+ {
+ await _sidecarPortsReady.Task.WaitAsync(cancellationToken);
+ await startApp(AppPort);
+ }, cancellationToken);
+ }
+
+ await Task.WhenAll(daprdTask, appTask ?? Task.CompletedTask);
+ }
+
+ ///
+ /// Disposes the resources in this harness.
+ ///
+ public virtual async ValueTask DisposeAsync()
+ {
+ await OnDisposeAsync();
+
+ if (_daprd is not null)
+ await _daprd.DisposeAsync();
+
+ // Clean up the per-instance network
+ await Network.DisposeAsync();
+
+ // Clean up generated YAML files
+ CleanupComponents(ComponentsDirectory);
+
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Override this to dispose harness-specific resources before base cleanup.
+ ///
+ protected virtual ValueTask OnDisposeAsync() => ValueTask.CompletedTask;
+
+ ///
+ /// Deletes the specified directory recursively as part of a clean-up operation.
+ ///
+ /// The clean to clean up.
+ protected static void CleanupComponents(string path)
+ {
+ if (Directory.Exists(path))
+ {
+ try
+ {
+ Directory.Delete(path, true);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+ }
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs
new file mode 100644
index 000000000..ced196129
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/ConversationHarness.cs
@@ -0,0 +1,58 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for conversation functionality.
+///
+public sealed class ConversationHarness : BaseHarness
+{
+ private readonly OllamaContainer _ollama;
+ private readonly string componentsDir;
+
+ ///
+ /// Provides an implementation harness for conversation functionality.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The Dapr runtime options.
+ public ConversationHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options)
+ {
+ this.componentsDir = componentsDir;
+ _ollama = new(Network);
+ }
+
+ ///
+ protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Start infrastructure
+ await _ollama.StartAsync(cancellationToken);
+
+ // Emit component YAMLs for Ollama (use the default tiny model)
+ OllamaContainer.Yaml.WriteConversationYamlToFolder(componentsDir,
+ endpoint: $"http://{_ollama.NetworkAlias}:{OllamaContainer.ContainerPort}/v1");
+ }
+
+ ///
+ protected override async ValueTask OnDisposeAsync()
+ {
+ await _ollama.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs
new file mode 100644
index 000000000..9bd9a78a8
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/CryptographyHarness.cs
@@ -0,0 +1,51 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for Dapr's cryptography building block.
+///
+public sealed class CryptographyHarness : BaseHarness
+{
+ private readonly string componentsDir;
+ private readonly string keyPath;
+
+ ///
+ /// Provides an implementation harness for Dapr's cryptography building block.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The Dapr runtime options.
+ /// The path locally to the cryptography keys to use.
+ public CryptographyHarness(string componentsDir, Func? startApp, string keyPath, DaprRuntimeOptions options) : base(componentsDir, startApp, options)
+ {
+ this.componentsDir = componentsDir;
+ this.keyPath = keyPath;
+ }
+
+ ///
+ protected override Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Emit the component YAML describing the local crypto key store
+ LocalStorageCryptographyContainer.Yaml.WriteCryptoYamlToFolder(componentsDir, keyPath);
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs
new file mode 100644
index 000000000..237f2cb13
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/DistributedLockHarness.cs
@@ -0,0 +1,57 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for Dapr's distributed lock building block.
+///
+public sealed class DistributedLockHarness : BaseHarness
+{
+ private readonly RedisContainer _redis;
+ private readonly string componentsDir;
+
+ ///
+ /// Provides an implementation harness for Dapr's distributed lock building block.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The Dapr runtime options.
+ public DistributedLockHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options)
+ {
+ this.componentsDir = componentsDir;
+ _redis = new(Network);
+ }
+
+ ///
+ protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Start infrastructure
+ await _redis.StartAsync(cancellationToken);
+
+ // Emit component YAMLs pointing to Redis
+ RedisContainer.Yaml.WriteDistributedLockYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");
+ }
+
+ ///
+ protected override async ValueTask OnDisposeAsync()
+ {
+ await _redis.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/JobsHarness.cs b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs
new file mode 100644
index 000000000..7dbc32a7c
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/JobsHarness.cs
@@ -0,0 +1,56 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers.Dapr;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for the Jobs building block.
+///
+public sealed class JobsHarness : BaseHarness
+{
+ private readonly DaprSchedulerContainer _scheduler;
+ private readonly string componentsDir;
+
+ ///
+ /// Provides an implementation harness for the Jobs building block.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The Dapr runtime options.
+ public JobsHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options)
+ {
+ this.componentsDir = componentsDir;
+ _scheduler = new DaprSchedulerContainer(options, Network);
+ }
+
+ ///
+ protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Start the infrastructure
+ await _scheduler.StartAsync(cancellationToken);
+ DaprSchedulerExternalPort = _scheduler.ExternalPort;
+ DaprSchedulerAlias = _scheduler.NetworkAlias;
+ }
+
+ ///
+ protected override async ValueTask OnDisposeAsync()
+ {
+ await _scheduler.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs
new file mode 100644
index 000000000..13931702d
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/PubSubHarness.cs
@@ -0,0 +1,57 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for Dapr's pub/sub building block.
+///
+public sealed class PubSubHarness : BaseHarness
+{
+ private readonly RabbitMqContainer _rabbitmq;
+ private readonly string componentsDir;
+
+ ///
+ /// Provides an implementation harness for Dapr's pub/sub building block.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The Dapr runtime options.
+ public PubSubHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options): base(componentsDir, startApp, options)
+ {
+ this.componentsDir = componentsDir;
+ _rabbitmq = new(Network);
+ }
+
+ ///
+ protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Start infrastructure
+ await _rabbitmq.StartAsync(cancellationToken);
+
+ // Emit component YAMLs pointing to RabbitMQ
+ RabbitMqContainer.Yaml.WritePubSubYamlToFolder(componentsDir, rabbitmqHost: $"{_rabbitmq.NetworkAlias}:{RabbitMqContainer.ContainerPort}");
+ }
+
+ ///
+ protected override async ValueTask OnDisposeAsync()
+ {
+ await _rabbitmq.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs
new file mode 100644
index 000000000..703c51bd8
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/StateManagementHarness.cs
@@ -0,0 +1,57 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for Dapr's state management building block.
+///
+public sealed class StateManagementHarness : BaseHarness
+{
+ private readonly RedisContainer _redis;
+ private readonly string componentsDir;
+
+ ///
+ /// Provides an implementation harness for Dapr's state management building block.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The Dapr runtime options.
+ public StateManagementHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options)
+ {
+ this.componentsDir = componentsDir;
+ _redis = new(Network);
+ }
+
+ ///
+ protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Start infrastructure
+ await _redis.StartAsync(cancellationToken);
+
+ // Emit component YAMLs pointing to Redis
+ RedisContainer.Yaml.WriteStateStoreYamlToFolder(componentsDir, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");
+ }
+
+ ///
+ protected override async ValueTask OnDisposeAsync()
+ {
+ await _redis.DisposeAsync();
+ }
+}
diff --git a/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs
new file mode 100644
index 000000000..6d869af00
--- /dev/null
+++ b/src/Dapr.TestContainers/Harnesses/WorkflowHarness.cs
@@ -0,0 +1,68 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.TestContainers.Common.Options;
+using Dapr.TestContainers.Containers;
+using Dapr.TestContainers.Containers.Dapr;
+
+namespace Dapr.TestContainers.Harnesses;
+
+///
+/// Provides an implementation harness for Dapr's Workflow building block.
+///
+public sealed class WorkflowHarness : BaseHarness
+{
+ private readonly RedisContainer _redis;
+ private readonly DaprPlacementContainer _placement;
+ private readonly DaprSchedulerContainer _scheduler;
+
+ ///
+ /// Provides an implementation harness for Dapr's Workflow building block.
+ ///
+ /// The directory to Dapr components.
+ /// The test app to validate in the harness.
+ /// The Dapr runtime options.
+ public WorkflowHarness(string componentsDir, Func? startApp, DaprRuntimeOptions options) : base(componentsDir, startApp, options)
+ {
+ _placement = new DaprPlacementContainer(options, Network);
+ _scheduler = new DaprSchedulerContainer(options, Network);
+ _redis = new(Network);
+ }
+
+ ///
+ protected override async Task OnInitializeAsync(CancellationToken cancellationToken)
+ {
+ // Start infrastructure
+ await _redis.StartAsync(cancellationToken);
+ await _placement.StartAsync(cancellationToken);
+ await _scheduler.StartAsync(cancellationToken);
+
+ // Emit component YAMLs pointing to Redis
+ RedisContainer.Yaml.WriteStateStoreYamlToFolder(ComponentsDirectory, redisHost: $"{_redis.NetworkAlias}:{RedisContainer.ContainerPort}");
+
+ // Set the service ports
+ this.DaprPlacementExternalPort = _placement.ExternalPort;
+ this.DaprSchedulerExternalPort = _scheduler.ExternalPort;
+ }
+
+ ///
+ protected override async ValueTask OnDisposeAsync()
+ {
+ await _placement.DisposeAsync();
+ await _scheduler.DisposeAsync();
+ await _redis.DisposeAsync();
+ }
+}
diff --git a/test/Dapr.E2E.Test.Jobs/Dapr.E2E.Test.Jobs.csproj b/test/Dapr.E2E.Test.Jobs/Dapr.E2E.Test.Jobs.csproj
new file mode 100644
index 000000000..bcbe137eb
--- /dev/null
+++ b/test/Dapr.E2E.Test.Jobs/Dapr.E2E.Test.Jobs.csproj
@@ -0,0 +1,27 @@
+
+
+
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.E2E.Test.Jobs/JobsTests.cs b/test/Dapr.E2E.Test.Jobs/JobsTests.cs
new file mode 100644
index 000000000..17e4c3b64
--- /dev/null
+++ b/test/Dapr.E2E.Test.Jobs/JobsTests.cs
@@ -0,0 +1,77 @@
+// ------------------------------------------------------------------------
+// Copyright 2025 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.Text;
+using Dapr.Jobs;
+using Dapr.Jobs.Extensions;
+using Dapr.Jobs.Models;
+using Dapr.Jobs.Models.Responses;
+using Dapr.TestContainers.Common;
+using Dapr.TestContainers.Common.Options;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Dapr.E2E.Test.Jobs;
+
+public sealed class JobsTests
+{
+ [Fact]
+ public async Task ShouldScheduleAndReceiveJob()
+ {
+ var options = new DaprRuntimeOptions();
+ var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), $"jobs-components-{Guid.NewGuid():N}");
+ var jobName = $"e2e-job-{Guid.NewGuid():N}";
+ var invocationTcs = new TaskCompletionSource<(string payload, string jobName)>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var harness = new DaprHarnessBuilder(options).BuildJobs(componentsDir);
+ await using var testApp = await DaprHarnessBuilder.ForHarness(harness)
+ .ConfigureServices(builder =>
+ {
+ // Explicitly configure the Dapr client with the correct endpoints
+ builder.Services.AddDaprJobsClient(configure: (sp, clientBuilder) =>
+ {
+ var config = sp.GetRequiredService();
+ var grpcEndpoint = config["DAPR_GRPC_ENDPOINT"];
+ var httpEndpoint = config["DAPR_HTTP_ENDPOINT"];
+
+ if (!string.IsNullOrEmpty(grpcEndpoint))
+ clientBuilder.UseGrpcEndpoint(grpcEndpoint);
+ if (!string.IsNullOrEmpty(httpEndpoint))
+ clientBuilder.UseHttpEndpoint(httpEndpoint);
+ });
+ })
+ .ConfigureApp(app =>
+ {
+ app.MapDaprScheduledJobHandler((string incomingJobName, ReadOnlyMemory payload,
+ ILogger? logger, CancellationToken _) =>
+ {
+ logger?.LogInformation("Received job {Job}", incomingJobName);
+ invocationTcs.TrySetResult((Encoding.UTF8.GetString(payload.Span), incomingJobName));
+ });
+ })
+ .BuildAndStartAsync();
+
+ // Clean test logic
+ using var scope = testApp.CreateScope();
+ var daprJobsClient = scope.ServiceProvider.GetRequiredService();
+
+ var payload = "Hello!"u8.ToArray();
+ await daprJobsClient.ScheduleJobAsync(jobName, DaprJobSchedule.FromDuration(TimeSpan.FromSeconds(2)),
+ payload, repeats: 1, overwrite: true);
+
+ var received = await invocationTcs.Task.WaitAsync(TimeSpan.FromSeconds(30));
+ Assert.Equal(Encoding.UTF8.GetString(payload), received.payload);
+ Assert.Equal(jobName, received.jobName);
+ }
+}
diff --git a/test/Dapr.E2E.Test.PubSub/Dapr.E2E.Test.PubSub.csproj b/test/Dapr.E2E.Test.PubSub/Dapr.E2E.Test.PubSub.csproj
new file mode 100644
index 000000000..60d871640
--- /dev/null
+++ b/test/Dapr.E2E.Test.PubSub/Dapr.E2E.Test.PubSub.csproj
@@ -0,0 +1,27 @@
+
+
+
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.E2E.Test.PubSub/PubsubTests.cs b/test/Dapr.E2E.Test.PubSub/PubsubTests.cs
new file mode 100644
index 000000000..920a2aa4c
--- /dev/null
+++ b/test/Dapr.E2E.Test.PubSub/PubsubTests.cs
@@ -0,0 +1,98 @@
+// // ------------------------------------------------------------------------
+// // Copyright 2025 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 Dapr.Client;
+// using Dapr.TestContainers;
+// using Dapr.TestContainers.Common;
+// using Dapr.TestContainers.Common.Options;
+// using Microsoft.AspNetCore.Builder;
+// using Microsoft.AspNetCore.Hosting;
+// using Microsoft.AspNetCore.Http;
+// using Microsoft.Extensions.DependencyInjection;
+// using Microsoft.Extensions.Hosting;
+//
+// namespace Dapr.E2E.Test.PubSub;
+//
+// public class PubsubTests
+// {
+// [Fact]
+// public async Task ShouldEnablePublishAndSubscribe()
+// {
+// var options = new DaprRuntimeOptions();
+// var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), "pubsub-components");
+// const string topicName = "test-topic";
+//
+// WebApplication? app = null;
+//
+// var messageReceived = new TaskCompletionSource();
+//
+// // Build and initialize the test harness
+// var harnessBuilder = new DaprHarnessBuilder(options, StartApp);
+// var harness = harnessBuilder.BuildPubSub(componentsDir);
+//
+// try
+// {
+// await harness.InitializeAsync();
+//
+// var testAppBuilder = new HostApplicationBuilder();
+// testAppBuilder.Services.AddDaprClient();
+// using var testApp = testAppBuilder.Build();
+// await using var scope = testApp.Services.CreateAsyncScope();
+// var daprClient = scope.ServiceProvider.GetRequiredService();
+//
+// // Use DaprClient to publish a message
+// const string testMessage = "Hello!";
+// await daprClient.PublishEventAsync(Constants.DaprComponentNames.PubSubComponentName, topicName,
+// testMessage);
+//
+// // Wait for the app to receive the message via the sidecar
+// var result = await messageReceived.Task.WaitAsync(TimeSpan.FromSeconds(10));
+// Assert.Equal(testMessage, result);
+// }
+// finally
+// {
+// await harness.DisposeAsync();
+//
+// if (app != null)
+// await app.DisposeAsync();
+// }
+//
+// return;
+//
+// // Define the app startup
+// async Task StartApp(int port)
+// {
+// var builder = WebApplication.CreateBuilder();
+// builder.WebHost.UseUrls($"http://localhost:{port}");
+// builder.Services.AddControllers().AddDapr();
+//
+// app = builder.Build();
+//
+// // Setup the subscription endpoint
+// app.UseCloudEvents();
+// app.MapSubscribeHandler();
+//
+// // Endpoint that Dapr will call when a message is published
+// app.MapPost("/message-handler", async (HttpContext context) =>
+// {
+// var data = await context.Request.ReadFromJsonAsync();
+// messageReceived.TrySetResult(data?.ToString() ?? "empty");
+// return Results.Ok();
+// })
+// .WithTopic(Constants.DaprComponentNames.PubSubComponentName, topicName);
+//
+// await app.StartAsync();
+// }
+// }
+//
+// }
diff --git a/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj b/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj
new file mode 100644
index 000000000..fc4a8b0db
--- /dev/null
+++ b/test/Dapr.E2E.Test.Workflow/Dapr.E2E.Test.Workflow.csproj
@@ -0,0 +1,26 @@
+
+
+
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.E2E.Test.Workflow/WorkflowTests.cs b/test/Dapr.E2E.Test.Workflow/WorkflowTests.cs
new file mode 100644
index 000000000..889c6d1e4
--- /dev/null
+++ b/test/Dapr.E2E.Test.Workflow/WorkflowTests.cs
@@ -0,0 +1,105 @@
+// // ------------------------------------------------------------------------
+// // Copyright 2025 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 Dapr.TestContainers.Common;
+// using Dapr.TestContainers.Common.Options;
+// using Dapr.Workflow;
+// using Microsoft.AspNetCore.Builder;
+// using Microsoft.AspNetCore.Hosting;
+// using Microsoft.Extensions.DependencyInjection;
+// using Microsoft.Extensions.Logging;
+//
+// namespace Dapr.E2E.Test.Workflow;
+//
+// public class WorkflowTests
+// {
+// [Fact]
+// public async Task ShouldTestTaskChaining()
+// {
+// var options = new DaprRuntimeOptions();
+// var componentsDir = Path.Combine(Directory.GetCurrentDirectory(), $"test-components-{Guid.NewGuid():N}");
+//
+// WebApplication? app = null;
+//
+// // Build and initialize the test harness
+// var harnessBuilder = new DaprHarnessBuilder(options, StartApp);
+// var harness = harnessBuilder.BuildWorkflow(componentsDir);
+//
+// try
+// {
+// await harness.InitializeAsync();
+//
+// await using var scope = app!.Services.CreateAsyncScope();
+// var daprWorkflowClient = scope.ServiceProvider.GetRequiredService();
+//
+// // Start the workflow
+// var workflowId = Guid.NewGuid().ToString("N");
+// const int startingValue = 8;
+//
+// await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(TestWorkflow), workflowId, startingValue);
+//
+// var result = await daprWorkflowClient.WaitForWorkflowCompletionAsync(workflowId, true);
+//
+// Assert.Equal(WorkflowRuntimeStatus.Completed, result.RuntimeStatus);
+// var resultValue = result.ReadOutputAs();
+//
+// Assert.Equal(16, resultValue);
+// }
+// finally
+// {
+// await harness.DisposeAsync();
+// if (app is not null)
+// await app.DisposeAsync();
+// }
+//
+// return;
+//
+// // Define the app startup
+// async Task StartApp(int port)
+// {
+// var builder = WebApplication.CreateBuilder();
+// builder.Logging.ClearProviders();
+// builder.Logging.AddSimpleConsole();
+// builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
+// builder.Services.AddDaprWorkflow(opt =>
+// {
+// opt.RegisterWorkflow();
+// opt.RegisterActivity();
+// });
+//
+// Console.WriteLine($"HTTP: {Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT")}");
+// Console.WriteLine($"GRPC: {Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT")}");
+//
+// app = builder.Build();
+// await app.StartAsync();
+// }
+// }
+//
+// private sealed class DoublingActivity : WorkflowActivity
+// {
+// public override Task RunAsync(WorkflowActivityContext context, int input)
+// {
+// var square = input * 2;
+// return Task.FromResult(square);
+// }
+// }
+//
+// private sealed class TestWorkflow : Workflow
+// {
+// public override async Task RunAsync(WorkflowContext context, int input)
+// {
+// var result = await context.CallActivityAsync(nameof(DoublingActivity), input);
+// return result;
+// }
+// }
+// }