From a8b3736d0dc08be4b1f0ab9e47570650e3434b6e Mon Sep 17 00:00:00 2001 From: Cecil Phillip Date: Sat, 1 Aug 2026 12:59:15 -0400 Subject: [PATCH] Migrate AspireIntegrations to hosting package --- Directory.Packages.props | 9 +- TemporalioSamples.sln | 15 -- src/AspireIntegrations/README.md | 15 +- .../Properties/AssemblyInfo.cs | 3 - .../Temporal.Extensions.Aspire.Hosting.csproj | 13 - .../TemporalArgsBuilder.cs | 62 ----- .../TemporalCliLocator.cs | 42 --- .../TemporalCliServerResource.cs | 23 -- .../TemporalCliServerResourceExtensions.cs | 92 ------- .../TemporalContainerOptions.cs | 10 - .../TemporalContainerResource.cs | 20 -- .../TemporalContainerResourceExtensions.cs | 83 ------ .../TemporalEnvironmentHelper.cs | 33 --- .../TemporalHealthCheck.cs | 36 --- .../TemporalHealthCheckBuilderExtensions.cs | 35 --- .../TemporalHealthCheckHelper.cs | 85 ------- .../TemporalLocalResource.cs | 16 -- .../TemporalLocalResourceExtensions.cs | 240 ------------------ .../TemporalLocalResourceSubscriber.cs | 168 ------------ .../TemporalResourceConstants.cs | 34 --- .../TemporalResourceOptions.cs | 97 ------- .../AppHost.cs | 2 +- .../TemporalioSamples.SampleAppHost.csproj | 3 +- ...emporalCliServerResourceExtensionsTests.cs | 165 ------------ .../TemporalEnvironmentHelperTests.cs | 131 ---------- .../TemporalHealthCheckHelperTests.cs | 47 ---- .../TemporalHealthCheckTests.cs | 60 ----- .../TemporalResourceOptionsTests.cs | 88 ------- tests/TemporalioSamples.Tests.csproj | 1 - 29 files changed, 15 insertions(+), 1613 deletions(-) delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Properties/AssemblyInfo.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Temporal.Extensions.Aspire.Hosting.csproj delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalArgsBuilder.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliLocator.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResource.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResourceExtensions.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerOptions.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResource.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResourceExtensions.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalEnvironmentHelper.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheck.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckBuilderExtensions.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckHelper.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResource.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceExtensions.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceSubscriber.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceConstants.cs delete mode 100644 src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceOptions.cs delete mode 100644 tests/AspireIntegrations/TemporalCliServerResourceExtensionsTests.cs delete mode 100644 tests/AspireIntegrations/TemporalEnvironmentHelperTests.cs delete mode 100644 tests/AspireIntegrations/TemporalHealthCheckHelperTests.cs delete mode 100644 tests/AspireIntegrations/TemporalHealthCheckTests.cs delete mode 100644 tests/AspireIntegrations/TemporalResourceOptionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 408c440..5d5161c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,14 +5,14 @@ - + - - - + + + @@ -25,6 +25,7 @@ + diff --git a/TemporalioSamples.sln b/TemporalioSamples.sln index 15c0868..a3ede5c 100644 --- a/TemporalioSamples.sln +++ b/TemporalioSamples.sln @@ -127,8 +127,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.SampleCli EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.SampleAppHost", "src\AspireIntegrations\TemporalioSamples.SampleAppHost\TemporalioSamples.SampleAppHost.csproj", "{CA136E75-FC34-44E1-B8B2-6E33D8AF520E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Temporal.Extensions.Aspire.Hosting", "src\AspireIntegrations\Temporal.Extensions.Aspire.Hosting\Temporal.Extensions.Aspire.Hosting.csproj", "{89D196AD-A6CE-42FB-BF46-C80BF579FE20}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StandaloneActivity", "StandaloneActivity", "{EAB0C45A-7620-D2D2-2901-5E7FCBFFDA77}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemporalioSamples.StandaloneActivity", "src\StandaloneActivity\TemporalioSamples.StandaloneActivity.csproj", "{240517A1-13B5-4A67-8519-BFCF2C4591B9}" @@ -729,18 +727,6 @@ Global {CA136E75-FC34-44E1-B8B2-6E33D8AF520E}.Release|x64.Build.0 = Release|Any CPU {CA136E75-FC34-44E1-B8B2-6E33D8AF520E}.Release|x86.ActiveCfg = Release|Any CPU {CA136E75-FC34-44E1-B8B2-6E33D8AF520E}.Release|x86.Build.0 = Release|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Debug|x64.ActiveCfg = Debug|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Debug|x64.Build.0 = Debug|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Debug|x86.ActiveCfg = Debug|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Debug|x86.Build.0 = Debug|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Release|Any CPU.Build.0 = Release|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Release|x64.ActiveCfg = Release|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Release|x64.Build.0 = Release|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Release|x86.ActiveCfg = Release|Any CPU - {89D196AD-A6CE-42FB-BF46-C80BF579FE20}.Release|x86.Build.0 = Release|Any CPU {240517A1-13B5-4A67-8519-BFCF2C4591B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {240517A1-13B5-4A67-8519-BFCF2C4591B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {240517A1-13B5-4A67-8519-BFCF2C4591B9}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -876,7 +862,6 @@ Global {FF13AD0E-4F24-4044-B8AD-5A57EF3AE398} = {8781BE47-D710-408E-B143-4D5E20C356E2} {035FF43C-D9C8-4CCE-A35A-E4ABF6F842C8} = {8781BE47-D710-408E-B143-4D5E20C356E2} {CA136E75-FC34-44E1-B8B2-6E33D8AF520E} = {8781BE47-D710-408E-B143-4D5E20C356E2} - {89D196AD-A6CE-42FB-BF46-C80BF579FE20} = {8781BE47-D710-408E-B143-4D5E20C356E2} {EAB0C45A-7620-D2D2-2901-5E7FCBFFDA77} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC} {240517A1-13B5-4A67-8519-BFCF2C4591B9} = {EAB0C45A-7620-D2D2-2901-5E7FCBFFDA77} {5D493692-53AB-4FAA-BA4D-33B1E54E9A48} = {1A647B41-53D0-4638-AE5A-6630BAAE45FC} diff --git a/src/AspireIntegrations/README.md b/src/AspireIntegrations/README.md index 4e73c91..8db5aab 100644 --- a/src/AspireIntegrations/README.md +++ b/src/AspireIntegrations/README.md @@ -1,8 +1,8 @@ -# Temporal Extensions for .NET Aspire +# Temporal Aspire Integration Sample ## Overview -This project provides custom Aspire resource definitions that enable developers to integrate Temporal workflow servers into their Aspire applications with minimal configuration. It supports three deployment models: +This sample demonstrates how to use the `TemporalCommunity.Aspire.Hosting` NuGet package to integrate Temporal workflow servers into .NET Aspire applications. It supports three deployment models: - **Local Testing** - Temporal server using `Temporalio.Testing.WorkflowEnvironment` for fast local development and testing - **Container-based** - Docker container running the official Temporal server image for development and staging environments @@ -29,7 +29,7 @@ This project provides custom Aspire resource definitions that enable developers ## Running the Project -Using the Aspire CLI +Running the AppHost 1. **Navigate to the AppHost project directory:** ```bash @@ -38,10 +38,9 @@ Using the Aspire CLI 2. **Run the project using the Aspire CLI:** ```bash - aspire run + dotnet run ``` -> You can also run the project directly with `dotnet run` from the AppHost directory, or use your IDE's run configuration. ## Setup Options @@ -51,7 +50,7 @@ The local server setup uses a Temporal environment for fast testing without exte **AppHost.cs:** ```csharp -using Temporal.Extensions.Aspire.Hosting; +using TemporalCommunity.Aspire.Hosting; var builder = DistributedApplication.CreateBuilder(args); @@ -112,7 +111,7 @@ Deploy Temporal using the CLI Docker container. **AppHost.cs:** ```csharp -using Temporal.Extensions.Aspire.Hosting; +using TemporalCommunity.Aspire.Hosting; var builder = DistributedApplication.CreateBuilder(args); @@ -163,7 +162,7 @@ Use the Temporal CLI server for environments without Docker. **AppHost.cs:** ```csharp -using Temporal.Extensions.Aspire.Hosting; +using TemporalCommunity.Aspire.Hosting; var builder = DistributedApplication.CreateBuilder(args); diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Properties/AssemblyInfo.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Properties/AssemblyInfo.cs deleted file mode 100644 index 11d991e..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("TemporalioSamples.Tests")] diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Temporal.Extensions.Aspire.Hosting.csproj b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Temporal.Extensions.Aspire.Hosting.csproj deleted file mode 100644 index 6f5e6dd..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/Temporal.Extensions.Aspire.Hosting.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - enable - enable - $(NoWarn);SA1010;SA1116;SA1413;SA1117;CA1031;SA1503;CS0109;CA1002;CA2227;CA1305 - - - - - - - diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalArgsBuilder.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalArgsBuilder.cs deleted file mode 100644 index ce7f157..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalArgsBuilder.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Builds command-line arguments for Temporal dev server startup. -/// -internal static class TemporalArgsBuilder -{ - /// - /// Builds the CLI arguments for a Temporal dev server. - /// When is true the IP is always "0.0.0.0" and the port - /// is always (container mode). - /// When false, IP and port are taken from and --ui-port is also emitted (CLI mode). - /// - /// The resource options containing host, ports, and namespace configuration. - /// If true, uses fixed container defaults; if false, uses options values and emits --ui-port. - /// An array of CLI arguments for the temporal server start-dev command. - internal static string[] BuildArgs(TemporalResourceOptions options, bool fixedIpAndPort = false) - { - var args = new List { "server", "start-dev" }; - - if (fixedIpAndPort) - { - args.AddRange(["--ip", "0.0.0.0"]); - args.AddRange(["--port", $"{TemporalResourceConstants.DefaultServiceEndpointPort}"]); - } - else - { - args.AddRange(["--ip", options.Ip]); - args.AddRange(["--port", options.Port.ToString()]); - args.AddRange(["--ui-port", options.UIPort.ToString()]); - } - - if (options.IsHeadless) - args.Add("--headless"); - - args.AddRange(["--log-level", options.DevServerOptions.LogLevel]); - args.AddRange(["--log-format", options.DevServerOptions.LogFormat]); - - foreach (var ns in options.AdditionalNamespaces) - args.AddRange(["--namespace", ns]); - - if (options.SearchAttributes != null) - { - foreach (var sa in options.SearchAttributes) - args.AddRange(["--search-attribute", $"{sa.Name}={sa.ValueType}"]); - } - - foreach (var dv in options.DynamicConfigValues) - args.AddRange(["--dynamic-config-value", dv]); - - if (!string.IsNullOrEmpty(options.CodecAuth)) - args.AddRange(["--codec-auth", options.CodecAuth]); - - if (!string.IsNullOrEmpty(options.CodecEndpoint)) - args.AddRange(["--codec-endpoint", options.CodecEndpoint]); - - if (!string.IsNullOrEmpty(options.ApiKey)) - args.AddRange(["--api-key", options.ApiKey]); - - return args.ToArray(); - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliLocator.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliLocator.cs deleted file mode 100644 index 69f9610..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliLocator.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Utility for locating and validating the Temporal CLI executable on the system PATH. -/// -internal static class TemporalCliLocator -{ - /// - /// Throws when the temporal CLI executable - /// cannot be found on the system PATH. - /// - /// - /// Optional override for the availability check. When null (default) the real PATH is - /// inspected. Pass a custom delegate in tests to simulate presence or absence of the CLI - /// without depending on the test machine's PATH. - /// - internal static void EnsureAvailable(Func? isAvailable = null) - { - if (!(isAvailable ?? IsOnPath)()) - { - throw new InvalidOperationException( - "The 'temporal' CLI executable was not found on PATH. " + - "Install it from https://docs.temporal.io/cli and ensure " + - "'temporal' is accessible on your PATH before using AddTemporalCliServer."); - } - } - - private static bool IsOnPath() - { - var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - var executableName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? "temporal.exe" - : "temporal"; - - return pathEnv - .Split(Path.PathSeparator) - .Select(dir => Path.Combine(dir, executableName)) - .Any(File.Exists); - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResource.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResource.cs deleted file mode 100644 index ee89906..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResource.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Represents a Temporal server running via the CLI executable. -/// -public class TemporalCliServerResource(string name, string workingDirectory = "./") - : ExecutableResource(name, "temporal", workingDirectory), IResourceWithConnectionString, - IResourceWithServiceDiscovery -{ - private EndpointReference? primaryEndpoint; - - /// Gets the primary gRPC service endpoint. - public EndpointReference PrimaryEndpoint => - primaryEndpoint ??= new(this, TemporalResourceConstants.ServiceEndpointName); - - /// Gets the connection string expression for dependent services. - public ReferenceExpression ConnectionStringExpression => - ReferenceExpression.Create( - $"{PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}"); - - /// Gets or sets the resource configuration options. - public TemporalResourceOptions Options { get; set; } = new(); -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResourceExtensions.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResourceExtensions.cs deleted file mode 100644 index 1140a79..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalCliServerResourceExtensions.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Extension methods for registering Temporal CLI server resources in Aspire. -/// -public static class TemporalCliServerResourceExtensions -{ - /// - /// Adds a Temporal CLI server resource to the distributed application. - /// - /// The distributed application builder. - /// The resource name. Default is "temporal-cli-server". - /// Optional action to configure the resource options. - /// A builder for the Temporal CLI server resource. - public static IResourceBuilder AddTemporalCliServer( - this IDistributedApplicationBuilder builder, - string name = "temporal-cli-server", - Action? configure = null) - { - return builder.AddTemporalCliServer(name, configure, isTemporalCliAvailable: null); - } - - /// - /// Adds a reference from a dependent service to a Temporal CLI server resource, - /// automatically injecting connection environment variables. - /// - /// The type of the destination resource. - /// The resource builder for the dependent service. - /// The Temporal CLI server resource builder. - /// The updated resource builder. - public static IResourceBuilder WithReference( - this IResourceBuilder builder, IResourceBuilder source) - where TDestination : IResourceWithEnvironment - { - return builder - .WithReference((IResourceBuilder)source) - .WithEnvironment(ctx => - { - TemporalEnvironmentHelper.AddEnvironmentVariables( - ctx, - source.Resource.Options, - source.Resource.ConnectionStringExpression, - source.GetEndpoint(TemporalResourceConstants.UIEndpointName)); - }); - } - - // Internal overload that accepts an injectable CLI availability check. - // Used by tests to simulate an absent 'temporal' binary without modifying PATH. - internal static IResourceBuilder AddTemporalCliServer( - this IDistributedApplicationBuilder builder, - string name, - Action? configure, - Func? isTemporalCliAvailable) - { - TemporalCliLocator.EnsureAvailable(isTemporalCliAvailable); - var resource = new TemporalCliServerResource(name); - configure?.Invoke(resource.Options); - - var clientAccessor = TemporalHealthCheckHelper.RegisterCachedClientAccessor( - builder, resource, resource.Options.Namespace); - - var healthCheckKey = $"{name}_check"; - builder.Services.AddHealthChecks() - .AddTemporalHealthCheck(clientAccessor, healthCheckKey); - - return builder.AddResource(resource) - .WithArgs(TemporalArgsBuilder.BuildArgs(resource.Options)) - .ExcludeFromManifest() - .WithEndpoint( - targetPort: resource.Options.Port, - port: resource.Options.Port, - isProxied: false, - name: TemporalResourceConstants.ServiceEndpointName) - .WithHttpEndpoint( - targetPort: resource.Options.UIPort, - port: resource.Options.UIPort, - isProxied: false, - name: TemporalResourceConstants.UIEndpointName) - .WithHttpEndpoint( - targetPort: resource.Options.MetricsPort, - port: resource.Options.MetricsPort, - isProxied: false, - name: TemporalResourceConstants.MetricsEndpointName) - .WithHealthCheck(healthCheckKey) - .WithUrlForEndpoint(TemporalResourceConstants.UIEndpointName, url => - { - url.DisplayText = "Dashboard"; - }); - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerOptions.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerOptions.cs deleted file mode 100644 index 573ae62..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Configuration options specific to Temporal Docker container deployments. -/// -public class TemporalContainerOptions : TemporalResourceOptions -{ - /// Gets or sets the Docker image tag. Default is "latest". - public string? ImageTag { get; set; } = TemporalResourceConstants.DefaultTag; -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResource.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResource.cs deleted file mode 100644 index 520faac..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResource.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Represents a Temporal server running as a Docker container. -/// -public class TemporalContainerResource(string name) : ContainerResource(name), IResourceWithConnectionString, IResourceWithServiceDiscovery -{ - private EndpointReference? primaryEndpoint; - - /// Gets the primary gRPC service endpoint. - public EndpointReference PrimaryEndpoint => primaryEndpoint ??= new(this, TemporalResourceConstants.ServiceEndpointName); - - /// Gets the connection string expression for dependent services. - public ReferenceExpression ConnectionStringExpression => - ReferenceExpression.Create( - $"{PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}"); - - /// Gets or sets the container configuration options. - public TemporalContainerOptions Options { get; set; } = new(); -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResourceExtensions.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResourceExtensions.cs deleted file mode 100644 index ba2b574..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalContainerResourceExtensions.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Extension methods for registering Temporal container resources in Aspire. -/// -public static class TemporalContainerResourceExtensions -{ - /// - /// Adds a Temporal Docker container resource to the distributed application. - /// - /// The distributed application builder. - /// The resource name. Default is "temporal-container". - /// Optional action to configure the container options. - /// A builder for the Temporal container resource. - public static IResourceBuilder AddTemporalDevContainer( - this IDistributedApplicationBuilder builder, - string name = "temporal-container", - Action? configure = null) - { - var resource = new TemporalContainerResource(name); - configure?.Invoke(resource.Options); - - var clientAccessor = TemporalHealthCheckHelper.RegisterCachedClientAccessor( - builder, resource, resource.Options.Namespace); - - var healthCheckKey = $"{name}_check"; - builder.Services.AddHealthChecks() - .AddTemporalHealthCheck(clientAccessor, healthCheckKey); - - return builder.AddResource(resource) - .WithImage(TemporalResourceConstants.TemporalImage, - resource.Options.ImageTag ?? TemporalResourceConstants.DefaultTag) - .WithImageRegistry("docker.io") - .WithArgs(TemporalArgsBuilder.BuildArgs(resource.Options, fixedIpAndPort: true)) - .ExcludeFromManifest() - .WithEndpoint( - targetPort: TemporalResourceConstants.DefaultServiceEndpointPort, - port: resource.Options.Port, - isProxied: false, - name: TemporalResourceConstants.ServiceEndpointName) - .WithHttpEndpoint( - targetPort: TemporalResourceConstants.DefaultUIEndpointPort, - port: resource.Options.UIPort, - isProxied: false, - name: TemporalResourceConstants.UIEndpointName) - .WithHttpEndpoint( - targetPort: TemporalResourceConstants.DefaultMetricsEndpointPort, - port: resource.Options.MetricsPort, - isProxied: false, - name: TemporalResourceConstants.MetricsEndpointName) - .WithHealthCheck(healthCheckKey) - .WithUrlForEndpoint(TemporalResourceConstants.UIEndpointName, url => - { - url.DisplayText = "Dashboard"; - }); - } - - /// - /// Adds a reference from a dependent service to a Temporal container resource, - /// automatically injecting connection environment variables. - /// - /// The type of the destination resource. - /// The resource builder for the dependent service. - /// The Temporal container resource builder. - /// The updated resource builder. - public static IResourceBuilder WithReference( - this IResourceBuilder builder, IResourceBuilder source) - where TDestination : IResourceWithEnvironment - { - return builder - .WithReference((IResourceBuilder)source) - .WithEnvironment(ctx => - { - TemporalEnvironmentHelper.AddEnvironmentVariables( - ctx, - source.Resource.Options, - source.Resource.ConnectionStringExpression, - source.GetEndpoint(TemporalResourceConstants.UIEndpointName)); - }); - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalEnvironmentHelper.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalEnvironmentHelper.cs deleted file mode 100644 index 67c6e66..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalEnvironmentHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Helper for injecting Temporal connection details as environment variables into dependent services. -/// -internal static class TemporalEnvironmentHelper -{ - /// - /// Adds Temporal connection and configuration environment variables to a dependent service. - /// This helper only maps the resource connection details and namespace into dependent services. - /// It intentionally does not duplicate all resource-specific Temporal environment variables. - /// - /// The environment callback context for the dependent service. - /// The Temporal resource options containing namespace and codec configuration. - /// The gRPC server address expression. - /// The Web UI address expression. - internal static void AddEnvironmentVariables( - EnvironmentCallbackContext ctx, - TemporalResourceOptions options, - object temporalAddress, - object temporalUiAddress) - { - ctx.EnvironmentVariables["TEMPORAL_ADDRESS"] = temporalAddress; - ctx.EnvironmentVariables["TEMPORAL_UI_ADDRESS"] = temporalUiAddress; - ctx.EnvironmentVariables["TEMPORAL_NAMESPACE"] = options.Namespace; - - if (!string.IsNullOrEmpty(options.CodecAuth)) - ctx.EnvironmentVariables["TEMPORAL_CODEC_AUTH"] = options.CodecAuth; - - if (!string.IsNullOrEmpty(options.CodecEndpoint)) - ctx.EnvironmentVariables["TEMPORAL_CODEC_ENDPOINT"] = options.CodecEndpoint; - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheck.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheck.cs deleted file mode 100644 index 0c517e1..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheck.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Temporalio.Api.WorkflowService.V1; -using Temporalio.Client; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Health check implementation for Temporal servers. -/// -public class TemporalHealthCheck(Func> clientAccessor) : IHealthCheck -{ - /// - /// Checks the health of the Temporal server by calling GetSystemInfoAsync. - /// - /// The health check context providing access to registered services. - /// The cancellation token for this operation. - /// A HealthCheckResult indicating whether the server is reachable and healthy. - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) - { - var client = await clientAccessor(cancellationToken); - if (client is null) - return HealthCheckResult.Unhealthy("Temporal client not yet initialized"); - - try - { - await client.WorkflowService.GetSystemInfoAsync( - new GetSystemInfoRequest(), - new RpcOptions { CancellationToken = cancellationToken }); - return HealthCheckResult.Healthy(); - } - catch (Exception e) - { - return HealthCheckResult.Unhealthy("Unable to reach Temporal server", e); - } - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckBuilderExtensions.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckBuilderExtensions.cs deleted file mode 100644 index 6a25ee4..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckBuilderExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Temporalio.Client; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Extension methods for adding Temporal health checks to Aspire applications. -/// -public static class TemporalHealthCheckBuilderExtensions -{ - /// - /// Adds a Temporal health check to the health checks builder. - /// - /// The health checks builder. - /// A function that provides access to a Temporal client. - /// The health check name. Default is "temporal". - /// Optional tags to associate with the health check. - /// Optional timeout for the health check. - /// The updated health checks builder. - public static IHealthChecksBuilder AddTemporalHealthCheck( - this IHealthChecksBuilder builder, - Func> clientAccessor, - string name = "temporal", - IEnumerable? tags = null, - TimeSpan? timeout = null) - { - return builder.Add(new HealthCheckRegistration( - name, - _ => new TemporalHealthCheck(clientAccessor), - HealthStatus.Unhealthy, - tags, - timeout)); - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckHelper.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckHelper.cs deleted file mode 100644 index b6aea70..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalHealthCheckHelper.cs +++ /dev/null @@ -1,85 +0,0 @@ -using Temporalio.Client; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Helper for registering cached Temporal client accessors for health checks. -/// -internal static class TemporalHealthCheckHelper -{ - /// - /// Subscribes to for a CLI or container resource, - /// creates a once the endpoint is available, and returns an accessor - /// that the health check can call on every probe. - /// The cached client is replaced on each subsequent event so restarts are covered. - /// - /// The distributed application builder for subscribing to events. - /// The Temporal resource being monitored for connection string availability. - /// The Temporal namespace for client connections. - /// A function that accepts a cancellation token and returns the cached ITemporalClient or null if not yet connected. - internal static Func> RegisterCachedClientAccessor( - IDistributedApplicationBuilder builder, - IResource resource, - string @namespace) - { - ITemporalClient? cachedClient = null; - string? hostPort = null; - - async Task EnsureClientConnectedAsync(CancellationToken cancellationToken) - { - if (cachedClient is not null) - return cachedClient; - - if (string.IsNullOrEmpty(hostPort)) - return null; - - try - { - cachedClient = await TemporalClient.ConnectAsync(new TemporalClientConnectOptions - { - Namespace = @namespace, - TargetHost = hostPort - }); - return cachedClient; - } - catch (InvalidOperationException) - { - cachedClient = null; - return null; - } - } - - builder.Eventing.Subscribe(resource, async (@event, _) => - { - try - { - if (!@event.Resource.TryGetEndpoints(out var endpoints)) - return; - - var serviceEndpoint = endpoints.Single(e => e.Name == TemporalResourceConstants.ServiceEndpointName); - hostPort = $"{serviceEndpoint.TargetHost}:{serviceEndpoint.Port}"; - cachedClient = null; - - // The endpoint can be published before Temporal is accepting connections. - // Retry a few times but never throw from this callback. - const int maxAttempts = 30; - for (var attempt = 1; attempt <= maxAttempts; attempt++) - { - var client = await EnsureClientConnectedAsync(CancellationToken.None); - if (client is not null) - break; - - if (attempt < maxAttempts) - await Task.Delay(TimeSpan.FromMilliseconds(500), CancellationToken.None); - } - } - catch - { - // Resource startup must not fail because client warm-up failed. - cachedClient = null; - } - }); - - return EnsureClientConnectedAsync; - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResource.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResource.cs deleted file mode 100644 index 084351f..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResource.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Temporalio.Testing; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Represents a Temporal server running locally via . -/// -public class TemporalLocalResource(string name) - : Resource(name), IResourceWithServiceDiscovery -{ - /// Gets or sets the underlying workflow environment instance. - public WorkflowEnvironment? WorkflowEnvironment { get; set; } - - /// Gets or sets the resource configuration options. - public TemporalResourceOptions Options { get; set; } = new(); -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceExtensions.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceExtensions.cs deleted file mode 100644 index b6f4bb5..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceExtensions.cs +++ /dev/null @@ -1,240 +0,0 @@ -using Aspire.Hosting.Eventing; -using Aspire.Hosting.Lifecycle; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Temporalio.Testing; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Extension methods for registering local Temporal server resources in Aspire. -/// -public static class TemporalLocalResourceExtensions -{ - /// - /// Adds a local Temporal server resource (via WorkflowEnvironment) to the distributed application. - /// - /// The distributed application builder. - /// The resource name. Default is "temporal-local". - /// Optional action to configure the resource options. - /// A builder for the local Temporal server resource. - public static IResourceBuilder AddTemporalLocalDevServer( - this IDistributedApplicationBuilder builder, - string name = "temporal-local", - Action? configure = null) - { - builder.Services.TryAddEventingSubscriber(); - - var resource = new TemporalLocalResource(name); - - configure?.Invoke(resource.Options); - - var healthCheckKey = $"{name}_check"; - builder.Services.AddHealthChecks() - .AddTemporalHealthCheck(_ => Task.FromResult(resource.WorkflowEnvironment?.Client), healthCheckKey); - - var resourceBuilder = builder.AddResource(resource) - .ExcludeFromManifest() - .WithEndpoint( - targetPort: resource.Options.Port, - port: resource.Options.Port, - isProxied: false, - name: TemporalResourceConstants.ServiceEndpointName) - .WithHttpEndpoint( - targetPort: resource.Options.UIPort, - port: resource.Options.UIPort, - isProxied: false, - name: TemporalResourceConstants.UIEndpointName) - .WithHttpEndpoint( - targetPort: resource.Options.MetricsPort, - port: resource.Options.MetricsPort, - isProxied: false, - name: TemporalResourceConstants.MetricsEndpointName) - .WithHealthCheck(healthCheckKey) - .WithUrlForEndpoint(TemporalResourceConstants.UIEndpointName, url => { url.DisplayText = "Dashboard"; }) - .WithInitialState(new CustomResourceSnapshot - { - ResourceType = "temporal-local", - CreationTimeStamp = DateTime.UtcNow, - State = KnownResourceStates.NotStarted, - Properties = - [ - new(CustomResourceKnownProperties.Source, "Temporalio.Testing.WorkflowEnvironment") - ] - }); - - resourceBuilder.WithCommand( - name: KnownResourceCommands.StopCommand, - displayName: "Stop", - executeCommand: async context => - { - var notifications = context.ServiceProvider - .GetRequiredService(); - var resourceLogger = context.ServiceProvider - .GetRequiredService() - .GetLogger(resource); - var eventing = context.ServiceProvider - .GetRequiredService(); - - await notifications.PublishUpdateAsync(resource, s => s with - { - State = KnownResourceStates.Stopping - }); - - try - { - // Publish ResourceStoppedEvent to trigger subscriber cleanup and keep _environments dictionary in sync - // The subscriber's OnResourceStoppedAsync performs the actual ShutdownAsync exactly once. - var resourceEvent = new ResourceEvent(resource, resource.Name, new CustomResourceSnapshot - { - ResourceType = "temporal-local", - CreationTimeStamp = DateTime.UtcNow, - State = KnownResourceStates.Exited, - Properties = [] - }); - var stoppedEvent = new ResourceStoppedEvent(resource, context.ServiceProvider, resourceEvent); - await eventing.PublishAsync(stoppedEvent, context.CancellationToken); - - await notifications.PublishUpdateAsync(resource, s => s with - { - State = KnownResourceStates.Exited - }); - - return CommandResults.Success(); - } - catch (Exception ex) - { - resourceLogger.LogError(ex, "Error shutting down Temporal test server '{ResourceName}'", resource.Name); - return CommandResults.Failure(ex.Message); - } - }, - commandOptions: new CommandOptions - { - IconName = "Stop", - IconVariant = IconVariant.Filled, - IsHighlighted = true, - UpdateState = context => - { - var state = context.ResourceSnapshot.State?.Text; - if (IsStarting(state) || HasNoState(state)) - { - return ResourceCommandState.Disabled; - } - else if (IsRunning(state)) - { - return ResourceCommandState.Enabled; - } - else - { - return ResourceCommandState.Hidden; - } - } - }); - - resourceBuilder.WithCommand( - name: KnownResourceCommands.StartCommand, - displayName: "Start", - executeCommand: async context => - { - var notifications = context.ServiceProvider - .GetRequiredService(); - var resourceLogger = context.ServiceProvider - .GetRequiredService() - .GetLogger(resource); - var eventing = context.ServiceProvider - .GetRequiredService(); - - await notifications.PublishUpdateAsync(resource, s => s with - { - State = KnownResourceStates.Starting - }); - - try - { - resourceLogger.LogInformation("Starting Temporal test server for resource '{ResourceName}'...", resource.Name); - var env = await WorkflowEnvironment.StartLocalAsync(resource.Options); - resource.WorkflowEnvironment = env; - - var targetHost = env.Client.Connection.Options.TargetHost ?? "unknown"; - - resourceLogger.LogInformation( - "Temporal test server started successfully. Target: {TargetHost}, Namespace: {Namespace}", - targetHost, - resource.Options.Namespace); - - await notifications.PublishUpdateAsync(resource, s => s with - { - State = KnownResourceStates.Running, - Properties = - [ - new ResourcePropertySnapshot(CustomResourceKnownProperties.Source, - "Temporalio.Testing.WorkflowEnvironment"), - new ResourcePropertySnapshot("temporal.target-host", targetHost), - new ResourcePropertySnapshot("temporal.namespace", resource.Options.Namespace) - ] - }); - - // Publish ResourceReadyEvent to signal that the resource is ready - await eventing.PublishAsync(new ResourceReadyEvent(resource, context.ServiceProvider), context.CancellationToken); - - return CommandResults.Success(); - } - catch (Exception ex) - { - resourceLogger.LogError(ex, "Failed to start Temporal test server for resource '{ResourceName}'", resource.Name); - await notifications.PublishUpdateAsync(resource, s => s with - { - State = new ResourceStateSnapshot(KnownResourceStates.FailedToStart, KnownResourceStateStyles.Error) - }); - return CommandResults.Failure(ex.Message); - } - }, - commandOptions: new CommandOptions - { - IconName = "Play", - IconVariant = IconVariant.Filled, - IsHighlighted = true, - UpdateState = context => - { - var state = context.ResourceSnapshot.State?.Text; - if (IsStarting(state) || IsRuntimeUnhealthy(state) || HasNoState(state)) - { - return ResourceCommandState.Disabled; - } - - if (IsStopped(state) || IsWaiting(state)) - { - return ResourceCommandState.Enabled; - } - - return ResourceCommandState.Hidden; - } - }); - - return resourceBuilder; - - static bool IsStopped(string? state) => KnownResourceStates.TerminalStates.Contains(state) || - state == KnownResourceStates.NotStarted || state == "Unknown"; - - static bool IsRunning(string? state) => state == KnownResourceStates.Running; - static bool IsStarting(string? state) => state == KnownResourceStates.Starting; - static bool IsWaiting(string? state) => state == KnownResourceStates.Waiting; - static bool IsRuntimeUnhealthy(string? state) => state == KnownResourceStates.RuntimeUnhealthy; - static bool HasNoState(string? state) => string.IsNullOrEmpty(state); - } - - public static IResourceBuilder WithReference( - this IResourceBuilder builder, IResourceBuilder source) - where TDestination : IResourceWithEnvironment - { - return builder - .WithEnvironment(ctx => - { - TemporalEnvironmentHelper.AddEnvironmentVariables( - ctx, - source.Resource.Options, - $"localhost:{source.Resource.Options.Port}", - $"http://localhost:{source.Resource.Options.UIPort}"); - }); - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceSubscriber.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceSubscriber.cs deleted file mode 100644 index 251a340..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalLocalResourceSubscriber.cs +++ /dev/null @@ -1,168 +0,0 @@ -using Aspire.Hosting.Eventing; -using Aspire.Hosting.Lifecycle; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Temporalio.Testing; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Event subscriber for managing local Temporal server lifecycle. -/// -public class TemporalLocalResourceSubscriber : IDistributedApplicationEventingSubscriber -{ - private readonly Dictionary environments = []; - - /// - /// Subscribes to resource events for initializing and managing local Temporal servers. - /// - /// The distributed application eventing service. - /// The execution context containing application state and mode information. - /// The cancellation token for this operation. - /// A completed task once event subscriptions are registered. - public Task SubscribeAsync(IDistributedApplicationEventing eventing, - DistributedApplicationExecutionContext executionContext, - CancellationToken cancellationToken) - { - // Don't start server in publish mode - if (executionContext.IsPublishMode) - { - return Task.CompletedTask; - } - - // Subscribe to InitializeResourceEvent for each TemporalLocalResource - eventing.Subscribe(OnInitializeAsync); - return Task.CompletedTask; - } - - /// - /// Handles initialization of local Temporal resources. - /// - private async Task OnInitializeAsync(InitializeResourceEvent @event, CancellationToken cancellationToken = default) - { - if (@event.Resource is not TemporalLocalResource resource) - { - return; - } - - var resourceLoggerService = @event.Services.GetRequiredService(); - - // Subscribe to ResourceStoppedEvent for this specific resource to handle cleanup - @event.Eventing.Subscribe(resource, - (stopEvent, _) => OnResourceStoppedAsync(stopEvent, resourceLoggerService, resource)); - - await StartTemporalTestServerAsync(resource, @event.Eventing, - @event.Notifications, resourceLoggerService, @event.Services, cancellationToken); - } - - /// - /// Starts the local Temporal test server and publishes connection events. - /// - private async Task StartTemporalTestServerAsync(TemporalLocalResource resource, - IDistributedApplicationEventing eventing, - ResourceNotificationService resourceNotificationService, - ResourceLoggerService resourceLoggerService, - IServiceProvider serviceProvider, - CancellationToken cancellationToken) - { - var resourceLogger = resourceLoggerService.GetLogger(resource); - - try - { - // Publish starting state - await resourceNotificationService.PublishUpdateAsync(resource, state => state with - { - State = new ResourceStateSnapshot(KnownResourceStates.Starting, KnownResourceStateStyles.Info), - StartTimeStamp = DateTime.UtcNow - }); - - resourceLogger.LogInformation("Starting Temporal test server for resource '{ResourceName}'...", - resource.Name); - - var env = await WorkflowEnvironment.StartLocalAsync(resource.Options); - - // Store the environment for later shutdown (before publishing events) - environments[resource.Name] = env; - - // Set the environment on the resource so it can be accessed - resource.WorkflowEnvironment = env; - - var targetHost = env.Client.Connection.Options.TargetHost ?? "unknown"; - var namespaces = string.Join(", ", resource.Options.AdditionalNamespaces); - - resourceLogger.LogInformation( - "Temporal test server started successfully. Target: {TargetHost}, Namespaces: {Namespaces}", - targetHost, - namespaces); - - // Publish running state with properties - await resourceNotificationService.PublishUpdateAsync(resource, state => state with - { - State = KnownResourceStates.Running, - Properties = - [ - new ResourcePropertySnapshot(CustomResourceKnownProperties.Source, - "Temporalio.Testing.WorkflowEnvironment"), - new ResourcePropertySnapshot("temporal.target-host", targetHost), - new ResourcePropertySnapshot("temporal.namespace", resource.Options.Namespace), - new ResourcePropertySnapshot("temporal.namespaces", namespaces) - ] - }); - - // Now publish events (after environment is properly stored and assigned) - var connectionStringAvailableEvent = new ConnectionStringAvailableEvent(resource, serviceProvider); - await eventing.PublishAsync(connectionStringAvailableEvent, cancellationToken).ConfigureAwait(false); - - // Publish ResourceReadyEvent to signal that the resource is ready - await eventing.PublishAsync(new ResourceReadyEvent(resource, serviceProvider), cancellationToken) - .ConfigureAwait(false); - } - catch (Exception ex) - { - resourceLogger.LogError(ex, "Failed to start Temporal test server for resource '{ResourceName}'", - resource.Name); - - // Publish failed state - await resourceNotificationService.PublishUpdateAsync(resource, state => state with - { - State = new ResourceStateSnapshot(KnownResourceStates.FailedToStart, KnownResourceStateStyles.Error) - }); - - throw; - } - } - - /// - /// Handles cleanup when a local Temporal resource is stopped. - /// - private async Task OnResourceStoppedAsync(ResourceStoppedEvent @event, - ResourceLoggerService resourceLoggerService, TemporalLocalResource resource) - { - var resourceName = @event.Resource.Name; - var resourceLogger = resourceLoggerService.GetLogger(resource); - - // Get environment from resource property first, fallback to tracking dictionary - var env = resource.WorkflowEnvironment ?? environments.GetValueOrDefault(resourceName); - - if (env != null) - { - try - { - resourceLogger.LogInformation("Shutting down Temporal test server '{ResourceName}'...", resourceName); - await env.ShutdownAsync(); - resource.WorkflowEnvironment = null; - resourceLogger.LogInformation("Temporal test server '{ResourceName}' shut down successfully.", - resourceName); - } - catch (Exception ex) - { - resourceLogger.LogError(ex, "Error shutting down Temporal test server '{ResourceName}'", resourceName); - throw; - } - finally - { - environments.Remove(resourceName); - } - } - } -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceConstants.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceConstants.cs deleted file mode 100644 index 83dcb54..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceConstants.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Constants used for Temporal resource configuration and endpoints. -/// -public static class TemporalResourceConstants -{ - /// The name of the gRPC service endpoint. - public const string ServiceEndpointName = "grpc"; - - /// The default port for the gRPC service endpoint. - public const int DefaultServiceEndpointPort = 7233; - - /// The name of the Web UI endpoint. - public const string UIEndpointName = "ui"; - - /// The default port for the Web UI endpoint. - public const int DefaultUIEndpointPort = 8233; - - /// The name of the metrics endpoint. - public const string MetricsEndpointName = "metrics"; - - /// The default port for the metrics endpoint. - public const int DefaultMetricsEndpointPort = 9233; - - /// The Docker image name for Temporal. - public const string TemporalImage = "temporalio/temporal"; - - /// The default Docker image tag. - public const string DefaultTag = "latest"; - - /// The default working directory for Temporal CLI execution. - public const string DefaultWorkingDirectory = "./"; -} diff --git a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceOptions.cs b/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceOptions.cs deleted file mode 100644 index 7f9ccd4..0000000 --- a/src/AspireIntegrations/Temporal.Extensions.Aspire.Hosting/TemporalResourceOptions.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Temporalio.Testing; - -namespace Temporal.Extensions.Aspire.Hosting; - -/// -/// Configuration options for Temporal resources (local, container, and CLI-based). -/// Extends with Aspire-specific settings. -/// -public class TemporalResourceOptions : WorkflowEnvironmentStartLocalOptions -{ - private List extraNamespaces = []; - - public TemporalResourceOptions() - { - // Set defaults that differ from base class - UIPort = TemporalResourceConstants.DefaultUIEndpointPort; - UI = true; - TargetHost = $"0.0.0.0:{TemporalResourceConstants.DefaultServiceEndpointPort}"; - } - - /// - /// Gets or sets additional namespaces beyond the primary namespace. - /// Always includes the primary in the returned list. - /// - public new List AdditionalNamespaces - { - get - { - var result = new List { Namespace }; - foreach (var ns in extraNamespaces) - { - if (ns != Namespace && !result.Contains(ns)) - result.Add(ns); - } - return result; - } - - set - { - extraNamespaces = value == null - ? [] - : value.Where(ns => !string.IsNullOrEmpty(ns) && ns != Namespace).Distinct().ToList(); - } - } - - /// - /// Gets the gRPC port, parsed from . - /// - public int Port - { - get - { - if (string.IsNullOrEmpty(TargetHost)) - throw new InvalidOperationException("TargetHost must be set before accessing Port."); - - var parts = TargetHost.Split(':'); - if (parts.Length == 2 && int.TryParse(parts[1], out var port)) - return port; - - throw new InvalidOperationException($"TargetHost '{TargetHost}' is not in the expected 'ip:port' format."); - } - } - - /// - /// Gets the IP address to bind to, parsed from TargetHost. - /// Maps to --ip CLI argument and DevServerOptions.Ip concept. - /// - public string Ip - { - get - { - if (string.IsNullOrEmpty(TargetHost)) - throw new InvalidOperationException("TargetHost must be set before accessing Ip."); - - var parts = TargetHost.Split(':'); - if (parts.Length > 0 && !string.IsNullOrEmpty(parts[0])) - return parts[0]; - - return "0.0.0.0"; - } - } - - /// Gets or sets the metrics endpoint port. Default is 9233. - public int MetricsPort { get; set; } = TemporalResourceConstants.DefaultMetricsEndpointPort; - - /// Gets a value indicating whether the UI is disabled. - public bool IsHeadless => !UI; - - /// Gets or sets dynamic configuration values for the Temporal server. - public List DynamicConfigValues { get; set; } = []; - - /// Gets or sets the codec authentication token for encrypted payloads. - public string? CodecAuth { get; set; } - - /// Gets or sets the codec server endpoint for encrypted payloads. - public string? CodecEndpoint { get; set; } -} diff --git a/src/AspireIntegrations/TemporalioSamples.SampleAppHost/AppHost.cs b/src/AspireIntegrations/TemporalioSamples.SampleAppHost/AppHost.cs index 75c4d7d..1c5b221 100644 --- a/src/AspireIntegrations/TemporalioSamples.SampleAppHost/AppHost.cs +++ b/src/AspireIntegrations/TemporalioSamples.SampleAppHost/AppHost.cs @@ -1,4 +1,4 @@ -using Temporal.Extensions.Aspire.Hosting; +using TemporalCommunity.Aspire.Hosting; var builder = DistributedApplication.CreateBuilder(args); diff --git a/src/AspireIntegrations/TemporalioSamples.SampleAppHost/TemporalioSamples.SampleAppHost.csproj b/src/AspireIntegrations/TemporalioSamples.SampleAppHost/TemporalioSamples.SampleAppHost.csproj index 4e39999..22fedb7 100644 --- a/src/AspireIntegrations/TemporalioSamples.SampleAppHost/TemporalioSamples.SampleAppHost.csproj +++ b/src/AspireIntegrations/TemporalioSamples.SampleAppHost/TemporalioSamples.SampleAppHost.csproj @@ -8,7 +8,8 @@ - + + diff --git a/tests/AspireIntegrations/TemporalCliServerResourceExtensionsTests.cs b/tests/AspireIntegrations/TemporalCliServerResourceExtensionsTests.cs deleted file mode 100644 index 770aaff..0000000 --- a/tests/AspireIntegrations/TemporalCliServerResourceExtensionsTests.cs +++ /dev/null @@ -1,165 +0,0 @@ -using Aspire.Hosting; -using Aspire.Hosting.ApplicationModel; -using Temporal.Extensions.Aspire.Hosting; -using Xunit; - -namespace TemporalioSamples.Tests.AspireIntegrations; - -public class TemporalCliServerResourceExtensionsTests -{ - // ----------------------------------------------------------------------- - // TemporalCliLocator — PATH guard that backs AddTemporalCliServer - // ----------------------------------------------------------------------- - [Fact] - public void EnsureAvailable_ThrowsInvalidOperationException_WhenCliNotFound() - { - // The isAvailable override lets tests simulate a machine without 'temporal' installed - // without manipulating the real PATH environment variable. - var ex = Assert.Throws( - () => TemporalCliLocator.EnsureAvailable(() => false)); - - Assert.Contains("temporal", ex.Message); - Assert.Contains("https://", ex.Message); - } - - [Fact] - public void EnsureAvailable_DoesNotThrow_WhenCliIsFound() - { - // Should complete without exception when the check reports the CLI is present. - TemporalCliLocator.EnsureAvailable(() => true); - } - - [Fact] - public void EnsureAvailable_ErrorMessage_ContainsInstallInstructions() - { - // Regression: the error must stay actionable — users must know where to get the CLI. - var ex = Assert.Throws( - () => TemporalCliLocator.EnsureAvailable(() => false)); - - Assert.Contains("docs.temporal.io/cli", ex.Message); - Assert.Contains("PATH", ex.Message); - } - - // ----------------------------------------------------------------------- - // AddTemporalCliServer — registration structure (PATH-independent) - // - // These tests use TemporalCliServerResource directly instead of calling - // AddTemporalCliServer so they do not depend on 'temporal' being installed - // on the test machine. - // ----------------------------------------------------------------------- - [Fact] - public void TemporalCliServerResource_Command_IsTemporalExecutable() - { - // The resource MUST use "temporal" as the Aspire executable command. - // Regression guard: a rename here would silently break all CLI server setups - // because Aspire would try to launch the wrong binary. - var resource = new TemporalCliServerResource("temporal-cli-server"); - - Assert.Equal("temporal", resource.Command); - } - - [Fact] - public void TemporalCliServerResource_DefaultArgs_ContainServerStartDev() - { - // Aspire launches the resource by appending args to the Command. - // The server subcommand must be present or temporal starts in the wrong mode. - var resource = new TemporalCliServerResource("temporal-cli-server"); - var args = TemporalArgsBuilder.BuildArgs(resource.Options); - - Assert.Contains("server", args); - Assert.Contains("start-dev", args); - } - - [Fact] - public void TemporalCliServerResource_DefaultArgs_ContainServicePort() - { - // The --port flag must map to the default service port so the Aspire-registered - // endpoint matches the port temporal actually binds. - var resource = new TemporalCliServerResource("temporal-cli-server"); - var args = TemporalArgsBuilder.BuildArgs(resource.Options); - - var portIndex = Array.IndexOf(args, "--port"); - Assert.True(portIndex >= 0, "Expected --port flag in args"); - Assert.Equal( - TemporalResourceConstants.DefaultServiceEndpointPort.ToString(System.Globalization.CultureInfo.InvariantCulture), - args[portIndex + 1]); - } - - [Fact] - public void TemporalCliServerResource_DefaultArgs_ContainUiPort() - { - // Regression: UI port must be emitted in CLI mode so the dashboard is reachable. - // This was added alongside the fix that wires the UI endpoint URL in the Aspire dashboard. - var resource = new TemporalCliServerResource("temporal-cli-server"); - var args = TemporalArgsBuilder.BuildArgs(resource.Options); - - var uiPortIndex = Array.IndexOf(args, "--ui-port"); - Assert.True(uiPortIndex >= 0, "Expected --ui-port flag in args"); - Assert.Equal( - TemporalResourceConstants.DefaultUIEndpointPort.ToString(System.Globalization.CultureInfo.InvariantCulture), - args[uiPortIndex + 1]); - } - - [Fact] - public void TemporalCliServerResource_CustomOptions_ReflectedInArgs() - { - var resource = new TemporalCliServerResource("temporal-cli-server"); - resource.Options.Namespace = "orders"; - resource.Options.CodecEndpoint = "http://localhost:8088"; - - var args = TemporalArgsBuilder.BuildArgs(resource.Options); - - Assert.Contains("orders", args); - Assert.Contains("http://localhost:8088", args); - } - - // ----------------------------------------------------------------------- - // AddTemporalCliServer — public path tests (PATH-independent via seam) - // ----------------------------------------------------------------------- - [Fact] - public void AddTemporalCliServer_Throws_WhenTemporalCliNotOnPath() - { - // Regression guard: the public extension method must throw early with an - // actionable error when the Temporal CLI is absent, rather than failing later - // with a cryptic FailedToStart from Aspire. The isTemporalCliAvailable seam - // lets tests simulate a machine without 'temporal' on PATH. - var appBuilder = DistributedApplication.CreateBuilder([]); - - var ex = Assert.Throws( - () => appBuilder.AddTemporalCliServer("temporal", configure: null, isTemporalCliAvailable: () => false)); - - Assert.Contains("temporal", ex.Message, StringComparison.OrdinalIgnoreCase); - Assert.Contains("PATH", ex.Message, StringComparison.Ordinal); - } - - [Fact] - public void AddTemporalCliServer_RegistersExplicitPortsOnAllEndpoints() - { - // Regression: the CLI resource must register explicit host ports on all endpoints - // so the Aspire dashboard renders clickable URLs. Without an explicit port the - // dashboard assigns a random port and the link is either blank or wrong. - var appBuilder = DistributedApplication.CreateBuilder([]); - - var resourceBuilder = appBuilder.AddTemporalCliServer( - "temporal", - configure: options => - { - options.TargetHost = "0.0.0.0:17233"; - options.UIPort = 18233; - options.MetricsPort = 19233; - }, - isTemporalCliAvailable: () => true); - - var annotations = resourceBuilder.Resource.Annotations - .OfType() - .ToList(); - - var service = annotations.Single(e => e.Name == TemporalResourceConstants.ServiceEndpointName); - var ui = annotations.Single(e => e.Name == TemporalResourceConstants.UIEndpointName); - var metrics = annotations.Single(e => e.Name == TemporalResourceConstants.MetricsEndpointName); - - Assert.Equal(17233, service.Port); - Assert.Equal(18233, ui.Port); - Assert.Equal(19233, metrics.Port); - } -} diff --git a/tests/AspireIntegrations/TemporalEnvironmentHelperTests.cs b/tests/AspireIntegrations/TemporalEnvironmentHelperTests.cs deleted file mode 100644 index 426bc83..0000000 --- a/tests/AspireIntegrations/TemporalEnvironmentHelperTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Aspire.Hosting; -using Aspire.Hosting.ApplicationModel; -using Temporal.Extensions.Aspire.Hosting; -using Xunit; - -namespace TemporalioSamples.Tests.AspireIntegrations; - -public class TemporalEnvironmentHelperTests -{ - [Fact] - public void AddEnvironmentVariables_DoesNotInjectDefaultNamespace() - { - var environmentVariables = new Dictionary(); - var context = new EnvironmentCallbackContext( - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), - environmentVariables, - CancellationToken.None); - - var options = new TemporalResourceOptions - { - Namespace = "orders", - }; - - TemporalEnvironmentHelper.AddEnvironmentVariables( - context, - options, - "localhost:7233", - "http://localhost:8233"); - - Assert.Equal("localhost:7233", environmentVariables["TEMPORAL_ADDRESS"]); - Assert.Equal("http://localhost:8233", environmentVariables["TEMPORAL_UI_ADDRESS"]); - Assert.Equal("orders", environmentVariables["TEMPORAL_NAMESPACE"]); - Assert.False(environmentVariables.ContainsKey("TEMPORAL_DEFAULT_NAMESPACE")); - } - - [Fact] - public void AddEnvironmentVariables_UsesIndependentContexts() - { - var firstVariables = new Dictionary(); - var firstContext = new EnvironmentCallbackContext( - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), - firstVariables, - CancellationToken.None); - var firstOptions = new TemporalResourceOptions { Namespace = "alpha" }; - - TemporalEnvironmentHelper.AddEnvironmentVariables( - firstContext, - firstOptions, - "alpha:7233", - "http://alpha:8233"); - - var secondVariables = new Dictionary(); - var secondContext = new EnvironmentCallbackContext( - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), - secondVariables, - CancellationToken.None); - var secondOptions = new TemporalResourceOptions { Namespace = "beta" }; - - TemporalEnvironmentHelper.AddEnvironmentVariables( - secondContext, - secondOptions, - "beta:7233", - "http://beta:8233"); - - Assert.Equal("alpha", firstVariables["TEMPORAL_NAMESPACE"]); - Assert.Equal("beta", secondVariables["TEMPORAL_NAMESPACE"]); - Assert.False(firstVariables.ContainsKey("TEMPORAL_DEFAULT_NAMESPACE")); - Assert.False(secondVariables.ContainsKey("TEMPORAL_DEFAULT_NAMESPACE")); - } - - [Fact] - public void AddEnvironmentVariables_InjectsCodecAuth_WhenSet() - { - var environmentVariables = new Dictionary(); - var context = new EnvironmentCallbackContext( - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), - environmentVariables, - CancellationToken.None); - - var options = new TemporalResourceOptions - { - Namespace = "default", - CodecAuth = "Bearer my-token", - }; - - TemporalEnvironmentHelper.AddEnvironmentVariables( - context, options, "localhost:7233", "http://localhost:8233"); - - Assert.Equal("Bearer my-token", environmentVariables["TEMPORAL_CODEC_AUTH"]); - } - - [Fact] - public void AddEnvironmentVariables_InjectsCodecEndpoint_WhenSet() - { - var environmentVariables = new Dictionary(); - var context = new EnvironmentCallbackContext( - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), - environmentVariables, - CancellationToken.None); - - var options = new TemporalResourceOptions - { - Namespace = "default", - CodecEndpoint = "http://localhost:8088", - }; - - TemporalEnvironmentHelper.AddEnvironmentVariables( - context, options, "localhost:7233", "http://localhost:8233"); - - Assert.Equal("http://localhost:8088", environmentVariables["TEMPORAL_CODEC_ENDPOINT"]); - } - - [Fact] - public void AddEnvironmentVariables_OmitsCodecKeys_WhenNotSet() - { - // Regression: env vars must not be injected with null/empty values when codec is unused. - var environmentVariables = new Dictionary(); - var context = new EnvironmentCallbackContext( - new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run), - environmentVariables, - CancellationToken.None); - - var options = new TemporalResourceOptions { Namespace = "default" }; - - TemporalEnvironmentHelper.AddEnvironmentVariables( - context, options, "localhost:7233", "http://localhost:8233"); - - Assert.False(environmentVariables.ContainsKey("TEMPORAL_CODEC_AUTH")); - Assert.False(environmentVariables.ContainsKey("TEMPORAL_CODEC_ENDPOINT")); - } -} diff --git a/tests/AspireIntegrations/TemporalHealthCheckHelperTests.cs b/tests/AspireIntegrations/TemporalHealthCheckHelperTests.cs deleted file mode 100644 index 1deaed8..0000000 --- a/tests/AspireIntegrations/TemporalHealthCheckHelperTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Aspire.Hosting; -using Temporal.Extensions.Aspire.Hosting; -using Xunit; - -namespace TemporalioSamples.Tests.AspireIntegrations; - -public class TemporalHealthCheckHelperTests -{ - [Fact] - public async Task AccessorReturnsNull_BeforeConnectionStringEventFires() - { - // Regression: the old design attempted to connect inside the ConnectionStringAvailableEvent - // callback and threw when the port wasn't bound yet, causing FailedToStart. The new design - // has the accessor return null (Unhealthy "not yet initialized") until the first successful - // connect, either during warm-up retries or on a later health probe. - // - // This test ensures that calling the accessor immediately after registration — before any - // Aspire event has fired — returns null rather than throwing. - var appBuilder = DistributedApplication.CreateBuilder([]); - var resource = new TemporalCliServerResource("test-temporal"); - - var accessor = TemporalHealthCheckHelper.RegisterCachedClientAccessor( - appBuilder, resource, "default"); - - var client = await accessor(CancellationToken.None); - - Assert.Null(client); - } - - [Fact] - public async Task AccessorIsIdempotent_WhenCalledMultipleTimes_BeforeEventFires() - { - // The accessor must be safe to call repeatedly (health checks probe on every interval). - // It must never throw when no connection string has been published yet. - var appBuilder = DistributedApplication.CreateBuilder([]); - var resource = new TemporalCliServerResource("test-temporal"); - - var accessor = TemporalHealthCheckHelper.RegisterCachedClientAccessor( - appBuilder, resource, "default"); - - for (var i = 0; i < 3; i++) - { - var client = await accessor(CancellationToken.None); - Assert.Null(client); - } - } -} diff --git a/tests/AspireIntegrations/TemporalHealthCheckTests.cs b/tests/AspireIntegrations/TemporalHealthCheckTests.cs deleted file mode 100644 index 2d2a2ef..0000000 --- a/tests/AspireIntegrations/TemporalHealthCheckTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Temporal.Extensions.Aspire.Hosting; -using Temporalio.Client; -using Xunit; - -namespace TemporalioSamples.Tests.AspireIntegrations; - -public class TemporalHealthCheckTests -{ - [Fact] - public async Task CheckHealthAsync_ReturnsUnhealthy_WhenClientAccessorReturnsNull() - { - // Regression: before the lazy-reconnect refactor, a null client meant the resource stayed - // permanently Unhealthy with no path to recovery. The fix ensures the description is - // "not yet initialized" (a transient state) rather than a hard failure. - var healthCheck = new TemporalHealthCheck(_ => Task.FromResult(null)); - - var result = await healthCheck.CheckHealthAsync(MakeContext(healthCheck), CancellationToken.None); - - Assert.Equal(HealthStatus.Unhealthy, result.Status); - Assert.Contains("not yet initialized", result.Description); - } - - [Fact] - public async Task CheckHealthAsync_ReturnsUnhealthy_WhenAccessorAlwaysReturnsNull_StatusIsTransient() - { - // A second probe with a still-null client also returns Unhealthy, confirming the accessor - // is called on every probe (lazy, not just once). - var callCount = 0; - var healthCheck = new TemporalHealthCheck(_ => - { - callCount++; - return Task.FromResult(null); - }); - - await healthCheck.CheckHealthAsync(MakeContext(healthCheck), CancellationToken.None); - await healthCheck.CheckHealthAsync(MakeContext(healthCheck), CancellationToken.None); - - Assert.Equal(2, callCount); - } - - [Fact] - public async Task CheckHealthAsync_PropagatesCancellation_WhenTokenCancelled() - { - using var cts = new CancellationTokenSource(); - await cts.CancelAsync(); - - var healthCheck = new TemporalHealthCheck(ct => - { - ct.ThrowIfCancellationRequested(); - return Task.FromResult(null); - }); - - await Assert.ThrowsAnyAsync( - () => healthCheck.CheckHealthAsync(MakeContext(healthCheck), cts.Token)); - } - - private static HealthCheckContext MakeContext(IHealthCheck instance) => - new() { Registration = new HealthCheckRegistration("temporal", instance, HealthStatus.Unhealthy, null) }; -} diff --git a/tests/AspireIntegrations/TemporalResourceOptionsTests.cs b/tests/AspireIntegrations/TemporalResourceOptionsTests.cs deleted file mode 100644 index 2792a4f..0000000 --- a/tests/AspireIntegrations/TemporalResourceOptionsTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Temporal.Extensions.Aspire.Hosting; -using Xunit; - -namespace TemporalioSamples.Tests.AspireIntegrations; - -public class TemporalResourceOptionsTests -{ - [Fact] - public void DefaultOptions_HaveExpectedPortValues() - { - var options = new TemporalResourceOptions(); - - Assert.Equal(TemporalResourceConstants.DefaultServiceEndpointPort, options.Port); - Assert.Equal(TemporalResourceConstants.DefaultUIEndpointPort, options.UIPort); - Assert.Equal(TemporalResourceConstants.DefaultMetricsEndpointPort, options.MetricsPort); - } - - [Fact] - public void DefaultOptions_HaveUIEnabled() - { - var options = new TemporalResourceOptions(); - - Assert.True(options.UI); - Assert.False(options.IsHeadless); - } - - [Fact] - public void Port_ParsedCorrectlyFromTargetHost() - { - var options = new TemporalResourceOptions { TargetHost = "0.0.0.0:7240" }; - - Assert.Equal(7240, options.Port); - } - - [Fact] - public void Ip_ParsedCorrectlyFromTargetHost() - { - var options = new TemporalResourceOptions { TargetHost = "127.0.0.1:7233" }; - - Assert.Equal("127.0.0.1", options.Ip); - } - - [Fact] - public void Port_ThrowsInvalidOperationException_WhenTargetHostIsEmpty() - { - var options = new TemporalResourceOptions { TargetHost = string.Empty }; - - Assert.Throws(() => _ = options.Port); - } - - [Fact] - public void Ip_ThrowsInvalidOperationException_WhenTargetHostIsEmpty() - { - var options = new TemporalResourceOptions { TargetHost = string.Empty }; - - Assert.Throws(() => _ = options.Ip); - } - - [Fact] - public void Port_ThrowsInvalidOperationException_WhenTargetHostLacksPort() - { - // Regression: TargetHost with no colon-separated port must not silently return 0. - var options = new TemporalResourceOptions { TargetHost = "localhost" }; - - Assert.Throws(() => _ = options.Port); - } - - [Fact] - public void AdditionalNamespaces_AlwaysIncludesPrimaryNamespace() - { - var options = new TemporalResourceOptions { Namespace = "orders" }; - options.AdditionalNamespaces = new List { "shipping", "payments" }; - - Assert.Contains("orders", options.AdditionalNamespaces); - Assert.Contains("shipping", options.AdditionalNamespaces); - Assert.Contains("payments", options.AdditionalNamespaces); - } - - [Fact] - public void AdditionalNamespaces_DeduplicatesPrimaryNamespace() - { - // Setting the primary namespace in the extra list must not create a duplicate. - var options = new TemporalResourceOptions { Namespace = "orders" }; - options.AdditionalNamespaces = new List { "orders", "shipping" }; - - Assert.Single(options.AdditionalNamespaces, ns => ns == "orders"); - } -} diff --git a/tests/TemporalioSamples.Tests.csproj b/tests/TemporalioSamples.Tests.csproj index 9077674..234002a 100644 --- a/tests/TemporalioSamples.Tests.csproj +++ b/tests/TemporalioSamples.Tests.csproj @@ -22,7 +22,6 @@ -