From fc053ecc196384c58dcefd7419cfbd57bbcf80be Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:55:02 -0800 Subject: [PATCH 01/58] durable task scheduler auth extension save initial --- Microsoft.DurableTask.sln | 10 + src/Extensions/Azure/Azure.csproj | 27 +++ .../DurableTaskSchedulerConnectionString.cs | 82 ++++++++ .../Azure/DurableTaskSchedulerExtensions.cs | 156 +++++++++++++++ .../Azure/DurableTaskSchedulerOptions.cs | 147 +++++++++++++++ ...rableTaskSchedulerConnectionStringTests.cs | 122 ++++++++++++ .../DurableTaskSchedulerExtensionsTests.cs | 178 ++++++++++++++++++ .../DurableTaskSchedulerOptionsTests.cs | 119 ++++++++++++ .../Extensions.Azure.Tests.csproj | 34 ++++ 9 files changed, 875 insertions(+) create mode 100644 src/Extensions/Azure/Azure.csproj create mode 100644 src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs create mode 100644 src/Extensions/Azure/DurableTaskSchedulerExtensions.cs create mode 100644 src/Extensions/Azure/DurableTaskSchedulerOptions.cs create mode 100644 test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs create mode 100644 test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs create mode 100644 test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs create mode 100644 test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 2b8bab8a..96d6355f 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -71,6 +71,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Ana EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp.Tests", "samples\AzureFunctionsUnitTests\AzureFunctionsApp.Tests.csproj", "{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{5227C712-2355-403F-90D6-51D0BCAE4D38}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azure\Azure.csproj", "{662BF73D-A4DD-4910-8625-7C12F1ACDBEC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -185,6 +189,10 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.Build.0 = Release|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -220,6 +228,8 @@ Global {998E9D97-BD36-4A9D-81FC-5DAC1CE40083} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {541FCCCE-1059-4691-B027-F761CD80DE92} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} + {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/src/Extensions/Azure/Azure.csproj b/src/Extensions/Azure/Azure.csproj new file mode 100644 index 00000000..8f1d2e35 --- /dev/null +++ b/src/Extensions/Azure/Azure.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0;net6.0 + Azure extensions for the Durable Task Framework. + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs new file mode 100644 index 00000000..ae71cf21 --- /dev/null +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -0,0 +1,82 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation.All rights reserved. +// ------------------------------------------------------------ + +using System.Data.Common; + +namespace DurableTask.Extensions.Azure; + +/// +/// Represents the constituent parts of a connection string for a Durable Task Scheduler service. +/// +public sealed class DurableTaskSchedulerConnectionString +{ + readonly DbConnectionStringBuilder builder; + + /// + /// Initializes a new instance of the class. + /// + /// A connection string for a Durable Task Scheduler service. + public DurableTaskSchedulerConnectionString(string connectionString) + { + this.builder = new() { ConnectionString = connectionString }; + } + + /// + /// Gets the authentication method specified in the connection string (if any). + /// + public string Authentication => this.GetRequiredValue("Authentication"); + + /// + /// Gets the managed identity or workload identity client ID specified in the connection string (if any). + /// + public string? ClientId => this.GetValue("ClientID"); + + /// + /// Gets the "AdditionallyAllowedTenants" property, optionally used by Workload Identity. + /// Multiple values can be separated by a comma. + /// + public IList? AdditionallyAllowedTenants => + string.IsNullOrEmpty(this.AdditionallyAllowedTenantsStr) + ? null + : this.AdditionallyAllowedTenantsStr!.Split(','); + + /// + /// Gets the "TenantId" property, optionally used by Workload Identity. + /// + public string? TenantId => this.GetValue("TenantId"); + + /// + /// Gets the "TokenFilePath" property, optionally used by Workload Identity. + /// + public string? TokenFilePath => this.GetValue("TokenFilePath"); + + /// + /// Gets the endpoint specified in the connection string (if any). + /// + public string Endpoint => this.GetRequiredValue("Endpoint"); + + /// + /// Gets the task hub name specified in the connection string. + /// + public string TaskHubName => this.GetRequiredValue("TaskHub"); + + string? AdditionallyAllowedTenantsStr => this.GetValue("AdditionallyAllowedTenants"); + + string? GetValue(string name) => + this.builder.TryGetValue(name, out object? value) + ? value as string + : null; + + string GetRequiredValue(string name) + { + string? value = this.GetValue(name); + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentNullException( + $"The connection string is missing the required '{name}' property."); + } + + return value!; + } +} diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs new file mode 100644 index 00000000..1ec6b3a7 --- /dev/null +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -0,0 +1,156 @@ +using Azure.Core; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using System.Diagnostics; + +namespace DurableTask.Extensions.Azure; + +// NOTE: These extension methods will eventually be provided by the Durable Task SDK itself. +public static class DurableTaskSchedulerExtensions +{ + // Configure the Durable Task *Worker* to use the Durable Task Scheduler service with the specified options. + public static void UseDurableTaskScheduler( + this IDurableTaskWorkerBuilder builder, + string endpointAddress, + string taskHubName, + TokenCredential credential, + Action? configure = null) + { + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + + configure?.Invoke(options); + + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + public static void UseDurableTaskScheduler( + this IDurableTaskWorkerBuilder builder, + string connectionString, + Action? configure = null) + { + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + // Configure the Durable Task *Client* to use the Durable Task Scheduler service with the specified options. + public static void UseDurableTaskScheduler( + this IDurableTaskClientBuilder builder, + string endpointAddress, + string taskHubName, + TokenCredential credential, + Action? configure = null) + { + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + + configure?.Invoke(options); + + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + public static void UseDurableTaskScheduler( + this IDurableTaskClientBuilder builder, + string connectionString, + Action? configure = null) + { + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); + } + + static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) + { + if (string.IsNullOrEmpty(options.EndpointAddress)) + { + throw RequiredOptionMissing(nameof(options.TaskHubName)); + } + + if (string.IsNullOrEmpty(options.TaskHubName)) + { + throw RequiredOptionMissing(nameof(options.TaskHubName)); + } + + TokenCredential credential = options.Credential ?? throw RequiredOptionMissing(nameof(options.Credential)); + + string taskHubName = options.TaskHubName; + string endpoint = options.EndpointAddress; + + if (!endpoint.Contains("://")) + { + endpoint = $"https://{endpoint}"; + } + + string resourceId = options.ResourceId ?? "https://durabletask.io"; +#if NET6_0 + int processId = Environment.ProcessId; +#else + int processId = Process.GetCurrentProcess().Id; +#endif + string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid()}"; + + TokenCache? cache = + options.Credential is not null + ? new( + options.Credential, + new(new[] { $"{options.ResourceId}/.default" }), + TimeSpan.FromMinutes(5)) + : null; + + CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( + async (context, metadata) => + { + metadata.Add("taskhub", taskHubName); + metadata.Add("workerid", workerId); + + if (cache is null) + { + return; + } + + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); + + metadata.Add("Authorization", $"Bearer {token.Token}"); + }); + + #if NET6_0 + return GrpcChannel.ForAddress( + endpoint, + new GrpcChannelOptions + { + Credentials = ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds), + }); + #else + return new GrpcChannel( + endpoint, + ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds)); + #endif + } + + static Exception RequiredOptionMissing(string optionName) + { + return new ArgumentException(message: $"Required option '{optionName}' was not provided."); + } + + sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) + { + readonly TokenCredential credential = credential; + readonly TokenRequestContext context = context; + readonly TimeSpan margin = margin; + + AccessToken? token; + + public async Task GetTokenAsync(CancellationToken cancellationToken) + { + DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); + + if (this.token is null + || this.token.Value.RefreshOn < nowWithMargin + || this.token.Value.ExpiresOn < nowWithMargin) + { + this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); + } + + return this.token.Value; + } + } +} \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs new file mode 100644 index 00000000..a089bcc9 --- /dev/null +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -0,0 +1,147 @@ +using System.Globalization; +using Azure.Core; +using Azure.Identity; + +namespace DurableTask.Extensions.Azure; + +// NOTE: These options definitions will eventually be provided by the Durable Task SDK itself. + +/// +/// Options for configuring the Durable Task Scheduler. +/// +public class DurableTaskSchedulerOptions +{ + internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) + { + this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); + this.TaskHubName = taskHubName ?? throw new ArgumentNullException(nameof(taskHubName)); + this.Credential = credential; + } + + /// + /// The endpoint address of the Durable Task Scheduler resource. + /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// + public string EndpointAddress { get; } + + /// + /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// + public string TaskHubName { get; } + + /// + /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// + public TokenCredential? Credential { get; } + + /// + /// The resource ID of the Durable Task Scheduler resource. + /// The default value is https://durabletask.io. + /// + public string? ResourceId { get; set; } + + /// + /// The worker ID used to identify the worker instance. + /// The default value is a string containing the machine name and the process ID. + /// + public string? WorkerId { get; set; } + + + public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) + { + return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); + } + + public static DurableTaskSchedulerOptions FromConnectionString( + DurableTaskSchedulerConnectionString connectionString) + { + // Example connection strings: + // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" + // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" + // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) + + string endpointAddress = connectionString.Endpoint; + + if (!endpointAddress.Contains("://")) + { + // If the protocol is missing, assume HTTPS. + endpointAddress = "https://" + endpointAddress; + } + + string authType = connectionString.Authentication; + + TokenCredential? credential; + + // Parse the supported auth types, in a case-insensitive way and without spaces + switch (authType.ToLower(CultureInfo.InvariantCulture).Replace(" ", string.Empty)) + { + case "defaultazure": + // Default Azure credentials, suitable for a variety of scenarios + // In many cases, users will need to pass additional configuration options via env vars + credential = new DefaultAzureCredential(); + break; + + case "managedidentity": + // Use Managed identity + // Suitable for Azure-hosted scenarios + // Note that ClientId could be null for system-assigned managed identities + credential = new ManagedIdentityCredential(connectionString.ClientId); + break; + + case "workloadidentity": + // Use Workload Identity Federation. + // This is commonly-used in Kubernetes (hosted on Azure or anywhere), or in CI environments like + // Azure Pipelines or GitHub Actions. It can also be used with SPIFFE. + WorkloadIdentityCredentialOptions opts = new() { }; + if (!string.IsNullOrEmpty(connectionString.ClientId)) + { + opts.ClientId = connectionString.ClientId; + } + + if (!string.IsNullOrEmpty(connectionString.TenantId)) + { + opts.TenantId = connectionString.TenantId; + } + + if (connectionString.AdditionallyAllowedTenants is not null) + { + foreach (string tenant in connectionString.AdditionallyAllowedTenants) + { + opts.AdditionallyAllowedTenants.Add(tenant); + } + } + + credential = new WorkloadIdentityCredential(opts); + break; + + case "environment": + // Use credentials from the environment + credential = new EnvironmentCredential(); + break; + + case "azurecli": + // Use credentials from the Azure CLI + credential = new AzureCliCredential(); + break; + + case "azurepowershell": + // Use credentials from the Azure PowerShell modules + credential = new AzurePowerShellCredential(); + break; + + case "none": + // Do not use any authentication/authorization (for testing only) + // This is a no-op + credential = null; + break; + + default: + throw new ArgumentException( + $"The connection string contains an unsupported authentication type '{authType}'.", + nameof(connectionString)); + } + + DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); + return options; + } +} \ No newline at end of file diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs new file mode 100644 index 00000000..bb56ad55 --- /dev/null +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FluentAssertions; +using System.Data.Common; +using Xunit; + +namespace DurableTask.Extensions.Azure.Tests; + +public class DurableTaskSchedulerConnectionStringTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + private const string ValidClientId = "00000000-0000-0000-0000-000000000000"; + private const string ValidTenantId = "11111111-1111-1111-1111-111111111111"; + + [Fact] + public void Constructor_WithValidConnectionString_ShouldParseCorrectly() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.Endpoint.Should().Be(ValidEndpoint); + parsedConnectionString.TaskHubName.Should().Be(ValidTaskHub); + parsedConnectionString.Authentication.Should().Be("DefaultAzure"); + } + + [Fact] + public void Constructor_WithManagedIdentity_ShouldParseClientId() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={ValidClientId};TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.ClientId.Should().Be(ValidClientId); + } + + [Fact] + public void Constructor_WithWorkloadIdentity_ShouldParseAllProperties() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={ValidClientId};TenantId={ValidTenantId};TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.ClientId.Should().Be(ValidClientId); + parsedConnectionString.TenantId.Should().Be(ValidTenantId); + } + + [Fact] + public void Constructor_WithAdditionallyAllowedTenants_ShouldParseTenantList() + { + // Arrange + const string tenants = "tenant1,tenant2,tenant3"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;AdditionallyAllowedTenants={tenants};TaskHub={ValidTaskHub}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.AdditionallyAllowedTenants.Should().NotBeNull(); + parsedConnectionString.AdditionallyAllowedTenants.Should().BeEquivalentTo(new[] { "tenant1", "tenant2", "tenant3" }); + } + + [Fact] + public void Constructor_WithMissingRequiredProperties_ShouldThrowArgumentNullException() + { + // Arrange + string connectionString = $"Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Missing Endpoint + + // Act & Assert + var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; + var exception = action.Should().Throw().Which; + exception.Message.Should().Contain("'Endpoint' property"); + } + + [Fact] + public void Constructor_WithInvalidConnectionString_ShouldThrowArgumentException() + { + // Arrange + string connectionString = "This is not a valid connection string"; + + // Act & Assert + var action = () => new DurableTaskSchedulerConnectionString(connectionString); + action.Should().Throw() + .WithMessage("*Format of the initialization string does not conform to specification*"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Constructor_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string? connectionString) + { + // Act & Assert + var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString!).Endpoint; + action.Should().Throw() + .WithMessage("*'Endpoint' property*"); + } + + [Fact] + public void GetValue_WithNonExistentProperty_ShouldReturnNull() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.ClientId.Should().BeNull(); + parsedConnectionString.TenantId.Should().BeNull(); + parsedConnectionString.TokenFilePath.Should().BeNull(); + parsedConnectionString.AdditionallyAllowedTenants.Should().BeNull(); + } +} diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs new file mode 100644 index 00000000..440cab0f --- /dev/null +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Identity; +using FluentAssertions; +using Grpc.Net.Client; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Grpc; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.Grpc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace DurableTask.Extensions.Azure.Tests; + +public class DurableTaskSchedulerExtensionsTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + + [Fact] + public void UseDurableTaskScheduler_Worker_WithEndpointAndCredential_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); + + // Assert - Verify that the options were registered + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_Worker_WithConnectionString_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler(connectionString); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_Client_WithEndpointAndCredential_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_Client_WithConnectionString_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler(connectionString); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_WithOptions_ShouldApplyConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + string workerId = "customWorker"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler( + ValidEndpoint, + ValidTaskHub, + credential, + options => options.WorkerId = workerId); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Theory] + [InlineData(null, ValidTaskHub)] + [InlineData(ValidEndpoint, null)] + public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + action.Should().Throw(); + } + + [Fact] + public void UseDurableTaskScheduler_WithNullCredential_ShouldThrowArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + TokenCredential? credential = null; + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); + action.Should().Throw() + .WithMessage("*Required option 'Credential' was not provided*"); + } + + [Fact] + public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + string invalidConnectionString = "This is not a valid connection string"; + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(invalidConnectionString); + action.Should().Throw(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string connectionString) + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + action.Should().Throw(); + } +} diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs new file mode 100644 index 00000000..c7e8fd0a --- /dev/null +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Identity; +using FluentAssertions; +using Xunit; + +namespace DurableTask.Extensions.Azure.Tests; + +public class DurableTaskSchedulerOptionsTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + + [Fact] + public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void FromConnectionString_WithManagedIdentity_ShouldCreateValidInstance() + { + // Arrange + const string clientId = "00000000-0000-0000-0000-000000000000"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void FromConnectionString_WithWorkloadIdentity_ShouldCreateValidInstance() + { + // Arrange + const string clientId = "00000000-0000-0000-0000-000000000000"; + const string tenantId = "11111111-1111-1111-1111-111111111111"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={clientId};TenantId={tenantId};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Theory] + [InlineData("Environment")] + [InlineData("AzureCLI")] + [InlineData("AzurePowerShell")] + public void FromConnectionString_WithValidAuthTypes_ShouldCreateValidInstance(string authType) + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication={authType};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().NotBeNull(); + } + + [Fact] + public void FromConnectionString_WithInvalidAuthType_ShouldThrowArgumentException() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=InvalidAuth;TaskHub={ValidTaskHub}"; + + // Act & Assert + var action = () => DurableTaskSchedulerOptions.FromConnectionString(connectionString); + action.Should().Throw() + .WithMessage("*contains an unsupported authentication type*"); + } + + [Fact] + public void FromConnectionString_WithMissingRequiredProperties_ShouldThrowArgumentNullException() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure"; // Missing TaskHub + + // Act & Assert + var action = () => DurableTaskSchedulerOptions.FromConnectionString(connectionString); + action.Should().Throw(); + } + + [Fact] + public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=None;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeNull(); + } +} diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj new file mode 100644 index 00000000..8ea58bf8 --- /dev/null +++ b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + enable + enable + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + From 6f1d2bc9111819a941be34501d094dec9ddda2b3 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:46:31 -0800 Subject: [PATCH 02/58] support local conn with no auth and via http --- .../Azure/DurableTaskSchedulerExtensions.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 1ec6b3a7..71c81877 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -70,8 +70,6 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) throw RequiredOptionMissing(nameof(options.TaskHubName)); } - TokenCredential credential = options.Credential ?? throw RequiredOptionMissing(nameof(options.Credential)); - string taskHubName = options.TaskHubName; string endpoint = options.EndpointAddress; @@ -112,17 +110,35 @@ options.Credential is not null metadata.Add("Authorization", $"Bearer {token.Token}"); }); + // Production will use HTTPS, but local testing will use HTTP + ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? + ChannelCredentials.SecureSsl : + ChannelCredentials.Insecure; #if NET6_0 - return GrpcChannel.ForAddress( - endpoint, - new GrpcChannelOptions + return GrpcChannel.ForAddress(this.options.Address, new GrpcChannelOptions { - Credentials = ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds), + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, }); #else return new GrpcChannel( endpoint, - ChannelCredentials.Create(ChannelCredentials.SecureSsl, managedBackendCreds)); + ChannelCredentials.Create(channelCreds, managedBackendCreds), + new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); #endif } From 51e39bde6e8987b6b9c444fe84ca86cecc1ddf9a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:55:07 -0800 Subject: [PATCH 03/58] be consistent with azuremanaged targetversion --- src/Extensions/Azure/Azure.csproj | 2 +- .../Azure/DurableTaskSchedulerExtensions.cs | 25 +++---------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Extensions/Azure/Azure.csproj b/src/Extensions/Azure/Azure.csproj index 8f1d2e35..25704be0 100644 --- a/src/Extensions/Azure/Azure.csproj +++ b/src/Extensions/Azure/Azure.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net6.0 + net6.0 Azure extensions for the Durable Task Framework. true diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 71c81877..0933921b 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,5 @@ using Azure.Core; +using Grpc.Net.Client; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; using System.Diagnostics; @@ -79,12 +80,8 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) } string resourceId = options.ResourceId ?? "https://durabletask.io"; -#if NET6_0 int processId = Environment.ProcessId; -#else - int processId = Process.GetCurrentProcess().Id; -#endif - string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid()}"; + string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; TokenCache? cache = options.Credential is not null @@ -114,8 +111,7 @@ options.Credential is not null ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; - #if NET6_0 - return GrpcChannel.ForAddress(this.options.Address, new GrpcChannelOptions + return GrpcChannel.ForAddress(options.EndpointAddress, new GrpcChannelOptions { // The same credential is being used for all operations. // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials @@ -125,21 +121,6 @@ options.Credential is not null // only be done for local testing. We should hide this setting behind some kind of flag. UnsafeUseInsecureChannelCallCredentials = true, }); - #else - return new GrpcChannel( - endpoint, - ChannelCredentials.Create(channelCreds, managedBackendCreds), - new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); - #endif } static Exception RequiredOptionMissing(string optionName) From 6987f1bcef3e0a26813c5de33410cf2d4aac9125 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 22:56:39 -0800 Subject: [PATCH 04/58] clean --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 0933921b..395e8263 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,12 +1,9 @@ using Azure.Core; -using Grpc.Net.Client; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; -using System.Diagnostics; namespace DurableTask.Extensions.Azure; -// NOTE: These extension methods will eventually be provided by the Durable Task SDK itself. public static class DurableTaskSchedulerExtensions { // Configure the Durable Task *Worker* to use the Durable Task Scheduler service with the specified options. From e1628154aa884420b0de30bab2a07050c7f25dfe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 23:01:07 -0800 Subject: [PATCH 05/58] namespace --- src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs | 2 +- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 2 +- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 2 +- .../DurableTaskSchedulerConnectionStringTests.cs | 2 +- .../DurableTaskSchedulerExtensionsTests.cs | 2 +- test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs index ae71cf21..04a7ad65 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -4,7 +4,7 @@ using System.Data.Common; -namespace DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Extensions.Azure; /// /// Represents the constituent parts of a connection string for a Durable Task Scheduler service. diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 395e8263..39ef3b22 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; -namespace DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Extensions.Azure; public static class DurableTaskSchedulerExtensions { diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index a089bcc9..4cc5c691 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -2,7 +2,7 @@ using Azure.Core; using Azure.Identity; -namespace DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Extensions.Azure; // NOTE: These options definitions will eventually be provided by the Durable Task SDK itself. diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs index bb56ad55..aaca0a66 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs @@ -5,7 +5,7 @@ using System.Data.Common; using Xunit; -namespace DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Extensions.Azure.Tests; public class DurableTaskSchedulerConnectionStringTests { diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs index 440cab0f..24db69ca 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs @@ -14,7 +14,7 @@ using Moq; using Xunit; -namespace DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Extensions.Azure.Tests; public class DurableTaskSchedulerExtensionsTests { diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs index c7e8fd0a..b1c903c2 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs @@ -6,7 +6,7 @@ using FluentAssertions; using Xunit; -namespace DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Extensions.Azure.Tests; public class DurableTaskSchedulerOptionsTests { From e19de446b996da4965fa537178c646ec79e3a3e9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 8 Jan 2025 23:02:42 -0800 Subject: [PATCH 06/58] fix --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 6 +++++- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 39ef3b22..1dce80d0 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,8 @@ -using Azure.Core; +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation.All rights reserved. +// ------------------------------------------------------------ + +using Azure.Core; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 4cc5c691..e58e0c25 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,4 +1,8 @@ -using System.Globalization; +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation.All rights reserved. +// ------------------------------------------------------------ + +using System.Globalization; using Azure.Core; using Azure.Identity; From 3a6dc52aaaa636924033c6e57b995837a785582c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 00:36:10 -0800 Subject: [PATCH 07/58] fix --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 10 +++------- .../DurableTaskSchedulerExtensionsTests.cs | 5 ++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 1dce80d0..4a9638b6 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -19,9 +19,7 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); } @@ -44,9 +42,7 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); } @@ -64,7 +60,7 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) { if (string.IsNullOrEmpty(options.EndpointAddress)) { - throw RequiredOptionMissing(nameof(options.TaskHubName)); + throw RequiredOptionMissing(nameof(options.EndpointAddress)); } if (string.IsNullOrEmpty(options.TaskHubName)) @@ -88,7 +84,7 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) options.Credential is not null ? new( options.Credential, - new(new[] { $"{options.ResourceId}/.default" }), + new(new[] { $"{resourceId}/.default" }), TimeSpan.FromMinutes(5)) : null; @@ -112,7 +108,7 @@ options.Credential is not null ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; - return GrpcChannel.ForAddress(options.EndpointAddress, new GrpcChannelOptions + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions { // The same credential is being used for all operations. // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs index 24db69ca..518b53c1 100644 --- a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs +++ b/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs @@ -133,7 +133,7 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx } [Fact] - public void UseDurableTaskScheduler_WithNullCredential_ShouldThrowArgumentException() + public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() { // Arrange var services = new ServiceCollection(); @@ -143,8 +143,7 @@ public void UseDurableTaskScheduler_WithNullCredential_ShouldThrowArgumentExcept // Act & Assert var action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); - action.Should().Throw() - .WithMessage("*Required option 'Credential' was not provided*"); + action.Should().NotThrow(); } [Fact] From 65dfb85bcae00e6ee2dec5c58ad707bf3de4297f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 00:44:42 -0800 Subject: [PATCH 08/58] doc --- .../Azure/DurableTaskSchedulerExtensions.cs | 37 ++++++++++++++++++- .../Azure/DurableTaskSchedulerOptions.cs | 15 ++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 4a9638b6..21ccf76e 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -8,9 +8,19 @@ namespace Microsoft.DurableTask.Extensions.Azure; +/// +/// Extension methods for configuring Durable Task workers and clients to use the Azure Durable Task Scheduler service. +/// public static class DurableTaskSchedulerExtensions { - // Configure the Durable Task *Worker* to use the Durable Task Scheduler service with the specified options. + /// + /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. + /// + /// The worker builder to configure. + /// The endpoint address of the Durable Task Scheduler service. + /// The name of the task hub to connect to. + /// The credential to use for authentication. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, @@ -19,10 +29,18 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); } + /// + /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. + /// + /// The worker builder to configure. + /// The connection string for the Durable Task Scheduler service. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string connectionString, @@ -33,7 +51,14 @@ public static void UseDurableTaskScheduler( builder.UseGrpc(GetGrpcChannelForOptions(options)); } - // Configure the Durable Task *Client* to use the Durable Task Scheduler service with the specified options. + /// + /// Configures Durable Task client to use the Azure Durable Task Scheduler service. + /// + /// The client builder to configure. + /// The endpoint address of the Durable Task Scheduler service. + /// The name of the task hub to connect to. + /// The credential to use for authentication. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, @@ -42,10 +67,18 @@ public static void UseDurableTaskScheduler( Action? configure = null) { DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + configure?.Invoke(options); + builder.UseGrpc(GetGrpcChannelForOptions(options)); } + /// + /// Configures Durable Task client to use the Azure Durable Task Scheduler service using a connection string. + /// + /// The client builder to configure. + /// The connection string for the Durable Task Scheduler service. + /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string connectionString, diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index e58e0c25..94bc63bc 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -8,8 +8,6 @@ namespace Microsoft.DurableTask.Extensions.Azure; -// NOTE: These options definitions will eventually be provided by the Durable Task SDK itself. - /// /// Options for configuring the Durable Task Scheduler. /// @@ -50,12 +48,23 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// public string? WorkerId { get; set; } - + /// + /// Creates a new instance of from a connection string. + /// + /// The connection string containing the configuration settings. + /// A new instance of configured with the connection string settings. + /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } + /// + /// Creates a new instance of from a parsed connection string. + /// + /// The parsed connection string containing the configuration settings. + /// A new instance of configured with the connection string settings. + /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) { From 29445a04f445a4f65256fa99b22eda10dd8ffe92 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:06:41 -0800 Subject: [PATCH 09/58] ppl --- .github/workflows/validate-build.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-build.yml b/.github/workflows/validate-build.yml index 65153017..5f3694b4 100644 --- a/.github/workflows/validate-build.yml +++ b/.github/workflows/validate-build.yml @@ -25,7 +25,12 @@ jobs: with: submodules: true - - name: Setup .NET + - name: Setup .NET 6.0 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + + - name: Setup .NET from global.json uses: actions/setup-dotnet@v3 with: global-json-file: global.json From e63f12a0a8ed38a410aa1346216f7caf2c6219bc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:15:36 -0800 Subject: [PATCH 10/58] remove --- test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj index 8ea58bf8..a62bb109 100644 --- a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj +++ b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj @@ -2,10 +2,6 @@ net6.0 - enable - enable - false - true From 5c72ee7137d69384bc1a3560b3ac16c958142258 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:20:13 -0800 Subject: [PATCH 11/58] test proj to sln --- Microsoft.DurableTask.sln | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 96d6355f..59acc82d 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -75,6 +75,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azure\Azure.csproj", "{662BF73D-A4DD-4910-8625-7C12F1ACDBEC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.Azure.Tests", "test\Extensions.Azure.Tests\Extensions.Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -193,6 +195,10 @@ Global {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.Build.0 = Debug|Any CPU {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.ActiveCfg = Release|Any CPU {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.Build.0 = Release|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -230,6 +236,7 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} + {DBB5DB4E-A1B0-4C86-A233-213789C46929} = {E5637F81-2FB9-4CD7-900D-455363B142A7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} From c6e42c54c096ff7c1f8c0ddccff88be0fbd81823 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:24:57 -0800 Subject: [PATCH 12/58] fix warning --- .../DurableTaskSchedulerConnectionString.cs | 5 ++--- .../Azure/DurableTaskSchedulerExtensions.cs | 5 ++--- .../Azure/DurableTaskSchedulerOptions.cs | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs index 04a7ad65..4599cd64 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -1,6 +1,5 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation.All rights reserved. -// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using System.Data.Common; diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 21ccf76e..6e933d41 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,6 +1,5 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation.All rights reserved. -// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using Azure.Core; using Microsoft.DurableTask.Client; diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 94bc63bc..5e603515 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,6 +1,5 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation.All rights reserved. -// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using System.Globalization; using Azure.Core; @@ -13,6 +12,9 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public class DurableTaskSchedulerOptions { + /// + /// Initializes a new instance of the class. + /// internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); @@ -21,29 +23,29 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, } /// - /// The endpoint address of the Durable Task Scheduler resource. + /// Gets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// public string EndpointAddress { get; } /// - /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. /// public string TaskHubName { get; } /// - /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Gets the credential used to authenticate with the Durable Task Scheduler task hub resource. /// public TokenCredential? Credential { get; } /// - /// The resource ID of the Durable Task Scheduler resource. + /// Gets or sets the resource ID of the Durable Task Scheduler resource. /// The default value is https://durabletask.io. /// public string? ResourceId { get; set; } /// - /// The worker ID used to identify the worker instance. + /// Gets or sets the worker ID used to identify the worker instance. /// The default value is a string containing the machine name and the process ID. /// public string? WorkerId { get; set; } From 6a66aaa1f03bfa84a28e8db4376870a4168ba0ad Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:35:00 -0800 Subject: [PATCH 13/58] remove dup --- .../Extensions.Azure.Tests.csproj | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj index a62bb109..e9c83d3e 100644 --- a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj +++ b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj @@ -5,19 +5,7 @@ - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - From 131c575bed4a909e27d7ed9aaeb6ea79d9f804bd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:45:56 -0800 Subject: [PATCH 14/58] fix --- .../Azure/DurableTaskSchedulerExtensions.cs | 24 ++++++++++--------- .../Azure/DurableTaskSchedulerOptions.cs | 9 +++---- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 6e933d41..1f1bfb02 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; @@ -141,18 +141,20 @@ options.Credential is not null ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); } - static Exception RequiredOptionMissing(string optionName) + static ArgumentException RequiredOptionMissing(string optionName) { return new ArgumentException(message: $"Required option '{optionName}' was not provided."); } @@ -179,4 +181,4 @@ public async Task GetTokenAsync(CancellationToken cancellationToken return this.token.Value; } } -} \ No newline at end of file +} diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 5e603515..96e7692d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Licensed under the MIT License. using System.Globalization; using Azure.Core; @@ -15,6 +15,9 @@ public class DurableTaskSchedulerOptions /// /// Initializes a new instance of the class. /// + /// The endpoint address of the Durable Task Scheduler service. + /// The name of the task hub to connect to. + /// The credential to use for authentication, or null for no authentication. internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); @@ -74,7 +77,6 @@ public static DurableTaskSchedulerOptions FromConnectionString( // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) - string endpointAddress = connectionString.Endpoint; if (!endpointAddress.Contains("://")) @@ -84,7 +86,6 @@ public static DurableTaskSchedulerOptions FromConnectionString( } string authType = connectionString.Authentication; - TokenCredential? credential; // Parse the supported auth types, in a case-insensitive way and without spaces @@ -159,4 +160,4 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } -} \ No newline at end of file +} From 65fa60729223649af33f52db2ecd3c94c7771197 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Thu, 9 Jan 2025 01:57:27 -0800 Subject: [PATCH 15/58] Revert "fix" This reverts commit 131c575bed4a909e27d7ed9aaeb6ea79d9f804bd. --- .../Azure/DurableTaskSchedulerExtensions.cs | 24 +++++++++---------- .../Azure/DurableTaskSchedulerOptions.cs | 9 ++++--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 1f1bfb02..6e933d41 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; @@ -141,20 +141,18 @@ options.Credential is not null ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); } - static ArgumentException RequiredOptionMissing(string optionName) + static Exception RequiredOptionMissing(string optionName) { return new ArgumentException(message: $"Required option '{optionName}' was not provided."); } @@ -181,4 +179,4 @@ public async Task GetTokenAsync(CancellationToken cancellationToken return this.token.Value; } } -} +} \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 96e7692d..5e603515 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. +// Licensed under the MIT License. using System.Globalization; using Azure.Core; @@ -15,9 +15,6 @@ public class DurableTaskSchedulerOptions /// /// Initializes a new instance of the class. /// - /// The endpoint address of the Durable Task Scheduler service. - /// The name of the task hub to connect to. - /// The credential to use for authentication, or null for no authentication. internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); @@ -77,6 +74,7 @@ public static DurableTaskSchedulerOptions FromConnectionString( // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) + string endpointAddress = connectionString.Endpoint; if (!endpointAddress.Contains("://")) @@ -86,6 +84,7 @@ public static DurableTaskSchedulerOptions FromConnectionString( } string authType = connectionString.Authentication; + TokenCredential? credential; // Parse the supported auth types, in a case-insensitive way and without spaces @@ -160,4 +159,4 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } -} +} \ No newline at end of file From 4f45ec553fdf83eb221cb0fadb576211b4298ddd Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:52:46 -0800 Subject: [PATCH 16/58] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 7 +------ src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 12 +++++++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 6e933d41..d3809cfd 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; @@ -103,11 +103,6 @@ static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) string taskHubName = options.TaskHubName; string endpoint = options.EndpointAddress; - if (!endpoint.Contains("://")) - { - endpoint = $"https://{endpoint}"; - } - string resourceId = options.ResourceId ?? "https://durabletask.io"; int processId = Environment.ProcessId; string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 5e603515..aa3d2a1f 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -26,7 +26,17 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// Gets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// - public string EndpointAddress { get; } + public string EndpointAddress + { + get => this.endpointAddress; + set + { + // Add https:// prefix if no protocol is specified + this.endpointAddress = !value.Contains("://") + ? $"https://{value}" + : value; + } + } /// /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. From d4607e49208ba2e8687daca1efb96e1ace51244a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:03:51 -0800 Subject: [PATCH 17/58] fix --- .../Azure/DurableTaskSchedulerConnectionString.cs | 8 +------- .../Azure/DurableTaskSchedulerExtensions.cs | 11 ++--------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs index 4599cd64..9002f77b 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs @@ -70,12 +70,6 @@ public DurableTaskSchedulerConnectionString(string connectionString) string GetRequiredValue(string name) { string? value = this.GetValue(name); - if (string.IsNullOrEmpty(value)) - { - throw new ArgumentNullException( - $"The connection string is missing the required '{name}' property."); - } - - return value!; + return Check.NotNullOrEmpty(value, $"The connection string is missing the required '{name}' property."); } } diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index d3809cfd..8fbce95c 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -90,15 +90,8 @@ public static void UseDurableTaskScheduler( static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) { - if (string.IsNullOrEmpty(options.EndpointAddress)) - { - throw RequiredOptionMissing(nameof(options.EndpointAddress)); - } - - if (string.IsNullOrEmpty(options.TaskHubName)) - { - throw RequiredOptionMissing(nameof(options.TaskHubName)); - } + Check.NotNullOrEmpty(options.EndpointAddress, nameof(options.EndpointAddress)); + Check.NotNullOrEmpty(options.TaskHubName, nameof(options.TaskHubName)); string taskHubName = options.TaskHubName; string endpoint = options.EndpointAddress; From 552a9c83f136edd391b75fe4fa42f0b9b37b0576 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:44:16 -0800 Subject: [PATCH 18/58] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 83 +--------------- .../Azure/DurableTaskSchedulerOptions.cs | 99 ++++++++++++++++--- 2 files changed, 90 insertions(+), 92 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 8fbce95c..4fda6df8 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -31,7 +31,7 @@ public static void UseDurableTaskScheduler( configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); + builder.UseGrpc(options.GetGrpcChannel()); } /// @@ -47,7 +47,7 @@ public static void UseDurableTaskScheduler( { var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); + builder.UseGrpc(options.GetGrpcChannel()); } /// @@ -69,7 +69,7 @@ public static void UseDurableTaskScheduler( configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); + builder.UseGrpc(options.GetGrpcChannel()); } /// @@ -85,86 +85,11 @@ public static void UseDurableTaskScheduler( { var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); configure?.Invoke(options); - builder.UseGrpc(GetGrpcChannelForOptions(options)); - } - - static GrpcChannel GetGrpcChannelForOptions(DurableTaskSchedulerOptions options) - { - Check.NotNullOrEmpty(options.EndpointAddress, nameof(options.EndpointAddress)); - Check.NotNullOrEmpty(options.TaskHubName, nameof(options.TaskHubName)); - - string taskHubName = options.TaskHubName; - string endpoint = options.EndpointAddress; - - string resourceId = options.ResourceId ?? "https://durabletask.io"; - int processId = Environment.ProcessId; - string workerId = options.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; - - TokenCache? cache = - options.Credential is not null - ? new( - options.Credential, - new(new[] { $"{resourceId}/.default" }), - TimeSpan.FromMinutes(5)) - : null; - - CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( - async (context, metadata) => - { - metadata.Add("taskhub", taskHubName); - metadata.Add("workerid", workerId); - - if (cache is null) - { - return; - } - - AccessToken token = await cache.GetTokenAsync(context.CancellationToken); - - metadata.Add("Authorization", $"Bearer {token.Token}"); - }); - - // Production will use HTTPS, but local testing will use HTTP - ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? - ChannelCredentials.SecureSsl : - ChannelCredentials.Insecure; - return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + builder.UseGrpc(options.GetGrpcChannel()); } static Exception RequiredOptionMissing(string optionName) { return new ArgumentException(message: $"Required option '{optionName}' was not provided."); } - - sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) - { - readonly TokenCredential credential = credential; - readonly TokenRequestContext context = context; - readonly TimeSpan margin = margin; - - AccessToken? token; - - public async Task GetTokenAsync(CancellationToken cancellationToken) - { - DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); - - if (this.token is null - || this.token.Value.RefreshOn < nowWithMargin - || this.token.Value.ExpiresOn < nowWithMargin) - { - this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); - } - - return this.token.Value; - } - } } \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index aa3d2a1f..4151cbf2 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -4,6 +4,7 @@ using System.Globalization; using Azure.Core; using Azure.Identity; +using Grpc.Core; namespace Microsoft.DurableTask.Extensions.Azure; @@ -17,8 +18,15 @@ public class DurableTaskSchedulerOptions /// internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) { - this.EndpointAddress = endpointAddress ?? throw new ArgumentNullException(nameof(endpointAddress)); - this.TaskHubName = taskHubName ?? throw new ArgumentNullException(nameof(taskHubName)); + Check.NotNullOrEmpty(endpointAddress, nameof(endpointAddress)); + Check.NotNullOrEmpty(taskHubName, nameof(taskHubName)); + + // Add https:// prefix if no protocol is specified + this.EndpointAddress = !endpointAddress.Contains("://") + ? $"https://{endpointAddress}" + : endpointAddress; + + this.TaskHubName = taskHubName; this.Credential = credential; } @@ -26,17 +34,7 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// Gets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// - public string EndpointAddress - { - get => this.endpointAddress; - set - { - // Add https:// prefix if no protocol is specified - this.endpointAddress = !value.Contains("://") - ? $"https://{value}" - : value; - } - } + public string EndpointAddress { get; } /// /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. @@ -71,6 +69,58 @@ public static DurableTaskSchedulerOptions FromConnectionString(string connection return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } + internal GrpcChannel GetGrpcChannel() + { + Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); + Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); + + string taskHubName = this.TaskHubName; + string endpoint = this.EndpointAddress; + + string resourceId = this.ResourceId ?? "https://durabletask.io"; + int processId = Environment.ProcessId; + string workerId = this.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; + + TokenCache? cache = + this.Credential is not null + ? new( + this.Credential, + new(new[] { $"{resourceId}/.default" }), + TimeSpan.FromMinutes(5)) + : null; + + CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( + async (context, metadata) => + { + metadata.Add("taskhub", taskHubName); + metadata.Add("workerid", workerId); + + if (cache is null) + { + return; + } + + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); + + metadata.Add("Authorization", $"Bearer {token.Token}"); + }); + + // Production will use HTTPS, but local testing will use HTTP + ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? + ChannelCredentials.SecureSsl : + ChannelCredentials.Insecure; + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + // The same credential is being used for all operations. + // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + + // TODO: This is not appropriate for use in production settings. Setting this to true should + // only be done for local testing. We should hide this setting behind some kind of flag. + UnsafeUseInsecureChannelCallCredentials = true, + }); + } + /// /// Creates a new instance of from a parsed connection string. /// @@ -169,4 +219,27 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } + + sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) + { + readonly TokenCredential credential = credential; + readonly TokenRequestContext context = context; + readonly TimeSpan margin = margin; + + AccessToken? token; + + public async Task GetTokenAsync(CancellationToken cancellationToken) + { + DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); + + if (this.token is null + || this.token.Value.RefreshOn < nowWithMargin + || this.token.Value.ExpiresOn < nowWithMargin) + { + this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); + } + + return this.token.Value; + } + } } \ No newline at end of file From 54dba769f9face0ed18a1e4cbadd978e0e2aa9df Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:46:24 -0800 Subject: [PATCH 19/58] save --- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 4151cbf2..9bf17460 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -20,12 +20,12 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, { Check.NotNullOrEmpty(endpointAddress, nameof(endpointAddress)); Check.NotNullOrEmpty(taskHubName, nameof(taskHubName)); - + // Add https:// prefix if no protocol is specified - this.EndpointAddress = !endpointAddress.Contains("://") - ? $"https://{endpointAddress}" + this.EndpointAddress = !endpointAddress.Contains("://") + ? $"https://{endpointAddress}" : endpointAddress; - + this.TaskHubName = taskHubName; this.Credential = credential; } From 5e97555a2ac574a0fc350a15a003888227b6a311 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:57:32 -0800 Subject: [PATCH 20/58] some fb --- src/Extensions/Azure/AccessTokenCache.cs | 50 +++++++++++++++++++ .../Azure/DurableTaskSchedulerOptions.cs | 34 ++----------- 2 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 src/Extensions/Azure/AccessTokenCache.cs diff --git a/src/Extensions/Azure/AccessTokenCache.cs b/src/Extensions/Azure/AccessTokenCache.cs new file mode 100644 index 00000000..0a273170 --- /dev/null +++ b/src/Extensions/Azure/AccessTokenCache.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; + +namespace Microsoft.DurableTask.Extensions.Azure; + +/// +/// Caches and manages refresh for Azure access tokens. +/// +internal sealed class AccessTokenCache +{ + readonly TokenCredential credential; + readonly TokenRequestContext context; + readonly TimeSpan margin; + + AccessToken? token; + + /// + /// Initializes a new instance of the class. + /// + /// The token credential to use for authentication. + /// The token request context. + /// The time margin to use for token refresh. + public AccessTokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) + { + this.credential = credential; + this.context = context; + this.margin = margin; + } + + /// + /// Gets a token, either from cache or by requesting a new one if needed. + /// + /// A cancellation token. + /// An access token. + public async Task GetTokenAsync(CancellationToken cancellationToken) + { + DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); + + if (this.token is null + || this.token.Value.RefreshOn < nowWithMargin + || this.token.Value.ExpiresOn < nowWithMargin) + { + this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); + } + + return this.token.Value; + } +} diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 9bf17460..c7dc157d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -81,11 +81,11 @@ internal GrpcChannel GetGrpcChannel() int processId = Environment.ProcessId; string workerId = this.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; - TokenCache? cache = + AccessTokenCache? cache = this.Credential is not null - ? new( + ? new AccessTokenCache( this.Credential, - new(new[] { $"{resourceId}/.default" }), + new TokenRequestContext(new[] { $"{resourceId}/.default" }), TimeSpan.FromMinutes(5)) : null; @@ -95,13 +95,12 @@ this.Credential is not null metadata.Add("taskhub", taskHubName); metadata.Add("workerid", workerId); - if (cache is null) + if (cache == null) { return; } - + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); - metadata.Add("Authorization", $"Bearer {token.Token}"); }); @@ -219,27 +218,4 @@ public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); return options; } - - sealed class TokenCache(TokenCredential credential, TokenRequestContext context, TimeSpan margin) - { - readonly TokenCredential credential = credential; - readonly TokenRequestContext context = context; - readonly TimeSpan margin = margin; - - AccessToken? token; - - public async Task GetTokenAsync(CancellationToken cancellationToken) - { - DateTimeOffset nowWithMargin = DateTimeOffset.UtcNow.Add(this.margin); - - if (this.token is null - || this.token.Value.RefreshOn < nowWithMargin - || this.token.Value.ExpiresOn < nowWithMargin) - { - this.token = await this.credential.GetTokenAsync(this.context, cancellationToken); - } - - return this.token.Value; - } - } } \ No newline at end of file From 9c890c5a2b9d9c83dd7b5381ed649d929a57394a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:05:03 -0800 Subject: [PATCH 21/58] fix --- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index c7dc157d..23d85bea 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -13,6 +13,8 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public class DurableTaskSchedulerOptions { + private readonly string defaultWorkerId; + /// /// Initializes a new instance of the class. /// @@ -28,6 +30,9 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, this.TaskHubName = taskHubName; this.Credential = credential; + + // Generate the default worker ID once at construction time + this.defaultWorkerId = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; } /// @@ -54,7 +59,7 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, /// /// Gets or sets the worker ID used to identify the worker instance. - /// The default value is a string containing the machine name and the process ID. + /// The default value is a string containing the machine name, process ID, and a unique identifier. /// public string? WorkerId { get; set; } @@ -78,8 +83,7 @@ internal GrpcChannel GetGrpcChannel() string endpoint = this.EndpointAddress; string resourceId = this.ResourceId ?? "https://durabletask.io"; - int processId = Environment.ProcessId; - string workerId = this.WorkerId ?? $"{Environment.MachineName},{processId},{Guid.NewGuid():N}"; + string workerId = this.WorkerId ?? this.defaultWorkerId; AccessTokenCache? cache = this.Credential is not null From 49c62822ede2637569e67bdc3274a93d626e27bb Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:06:15 -0800 Subject: [PATCH 22/58] update --- src/Extensions/Azure/DurableTaskSchedulerOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 23d85bea..3a0c6abd 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -32,6 +32,7 @@ internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, this.Credential = credential; // Generate the default worker ID once at construction time + // TODO: More iteration needed over time https://github.com/microsoft/durabletask-dotnet/pull/362#discussion_r1909547102 this.defaultWorkerId = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; } From 1adf7ccb85f017b246037d033ed950c8cbf26db8 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:16:28 -0800 Subject: [PATCH 23/58] update tests --- Microsoft.DurableTask.sln | 2 +- .../Azure/DurableTaskSchedulerExtensions.cs | 5 ----- .../Extensions.Azure.Tests.csproj | 18 ------------------ test/Extensions/Azure/Azure.Tests.csproj | 18 ++++++++++++++++++ ...urableTaskSchedulerConnectionStringTests.cs | 0 .../DurableTaskSchedulerExtensionsTests.cs | 0 .../Azure}/DurableTaskSchedulerOptionsTests.cs | 0 7 files changed, 19 insertions(+), 24 deletions(-) delete mode 100644 test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj create mode 100644 test/Extensions/Azure/Azure.Tests.csproj rename test/{Extensions.Azure.Tests => Extensions/Azure}/DurableTaskSchedulerConnectionStringTests.cs (100%) rename test/{Extensions.Azure.Tests => Extensions/Azure}/DurableTaskSchedulerExtensionsTests.cs (100%) rename test/{Extensions.Azure.Tests => Extensions/Azure}/DurableTaskSchedulerOptionsTests.cs (100%) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 59acc82d..4e5a4451 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -75,7 +75,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azure\Azure.csproj", "{662BF73D-A4DD-4910-8625-7C12F1ACDBEC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Extensions.Azure.Tests", "test\Extensions.Azure.Tests\Extensions.Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 4fda6df8..ed823d29 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -87,9 +87,4 @@ public static void UseDurableTaskScheduler( configure?.Invoke(options); builder.UseGrpc(options.GetGrpcChannel()); } - - static Exception RequiredOptionMissing(string optionName) - { - return new ArgumentException(message: $"Required option '{optionName}' was not provided."); - } } \ No newline at end of file diff --git a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj b/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj deleted file mode 100644 index e9c83d3e..00000000 --- a/test/Extensions.Azure.Tests/Extensions.Azure.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - diff --git a/test/Extensions/Azure/Azure.Tests.csproj b/test/Extensions/Azure/Azure.Tests.csproj new file mode 100644 index 00000000..53146103 --- /dev/null +++ b/test/Extensions/Azure/Azure.Tests.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + + + + + + + + + + + + + + diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs similarity index 100% rename from test/Extensions.Azure.Tests/DurableTaskSchedulerConnectionStringTests.cs rename to test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs similarity index 100% rename from test/Extensions.Azure.Tests/DurableTaskSchedulerExtensionsTests.cs rename to test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs diff --git a/test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs b/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs similarity index 100% rename from test/Extensions.Azure.Tests/DurableTaskSchedulerOptionsTests.cs rename to test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs From 14b94ebdd51119d92110b9547ee221cf3568a565 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:24:38 -0800 Subject: [PATCH 24/58] sample sample fix --- Microsoft.DurableTask.sln | 7 + .../dotnet/AspNetWebApp/AspNetWebApp.csproj | 27 ++++ .../dotnet/AspNetWebApp/DockerFile | 22 +++ .../Orchestrations/HelloCities.cs | 30 ++++ .../dotnet/AspNetWebApp/Program.cs | 55 +++++++ .../Properties/launchSettings.json | 23 +++ .../dotnet/AspNetWebApp/README.md | 147 ++++++++++++++++++ .../AspNetWebApp/ScenariosController.cs | 50 ++++++ .../portable-sdk/dotnet/AspNetWebApp/Utils.cs | 38 +++++ .../AspNetWebApp/appsettings.Development.json | 8 + .../AspNetWebApp/appsettings.Production.json | 11 ++ .../dotnet/AspNetWebApp/appsettings.json | 9 ++ 12 files changed, 427 insertions(+) create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/DockerFile create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Program.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/README.md create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json create mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 4e5a4451..b7758f81 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetWebApp", "samples\portable-sdk\dotnet\AspNetWebApp\AspNetWebApp.csproj", "{869D2D51-9372-4764-B059-C43B6C1180A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -199,6 +201,10 @@ Global {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.Build.0 = Release|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -237,6 +243,7 @@ Global {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} {DBB5DB4E-A1B0-4C86-A233-213789C46929} = {E5637F81-2FB9-4CD7-900D-455363B142A7} + {869D2D51-9372-4764-B059-C43B6C1180A3} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj b/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj new file mode 100644 index 00000000..14516742 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + true + $(BaseIntermediateOutputPath)Generated + + + false + false + + + + + + + + + + + + + + + diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile b/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile new file mode 100644 index 00000000..5ddb3ae9 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["AspNetWebApp.csproj", "."] +RUN dotnet restore "./AspNetWebApp.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "AspNetWebApp.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "AspNetWebApp.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENV ASPNETCORE_ENVIRONMENT=Production +ENTRYPOINT ["dotnet", "AspNetWebApp.dll"] diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs new file mode 100644 index 00000000..5c48dcaa --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs @@ -0,0 +1,30 @@ +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +namespace AspNetWebApp.Scenarios; + +[DurableTask] +class HelloCities : TaskOrchestrator> +{ + public override async Task> RunAsync(TaskOrchestrationContext context, string input) + { + List results = + [ + await context.CallSayHelloAsync("Seattle"), + await context.CallSayHelloAsync("Amsterdam"), + await context.CallSayHelloAsync("Hyderabad"), + await context.CallSayHelloAsync("Shanghai"), + await context.CallSayHelloAsync("Tokyo"), + ]; + return results; + } +} + +[DurableTask] +class SayHello : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string cityName) + { + return Task.FromResult($"Hello, {cityName}!"); + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs new file mode 100644 index 00000000..4c21b7b2 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Serialization; +using Azure.Core; +using Azure.Identity; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Extensions.Azure; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +string endpointAddress = builder.Configuration["DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS"] + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS'"); + +string taskHubName = builder.Configuration["DURABLE_TASK_SCHEDULER_TASK_HUB_NAME"] + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_TASK_HUB_NAME'"); + +TokenCredential credential = builder.Environment.IsProduction() + ? new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = builder.Configuration["CONTAINER_APP_UMI_CLIENT_ID"] }) + : new DefaultAzureCredential(); + +// Add all the generated orchestrations and activities automatically +builder.Services.AddDurableTaskWorker(builder => +{ + builder.AddTasks(r => r.AddAllGeneratedTasks()); + builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); +}); + +// Register the client, which can be used to start orchestrations +builder.Services.AddDurableTaskClient(builder => +{ + builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); +}); + +// Configure console logging using the simpler, more compact format +builder.Services.AddLogging(logging => +{ + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); +}); + +// Configure the HTTP request pipeline +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +}); + +// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS" +WebApplication app = builder.Build(); +app.MapControllers(); +app.Run(); diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json b/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json new file mode 100644 index 00000000..4ee2ec75 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36209", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5008", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://localhost:8082", + "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "samples" + } + } + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/README.md b/samples/portable-sdk/dotnet/AspNetWebApp/README.md new file mode 100644 index 00000000..4875b63e --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/README.md @@ -0,0 +1,147 @@ +# Hello World with the Durable Task SDK for .NET + +In addition to [Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview), the [Durable Task SDK for .NET](https://github.com/microsoft/durabletask-dotnet) can also use the Durable Task Scheduler service for managing orchestration state. + +This directory includes a sample .NET console app that demonstrates how to use the Durable Task Scheduler with the Durable Task SDK for .NET (without any Azure Functions dependency). + +## Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- [PowerShell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell) +- [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) + +## Creating a Durable Task Scheduler task hub + +Before you can run the app, you need to create a Durable Task Scheduler task hub in Azure and produce a connection string that references it. + +> **NOTE**: These are abbreviated instructions for simplicity. For a full set of instructions, see the Azure Durable Functions [QuickStart guide](../../../../quickstarts/HelloCities/README.md#create-a-durable-task-scheduler-namespace-and-task-hub). + +1. Install the Durable Task Scheduler CLI extension: + + ```bash + az upgrade + az extension add --name durabletask --allow-preview true + ``` + +1. Create a resource group: + + ```powershell + az group create --name my-resource-group --location northcentralus + ``` + +1. Create a Durable Task Scheduler namespace: + + ```powershell + az durabletask namespace create -g my-resource-group --name my-namespace + ``` + +1. Create a task hub within the namespace: + + ```powershell + az durabletask taskhub create -g my-resource-group --namespace my-namespace --name "portable-dotnet" + ``` + +1. Grant the current user permission to connect to the `portable-dotnet` task hub: + + ```powershell + $subscriptionId = az account show --query "id" -o tsv + $loggedInUser = az account show --query "user.name" -o tsv + + az role assignment create ` + --assignee $loggedInUser ` + --role "Durable Task Data Contributor" ` + --scope "/subscriptions/$subscriptionId/resourceGroups/my-resource-group/providers/Microsoft.DurableTask/namespaces/my-namespace/taskHubs/portable-dotnet" + ``` + + Note that it may take a minute for the role assignment to take effect. + +1. Get the endpoint for the scheduler resource and save it to the `DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS` environment variable: + + ```powershell + $endpoint = az durabletask namespace show ` + -g my-resource-group ` + -n my-namespace ` + --query "properties.url" ` + -o tsv + $env:DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS = $endpoint + ``` + + The `DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS` environment variable is used by the sample app to connect to the Durable Task Scheduler resource. + +1. Save the task hub name to the `DURABLE_TASK_SCHEDULER_TASK_HUB_NAME` environment variable: + + ```powershell + $env:DURABLE_TASK_SCHEDULER_TASK_HUB_NAME = "portable-dotnet" + ``` + + The `DURABLE_TASK_SCHEDULER_TASK_HUB_NAME` environment variable is to configure the sample app with the correct task hub resource name. + +## Running the sample + +In the same terminal window as above, use the following steps to run the sample on your local machine. + +1. Clone this repository. + +1. Open a terminal window and navigate to the `samples/portable-sdk/dotnet/AspNetWebApp` directory. + +1. Run the following command to build and run the sample: + + ```bash + dotnet run + ``` + +You should see output similar to the following: + +```plaintext +Building... +info: Microsoft.DurableTask[1] + Durable Task gRPC worker starting. +info: Microsoft.Hosting.Lifetime[14] + Now listening on: http://localhost:5008 +info: Microsoft.Hosting.Lifetime[0] + Application started. Press Ctrl+C to shut down. +info: Microsoft.Hosting.Lifetime[0] + Hosting environment: Development +info: Microsoft.Hosting.Lifetime[0] + Content root path: D:\projects\Azure-Functions-Durable-Task-Scheduler-Private-Preview\samples\portable-sdk\dotnet\AspNetWebApp +info: Microsoft.DurableTask[4] + Sidecar work-item streaming connection established. +``` + +## View orchestrations in the dashboard + +You can view the orchestrations in the Durable Task Scheduler dashboard by navigating to the namespace-specific dashboard URL in your browser. + +Use the following PowerShell command to get the dashboard URL: + +```powershell +$baseUrl = az durabletask namespace show ` + -g my-resource-group ` + -n my-namespace ` + --query "properties.dashboardUrl" ` + -o tsv +$dashboardUrl = "$baseUrl/taskHubs/portable-dotnet" +$dashboardUrl +``` + +The URL should look something like the following: + +```plaintext +https://my-namespace-atdngmgxfsh0-db.northcentralus.durabletask.io/taskHubs/portable-dotnet +``` + +Once logged in, you should see the orchestrations that were created by the sample app. Below is an example of what the dashboard might look like (note that some of the details will be different than the screenshot): + +![Durable Task Scheduler dashboard](/media/images/dtfx-sample-dashboard.png) + + +## Optional: Deploy to Azure Container Apps +1. Create an container app following the instructions in the [Azure Container App documentation](https://learn.microsoft.com/azure/container-apps/get-started?tabs=bash). +2. During step 1, specify the deployed container app code folder at samples\portable-sdk\dotnet\AspNetWebApp +3. Follow the instructions to create a user managed identity and assign the `Durable Task Data Contributor` role then attach it to the container app you created in step 1 at [Azure-Functions-Durable-Task-Scheduler-Private-Preview](..\..\..\..\docs\configure-existing-app.md#run-the-app-on-azure-net). Please skip section "Add required environment variables to app" since these environment variables are not required for deploying to container app. +4. Call the container app endpoint at `http://sampleapi-.azurecontainerapps.io/api/orchestrators/HelloCities`, Sample curl command: + + ```bash + curl -X POST "https://sampleapi-.azurecontainerapps.io/api/orchestrators/HelloCities" + ``` +5. You should see the orchestration created in the Durable Task Scheduler dashboard. diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs b/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs new file mode 100644 index 00000000..d6049129 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +namespace AspNetWebApp; + +[Route("scenarios")] +[ApiController] +public partial class ScenariosController( + DurableTaskClient durableTaskClient, + ILogger logger) : ControllerBase +{ + readonly DurableTaskClient durableTaskClient = durableTaskClient; + readonly ILogger logger = logger; + + [HttpPost("hellocities")] + public async Task RunHelloCities([FromQuery] int? count, [FromQuery] string? prefix) + { + if (count is null || count < 1) + { + return this.BadRequest(new { error = "A 'count' query string parameter is required and it must contain a positive number." }); + } + + // Generate a semi-unique prefix for the instance IDs to simplify tracking + prefix ??= $"hellocities-{count}-"; + prefix += DateTime.UtcNow.ToString("yyyyMMdd-hhmmss"); + + this.logger.LogInformation("Scheduling {count} orchestrations with a prefix of '{prefix}'...", count, prefix); + + Stopwatch sw = Stopwatch.StartNew(); + await Enumerable.Range(0, count.Value).ParallelForEachAsync(1000, i => + { + string instanceId = $"{prefix}-{i:X16}"; + return this.durableTaskClient.ScheduleNewHelloCitiesInstanceAsync( + input: null!, + new StartOrchestrationOptions(instanceId)); + }); + + sw.Stop(); + this.logger.LogInformation( + "All {count} orchestrations were scheduled successfully in {time}ms!", + count, + sw.ElapsedMilliseconds); + return this.Ok(new + { + message = $"Scheduled {count} orchestrations prefixed with '{prefix}' in {sw.ElapsedMilliseconds}." + }); + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs new file mode 100644 index 00000000..6ddabf39 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs @@ -0,0 +1,38 @@ +namespace AspNetWebApp; + +static class Utils +{ + public static async Task ParallelForEachAsync(this IEnumerable items, int maxConcurrency, Func action) + { + List tasks; + if (items is ICollection itemCollection) + { + tasks = new List(itemCollection.Count); + } + else + { + tasks = []; + } + + using SemaphoreSlim semaphore = new(maxConcurrency); + foreach (T item in items) + { + tasks.Add(InvokeThrottledAction(item, action, semaphore)); + } + + await Task.WhenAll(tasks); + } + + static async Task InvokeThrottledAction(T item, Func action, SemaphoreSlim semaphore) + { + await semaphore.WaitAsync(); + try + { + await action(item); + } + finally + { + semaphore.Release(); + } + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json new file mode 100644 index 00000000..a6e86ace --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json new file mode 100644 index 00000000..70b7d1d9 --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://{your-durable-task-endpoint}.durabletask.io", + "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "{your-task-hub-name}", + "CONTAINER_APP_UMI_CLIENT_ID": "{your-user-managed-identity-client-id}" +} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 5696b2f8116752f86c53ab6f4ef8a655f4111e5c Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 20:43:26 -0800 Subject: [PATCH 25/58] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index ed823d29..c7102bfb 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -27,10 +27,12 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - - configure?.Invoke(options); + if (configure is not null) + { + builder.Services.Configure("DurableTaskSchedulerOptionsForWorker", configure); + } + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -45,8 +47,12 @@ public static void UseDurableTaskScheduler( string connectionString, Action? configure = null) { + if (configure is not null) + { + builder.Services.Configure(builder.Name, configure); + } + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); - configure?.Invoke(options); builder.UseGrpc(options.GetGrpcChannel()); } @@ -65,10 +71,12 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); - - configure?.Invoke(options); + if (configure is not null) + { + builder.Services.Configure(builder.Name, configure); + } + DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -83,8 +91,12 @@ public static void UseDurableTaskScheduler( string connectionString, Action? configure = null) { + if (configure is not null) + { + builder.Services.Configure(builder.Name, configure); + } + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); - configure?.Invoke(options); builder.UseGrpc(options.GetGrpcChannel()); } } \ No newline at end of file From 62e2b3043c3d1c49f9494ad6d672bbeb740ded01 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 21:02:02 -0800 Subject: [PATCH 26/58] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index c7102bfb..8e58071d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -4,6 +4,8 @@ using Azure.Core; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.DurableTask.Extensions.Azure; @@ -12,6 +14,16 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public static class DurableTaskSchedulerExtensions { + /// + /// Configures DurableTaskScheduler options. + /// + public static void ConfigureDurableTaskSchedulerOptions( + IServiceCollection services, + Action configure) + { + services.Configure(Options.DefaultName, configure); + } + /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. /// @@ -24,14 +36,8 @@ public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential, - Action? configure = null) + TokenCredential credential) { - if (configure is not null) - { - builder.Services.Configure("DurableTaskSchedulerOptionsForWorker", configure); - } - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -44,14 +50,8 @@ public static void UseDurableTaskScheduler( /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, - string connectionString, - Action? configure = null) + string connectionString) { - if (configure is not null) - { - builder.Services.Configure(builder.Name, configure); - } - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.UseGrpc(options.GetGrpcChannel()); } @@ -68,14 +68,8 @@ public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential, - Action? configure = null) + TokenCredential credential) { - if (configure is not null) - { - builder.Services.Configure(builder.Name, configure); - } - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); builder.UseGrpc(options.GetGrpcChannel()); } @@ -88,14 +82,8 @@ public static void UseDurableTaskScheduler( /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, - string connectionString, - Action? configure = null) + string connectionString) { - if (configure is not null) - { - builder.Services.Configure(builder.Name, configure); - } - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.UseGrpc(options.GetGrpcChannel()); } From 02b02c733253878d06229357a78feef33fbafee5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 23:23:41 -0800 Subject: [PATCH 27/58] update --- src/Extensions/Azure/Azure.csproj | 1 + .../Azure/DurableTaskSchedulerExtensions.cs | 107 +++++++++---- .../Azure/DurableTaskSchedulerOptions.cs | 148 ++++++------------ 3 files changed, 121 insertions(+), 135 deletions(-) diff --git a/src/Extensions/Azure/Azure.csproj b/src/Extensions/Azure/Azure.csproj index 25704be0..d82dd02f 100644 --- a/src/Extensions/Azure/Azure.csproj +++ b/src/Extensions/Azure/Azure.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 8e58071d..2e8f2e22 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; + namespace Microsoft.DurableTask.Extensions.Azure; /// @@ -14,77 +15,115 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public static class DurableTaskSchedulerExtensions { - /// - /// Configures DurableTaskScheduler options. - /// - public static void ConfigureDurableTaskSchedulerOptions( - IServiceCollection services, - Action configure) - { - services.Configure(Options.DefaultName, configure); - } - /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. /// - /// The worker builder to configure. - /// The endpoint address of the Durable Task Scheduler service. - /// The name of the task hub to connect to. - /// The credential to use for authentication. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential) + TokenCredential credential, + Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(builder.Name); + builder.UseGrpc(options.GetGrpcChannel()); } /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. /// - /// The worker builder to configure. - /// The connection string for the Durable Task Scheduler service. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, - string connectionString) + string connectionString, + Action? configure = null) { - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(builder.Name); + builder.UseGrpc(options.GetGrpcChannel()); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service. /// - /// The client builder to configure. - /// The endpoint address of the Durable Task Scheduler service. - /// The name of the task hub to connect to. - /// The credential to use for authentication. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, string taskHubName, - TokenCredential credential) + TokenCredential credential, + Action? configure = null) { - DurableTaskSchedulerOptions options = new(endpointAddress, taskHubName, credential); + builder.Services.AddOptions(Options.DefaultName) + .Configure(options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(Options.DefaultName); + builder.UseGrpc(options.GetGrpcChannel()); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service using a connection string. /// - /// The client builder to configure. - /// The connection string for the Durable Task Scheduler service. - /// Optional callback to configure additional options. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, - string connectionString) + string connectionString, + Action? configure = null) { - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + builder.Services.AddOptions(Options.DefaultName) + .Configure(options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var options = builder.Services.BuildServiceProvider() + .GetRequiredService>() + .Get(Options.DefaultName); + builder.UseGrpc(options.GetGrpcChannel()); } } \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 3a0c6abd..06fdbf5d 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel.DataAnnotations; using System.Globalization; using Azure.Core; using Azure.Identity; @@ -13,63 +14,45 @@ namespace Microsoft.DurableTask.Extensions.Azure; /// public class DurableTaskSchedulerOptions { - private readonly string defaultWorkerId; - - /// - /// Initializes a new instance of the class. - /// - internal DurableTaskSchedulerOptions(string endpointAddress, string taskHubName, TokenCredential? credential = null) - { - Check.NotNullOrEmpty(endpointAddress, nameof(endpointAddress)); - Check.NotNullOrEmpty(taskHubName, nameof(taskHubName)); - - // Add https:// prefix if no protocol is specified - this.EndpointAddress = !endpointAddress.Contains("://") - ? $"https://{endpointAddress}" - : endpointAddress; - - this.TaskHubName = taskHubName; - this.Credential = credential; - - // Generate the default worker ID once at construction time - // TODO: More iteration needed over time https://github.com/microsoft/durabletask-dotnet/pull/362#discussion_r1909547102 - this.defaultWorkerId = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; - } - /// - /// Gets the endpoint address of the Durable Task Scheduler resource. + /// Gets or sets the endpoint address of the Durable Task Scheduler resource. /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// - public string EndpointAddress { get; } + [Required(ErrorMessage = "Endpoint address is required")] + public string EndpointAddress { get; set; } = string.Empty; /// - /// Gets the name of the task hub resource associated with the Durable Task Scheduler resource. + /// Gets or sets the name of the task hub resource associated with the Durable Task Scheduler resource. /// - public string TaskHubName { get; } + [Required(ErrorMessage = "Task hub name is required")] + public string TaskHubName { get; set; } = string.Empty; /// - /// Gets the credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Gets or sets the credential used to authenticate with the Durable Task Scheduler task hub resource. /// - public TokenCredential? Credential { get; } + public TokenCredential? Credential { get; set; } /// /// Gets or sets the resource ID of the Durable Task Scheduler resource. /// The default value is https://durabletask.io. /// - public string? ResourceId { get; set; } + public string ResourceId { get; set; } = "https://durabletask.io"; /// /// Gets or sets the worker ID used to identify the worker instance. /// The default value is a string containing the machine name, process ID, and a unique identifier. /// - public string? WorkerId { get; set; } + public string WorkerId { get; set; } = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; + + /// + /// Gets or sets a value indicating whether to allow insecure channel credentials. + /// This should only be set to true in development/testing scenarios. + /// + public bool AllowInsecureCredentials { get; set; } /// /// Creates a new instance of from a connection string. /// - /// The connection string containing the configuration settings. - /// A new instance of configured with the connection string settings. - /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); @@ -81,16 +64,15 @@ internal GrpcChannel GetGrpcChannel() Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); string taskHubName = this.TaskHubName; - string endpoint = this.EndpointAddress; - - string resourceId = this.ResourceId ?? "https://durabletask.io"; - string workerId = this.WorkerId ?? this.defaultWorkerId; + string endpoint = !this.EndpointAddress.Contains("://") + ? $"https://{this.EndpointAddress}" + : this.EndpointAddress; AccessTokenCache? cache = this.Credential is not null ? new AccessTokenCache( this.Credential, - new TokenRequestContext(new[] { $"{resourceId}/.default" }), + new TokenRequestContext(new[] { $"{this.ResourceId}/.default" }), TimeSpan.FromMinutes(5)) : null; @@ -98,90 +80,68 @@ this.Credential is not null async (context, metadata) => { metadata.Add("taskhub", taskHubName); - metadata.Add("workerid", workerId); + metadata.Add("workerid", this.WorkerId); if (cache == null) { return; } - + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); metadata.Add("Authorization", $"Bearer {token.Token}"); }); // Production will use HTTPS, but local testing will use HTTP - ChannelCredentials channelCreds = endpoint.StartsWith("https://") ? + ChannelCredentials channelCreds = endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; - return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions - { - // The same credential is being used for all operations. - // https://learn.microsoft.com/aspnet/core/grpc/authn-and-authz#set-the-bearer-token-with-callcredentials - Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), - // TODO: This is not appropriate for use in production settings. Setting this to true should - // only be done for local testing. We should hide this setting behind some kind of flag. - UnsafeUseInsecureChannelCallCredentials = true, - }); + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + UnsafeUseInsecureChannelCallCredentials = this.AllowInsecureCredentials, + }); } /// /// Creates a new instance of from a parsed connection string. /// - /// The parsed connection string containing the configuration settings. - /// A new instance of configured with the connection string settings. - /// Thrown when the connection string contains an unsupported authentication type. public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) { - // Example connection strings: - // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=ManagedIdentity;ClientID=00000000-0000-0000-0000-000000000000;TaskHubName=th01" - // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=DefaultAzure;TaskHubName=th01" - // "Endpoint=https://myaccount.westus3.durabletask.io/;Authentication=None;TaskHubName=th01" (undocumented and only intended for local testing) - - string endpointAddress = connectionString.Endpoint; - - if (!endpointAddress.Contains("://")) + var options = new DurableTaskSchedulerOptions { - // If the protocol is missing, assume HTTPS. - endpointAddress = "https://" + endpointAddress; - } + EndpointAddress = connectionString.Endpoint, + TaskHubName = connectionString.TaskHubName, + Credential = GetCredentialFromConnectionString(connectionString) + }; - string authType = connectionString.Authentication; + return options; + } - TokenCredential? credential; + static TokenCredential? GetCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) + { + string authType = connectionString.Authentication; // Parse the supported auth types, in a case-insensitive way and without spaces switch (authType.ToLower(CultureInfo.InvariantCulture).Replace(" ", string.Empty)) { case "defaultazure": - // Default Azure credentials, suitable for a variety of scenarios - // In many cases, users will need to pass additional configuration options via env vars - credential = new DefaultAzureCredential(); - break; + return new DefaultAzureCredential(); case "managedidentity": - // Use Managed identity - // Suitable for Azure-hosted scenarios - // Note that ClientId could be null for system-assigned managed identities - credential = new ManagedIdentityCredential(connectionString.ClientId); - break; + return new ManagedIdentityCredential(connectionString.ClientId); case "workloadidentity": - // Use Workload Identity Federation. - // This is commonly-used in Kubernetes (hosted on Azure or anywhere), or in CI environments like - // Azure Pipelines or GitHub Actions. It can also be used with SPIFFE. - WorkloadIdentityCredentialOptions opts = new() { }; + var opts = new WorkloadIdentityCredentialOptions(); if (!string.IsNullOrEmpty(connectionString.ClientId)) { opts.ClientId = connectionString.ClientId; } - if (!string.IsNullOrEmpty(connectionString.TenantId)) { opts.TenantId = connectionString.TenantId; } - if (connectionString.AdditionallyAllowedTenants is not null) { foreach (string tenant in connectionString.AdditionallyAllowedTenants) @@ -189,38 +149,24 @@ public static DurableTaskSchedulerOptions FromConnectionString( opts.AdditionallyAllowedTenants.Add(tenant); } } - - credential = new WorkloadIdentityCredential(opts); - break; + return new WorkloadIdentityCredential(opts); case "environment": - // Use credentials from the environment - credential = new EnvironmentCredential(); - break; + return new EnvironmentCredential(); case "azurecli": - // Use credentials from the Azure CLI - credential = new AzureCliCredential(); - break; + return new AzureCliCredential(); case "azurepowershell": - // Use credentials from the Azure PowerShell modules - credential = new AzurePowerShellCredential(); - break; + return new AzurePowerShellCredential(); case "none": - // Do not use any authentication/authorization (for testing only) - // This is a no-op - credential = null; - break; + return null; default: throw new ArgumentException( $"The connection string contains an unsupported authentication type '{authType}'.", nameof(connectionString)); } - - DurableTaskSchedulerOptions options = new(endpointAddress, connectionString.TaskHubName, credential); - return options; } } \ No newline at end of file From ee517d2583cdbd7431b7e4d1a413e2320f8a4852 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 08:45:52 -0800 Subject: [PATCH 28/58] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 48 +++++++++---------- .../Azure/DurableTaskSchedulerOptions.cs | 2 +- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 2e8f2e22..947caf77 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -36,11 +36,9 @@ public static void UseDurableTaskScheduler( .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(builder.Name); - - builder.UseGrpc(options.GetGrpcChannel()); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); } /// @@ -64,11 +62,9 @@ public static void UseDurableTaskScheduler( .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(builder.Name); - - builder.UseGrpc(options.GetGrpcChannel()); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); } /// @@ -92,11 +88,9 @@ public static void UseDurableTaskScheduler( .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(Options.DefaultName); - - builder.UseGrpc(options.GetGrpcChannel()); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); } /// @@ -110,20 +104,24 @@ public static void UseDurableTaskScheduler( var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.Services.AddOptions(Options.DefaultName) - .Configure(options => - { - options.EndpointAddress = connectionOptions.EndpointAddress; - options.TaskHubName = connectionOptions.TaskHubName; - options.Credential = connectionOptions.Credential; - }) .Configure(configure ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart(); - var options = builder.Services.BuildServiceProvider() - .GetRequiredService>() - .Get(Options.DefaultName); + builder.Services.TryAddEnumerable( + ServiceDescription.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); + } + + // helper internal class + class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions + { + public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); - builder.UseGrpc(options.GetGrpcChannel()); + public void Configure(string name, GrpcDurableTaskWorkerOptions options) + { + DurableTaskSchedulerOptions source = schedulerOptions.Get(name); + options.Channel = source.CreateChannel(); + } } } \ No newline at end of file diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs index 06fdbf5d..25f994dd 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerOptions.cs @@ -58,7 +58,7 @@ public static DurableTaskSchedulerOptions FromConnectionString(string connection return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } - internal GrpcChannel GetGrpcChannel() + internal GrpcChannel CreateChannel() { Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); From 34a47addc1bc452b191d2bb7bce422c2dc974062 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:05:56 -0800 Subject: [PATCH 29/58] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 947caf77..bc1e2374 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -3,11 +3,13 @@ using Azure.Core; using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Grpc; using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.Grpc; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; - namespace Microsoft.DurableTask.Extensions.Azure; /// @@ -18,6 +20,11 @@ public static class DurableTaskSchedulerExtensions /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. /// + /// The Durable Task worker builder to configure. + /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, @@ -37,13 +44,16 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } /// /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. /// + /// The Durable Task worker builder to configure. + /// The connection string used to connect to the Durable Task Scheduler service. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string connectionString, @@ -63,13 +73,18 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service. /// + /// The Durable Task client builder to configure. + /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, @@ -89,13 +104,16 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service using a connection string. /// + /// The Durable Task client builder to configure. + /// The connection string used to connect to the Durable Task Scheduler service. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string connectionString, @@ -109,19 +127,28 @@ public static void UseDurableTaskScheduler( .ValidateOnStart(); builder.Services.TryAddEnumerable( - ServiceDescription.Singleton, ConfigureGrpcChannel>()); + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); builder.UseGrpc(_ => { }); } - // helper internal class - class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions + class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + IConfigureNamedOptions, + IConfigureNamedOptions { public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); - public void Configure(string name, GrpcDurableTaskWorkerOptions options) + public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); + + public void Configure(string? name, GrpcDurableTaskWorkerOptions options) + { + DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); + options.Channel = source.CreateChannel(); + } + + public void Configure(string? name, GrpcDurableTaskClientOptions options) { - DurableTaskSchedulerOptions source = schedulerOptions.Get(name); + DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } } -} \ No newline at end of file +} From 2decb7940e4ad80164cf33f4d5c0d00fd615410a Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:07:23 -0800 Subject: [PATCH 30/58] save --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index bc1e2374..0be711d4 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -131,7 +131,7 @@ public static void UseDurableTaskScheduler( builder.UseGrpc(_ => { }); } - class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions, IConfigureNamedOptions { From ee295d0a0119d5acfc63e15391e4e14d884e453e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:14:32 -0800 Subject: [PATCH 31/58] save --- .../Azure/DurableTaskSchedulerExtensions.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 0be711d4..aabc23e1 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -131,20 +131,43 @@ public static void UseDurableTaskScheduler( builder.UseGrpc(_ => { }); } + /// + /// Internal configuration class that sets up gRPC channels for both worker and client options + /// using the provided Durable Task Scheduler options. + /// + /// Monitor for accessing the current scheduler options configuration. internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions, IConfigureNamedOptions { + /// + /// Configures worker options using the default options name. + /// + /// The worker options to configure. public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); + /// + /// Configures client options using the default options name. + /// + /// The client options to configure. public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); + /// + /// Configures named worker options by creating and assigning a gRPC channel. + /// + /// The name of the options instance being configured, or null for the default instance. + /// The worker options to configure. public void Configure(string? name, GrpcDurableTaskWorkerOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } + /// + /// Configures named client options by creating and assigning a gRPC channel. + /// + /// The name of the options instance being configured, or null for the default instance. + /// The client options to configure. public void Configure(string? name, GrpcDurableTaskClientOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); From 522d4b01c45667ba6513ff4398d0a86b4f65bcd5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:38:19 -0800 Subject: [PATCH 32/58] fix --- .../Azure/DurableTaskSchedulerExtensions.cs | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index aabc23e1..2be74649 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -92,7 +92,7 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - builder.Services.AddOptions(Options.DefaultName) + builder.Services.AddOptions(builder.Name) .Configure(options => { options.EndpointAddress = endpointAddress; @@ -121,7 +121,7 @@ public static void UseDurableTaskScheduler( { var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); - builder.Services.AddOptions(Options.DefaultName) + builder.Services.AddOptions(builder.Name) .Configure(configure ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart(); @@ -140,34 +140,16 @@ internal class ConfigureGrpcChannel(IOptionsMonitor IConfigureNamedOptions, IConfigureNamedOptions { - /// - /// Configures worker options using the default options name. - /// - /// The worker options to configure. public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); - /// - /// Configures client options using the default options name. - /// - /// The client options to configure. public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); - /// - /// Configures named worker options by creating and assigning a gRPC channel. - /// - /// The name of the options instance being configured, or null for the default instance. - /// The worker options to configure. public void Configure(string? name, GrpcDurableTaskWorkerOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } - /// - /// Configures named client options by creating and assigning a gRPC channel. - /// - /// The name of the options instance being configured, or null for the default instance. - /// The client options to configure. public void Configure(string? name, GrpcDurableTaskClientOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); From d289c10a51176df1110312bf6ee058f87c710da7 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 09:45:38 -0800 Subject: [PATCH 33/58] fix --- src/Extensions/Azure/DurableTaskSchedulerExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs index 2be74649..6692a6a3 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs @@ -122,6 +122,12 @@ public static void UseDurableTaskScheduler( var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }) .Configure(configure ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart(); From 0d2a20ca5e546feed38ce2045a8edb2628ba3a39 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:04:23 -0800 Subject: [PATCH 34/58] Revert "sample" This reverts commit 14b94ebdd51119d92110b9547ee221cf3568a565. --- Microsoft.DurableTask.sln | 7 - .../dotnet/AspNetWebApp/AspNetWebApp.csproj | 27 ---- .../dotnet/AspNetWebApp/DockerFile | 22 --- .../Orchestrations/HelloCities.cs | 30 ---- .../dotnet/AspNetWebApp/Program.cs | 55 ------- .../Properties/launchSettings.json | 23 --- .../dotnet/AspNetWebApp/README.md | 147 ------------------ .../AspNetWebApp/ScenariosController.cs | 50 ------ .../portable-sdk/dotnet/AspNetWebApp/Utils.cs | 38 ----- .../AspNetWebApp/appsettings.Development.json | 8 - .../AspNetWebApp/appsettings.Production.json | 11 -- .../dotnet/AspNetWebApp/appsettings.json | 9 -- 12 files changed, 427 deletions(-) delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/DockerFile delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Program.cs delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/README.md delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json delete mode 100644 samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index b7758f81..4e5a4451 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -77,8 +77,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetWebApp", "samples\portable-sdk\dotnet\AspNetWebApp\AspNetWebApp.csproj", "{869D2D51-9372-4764-B059-C43B6C1180A3}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,10 +199,6 @@ Global {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.Build.0 = Release|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -243,7 +237,6 @@ Global {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} {DBB5DB4E-A1B0-4C86-A233-213789C46929} = {E5637F81-2FB9-4CD7-900D-455363B142A7} - {869D2D51-9372-4764-B059-C43B6C1180A3} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj b/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj deleted file mode 100644 index 14516742..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/AspNetWebApp.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net8.0 - enable - enable - true - $(BaseIntermediateOutputPath)Generated - - - false - false - - - - - - - - - - - - - - - diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile b/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile deleted file mode 100644 index 5ddb3ae9..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/DockerFile +++ /dev/null @@ -1,22 +0,0 @@ -#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -WORKDIR /app -EXPOSE 8080 - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR /src -COPY ["AspNetWebApp.csproj", "."] -RUN dotnet restore "./AspNetWebApp.csproj" -COPY . . -WORKDIR "/src/." -RUN dotnet build "AspNetWebApp.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "AspNetWebApp.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENV ASPNETCORE_ENVIRONMENT=Production -ENTRYPOINT ["dotnet", "AspNetWebApp.dll"] diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs deleted file mode 100644 index 5c48dcaa..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/Orchestrations/HelloCities.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; - -namespace AspNetWebApp.Scenarios; - -[DurableTask] -class HelloCities : TaskOrchestrator> -{ - public override async Task> RunAsync(TaskOrchestrationContext context, string input) - { - List results = - [ - await context.CallSayHelloAsync("Seattle"), - await context.CallSayHelloAsync("Amsterdam"), - await context.CallSayHelloAsync("Hyderabad"), - await context.CallSayHelloAsync("Shanghai"), - await context.CallSayHelloAsync("Tokyo"), - ]; - return results; - } -} - -[DurableTask] -class SayHello : TaskActivity -{ - public override Task RunAsync(TaskActivityContext context, string cityName) - { - return Task.FromResult($"Hello, {cityName}!"); - } -} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs deleted file mode 100644 index 4c21b7b2..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/Program.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Text.Json.Serialization; -using Azure.Core; -using Azure.Identity; -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Worker; -using Microsoft.DurableTask.Extensions.Azure; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -string endpointAddress = builder.Configuration["DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS"] - ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS'"); - -string taskHubName = builder.Configuration["DURABLE_TASK_SCHEDULER_TASK_HUB_NAME"] - ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_TASK_HUB_NAME'"); - -TokenCredential credential = builder.Environment.IsProduction() - ? new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = builder.Configuration["CONTAINER_APP_UMI_CLIENT_ID"] }) - : new DefaultAzureCredential(); - -// Add all the generated orchestrations and activities automatically -builder.Services.AddDurableTaskWorker(builder => -{ - builder.AddTasks(r => r.AddAllGeneratedTasks()); - builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); -}); - -// Register the client, which can be used to start orchestrations -builder.Services.AddDurableTaskClient(builder => -{ - builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); -}); - -// Configure console logging using the simpler, more compact format -builder.Services.AddLogging(logging => -{ - logging.AddSimpleConsole(options => - { - options.SingleLine = true; - options.UseUtcTimestamp = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; - }); -}); - -// Configure the HTTP request pipeline -builder.Services.AddControllers().AddJsonOptions(options => -{ - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; -}); - -// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS" -WebApplication app = builder.Build(); -app.MapControllers(); -app.Run(); diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json b/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json deleted file mode 100644 index 4ee2ec75..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/Properties/launchSettings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:36209", - "sslPort": 0 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5008", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://localhost:8082", - "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "samples" - } - } - } -} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/README.md b/samples/portable-sdk/dotnet/AspNetWebApp/README.md deleted file mode 100644 index 4875b63e..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# Hello World with the Durable Task SDK for .NET - -In addition to [Durable Functions](https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-overview), the [Durable Task SDK for .NET](https://github.com/microsoft/durabletask-dotnet) can also use the Durable Task Scheduler service for managing orchestration state. - -This directory includes a sample .NET console app that demonstrates how to use the Durable Task Scheduler with the Durable Task SDK for .NET (without any Azure Functions dependency). - -## Prerequisites - -- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) -- [PowerShell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell) -- [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) - -## Creating a Durable Task Scheduler task hub - -Before you can run the app, you need to create a Durable Task Scheduler task hub in Azure and produce a connection string that references it. - -> **NOTE**: These are abbreviated instructions for simplicity. For a full set of instructions, see the Azure Durable Functions [QuickStart guide](../../../../quickstarts/HelloCities/README.md#create-a-durable-task-scheduler-namespace-and-task-hub). - -1. Install the Durable Task Scheduler CLI extension: - - ```bash - az upgrade - az extension add --name durabletask --allow-preview true - ``` - -1. Create a resource group: - - ```powershell - az group create --name my-resource-group --location northcentralus - ``` - -1. Create a Durable Task Scheduler namespace: - - ```powershell - az durabletask namespace create -g my-resource-group --name my-namespace - ``` - -1. Create a task hub within the namespace: - - ```powershell - az durabletask taskhub create -g my-resource-group --namespace my-namespace --name "portable-dotnet" - ``` - -1. Grant the current user permission to connect to the `portable-dotnet` task hub: - - ```powershell - $subscriptionId = az account show --query "id" -o tsv - $loggedInUser = az account show --query "user.name" -o tsv - - az role assignment create ` - --assignee $loggedInUser ` - --role "Durable Task Data Contributor" ` - --scope "/subscriptions/$subscriptionId/resourceGroups/my-resource-group/providers/Microsoft.DurableTask/namespaces/my-namespace/taskHubs/portable-dotnet" - ``` - - Note that it may take a minute for the role assignment to take effect. - -1. Get the endpoint for the scheduler resource and save it to the `DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS` environment variable: - - ```powershell - $endpoint = az durabletask namespace show ` - -g my-resource-group ` - -n my-namespace ` - --query "properties.url" ` - -o tsv - $env:DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS = $endpoint - ``` - - The `DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS` environment variable is used by the sample app to connect to the Durable Task Scheduler resource. - -1. Save the task hub name to the `DURABLE_TASK_SCHEDULER_TASK_HUB_NAME` environment variable: - - ```powershell - $env:DURABLE_TASK_SCHEDULER_TASK_HUB_NAME = "portable-dotnet" - ``` - - The `DURABLE_TASK_SCHEDULER_TASK_HUB_NAME` environment variable is to configure the sample app with the correct task hub resource name. - -## Running the sample - -In the same terminal window as above, use the following steps to run the sample on your local machine. - -1. Clone this repository. - -1. Open a terminal window and navigate to the `samples/portable-sdk/dotnet/AspNetWebApp` directory. - -1. Run the following command to build and run the sample: - - ```bash - dotnet run - ``` - -You should see output similar to the following: - -```plaintext -Building... -info: Microsoft.DurableTask[1] - Durable Task gRPC worker starting. -info: Microsoft.Hosting.Lifetime[14] - Now listening on: http://localhost:5008 -info: Microsoft.Hosting.Lifetime[0] - Application started. Press Ctrl+C to shut down. -info: Microsoft.Hosting.Lifetime[0] - Hosting environment: Development -info: Microsoft.Hosting.Lifetime[0] - Content root path: D:\projects\Azure-Functions-Durable-Task-Scheduler-Private-Preview\samples\portable-sdk\dotnet\AspNetWebApp -info: Microsoft.DurableTask[4] - Sidecar work-item streaming connection established. -``` - -## View orchestrations in the dashboard - -You can view the orchestrations in the Durable Task Scheduler dashboard by navigating to the namespace-specific dashboard URL in your browser. - -Use the following PowerShell command to get the dashboard URL: - -```powershell -$baseUrl = az durabletask namespace show ` - -g my-resource-group ` - -n my-namespace ` - --query "properties.dashboardUrl" ` - -o tsv -$dashboardUrl = "$baseUrl/taskHubs/portable-dotnet" -$dashboardUrl -``` - -The URL should look something like the following: - -```plaintext -https://my-namespace-atdngmgxfsh0-db.northcentralus.durabletask.io/taskHubs/portable-dotnet -``` - -Once logged in, you should see the orchestrations that were created by the sample app. Below is an example of what the dashboard might look like (note that some of the details will be different than the screenshot): - -![Durable Task Scheduler dashboard](/media/images/dtfx-sample-dashboard.png) - - -## Optional: Deploy to Azure Container Apps -1. Create an container app following the instructions in the [Azure Container App documentation](https://learn.microsoft.com/azure/container-apps/get-started?tabs=bash). -2. During step 1, specify the deployed container app code folder at samples\portable-sdk\dotnet\AspNetWebApp -3. Follow the instructions to create a user managed identity and assign the `Durable Task Data Contributor` role then attach it to the container app you created in step 1 at [Azure-Functions-Durable-Task-Scheduler-Private-Preview](..\..\..\..\docs\configure-existing-app.md#run-the-app-on-azure-net). Please skip section "Add required environment variables to app" since these environment variables are not required for deploying to container app. -4. Call the container app endpoint at `http://sampleapi-.azurecontainerapps.io/api/orchestrators/HelloCities`, Sample curl command: - - ```bash - curl -X POST "https://sampleapi-.azurecontainerapps.io/api/orchestrators/HelloCities" - ``` -5. You should see the orchestration created in the Durable Task Scheduler dashboard. diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs b/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs deleted file mode 100644 index d6049129..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/ScenariosController.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Mvc; -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; - -namespace AspNetWebApp; - -[Route("scenarios")] -[ApiController] -public partial class ScenariosController( - DurableTaskClient durableTaskClient, - ILogger logger) : ControllerBase -{ - readonly DurableTaskClient durableTaskClient = durableTaskClient; - readonly ILogger logger = logger; - - [HttpPost("hellocities")] - public async Task RunHelloCities([FromQuery] int? count, [FromQuery] string? prefix) - { - if (count is null || count < 1) - { - return this.BadRequest(new { error = "A 'count' query string parameter is required and it must contain a positive number." }); - } - - // Generate a semi-unique prefix for the instance IDs to simplify tracking - prefix ??= $"hellocities-{count}-"; - prefix += DateTime.UtcNow.ToString("yyyyMMdd-hhmmss"); - - this.logger.LogInformation("Scheduling {count} orchestrations with a prefix of '{prefix}'...", count, prefix); - - Stopwatch sw = Stopwatch.StartNew(); - await Enumerable.Range(0, count.Value).ParallelForEachAsync(1000, i => - { - string instanceId = $"{prefix}-{i:X16}"; - return this.durableTaskClient.ScheduleNewHelloCitiesInstanceAsync( - input: null!, - new StartOrchestrationOptions(instanceId)); - }); - - sw.Stop(); - this.logger.LogInformation( - "All {count} orchestrations were scheduled successfully in {time}ms!", - count, - sw.ElapsedMilliseconds); - return this.Ok(new - { - message = $"Scheduled {count} orchestrations prefixed with '{prefix}' in {sw.ElapsedMilliseconds}." - }); - } -} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs b/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs deleted file mode 100644 index 6ddabf39..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/Utils.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace AspNetWebApp; - -static class Utils -{ - public static async Task ParallelForEachAsync(this IEnumerable items, int maxConcurrency, Func action) - { - List tasks; - if (items is ICollection itemCollection) - { - tasks = new List(itemCollection.Count); - } - else - { - tasks = []; - } - - using SemaphoreSlim semaphore = new(maxConcurrency); - foreach (T item in items) - { - tasks.Add(InvokeThrottledAction(item, action, semaphore)); - } - - await Task.WhenAll(tasks); - } - - static async Task InvokeThrottledAction(T item, Func action, SemaphoreSlim semaphore) - { - await semaphore.WaitAsync(); - try - { - await action(item); - } - finally - { - semaphore.Release(); - } - } -} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json deleted file mode 100644 index a6e86ace..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json deleted file mode 100644 index 70b7d1d9..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.Production.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://{your-durable-task-endpoint}.durabletask.io", - "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "{your-task-hub-name}", - "CONTAINER_APP_UMI_CLIENT_ID": "{your-user-managed-identity-client-id}" -} diff --git a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json b/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json deleted file mode 100644 index 10f68b8c..00000000 --- a/samples/portable-sdk/dotnet/AspNetWebApp/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} From f4f03fcc865a16e4e17fc951eeb744ab8af13cc1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:31:06 -0800 Subject: [PATCH 35/58] fix tests --- ...rableTaskSchedulerConnectionStringTests.cs | 80 +++++++++- .../DurableTaskSchedulerExtensionsTests.cs | 137 +++++++++++++++++- .../Azure/DurableTaskSchedulerOptionsTests.cs | 105 +++++++++++++- 3 files changed, 304 insertions(+), 18 deletions(-) diff --git a/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs b/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs index aaca0a66..75372d6d 100644 --- a/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs +++ b/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs @@ -71,6 +71,77 @@ public void Constructor_WithAdditionallyAllowedTenants_ShouldParseTenantList() parsedConnectionString.AdditionallyAllowedTenants.Should().BeEquivalentTo(new[] { "tenant1", "tenant2", "tenant3" }); } + [Fact] + public void Constructor_WithMultipleAdditionallyAllowedTenants_ShouldParseCorrectly() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;TaskHub={ValidTaskHub};" + + "AdditionallyAllowedTenants=tenant1,tenant2,tenant3"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.AdditionallyAllowedTenants.Should().NotBeNull(); + parsedConnectionString.AdditionallyAllowedTenants!.Should().HaveCount(3); + parsedConnectionString.AdditionallyAllowedTenants.Should().Contain(new[] { "tenant1", "tenant2", "tenant3" }); + } + + [Fact] + public void Constructor_WithCaseInsensitivePropertyNames_ShouldParseCorrectly() + { + // Arrange + string connectionString = $"endpoint={ValidEndpoint};AUTHENTICATION=DefaultAzure;taskhub={ValidTaskHub};" + + $"clientid={ValidClientId};tenantid={ValidTenantId}"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.Endpoint.Should().Be(ValidEndpoint); + parsedConnectionString.Authentication.Should().Be("DefaultAzure"); + parsedConnectionString.TaskHubName.Should().Be(ValidTaskHub); + parsedConnectionString.ClientId.Should().Be(ValidClientId); + parsedConnectionString.TenantId.Should().Be(ValidTenantId); + } + + [Fact] + public void Constructor_WithInvalidConnectionStringFormat_ShouldThrowFormatException() + { + // Arrange + var connectionString = "This is not a valid=connection string format"; + + // Act & Assert + var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; + action.Should().Throw() + .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); + } + + [Fact] + public void Constructor_WithEmptyConnectionString_ShouldThrowArgumentNullException() + { + // Arrange + var connectionString = string.Empty; + + // Act & Assert + var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; + action.Should().Throw() + .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); + } + + [Fact] + public void Constructor_WithDuplicateKeys_ShouldUseLastValue() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub=hub1;TaskHub=hub2"; + + // Act + var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + + // Assert + parsedConnectionString.TaskHubName.Should().Be("hub2"); + } + [Fact] public void Constructor_WithMissingRequiredProperties_ShouldThrowArgumentNullException() { @@ -79,8 +150,8 @@ public void Constructor_WithMissingRequiredProperties_ShouldThrowArgumentNullExc // Act & Assert var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; - var exception = action.Should().Throw().Which; - exception.Message.Should().Contain("'Endpoint' property"); + action.Should().Throw() + .WithMessage("*'Endpoint' property*"); } [Fact] @@ -98,12 +169,11 @@ public void Constructor_WithInvalidConnectionString_ShouldThrowArgumentException [Theory] [InlineData("")] [InlineData(null)] - public void Constructor_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string? connectionString) + public void Constructor_WithNullOrEmptyConnectionString_ShouldThrowArgumentNullException(string? connectionString) { // Act & Assert var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString!).Endpoint; - action.Should().Throw() - .WithMessage("*'Endpoint' property*"); + action.Should().Throw(); } [Fact] diff --git a/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs b/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs index 518b53c1..4bd73bfa 100644 --- a/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs +++ b/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; +using System.ComponentModel.DataAnnotations; using Xunit; namespace Microsoft.DurableTask.Extensions.Azure.Tests; @@ -117,8 +118,8 @@ public void UseDurableTaskScheduler_WithOptions_ShouldApplyConfiguration() } [Theory] - [InlineData(null, ValidTaskHub)] - [InlineData(ValidEndpoint, null)] + [InlineData(null, "testhub")] + [InlineData("myaccount.westus3.durabletask.io", null)] public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) { // Arrange @@ -127,9 +128,21 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx mockBuilder.Setup(b => b.Services).Returns(services); var credential = new DefaultAzureCredential(); - // Act & Assert + // Act var action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); - action.Should().Throw(); + + // Assert + action.Should().NotThrow(); // The validation happens when building the service provider + + if (endpoint == null || taskHub == null) + { + var provider = services.BuildServiceProvider(); + var ex = Assert.Throws(() => + { + var options = provider.GetRequiredService>().Value; + }); + Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); + } } [Fact] @@ -153,11 +166,14 @@ public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgum var services = new ServiceCollection(); var mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - string invalidConnectionString = "This is not a valid connection string"; + var connectionString = "This is not a valid=connection string format"; - // Act & Assert - var action = () => mockBuilder.Object.UseDurableTaskScheduler(invalidConnectionString); - action.Should().Throw(); + // Act + var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + + // Assert + action.Should().Throw() + .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); } [Theory] @@ -174,4 +190,109 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); action.Should().Throw(); } + + [Fact] + public void UseDurableTaskScheduler_Worker_WithValidationFailure_ShouldThrowValidationException() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(string.Empty, ValidTaskHub, credential); + + // Assert + var ex = Assert.Throws(() => + { + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().Value; + }); + + Assert.Contains("EndpointAddress", ex.Message); + } + + [Fact] + public void UseDurableTaskScheduler_Client_WithNamedOptions_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + mockBuilder.Setup(b => b.Name).Returns("CustomName"); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); + + // Assert + var provider = services.BuildServiceProvider(); + var optionsMonitor = provider.GetService>(); + optionsMonitor.Should().NotBeNull(); + var options = optionsMonitor!.Get("CustomName"); + options.Should().NotBeNull(); + options.EndpointAddress.Should().Be(ValidEndpoint); // The https:// prefix is added by CreateChannel, not in the extension method + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void ConfigureGrpcChannel_ShouldConfigureWorkerAndClientOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddOptions() + .Configure(options => + { + options.EndpointAddress = $"https://{ValidEndpoint}"; + options.TaskHubName = ValidTaskHub; + options.Credential = new DefaultAzureCredential(); + }); + + var provider = services.BuildServiceProvider(); + var schedulerOptions = provider.GetService>(); + schedulerOptions.Should().NotBeNull("SchedulerOptions should be registered"); + var configureChannel = new DurableTaskSchedulerExtensions.ConfigureGrpcChannel(schedulerOptions!); + + var workerOptions = new GrpcDurableTaskWorkerOptions(); + var clientOptions = new GrpcDurableTaskClientOptions(); + + // Act + configureChannel.Configure(Options.DefaultName, workerOptions); + configureChannel.Configure(Options.DefaultName, clientOptions); + + // Assert + workerOptions.Channel.Should().NotBeNull(); + clientOptions.Channel.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_WithCustomConfiguration_ShouldApplyConfiguration() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + string customWorkerId = "custom-worker"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler( + ValidEndpoint, + ValidTaskHub, + credential, + options => + { + options.WorkerId = customWorkerId; + options.AllowInsecureCredentials = true; + }); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + options.Value.WorkerId.Should().Be(customWorkerId); + options.Value.AllowInsecureCredentials.Should().BeTrue(); + } } diff --git a/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs b/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs index b1c903c2..7dc0c591 100644 --- a/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs +++ b/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs @@ -23,7 +23,7 @@ public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); // Assert - options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.EndpointAddress.Should().Be(ValidEndpoint); options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeOfType(); } @@ -39,7 +39,7 @@ public void FromConnectionString_WithManagedIdentity_ShouldCreateValidInstance() var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); // Assert - options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.EndpointAddress.Should().Be(ValidEndpoint); options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeOfType(); } @@ -56,7 +56,7 @@ public void FromConnectionString_WithWorkloadIdentity_ShouldCreateValidInstance( var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); // Assert - options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.EndpointAddress.Should().Be(ValidEndpoint); options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeOfType(); } @@ -74,7 +74,7 @@ public void FromConnectionString_WithValidAuthTypes_ShouldCreateValidInstance(st var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); // Assert - options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.EndpointAddress.Should().Be(ValidEndpoint); options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().NotBeNull(); } @@ -112,8 +112,103 @@ public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); // Assert - options.EndpointAddress.Should().Be($"https://{ValidEndpoint}"); + options.EndpointAddress.Should().Be(ValidEndpoint); options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeNull(); } + + [Fact] + public void DefaultProperties_ShouldHaveExpectedValues() + { + // Arrange & Act + var options = new DurableTaskSchedulerOptions(); + + // Assert + options.ResourceId.Should().Be("https://durabletask.io"); + options.WorkerId.Should().NotBeNullOrEmpty(); + options.WorkerId.Should().Contain(Environment.MachineName); + options.WorkerId.Should().Contain(Environment.ProcessId.ToString()); + options.AllowInsecureCredentials.Should().BeFalse(); + } + + [Fact] + public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() + { + // Arrange + var options = new DurableTaskSchedulerOptions + { + EndpointAddress = $"https://{ValidEndpoint}", + TaskHubName = ValidTaskHub, + Credential = new DefaultAzureCredential() + }; + + // Act + var channel = options.CreateChannel(); + + // Assert + channel.Should().NotBeNull(); + } + + [Fact] + public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() + { + // Arrange + var options = new DurableTaskSchedulerOptions + { + EndpointAddress = $"http://{ValidEndpoint}", + TaskHubName = ValidTaskHub, + AllowInsecureCredentials = true + }; + + // Act + var channel = options.CreateChannel(); + + // Assert + channel.Should().NotBeNull(); + } + + [Fact] + public void FromConnectionString_WithInvalidEndpoint_ShouldThrowArgumentException() + { + // Arrange + var connectionString = "Endpoint=not a valid endpoint;Authentication=DefaultAzure;TaskHub=testhub;"; + + // Act & Assert + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var action = () => options.CreateChannel(); + action.Should().Throw() + .WithMessage("Invalid URI: The hostname could not be parsed."); + } + + [Fact] + public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be(ValidEndpoint); + } + + [Fact] + public void CreateChannel_ShouldAddHttpsPrefix() + { + // Arrange + var options = new DurableTaskSchedulerOptions + { + EndpointAddress = ValidEndpoint, + TaskHubName = ValidTaskHub, + Credential = new DefaultAzureCredential() + }; + + // Act + var channel = options.CreateChannel(); + + // Assert + channel.Should().NotBeNull(); + // Note: We can't directly test the endpoint in the channel as it's not exposed + } } From 59c5e9c1e9f4d2a46c7b31d617607a925d02cfc2 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:42:04 -0800 Subject: [PATCH 36/58] test accesstokencache --- .../Extensions/Azure/AccessTokenCacheTests.cs | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 test/Extensions/Azure/AccessTokenCacheTests.cs diff --git a/test/Extensions/Azure/AccessTokenCacheTests.cs b/test/Extensions/Azure/AccessTokenCacheTests.cs new file mode 100644 index 00000000..8c766010 --- /dev/null +++ b/test/Extensions/Azure/AccessTokenCacheTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using FluentAssertions; +using Microsoft.DurableTask.Extensions.Azure; +using Moq; +using Xunit; + +namespace Microsoft.DurableTask.Tests.Extensions.Azure; + +public class AccessTokenCacheTests +{ + private readonly Mock mockCredential; + private readonly TokenRequestContext tokenRequestContext; + private readonly TimeSpan margin; + private readonly CancellationToken cancellationToken; + + public AccessTokenCacheTests() + { + mockCredential = new Mock(); + tokenRequestContext = new TokenRequestContext(new[] { "https://durabletask.azure.com/.default" }); + margin = TimeSpan.FromMinutes(5); + cancellationToken = CancellationToken.None; + } + + [Fact] + public async Task GetTokenAsync_WhenCalled_ShouldReturnToken() + { + // Arrange + var expectedToken = new AccessToken("test-token", DateTimeOffset.UtcNow.AddHours(1)); + mockCredential.Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedToken); + var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, margin); + + // Act + var token = await cache.GetTokenAsync(cancellationToken); + + // Assert + token.Should().Be(expectedToken); + mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task GetTokenAsync_WhenTokenExpired_ShouldRequestNewToken() + { + // Arrange + var expiredToken = new AccessToken("expired-token", DateTimeOffset.UtcNow.AddMinutes(-5)); + var newToken = new AccessToken("new-token", DateTimeOffset.UtcNow.AddHours(1)); + var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, margin); + + mockCredential.SetupSequence(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expiredToken) + .ReturnsAsync(newToken); + + // Act + var firstToken = await cache.GetTokenAsync(cancellationToken); + var secondToken = await cache.GetTokenAsync(cancellationToken); + + // Assert + firstToken.Should().Be(expiredToken); + secondToken.Should().Be(newToken); + mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task GetTokenAsync_WhenTokenValid_ShouldReturnCachedToken() + { + // Arrange + var validToken = new AccessToken("valid-token", DateTimeOffset.UtcNow.AddHours(1)); + mockCredential.Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validToken); + var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, margin); + + // Act + var firstToken = await cache.GetTokenAsync(cancellationToken); + var secondToken = await cache.GetTokenAsync(cancellationToken); + + // Assert + firstToken.Should().Be(validToken); + secondToken.Should().Be(validToken); + mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task Constructor_WithNullCredential_ShouldThrowNullReferenceException() + { + // Arrange + var cache = new AccessTokenCache(null!, tokenRequestContext, margin); + + // Act & Assert + // TODO: The constructor should validate its parameters and throw ArgumentNullException, + // but currently it allows null parameters and throws NullReferenceException when used. + var action = () => cache.GetTokenAsync(cancellationToken); + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task GetTokenAsync_WhenTokenNearExpiry_ShouldRequestNewToken() + { + // Arrange + var expiryTime = DateTimeOffset.UtcNow.AddMinutes(10); + var nearExpiryToken = new AccessToken("near-expiry-token", expiryTime); + var newToken = new AccessToken("new-token", expiryTime.AddHours(1)); + var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, TimeSpan.FromMinutes(15)); + + mockCredential.SetupSequence(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(nearExpiryToken) + .ReturnsAsync(newToken); + + // Act + var firstToken = await cache.GetTokenAsync(cancellationToken); + var secondToken = await cache.GetTokenAsync(cancellationToken); + + // Assert + firstToken.Should().Be(nearExpiryToken); + secondToken.Should().Be(newToken); + mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } +} From 3dfbb7250b879d7b320fb6e246a1fe63378b5163 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 02:59:20 -0800 Subject: [PATCH 37/58] split --- Microsoft.DurableTask.sln | 22 ++- .../AzureManaged/Client.AzureManaged.csproj} | 8 +- .../DurableTaskSchedulerClientExtensions.cs} | 83 +------- .../AzureManaged}/AccessTokenCache.cs | 4 +- .../DurableTaskSchedulerConnectionString.cs | 3 +- .../DurableTaskSchedulerOptions.cs | 30 +-- .../AzureManaged/Shared.AzureManaged.csproj | 20 ++ .../DurableTaskSchedulerWorkerExtensions.cs | 94 +++++++++ .../AzureManaged/Worker.AzureManaged.csproj | 21 ++ .../AzureManaged/Client.AzureManaged.csproj | 26 +++ ...rableTaskSchedulerClientExtensionsTests.cs | 182 ++++++++++++++++++ .../AzureManaged}/AccessTokenCacheTests.cs | 3 +- ...rableTaskSchedulerConnectionStringTests.cs | 2 +- .../DurableTaskSchedulerOptionsTests.cs | 2 +- .../AzureManaged/Shared.AzureManaged.csproj} | 7 +- ...ableTaskSchedulerWorkerExtensionsTests.cs} | 142 ++------------ .../AzureManaged/Worker.AzureManaged.csproj | 26 +++ 17 files changed, 424 insertions(+), 251 deletions(-) rename src/{Extensions/Azure/Azure.csproj => Client/AzureManaged/Client.AzureManaged.csproj} (76%) rename src/{Extensions/Azure/DurableTaskSchedulerExtensions.cs => Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs} (51%) rename src/{Extensions/Azure => Shared/AzureManaged}/AccessTokenCache.cs (94%) rename src/{Extensions/Azure => Shared/AzureManaged}/DurableTaskSchedulerConnectionString.cs (98%) rename src/{Extensions/Azure => Shared/AzureManaged}/DurableTaskSchedulerOptions.cs (96%) create mode 100644 src/Shared/AzureManaged/Shared.AzureManaged.csproj create mode 100644 src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs create mode 100644 src/Worker/AzureManaged/Worker.AzureManaged.csproj create mode 100644 test/Client/AzureManaged/Client.AzureManaged.csproj create mode 100644 test/Client/AzureManaged/DurableTaskSchedulerClientExtensionsTests.cs rename test/{Extensions/Azure => Shared/AzureManaged}/AccessTokenCacheTests.cs (97%) rename test/{Extensions/Azure => Shared/AzureManaged}/DurableTaskSchedulerConnectionStringTests.cs (99%) rename test/{Extensions/Azure => Shared/AzureManaged}/DurableTaskSchedulerOptionsTests.cs (99%) rename test/{Extensions/Azure/Azure.Tests.csproj => Shared/AzureManaged/Shared.AzureManaged.csproj} (69%) rename test/{Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs => Worker/AzureManaged/DurableTaskSchedulerWorkerExtensionsTests.cs} (54%) create mode 100644 test/Worker/AzureManaged/Worker.AzureManaged.csproj diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 4e5a4451..45dae8aa 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -71,11 +71,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Ana EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp.Tests", "samples\AzureFunctionsUnitTests\AzureFunctionsApp.Tests.csproj", "{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{5227C712-2355-403F-90D6-51D0BCAE4D38}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure", "src\Extensions\Azure\Azure.csproj", "{662BF73D-A4DD-4910-8625-7C12F1ACDBEC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged", "src\Shared\AzureManaged\Shared.AzureManaged.csproj", "{82D06CA7-90B3-4791-9CAB-222F1AA113D5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged", "test\Shared\AzureManaged\Shared.AzureManaged.csproj", "{F855ACBF-2A6A-4A43-A9E8-51B8599A539E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -191,14 +191,18 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.Build.0 = Release|Any CPU - {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {662BF73D-A4DD-4910-8625-7C12F1ACDBEC}.Release|Any CPU.Build.0 = Release|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.Build.0 = Release|Any CPU + {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Release|Any CPU.Build.0 = Release|Any CPU + {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -234,9 +238,9 @@ Global {998E9D97-BD36-4A9D-81FC-5DAC1CE40083} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {541FCCCE-1059-4691-B027-F761CD80DE92} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} - {5227C712-2355-403F-90D6-51D0BCAE4D38} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} - {662BF73D-A4DD-4910-8625-7C12F1ACDBEC} = {5227C712-2355-403F-90D6-51D0BCAE4D38} {DBB5DB4E-A1B0-4C86-A233-213789C46929} = {E5637F81-2FB9-4CD7-900D-455363B142A7} + {82D06CA7-90B3-4791-9CAB-222F1AA113D5} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {F855ACBF-2A6A-4A43-A9E8-51B8599A539E} = {E5637F81-2FB9-4CD7-900D-455363B142A7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/src/Extensions/Azure/Azure.csproj b/src/Client/AzureManaged/Client.AzureManaged.csproj similarity index 76% rename from src/Extensions/Azure/Azure.csproj rename to src/Client/AzureManaged/Client.AzureManaged.csproj index d82dd02f..6d0fc498 100644 --- a/src/Extensions/Azure/Azure.csproj +++ b/src/Client/AzureManaged/Client.AzureManaged.csproj @@ -2,7 +2,7 @@ net6.0 - Azure extensions for the Durable Task Framework. + Azure Managed extensions for the Durable Task Framework client. true @@ -14,15 +14,13 @@ - - + - + - diff --git a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs similarity index 51% rename from src/Extensions/Azure/DurableTaskSchedulerExtensions.cs rename to src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index 6692a6a3..a4e9adc4 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -1,82 +1,20 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Azure.Core; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Grpc; -using Microsoft.DurableTask.Worker; -using Microsoft.DurableTask.Worker.Grpc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -namespace Microsoft.DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Client.AzureManaged; /// -/// Extension methods for configuring Durable Task workers and clients to use the Azure Durable Task Scheduler service. +/// Extension methods for configuring Durable Task clients to use the Azure Durable Task Scheduler service. /// -public static class DurableTaskSchedulerExtensions +public static class DurableTaskSchedulerClientExtensions { - /// - /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. - /// - /// The Durable Task worker builder to configure. - /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". - /// The name of the task hub resource associated with the Durable Task Scheduler resource. - /// The credential used to authenticate with the Durable Task Scheduler task hub resource. - /// Optional callback to dynamically configure DurableTaskSchedulerOptions. - public static void UseDurableTaskScheduler( - this IDurableTaskWorkerBuilder builder, - string endpointAddress, - string taskHubName, - TokenCredential credential, - Action? configure = null) - { - builder.Services.AddOptions(builder.Name) - .Configure(options => - { - options.EndpointAddress = endpointAddress; - options.TaskHubName = taskHubName; - options.Credential = credential; - }) - .Configure(configure ?? (_ => { })) - .ValidateDataAnnotations() - .ValidateOnStart(); - - builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); - builder.UseGrpc(_ => { }); - } - - /// - /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. - /// - /// The Durable Task worker builder to configure. - /// The connection string used to connect to the Durable Task Scheduler service. - /// Optional callback to dynamically configure DurableTaskSchedulerOptions. - public static void UseDurableTaskScheduler( - this IDurableTaskWorkerBuilder builder, - string connectionString, - Action? configure = null) - { - var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); - - builder.Services.AddOptions(builder.Name) - .Configure(options => - { - options.EndpointAddress = connectionOptions.EndpointAddress; - options.TaskHubName = connectionOptions.TaskHubName; - options.Credential = connectionOptions.Credential; - }) - .Configure(configure ?? (_ => { })) - .ValidateDataAnnotations() - .ValidateOnStart(); - - builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); - builder.UseGrpc(_ => { }); - } - /// /// Configures Durable Task client to use the Azure Durable Task Scheduler service. /// @@ -138,28 +76,19 @@ public static void UseDurableTaskScheduler( } /// - /// Internal configuration class that sets up gRPC channels for both worker and client options + /// Internal configuration class that sets up gRPC channels for client options /// using the provided Durable Task Scheduler options. /// /// Monitor for accessing the current scheduler options configuration. internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : - IConfigureNamedOptions, IConfigureNamedOptions { - public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); - public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); - public void Configure(string? name, GrpcDurableTaskWorkerOptions options) - { - DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); - options.Channel = source.CreateChannel(); - } - public void Configure(string? name, GrpcDurableTaskClientOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } } -} +} diff --git a/src/Extensions/Azure/AccessTokenCache.cs b/src/Shared/AzureManaged/AccessTokenCache.cs similarity index 94% rename from src/Extensions/Azure/AccessTokenCache.cs rename to src/Shared/AzureManaged/AccessTokenCache.cs index 0a273170..bb4305c6 100644 --- a/src/Extensions/Azure/AccessTokenCache.cs +++ b/src/Shared/AzureManaged/AccessTokenCache.cs @@ -3,12 +3,12 @@ using Azure.Core; -namespace Microsoft.DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Shared.AzureManaged; /// /// Caches and manages refresh for Azure access tokens. /// -internal sealed class AccessTokenCache +sealed class AccessTokenCache { readonly TokenCredential credential; readonly TokenRequestContext context; diff --git a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs similarity index 98% rename from src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs rename to src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs index 9002f77b..c1077405 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerConnectionString.cs +++ b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - using System.Data.Common; -namespace Microsoft.DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Shared.AzureManaged; /// /// Represents the constituent parts of a connection string for a Durable Task Scheduler service. diff --git a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs b/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs similarity index 96% rename from src/Extensions/Azure/DurableTaskSchedulerOptions.cs rename to src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs index 25f994dd..9878e624 100644 --- a/src/Extensions/Azure/DurableTaskSchedulerOptions.cs +++ b/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs @@ -1,13 +1,11 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. using System.ComponentModel.DataAnnotations; using System.Globalization; using Azure.Core; using Azure.Identity; -using Grpc.Core; -namespace Microsoft.DurableTask.Extensions.Azure; +namespace Microsoft.DurableTask.Shared.AzureManaged; /// /// Options for configuring the Durable Task Scheduler. @@ -53,6 +51,7 @@ public class DurableTaskSchedulerOptions /// /// Creates a new instance of from a connection string. /// + /// public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); @@ -62,12 +61,10 @@ internal GrpcChannel CreateChannel() { Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); - string taskHubName = this.TaskHubName; string endpoint = !this.EndpointAddress.Contains("://") ? $"https://{this.EndpointAddress}" : this.EndpointAddress; - AccessTokenCache? cache = this.Credential is not null ? new AccessTokenCache( @@ -75,13 +72,11 @@ this.Credential is not null new TokenRequestContext(new[] { $"{this.ResourceId}/.default" }), TimeSpan.FromMinutes(5)) : null; - CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( async (context, metadata) => { metadata.Add("taskhub", taskHubName); metadata.Add("workerid", this.WorkerId); - if (cache == null) { return; @@ -95,7 +90,6 @@ this.Credential is not null ChannelCredentials channelCreds = endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? ChannelCredentials.SecureSsl : ChannelCredentials.Insecure; - return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions { Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), @@ -106,6 +100,7 @@ this.Credential is not null /// /// Creates a new instance of from a parsed connection string. /// + /// public static DurableTaskSchedulerOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) { @@ -113,9 +108,8 @@ public static DurableTaskSchedulerOptions FromConnectionString( { EndpointAddress = connectionString.Endpoint, TaskHubName = connectionString.TaskHubName, - Credential = GetCredentialFromConnectionString(connectionString) + Credential = GetCredentialFromConnectionString(connectionString), }; - return options; } @@ -128,20 +122,20 @@ public static DurableTaskSchedulerOptions FromConnectionString( { case "defaultazure": return new DefaultAzureCredential(); - case "managedidentity": return new ManagedIdentityCredential(connectionString.ClientId); - case "workloadidentity": var opts = new WorkloadIdentityCredentialOptions(); if (!string.IsNullOrEmpty(connectionString.ClientId)) { opts.ClientId = connectionString.ClientId; } + if (!string.IsNullOrEmpty(connectionString.TenantId)) { opts.TenantId = connectionString.TenantId; } + if (connectionString.AdditionallyAllowedTenants is not null) { foreach (string tenant in connectionString.AdditionallyAllowedTenants) @@ -149,24 +143,20 @@ public static DurableTaskSchedulerOptions FromConnectionString( opts.AdditionallyAllowedTenants.Add(tenant); } } - return new WorkloadIdentityCredential(opts); + return new WorkloadIdentityCredential(opts); case "environment": return new EnvironmentCredential(); - case "azurecli": return new AzureCliCredential(); - case "azurepowershell": return new AzurePowerShellCredential(); - case "none": return null; - default: throw new ArgumentException( $"The connection string contains an unsupported authentication type '{authType}'.", nameof(connectionString)); } } -} \ No newline at end of file +} diff --git a/src/Shared/AzureManaged/Shared.AzureManaged.csproj b/src/Shared/AzureManaged/Shared.AzureManaged.csproj new file mode 100644 index 00000000..e90c2dc4 --- /dev/null +++ b/src/Shared/AzureManaged/Shared.AzureManaged.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + Shared components for Azure Managed extensions for the Durable Task Framework. + true + + + + + + + + + + + + + + diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs new file mode 100644 index 00000000..fc034e87 --- /dev/null +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.Grpc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.DurableTask.Worker.AzureManaged; + +/// +/// Extension methods for configuring Durable Task workers to use the Azure Durable Task Scheduler service. +/// +public static class DurableTaskSchedulerWorkerExtensions +{ + /// + /// Configures Durable Task worker to use the Azure Durable Task Scheduler service. + /// + /// The Durable Task worker builder to configure. + /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// The name of the task hub resource associated with the Durable Task Scheduler resource. + /// The credential used to authenticate with the Durable Task Scheduler task hub resource. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. + public static void UseDurableTaskScheduler( + this IDurableTaskWorkerBuilder builder, + string endpointAddress, + string taskHubName, + TokenCredential credential, + Action? configure = null) + { + builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); + } + + /// + /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using a connection string. + /// + /// The Durable Task worker builder to configure. + /// The connection string used to connect to the Durable Task Scheduler service. + /// Optional callback to dynamically configure DurableTaskSchedulerOptions. + public static void UseDurableTaskScheduler( + this IDurableTaskWorkerBuilder builder, + string connectionString, + Action? configure = null) + { + var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + + builder.Services.AddOptions(builder.Name) + .Configure(options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }) + .Configure(configure ?? (_ => { })) + .ValidateDataAnnotations() + .ValidateOnStart(); + + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); + builder.UseGrpc(_ => { }); + } + + /// + /// Internal configuration class that sets up gRPC channels for worker options + /// using the provided Durable Task Scheduler options. + /// + /// Monitor for accessing the current scheduler options configuration. + internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + IConfigureNamedOptions + { + public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); + + public void Configure(string? name, GrpcDurableTaskWorkerOptions options) + { + DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); + options.Channel = source.CreateChannel(); + } + } +} diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj new file mode 100644 index 00000000..ffc8e101 --- /dev/null +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + Azure Managed extensions for the Durable Task Framework worker. + true + + + + + + + + + + + + + + + diff --git a/test/Client/AzureManaged/Client.AzureManaged.csproj b/test/Client/AzureManaged/Client.AzureManaged.csproj new file mode 100644 index 00000000..0cd156c5 --- /dev/null +++ b/test/Client/AzureManaged/Client.AzureManaged.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + diff --git a/test/Client/AzureManaged/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged/DurableTaskSchedulerClientExtensionsTests.cs new file mode 100644 index 00000000..5a0099dd --- /dev/null +++ b/test/Client/AzureManaged/DurableTaskSchedulerClientExtensionsTests.cs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Identity; +using FluentAssertions; +using Grpc.Net.Client; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Client.Grpc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using System.ComponentModel.DataAnnotations; +using Xunit; + +namespace Microsoft.DurableTask.Client.AzureManaged.Tests; + +public class DurableTaskSchedulerClientExtensionsTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + + [Fact] + public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Fact] + public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + mockBuilder.Object.UseDurableTaskScheduler(connectionString); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetService>(); + options.Should().NotBeNull(); + } + + [Theory] + [InlineData(null, "testhub")] + [InlineData("myaccount.westus3.durabletask.io", null)] + public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var credential = new DefaultAzureCredential(); + + // Act + var action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + + // Assert + action.Should().NotThrow(); // The validation happens when building the service provider + + if (endpoint == null || taskHub == null) + { + var provider = services.BuildServiceProvider(); + var ex = Assert.Throws(() => + { + var options = provider.GetRequiredService>().Value; + }); + Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); + } + } + + [Fact] + public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + TokenCredential? credential = null; + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); + action.Should().NotThrow(); + } + + [Fact] + public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgumentException() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + var connectionString = "This is not a valid=connection string format"; + + // Act + var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + + // Assert + action.Should().Throw() + .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string connectionString) + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + + // Act & Assert + var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + action.Should().Throw(); + } + + [Fact] + public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() + { + // Arrange + var services = new ServiceCollection(); + var mockBuilder = new Mock(); + mockBuilder.Setup(b => b.Services).Returns(services); + mockBuilder.Setup(b => b.Name).Returns("CustomName"); + var credential = new DefaultAzureCredential(); + + // Act + mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); + + // Assert + var provider = services.BuildServiceProvider(); + var optionsMonitor = provider.GetService>(); + optionsMonitor.Should().NotBeNull(); + var options = optionsMonitor!.Get("CustomName"); + options.Should().NotBeNull(); + options.EndpointAddress.Should().Be(ValidEndpoint); // The https:// prefix is added by CreateChannel, not in the extension method + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void ConfigureGrpcChannel_ShouldConfigureClientOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddOptions() + .Configure(options => + { + options.EndpointAddress = $"https://{ValidEndpoint}"; + options.TaskHubName = ValidTaskHub; + options.Credential = new DefaultAzureCredential(); + }); + + var provider = services.BuildServiceProvider(); + var schedulerOptions = provider.GetRequiredService>(); + var configureGrpcChannel = new DurableTaskSchedulerClientExtensions.ConfigureGrpcChannel(schedulerOptions); + + // Act + var clientOptions = new GrpcDurableTaskClientOptions(); + configureGrpcChannel.Configure(clientOptions); + + // Assert + clientOptions.Channel.Should().NotBeNull(); + clientOptions.Channel.Should().BeOfType(); + } +} diff --git a/test/Extensions/Azure/AccessTokenCacheTests.cs b/test/Shared/AzureManaged/AccessTokenCacheTests.cs similarity index 97% rename from test/Extensions/Azure/AccessTokenCacheTests.cs rename to test/Shared/AzureManaged/AccessTokenCacheTests.cs index 8c766010..525342b6 100644 --- a/test/Extensions/Azure/AccessTokenCacheTests.cs +++ b/test/Shared/AzureManaged/AccessTokenCacheTests.cs @@ -3,11 +3,10 @@ using System.Threading.Tasks; using Azure.Core; using FluentAssertions; -using Microsoft.DurableTask.Extensions.Azure; using Moq; using Xunit; -namespace Microsoft.DurableTask.Tests.Extensions.Azure; +namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; public class AccessTokenCacheTests { diff --git a/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs b/test/Shared/AzureManaged/DurableTaskSchedulerConnectionStringTests.cs similarity index 99% rename from test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs rename to test/Shared/AzureManaged/DurableTaskSchedulerConnectionStringTests.cs index 75372d6d..bf5631f5 100644 --- a/test/Extensions/Azure/DurableTaskSchedulerConnectionStringTests.cs +++ b/test/Shared/AzureManaged/DurableTaskSchedulerConnectionStringTests.cs @@ -5,7 +5,7 @@ using System.Data.Common; using Xunit; -namespace Microsoft.DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; public class DurableTaskSchedulerConnectionStringTests { diff --git a/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs b/test/Shared/AzureManaged/DurableTaskSchedulerOptionsTests.cs similarity index 99% rename from test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs rename to test/Shared/AzureManaged/DurableTaskSchedulerOptionsTests.cs index 7dc0c591..4c266cc1 100644 --- a/test/Extensions/Azure/DurableTaskSchedulerOptionsTests.cs +++ b/test/Shared/AzureManaged/DurableTaskSchedulerOptionsTests.cs @@ -6,7 +6,7 @@ using FluentAssertions; using Xunit; -namespace Microsoft.DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; public class DurableTaskSchedulerOptionsTests { diff --git a/test/Extensions/Azure/Azure.Tests.csproj b/test/Shared/AzureManaged/Shared.AzureManaged.csproj similarity index 69% rename from test/Extensions/Azure/Azure.Tests.csproj rename to test/Shared/AzureManaged/Shared.AzureManaged.csproj index 53146103..3eeeacb7 100644 --- a/test/Extensions/Azure/Azure.Tests.csproj +++ b/test/Shared/AzureManaged/Shared.AzureManaged.csproj @@ -8,11 +8,12 @@ + - - + + - + diff --git a/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs b/test/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensionsTests.cs similarity index 54% rename from test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs rename to test/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensionsTests.cs index 4bd73bfa..2dd901b9 100644 --- a/test/Extensions/Azure/DurableTaskSchedulerExtensionsTests.cs +++ b/test/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -5,8 +5,6 @@ using Azure.Identity; using FluentAssertions; using Grpc.Net.Client; -using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Client.Grpc; using Microsoft.DurableTask.Worker; using Microsoft.DurableTask.Worker.Grpc; using Microsoft.Extensions.DependencyInjection; @@ -15,15 +13,15 @@ using System.ComponentModel.DataAnnotations; using Xunit; -namespace Microsoft.DurableTask.Extensions.Azure.Tests; +namespace Microsoft.DurableTask.Worker.AzureManaged.Tests; -public class DurableTaskSchedulerExtensionsTests +public class DurableTaskSchedulerWorkerExtensionsTests { private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; private const string ValidTaskHub = "testhub"; [Fact] - public void UseDurableTaskScheduler_Worker_WithEndpointAndCredential_ShouldConfigureCorrectly() + public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCorrectly() { // Arrange var services = new ServiceCollection(); @@ -34,24 +32,6 @@ public void UseDurableTaskScheduler_Worker_WithEndpointAndCredential_ShouldConfi // Act mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); - // Assert - Verify that the options were registered - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); - options.Should().NotBeNull(); - } - - [Fact] - public void UseDurableTaskScheduler_Worker_WithConnectionString_ShouldConfigureCorrectly() - { - // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); - mockBuilder.Setup(b => b.Services).Returns(services); - string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; - - // Act - mockBuilder.Object.UseDurableTaskScheduler(connectionString); - // Assert var provider = services.BuildServiceProvider(); var options = provider.GetService>(); @@ -59,58 +39,17 @@ public void UseDurableTaskScheduler_Worker_WithConnectionString_ShouldConfigureC } [Fact] - public void UseDurableTaskScheduler_Client_WithEndpointAndCredential_ShouldConfigureCorrectly() + public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectly() { // Arrange var services = new ServiceCollection(); - var mockBuilder = new Mock(); - mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); - - // Act - mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); - - // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); - options.Should().NotBeNull(); - } - - [Fact] - public void UseDurableTaskScheduler_Client_WithConnectionString_ShouldConfigureCorrectly() - { - // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + var mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act mockBuilder.Object.UseDurableTaskScheduler(connectionString); - // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); - options.Should().NotBeNull(); - } - - [Fact] - public void UseDurableTaskScheduler_WithOptions_ShouldApplyConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); - mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); - string workerId = "customWorker"; - - // Act - mockBuilder.Object.UseDurableTaskScheduler( - ValidEndpoint, - ValidTaskHub, - credential, - options => options.WorkerId = workerId); - // Assert var provider = services.BuildServiceProvider(); var options = provider.GetService>(); @@ -192,34 +131,12 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA } [Fact] - public void UseDurableTaskScheduler_Worker_WithValidationFailure_ShouldThrowValidationException() + public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() { // Arrange var services = new ServiceCollection(); var mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); - - // Act - mockBuilder.Object.UseDurableTaskScheduler(string.Empty, ValidTaskHub, credential); - - // Assert - var ex = Assert.Throws(() => - { - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>().Value; - }); - - Assert.Contains("EndpointAddress", ex.Message); - } - - [Fact] - public void UseDurableTaskScheduler_Client_WithNamedOptions_ShouldConfigureCorrectly() - { - // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); - mockBuilder.Setup(b => b.Services).Returns(services); mockBuilder.Setup(b => b.Name).Returns("CustomName"); var credential = new DefaultAzureCredential(); @@ -238,7 +155,7 @@ public void UseDurableTaskScheduler_Client_WithNamedOptions_ShouldConfigureCorre } [Fact] - public void ConfigureGrpcChannel_ShouldConfigureWorkerAndClientOptions() + public void ConfigureGrpcChannel_ShouldConfigureWorkerOptions() { // Arrange var services = new ServiceCollection(); @@ -251,48 +168,15 @@ public void ConfigureGrpcChannel_ShouldConfigureWorkerAndClientOptions() }); var provider = services.BuildServiceProvider(); - var schedulerOptions = provider.GetService>(); - schedulerOptions.Should().NotBeNull("SchedulerOptions should be registered"); - var configureChannel = new DurableTaskSchedulerExtensions.ConfigureGrpcChannel(schedulerOptions!); - - var workerOptions = new GrpcDurableTaskWorkerOptions(); - var clientOptions = new GrpcDurableTaskClientOptions(); + var schedulerOptions = provider.GetRequiredService>(); + var configureGrpcChannel = new DurableTaskSchedulerWorkerExtensions.ConfigureGrpcChannel(schedulerOptions); // Act - configureChannel.Configure(Options.DefaultName, workerOptions); - configureChannel.Configure(Options.DefaultName, clientOptions); + var workerOptions = new GrpcDurableTaskWorkerOptions(); + configureGrpcChannel.Configure(workerOptions); // Assert workerOptions.Channel.Should().NotBeNull(); - clientOptions.Channel.Should().NotBeNull(); - } - - [Fact] - public void UseDurableTaskScheduler_WithCustomConfiguration_ShouldApplyConfiguration() - { - // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); - mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); - string customWorkerId = "custom-worker"; - - // Act - mockBuilder.Object.UseDurableTaskScheduler( - ValidEndpoint, - ValidTaskHub, - credential, - options => - { - options.WorkerId = customWorkerId; - options.AllowInsecureCredentials = true; - }); - - // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); - options.Should().NotBeNull(); - options.Value.WorkerId.Should().Be(customWorkerId); - options.Value.AllowInsecureCredentials.Should().BeTrue(); + workerOptions.Channel.Should().BeOfType(); } -} +} diff --git a/test/Worker/AzureManaged/Worker.AzureManaged.csproj b/test/Worker/AzureManaged/Worker.AzureManaged.csproj new file mode 100644 index 00000000..3b38e5d1 --- /dev/null +++ b/test/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + From 5083703d48b7d26a81369a7376b6b24b7d66ce87 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:02:40 -0800 Subject: [PATCH 38/58] fix shared --- src/Shared/AzureManaged/AccessTokenCache.cs | 2 +- .../DurableTaskSchedulerConnectionString.cs | 2 +- .../DurableTaskSchedulerOptions.cs | 38 ++++++++++--------- .../AzureManaged/Shared.AzureManaged.csproj | 5 +++ 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/Shared/AzureManaged/AccessTokenCache.cs b/src/Shared/AzureManaged/AccessTokenCache.cs index bb4305c6..54091f3c 100644 --- a/src/Shared/AzureManaged/AccessTokenCache.cs +++ b/src/Shared/AzureManaged/AccessTokenCache.cs @@ -3,7 +3,7 @@ using Azure.Core; -namespace Microsoft.DurableTask.Shared.AzureManaged; +namespace Microsoft.DurableTask; /// /// Caches and manages refresh for Azure access tokens. diff --git a/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs index c1077405..7d3d18b4 100644 --- a/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs +++ b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using System.Data.Common; -namespace Microsoft.DurableTask.Shared.AzureManaged; +namespace Microsoft.DurableTask; /// /// Represents the constituent parts of a connection string for a Durable Task Scheduler service. diff --git a/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs b/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs index 9878e624..20a34093 100644 --- a/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs +++ b/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs @@ -5,7 +5,7 @@ using Azure.Core; using Azure.Identity; -namespace Microsoft.DurableTask.Shared.AzureManaged; +namespace Microsoft.DurableTask; /// /// Options for configuring the Durable Task Scheduler. @@ -51,12 +51,30 @@ public class DurableTaskSchedulerOptions /// /// Creates a new instance of from a connection string. /// - /// + /// The connection string to parse. + /// A new instance of . public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } + /// + /// Creates a new instance of from a parsed connection string. + /// + /// The connection string to parse. + /// A new instance of . + public static DurableTaskSchedulerOptions FromConnectionString( + DurableTaskSchedulerConnectionString connectionString) => new() + { + EndpointAddress = connectionString.Endpoint, + TaskHubName = connectionString.TaskHubName, + Credential = GetCredentialFromConnectionString(connectionString), + }; + + /// + /// Creates a gRPC channel for communicating with the Durable Task Scheduler service. + /// + /// A configured instance that can be used to make gRPC calls. internal GrpcChannel CreateChannel() { Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); @@ -97,22 +115,6 @@ this.Credential is not null }); } - /// - /// Creates a new instance of from a parsed connection string. - /// - /// - public static DurableTaskSchedulerOptions FromConnectionString( - DurableTaskSchedulerConnectionString connectionString) - { - var options = new DurableTaskSchedulerOptions - { - EndpointAddress = connectionString.Endpoint, - TaskHubName = connectionString.TaskHubName, - Credential = GetCredentialFromConnectionString(connectionString), - }; - return options; - } - static TokenCredential? GetCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) { string authType = connectionString.Authentication; diff --git a/src/Shared/AzureManaged/Shared.AzureManaged.csproj b/src/Shared/AzureManaged/Shared.AzureManaged.csproj index e90c2dc4..2ee55cde 100644 --- a/src/Shared/AzureManaged/Shared.AzureManaged.csproj +++ b/src/Shared/AzureManaged/Shared.AzureManaged.csproj @@ -17,4 +17,9 @@ + + + + + From a50500477cef532536e3f3ef1c0f3bf7d9163dd5 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:10:11 -0800 Subject: [PATCH 39/58] save --- .../AzureManaged/DurableTaskSchedulerClientExtensions.cs | 3 +-- src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index a4e9adc4..58ac05b7 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -2,13 +2,12 @@ // Licensed under the MIT License. using Azure.Core; -using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Grpc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -namespace Microsoft.DurableTask.Client.AzureManaged; +namespace Microsoft.DurableTask.Client; /// /// Extension methods for configuring Durable Task clients to use the Azure Durable Task Scheduler service. diff --git a/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs b/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs index 20a34093..5a9ce98b 100644 --- a/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs +++ b/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs @@ -75,7 +75,7 @@ public static DurableTaskSchedulerOptions FromConnectionString( /// Creates a gRPC channel for communicating with the Durable Task Scheduler service. /// /// A configured instance that can be used to make gRPC calls. - internal GrpcChannel CreateChannel() + public GrpcChannel CreateChannel() { Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); From 443613ec053938ac69c6e698b69490a1a112b047 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:11:32 -0800 Subject: [PATCH 40/58] fix client managed --- .../DurableTaskSchedulerClientExtensions.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index 58ac05b7..268617a0 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -82,12 +82,21 @@ public static void UseDurableTaskScheduler( internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions { + /// + /// Configures the default named options instance. + /// + /// The options instance to configure. public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options); + /// + /// Configures a named options instance. + /// + /// The name of the options instance to configure. + /// The options instance to configure. public void Configure(string? name, GrpcDurableTaskClientOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } } -} +} From 8744f8ab15107249e56a5190ac234312240ed040 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:07:32 -0800 Subject: [PATCH 41/58] fix worker --- src/Client/AzureManaged/Client.AzureManaged.csproj | 14 +------------- .../DurableTaskSchedulerWorkerExtensions.cs | 11 ++++++++++- src/Worker/AzureManaged/Worker.AzureManaged.csproj | 7 ------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/Client/AzureManaged/Client.AzureManaged.csproj b/src/Client/AzureManaged/Client.AzureManaged.csproj index 6d0fc498..1cf0934c 100644 --- a/src/Client/AzureManaged/Client.AzureManaged.csproj +++ b/src/Client/AzureManaged/Client.AzureManaged.csproj @@ -7,20 +7,8 @@ - - - - - - - - - - - - - + diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index fc034e87..b8dff6b4 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -83,12 +83,21 @@ public static void UseDurableTaskScheduler( internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions { + /// + /// Configures the default named options instance. + /// + /// The options instance to configure. public void Configure(GrpcDurableTaskWorkerOptions options) => this.Configure(Options.DefaultName, options); + /// + /// Configures a named options instance. + /// + /// The name of the options instance to configure. + /// The options instance to configure. public void Configure(string? name, GrpcDurableTaskWorkerOptions options) { DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } } -} +} diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index ffc8e101..e8647fa6 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -7,13 +7,6 @@ - - - - - - - From 37d3d5e4fa1e37949282be37a4061a75ca72bc0f Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:16:27 -0800 Subject: [PATCH 42/58] fix shared tests --- ...ed.AzureManaged.csproj => Shared.AzureManaged.Tests.csproj} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename test/Shared/AzureManaged/{Shared.AzureManaged.csproj => Shared.AzureManaged.Tests.csproj} (87%) diff --git a/test/Shared/AzureManaged/Shared.AzureManaged.csproj b/test/Shared/AzureManaged/Shared.AzureManaged.Tests.csproj similarity index 87% rename from test/Shared/AzureManaged/Shared.AzureManaged.csproj rename to test/Shared/AzureManaged/Shared.AzureManaged.Tests.csproj index 3eeeacb7..8174e5c5 100644 --- a/test/Shared/AzureManaged/Shared.AzureManaged.csproj +++ b/test/Shared/AzureManaged/Shared.AzureManaged.Tests.csproj @@ -8,12 +8,11 @@ - - + From c7f499aaa1f8855c5f159621b2c4c292698cc43d Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:20:53 -0800 Subject: [PATCH 43/58] fix client tests --- .../Client.AzureManaged.Tests.csproj | 17 ++++++++++++ .../AzureManaged/Client.AzureManaged.csproj | 26 ------------------- 2 files changed, 17 insertions(+), 26 deletions(-) create mode 100644 test/Client/AzureManaged/Client.AzureManaged.Tests.csproj delete mode 100644 test/Client/AzureManaged/Client.AzureManaged.csproj diff --git a/test/Client/AzureManaged/Client.AzureManaged.Tests.csproj b/test/Client/AzureManaged/Client.AzureManaged.Tests.csproj new file mode 100644 index 00000000..e19f0de9 --- /dev/null +++ b/test/Client/AzureManaged/Client.AzureManaged.Tests.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + + + + + + + + + + + + + diff --git a/test/Client/AzureManaged/Client.AzureManaged.csproj b/test/Client/AzureManaged/Client.AzureManaged.csproj deleted file mode 100644 index 0cd156c5..00000000 --- a/test/Client/AzureManaged/Client.AzureManaged.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net6.0 - enable - enable - false - true - - - - - - - - - - - - - - - - - - From f077e9840c6bb2c5afc7afff04a5c9364b09aa75 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:23:14 -0800 Subject: [PATCH 44/58] fix worker tests --- .../Worker.AzureManaged.Tests.csproj | 17 ++++++++++++ .../AzureManaged/Worker.AzureManaged.csproj | 26 ------------------- 2 files changed, 17 insertions(+), 26 deletions(-) create mode 100644 test/Worker/AzureManaged/Worker.AzureManaged.Tests.csproj delete mode 100644 test/Worker/AzureManaged/Worker.AzureManaged.csproj diff --git a/test/Worker/AzureManaged/Worker.AzureManaged.Tests.csproj b/test/Worker/AzureManaged/Worker.AzureManaged.Tests.csproj new file mode 100644 index 00000000..6e43cac1 --- /dev/null +++ b/test/Worker/AzureManaged/Worker.AzureManaged.Tests.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + + + + + + + + + + + + + diff --git a/test/Worker/AzureManaged/Worker.AzureManaged.csproj b/test/Worker/AzureManaged/Worker.AzureManaged.csproj deleted file mode 100644 index 3b38e5d1..00000000 --- a/test/Worker/AzureManaged/Worker.AzureManaged.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net6.0 - enable - enable - false - true - - - - - - - - - - - - - - - - - - From 89b70711ec44781d6be0e87a9aad99d42acd81c1 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 12:35:12 -0800 Subject: [PATCH 45/58] fix tests path --- Microsoft.DurableTask.sln | 21 ------------------- .../Client.AzureManaged.Tests.csproj | 0 ...rableTaskSchedulerClientExtensionsTests.cs | 0 .../AccessTokenCacheTests.cs | 0 ...rableTaskSchedulerConnectionStringTests.cs | 0 .../DurableTaskSchedulerOptionsTests.cs | 0 .../Shared.AzureManaged.Tests.csproj | 0 ...rableTaskSchedulerWorkerExtensionsTests.cs | 0 .../Worker.AzureManaged.Tests.csproj | 0 9 files changed, 21 deletions(-) rename test/Client/{AzureManaged => AzureManaged.Tests}/Client.AzureManaged.Tests.csproj (100%) rename test/Client/{AzureManaged => AzureManaged.Tests}/DurableTaskSchedulerClientExtensionsTests.cs (100%) rename test/Shared/{AzureManaged => AzureManaged.Tests}/AccessTokenCacheTests.cs (100%) rename test/Shared/{AzureManaged => AzureManaged.Tests}/DurableTaskSchedulerConnectionStringTests.cs (100%) rename test/Shared/{AzureManaged => AzureManaged.Tests}/DurableTaskSchedulerOptionsTests.cs (100%) rename test/Shared/{AzureManaged => AzureManaged.Tests}/Shared.AzureManaged.Tests.csproj (100%) rename test/Worker/{AzureManaged => AzureManaged.Tests}/DurableTaskSchedulerWorkerExtensionsTests.cs (100%) rename test/Worker/{AzureManaged => AzureManaged.Tests}/Worker.AzureManaged.Tests.csproj (100%) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 45dae8aa..2b8bab8a 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -71,12 +71,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Ana EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp.Tests", "samples\AzureFunctionsUnitTests\AzureFunctionsApp.Tests.csproj", "{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Tests", "test\Extensions\Azure\Azure.Tests.csproj", "{DBB5DB4E-A1B0-4C86-A233-213789C46929}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged", "src\Shared\AzureManaged\Shared.AzureManaged.csproj", "{82D06CA7-90B3-4791-9CAB-222F1AA113D5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged", "test\Shared\AzureManaged\Shared.AzureManaged.csproj", "{F855ACBF-2A6A-4A43-A9E8-51B8599A539E}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -191,18 +185,6 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.Build.0 = Release|Any CPU - {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DBB5DB4E-A1B0-4C86-A233-213789C46929}.Release|Any CPU.Build.0 = Release|Any CPU - {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {82D06CA7-90B3-4791-9CAB-222F1AA113D5}.Release|Any CPU.Build.0 = Release|Any CPU - {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F855ACBF-2A6A-4A43-A9E8-51B8599A539E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -238,9 +220,6 @@ Global {998E9D97-BD36-4A9D-81FC-5DAC1CE40083} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {541FCCCE-1059-4691-B027-F761CD80DE92} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} - {DBB5DB4E-A1B0-4C86-A233-213789C46929} = {E5637F81-2FB9-4CD7-900D-455363B142A7} - {82D06CA7-90B3-4791-9CAB-222F1AA113D5} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} - {F855ACBF-2A6A-4A43-A9E8-51B8599A539E} = {E5637F81-2FB9-4CD7-900D-455363B142A7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} diff --git a/test/Client/AzureManaged/Client.AzureManaged.Tests.csproj b/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj similarity index 100% rename from test/Client/AzureManaged/Client.AzureManaged.Tests.csproj rename to test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj diff --git a/test/Client/AzureManaged/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs similarity index 100% rename from test/Client/AzureManaged/DurableTaskSchedulerClientExtensionsTests.cs rename to test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs diff --git a/test/Shared/AzureManaged/AccessTokenCacheTests.cs b/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs similarity index 100% rename from test/Shared/AzureManaged/AccessTokenCacheTests.cs rename to test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs diff --git a/test/Shared/AzureManaged/DurableTaskSchedulerConnectionStringTests.cs b/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs similarity index 100% rename from test/Shared/AzureManaged/DurableTaskSchedulerConnectionStringTests.cs rename to test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs diff --git a/test/Shared/AzureManaged/DurableTaskSchedulerOptionsTests.cs b/test/Shared/AzureManaged.Tests/DurableTaskSchedulerOptionsTests.cs similarity index 100% rename from test/Shared/AzureManaged/DurableTaskSchedulerOptionsTests.cs rename to test/Shared/AzureManaged.Tests/DurableTaskSchedulerOptionsTests.cs diff --git a/test/Shared/AzureManaged/Shared.AzureManaged.Tests.csproj b/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj similarity index 100% rename from test/Shared/AzureManaged/Shared.AzureManaged.Tests.csproj rename to test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj diff --git a/test/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs similarity index 100% rename from test/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensionsTests.cs rename to test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs diff --git a/test/Worker/AzureManaged/Worker.AzureManaged.Tests.csproj b/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj similarity index 100% rename from test/Worker/AzureManaged/Worker.AzureManaged.Tests.csproj rename to test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj From 77cd2d9b8b6ccd7548535762d8afdabd60a9cd85 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:18:57 -0800 Subject: [PATCH 46/58] update sln --- Microsoft.DurableTask.sln | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 2b8bab8a..45bc8632 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -71,6 +71,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Ana EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp.Tests", "samples\AzureFunctionsUnitTests\AzureFunctionsApp.Tests.csproj", "{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged", "src\Worker\AzureManaged\Worker.AzureManaged.csproj", "{6106872F-A730-4A75-9267-1B2E2C2DC18C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged", "src\Client\AzureManaged\Client.AzureManaged.csproj", "{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{11357B31-9A63-4A5A-9BC5-091952B25BC0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged.Tests", "test\Client\AzureManaged.Tests\Client.AzureManaged.Tests.csproj", "{A15BA625-DC6B-4C6D-8673-0CB08F1B9737}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged.Tests", "test\Worker\AzureManaged.Tests\Worker.AzureManaged.Tests.csproj", "{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -185,6 +195,26 @@ Global {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.Build.0 = Release|Any CPU + {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6106872F-A730-4A75-9267-1B2E2C2DC18C}.Release|Any CPU.Build.0 = Release|Any CPU + {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Release|Any CPU.Build.0 = Release|Any CPU + {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11357B31-9A63-4A5A-9BC5-091952B25BC0}.Release|Any CPU.Build.0 = Release|Any CPU + {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Release|Any CPU.Build.0 = Release|Any CPU + {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -220,6 +250,11 @@ Global {998E9D97-BD36-4A9D-81FC-5DAC1CE40083} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} {541FCCCE-1059-4691-B027-F761CD80DE92} = {E5637F81-2FB9-4CD7-900D-455363B142A7} {FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17} + {6106872F-A730-4A75-9267-1B2E2C2DC18C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {11357B31-9A63-4A5A-9BC5-091952B25BC0} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {A15BA625-DC6B-4C6D-8673-0CB08F1B9737} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} + {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71} From 3af8b629f2628fa75890cf097bd5ce4c1948d6b9 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:24:38 -0800 Subject: [PATCH 47/58] add sample --- Microsoft.DurableTask.sln | 2 + samples/AspNetWebApp/AspNetWebApp.csproj | 26 +++++++++ samples/AspNetWebApp/DockerFile | 22 ++++++++ .../Orchestrations/HelloCities.cs | 30 ++++++++++ samples/AspNetWebApp/Program.cs | 56 +++++++++++++++++++ .../Properties/launchSettings.json | 23 ++++++++ samples/AspNetWebApp/ScenariosController.cs | 50 +++++++++++++++++ samples/AspNetWebApp/Utils.cs | 38 +++++++++++++ .../AspNetWebApp/appsettings.Development.json | 8 +++ .../AspNetWebApp/appsettings.Production.json | 11 ++++ samples/AspNetWebApp/appsettings.json | 9 +++ .../DurableTaskSchedulerClientExtensions.cs | 2 +- 12 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 samples/AspNetWebApp/AspNetWebApp.csproj create mode 100644 samples/AspNetWebApp/DockerFile create mode 100644 samples/AspNetWebApp/Orchestrations/HelloCities.cs create mode 100644 samples/AspNetWebApp/Program.cs create mode 100644 samples/AspNetWebApp/Properties/launchSettings.json create mode 100644 samples/AspNetWebApp/ScenariosController.cs create mode 100644 samples/AspNetWebApp/Utils.cs create mode 100644 samples/AspNetWebApp/appsettings.Development.json create mode 100644 samples/AspNetWebApp/appsettings.Production.json create mode 100644 samples/AspNetWebApp/appsettings.json diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index 45bc8632..d09e0550 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -81,6 +81,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged.Tests", "test\Worker\AzureManaged.Tests\Worker.AzureManaged.Tests.csproj", "{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetWebApp", "samples\portable-sdk\dotnet\AspNetWebApp\AspNetWebApp.csproj", "{869D2D51-9372-4764-B059-C43B6C1180A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/samples/AspNetWebApp/AspNetWebApp.csproj b/samples/AspNetWebApp/AspNetWebApp.csproj new file mode 100644 index 00000000..8a88103b --- /dev/null +++ b/samples/AspNetWebApp/AspNetWebApp.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + true + $(BaseIntermediateOutputPath)Generated + + + false + false + + + + + + + + + + + + + + diff --git a/samples/AspNetWebApp/DockerFile b/samples/AspNetWebApp/DockerFile new file mode 100644 index 00000000..5ddb3ae9 --- /dev/null +++ b/samples/AspNetWebApp/DockerFile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["AspNetWebApp.csproj", "."] +RUN dotnet restore "./AspNetWebApp.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "AspNetWebApp.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "AspNetWebApp.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENV ASPNETCORE_ENVIRONMENT=Production +ENTRYPOINT ["dotnet", "AspNetWebApp.dll"] diff --git a/samples/AspNetWebApp/Orchestrations/HelloCities.cs b/samples/AspNetWebApp/Orchestrations/HelloCities.cs new file mode 100644 index 00000000..5c48dcaa --- /dev/null +++ b/samples/AspNetWebApp/Orchestrations/HelloCities.cs @@ -0,0 +1,30 @@ +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +namespace AspNetWebApp.Scenarios; + +[DurableTask] +class HelloCities : TaskOrchestrator> +{ + public override async Task> RunAsync(TaskOrchestrationContext context, string input) + { + List results = + [ + await context.CallSayHelloAsync("Seattle"), + await context.CallSayHelloAsync("Amsterdam"), + await context.CallSayHelloAsync("Hyderabad"), + await context.CallSayHelloAsync("Shanghai"), + await context.CallSayHelloAsync("Tokyo"), + ]; + return results; + } +} + +[DurableTask] +class SayHello : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, string cityName) + { + return Task.FromResult($"Hello, {cityName}!"); + } +} diff --git a/samples/AspNetWebApp/Program.cs b/samples/AspNetWebApp/Program.cs new file mode 100644 index 00000000..1887c4db --- /dev/null +++ b/samples/AspNetWebApp/Program.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; +using Azure.Core; +using Azure.Identity; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Worker.AzureManaged; +using Microsoft.DurableTask.Client.AzureManaged; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +string endpointAddress = builder.Configuration["DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS"] + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS'"); + +string taskHubName = builder.Configuration["DURABLE_TASK_SCHEDULER_TASK_HUB_NAME"] + ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_TASK_HUB_NAME'"); + +TokenCredential credential = builder.Environment.IsProduction() + ? new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = builder.Configuration["CONTAINER_APP_UMI_CLIENT_ID"] }) + : new DefaultAzureCredential(); + +// Add all the generated orchestrations and activities automatically +builder.Services.AddDurableTaskWorker(builder => +{ + builder.AddTasks(r => r.AddAllGeneratedTasks()); + builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); +}); + +// Register the client, which can be used to start orchestrations +builder.Services.AddDurableTaskClient(builder => +{ + builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); +}); + +// Configure console logging using the simpler, more compact format +builder.Services.AddLogging(logging => +{ + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + options.UseUtcTimestamp = true; + options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; + }); +}); + +// Configure the HTTP request pipeline +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; +}); + +// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS" +WebApplication app = builder.Build(); +app.MapControllers(); +app.Run(); diff --git a/samples/AspNetWebApp/Properties/launchSettings.json b/samples/AspNetWebApp/Properties/launchSettings.json new file mode 100644 index 00000000..9e08b95b --- /dev/null +++ b/samples/AspNetWebApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36209", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5008", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://wbtestdts02-g7ahczeycua9.westus2.durabletask.io", + "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "wbtb100" + } + } + } +} diff --git a/samples/AspNetWebApp/ScenariosController.cs b/samples/AspNetWebApp/ScenariosController.cs new file mode 100644 index 00000000..d6049129 --- /dev/null +++ b/samples/AspNetWebApp/ScenariosController.cs @@ -0,0 +1,50 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; + +namespace AspNetWebApp; + +[Route("scenarios")] +[ApiController] +public partial class ScenariosController( + DurableTaskClient durableTaskClient, + ILogger logger) : ControllerBase +{ + readonly DurableTaskClient durableTaskClient = durableTaskClient; + readonly ILogger logger = logger; + + [HttpPost("hellocities")] + public async Task RunHelloCities([FromQuery] int? count, [FromQuery] string? prefix) + { + if (count is null || count < 1) + { + return this.BadRequest(new { error = "A 'count' query string parameter is required and it must contain a positive number." }); + } + + // Generate a semi-unique prefix for the instance IDs to simplify tracking + prefix ??= $"hellocities-{count}-"; + prefix += DateTime.UtcNow.ToString("yyyyMMdd-hhmmss"); + + this.logger.LogInformation("Scheduling {count} orchestrations with a prefix of '{prefix}'...", count, prefix); + + Stopwatch sw = Stopwatch.StartNew(); + await Enumerable.Range(0, count.Value).ParallelForEachAsync(1000, i => + { + string instanceId = $"{prefix}-{i:X16}"; + return this.durableTaskClient.ScheduleNewHelloCitiesInstanceAsync( + input: null!, + new StartOrchestrationOptions(instanceId)); + }); + + sw.Stop(); + this.logger.LogInformation( + "All {count} orchestrations were scheduled successfully in {time}ms!", + count, + sw.ElapsedMilliseconds); + return this.Ok(new + { + message = $"Scheduled {count} orchestrations prefixed with '{prefix}' in {sw.ElapsedMilliseconds}." + }); + } +} diff --git a/samples/AspNetWebApp/Utils.cs b/samples/AspNetWebApp/Utils.cs new file mode 100644 index 00000000..6ddabf39 --- /dev/null +++ b/samples/AspNetWebApp/Utils.cs @@ -0,0 +1,38 @@ +namespace AspNetWebApp; + +static class Utils +{ + public static async Task ParallelForEachAsync(this IEnumerable items, int maxConcurrency, Func action) + { + List tasks; + if (items is ICollection itemCollection) + { + tasks = new List(itemCollection.Count); + } + else + { + tasks = []; + } + + using SemaphoreSlim semaphore = new(maxConcurrency); + foreach (T item in items) + { + tasks.Add(InvokeThrottledAction(item, action, semaphore)); + } + + await Task.WhenAll(tasks); + } + + static async Task InvokeThrottledAction(T item, Func action, SemaphoreSlim semaphore) + { + await semaphore.WaitAsync(); + try + { + await action(item); + } + finally + { + semaphore.Release(); + } + } +} diff --git a/samples/AspNetWebApp/appsettings.Development.json b/samples/AspNetWebApp/appsettings.Development.json new file mode 100644 index 00000000..a6e86ace --- /dev/null +++ b/samples/AspNetWebApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/AspNetWebApp/appsettings.Production.json b/samples/AspNetWebApp/appsettings.Production.json new file mode 100644 index 00000000..70b7d1d9 --- /dev/null +++ b/samples/AspNetWebApp/appsettings.Production.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://{your-durable-task-endpoint}.durabletask.io", + "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "{your-task-hub-name}", + "CONTAINER_APP_UMI_CLIENT_ID": "{your-user-managed-identity-client-id}" +} diff --git a/samples/AspNetWebApp/appsettings.json b/samples/AspNetWebApp/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/samples/AspNetWebApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index 268617a0..7c3c4c34 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -namespace Microsoft.DurableTask.Client; +namespace Microsoft.DurableTask.Client.AzureManaged; /// /// Extension methods for configuring Durable Task clients to use the Azure Durable Task Scheduler service. From 549e105f77e76b6b9bac519ad338042bbb32b121 Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 13 Jan 2025 14:58:05 -0800 Subject: [PATCH 48/58] Update src/Shared/AzureManaged/Shared.AzureManaged.csproj Co-authored-by: Jacob Viau --- src/Shared/AzureManaged/Shared.AzureManaged.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/AzureManaged/Shared.AzureManaged.csproj b/src/Shared/AzureManaged/Shared.AzureManaged.csproj index 2ee55cde..d4bfb201 100644 --- a/src/Shared/AzureManaged/Shared.AzureManaged.csproj +++ b/src/Shared/AzureManaged/Shared.AzureManaged.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0 Shared components for Azure Managed extensions for the Durable Task Framework. true From 3a40d0977eb8ff4963bfa676f013e5de230140bf Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:36:57 -0800 Subject: [PATCH 49/58] fix compile --- Microsoft.DurableTask.sln | 10 +- samples/AspNetWebApp/AspNetWebApp.csproj | 26 --- samples/AspNetWebApp/DockerFile | 22 -- .../Orchestrations/HelloCities.cs | 30 --- samples/AspNetWebApp/Program.cs | 56 ----- .../Properties/launchSettings.json | 23 -- samples/AspNetWebApp/ScenariosController.cs | 50 ----- samples/AspNetWebApp/Utils.cs | 38 ---- .../AspNetWebApp/appsettings.Development.json | 8 - .../AspNetWebApp/appsettings.Production.json | 11 - samples/AspNetWebApp/appsettings.json | 9 - .../AzureManaged/Client.AzureManaged.csproj | 12 +- .../DurableTaskSchedulerClientExtensions.cs | 18 +- .../DurableTaskSchedulerClientOptions.cs | 166 ++++++++++++++ .../DurableTaskSchedulerConnectionString.cs | 2 +- .../AzureManaged/Shared.AzureManaged.csproj | 25 --- .../DurableTaskSchedulerWorkerExtensions.cs | 14 +- .../DurableTaskSchedulerWorkerOptions.cs} | 8 +- .../AzureManaged/Worker.AzureManaged.csproj | 12 +- .../Client.AzureManaged.Tests.csproj | 8 +- ...rableTaskSchedulerClientExtensionsTests.cs | 8 +- .../DurableTaskSchedulerClientOptionsTests.cs | 211 ++++++++++++++++++ .../AccessTokenCacheTests.cs | 5 +- .../Shared.AzureManaged.Tests.csproj | 10 +- ...rableTaskSchedulerWorkerExtensionsTests.cs | 8 +- ...DurableTaskSchedulerWorkerOptionsTests.cs} | 28 +-- .../Worker.AzureManaged.Tests.csproj | 4 +- 27 files changed, 470 insertions(+), 352 deletions(-) delete mode 100644 samples/AspNetWebApp/AspNetWebApp.csproj delete mode 100644 samples/AspNetWebApp/DockerFile delete mode 100644 samples/AspNetWebApp/Orchestrations/HelloCities.cs delete mode 100644 samples/AspNetWebApp/Program.cs delete mode 100644 samples/AspNetWebApp/Properties/launchSettings.json delete mode 100644 samples/AspNetWebApp/ScenariosController.cs delete mode 100644 samples/AspNetWebApp/Utils.cs delete mode 100644 samples/AspNetWebApp/appsettings.Development.json delete mode 100644 samples/AspNetWebApp/appsettings.Production.json delete mode 100644 samples/AspNetWebApp/appsettings.json create mode 100644 src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs delete mode 100644 src/Shared/AzureManaged/Shared.AzureManaged.csproj rename src/{Shared/AzureManaged/DurableTaskSchedulerOptions.cs => Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs} (96%) create mode 100644 test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs rename test/{Shared/AzureManaged.Tests/DurableTaskSchedulerOptionsTests.cs => Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs} (84%) diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln index d09e0550..a3a2af17 100644 --- a/Microsoft.DurableTask.sln +++ b/Microsoft.DurableTask.sln @@ -81,8 +81,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged.Tests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged.Tests", "test\Worker\AzureManaged.Tests\Worker.AzureManaged.Tests.csproj", "{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetWebApp", "samples\portable-sdk\dotnet\AspNetWebApp\AspNetWebApp.csproj", "{869D2D51-9372-4764-B059-C43B6C1180A3}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -217,6 +215,14 @@ Global {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU {B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.Build.0 = Release|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.Build.0 = Release|Any CPU + {D4C87C0F-66CD-459D-B271-340C6D180448}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4C87C0F-66CD-459D-B271-340C6D180448}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4C87C0F-66CD-459D-B271-340C6D180448}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4C87C0F-66CD-459D-B271-340C6D180448}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/AspNetWebApp/AspNetWebApp.csproj b/samples/AspNetWebApp/AspNetWebApp.csproj deleted file mode 100644 index 8a88103b..00000000 --- a/samples/AspNetWebApp/AspNetWebApp.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - net8.0 - enable - enable - true - $(BaseIntermediateOutputPath)Generated - - - false - false - - - - - - - - - - - - - - diff --git a/samples/AspNetWebApp/DockerFile b/samples/AspNetWebApp/DockerFile deleted file mode 100644 index 5ddb3ae9..00000000 --- a/samples/AspNetWebApp/DockerFile +++ /dev/null @@ -1,22 +0,0 @@ -#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -WORKDIR /app -EXPOSE 8080 - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR /src -COPY ["AspNetWebApp.csproj", "."] -RUN dotnet restore "./AspNetWebApp.csproj" -COPY . . -WORKDIR "/src/." -RUN dotnet build "AspNetWebApp.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "AspNetWebApp.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENV ASPNETCORE_ENVIRONMENT=Production -ENTRYPOINT ["dotnet", "AspNetWebApp.dll"] diff --git a/samples/AspNetWebApp/Orchestrations/HelloCities.cs b/samples/AspNetWebApp/Orchestrations/HelloCities.cs deleted file mode 100644 index 5c48dcaa..00000000 --- a/samples/AspNetWebApp/Orchestrations/HelloCities.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; - -namespace AspNetWebApp.Scenarios; - -[DurableTask] -class HelloCities : TaskOrchestrator> -{ - public override async Task> RunAsync(TaskOrchestrationContext context, string input) - { - List results = - [ - await context.CallSayHelloAsync("Seattle"), - await context.CallSayHelloAsync("Amsterdam"), - await context.CallSayHelloAsync("Hyderabad"), - await context.CallSayHelloAsync("Shanghai"), - await context.CallSayHelloAsync("Tokyo"), - ]; - return results; - } -} - -[DurableTask] -class SayHello : TaskActivity -{ - public override Task RunAsync(TaskActivityContext context, string cityName) - { - return Task.FromResult($"Hello, {cityName}!"); - } -} diff --git a/samples/AspNetWebApp/Program.cs b/samples/AspNetWebApp/Program.cs deleted file mode 100644 index 1887c4db..00000000 --- a/samples/AspNetWebApp/Program.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Text.Json.Serialization; -using Azure.Core; -using Azure.Identity; -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; -using Microsoft.DurableTask.Worker; -using Microsoft.DurableTask.Worker.AzureManaged; -using Microsoft.DurableTask.Client.AzureManaged; - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -string endpointAddress = builder.Configuration["DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS"] - ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS'"); - -string taskHubName = builder.Configuration["DURABLE_TASK_SCHEDULER_TASK_HUB_NAME"] - ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_SCHEDULER_TASK_HUB_NAME'"); - -TokenCredential credential = builder.Environment.IsProduction() - ? new DefaultAzureCredential(new DefaultAzureCredentialOptions { ManagedIdentityClientId = builder.Configuration["CONTAINER_APP_UMI_CLIENT_ID"] }) - : new DefaultAzureCredential(); - -// Add all the generated orchestrations and activities automatically -builder.Services.AddDurableTaskWorker(builder => -{ - builder.AddTasks(r => r.AddAllGeneratedTasks()); - builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); -}); - -// Register the client, which can be used to start orchestrations -builder.Services.AddDurableTaskClient(builder => -{ - builder.UseDurableTaskScheduler(endpointAddress, taskHubName, credential); -}); - -// Configure console logging using the simpler, more compact format -builder.Services.AddLogging(logging => -{ - logging.AddSimpleConsole(options => - { - options.SingleLine = true; - options.UseUtcTimestamp = true; - options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; - }); -}); - -// Configure the HTTP request pipeline -builder.Services.AddControllers().AddJsonOptions(options => -{ - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; -}); - -// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS" -WebApplication app = builder.Build(); -app.MapControllers(); -app.Run(); diff --git a/samples/AspNetWebApp/Properties/launchSettings.json b/samples/AspNetWebApp/Properties/launchSettings.json deleted file mode 100644 index 9e08b95b..00000000 --- a/samples/AspNetWebApp/Properties/launchSettings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:36209", - "sslPort": 0 - } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "applicationUrl": "http://localhost:5008", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://wbtestdts02-g7ahczeycua9.westus2.durabletask.io", - "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "wbtb100" - } - } - } -} diff --git a/samples/AspNetWebApp/ScenariosController.cs b/samples/AspNetWebApp/ScenariosController.cs deleted file mode 100644 index d6049129..00000000 --- a/samples/AspNetWebApp/ScenariosController.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Mvc; -using Microsoft.DurableTask; -using Microsoft.DurableTask.Client; - -namespace AspNetWebApp; - -[Route("scenarios")] -[ApiController] -public partial class ScenariosController( - DurableTaskClient durableTaskClient, - ILogger logger) : ControllerBase -{ - readonly DurableTaskClient durableTaskClient = durableTaskClient; - readonly ILogger logger = logger; - - [HttpPost("hellocities")] - public async Task RunHelloCities([FromQuery] int? count, [FromQuery] string? prefix) - { - if (count is null || count < 1) - { - return this.BadRequest(new { error = "A 'count' query string parameter is required and it must contain a positive number." }); - } - - // Generate a semi-unique prefix for the instance IDs to simplify tracking - prefix ??= $"hellocities-{count}-"; - prefix += DateTime.UtcNow.ToString("yyyyMMdd-hhmmss"); - - this.logger.LogInformation("Scheduling {count} orchestrations with a prefix of '{prefix}'...", count, prefix); - - Stopwatch sw = Stopwatch.StartNew(); - await Enumerable.Range(0, count.Value).ParallelForEachAsync(1000, i => - { - string instanceId = $"{prefix}-{i:X16}"; - return this.durableTaskClient.ScheduleNewHelloCitiesInstanceAsync( - input: null!, - new StartOrchestrationOptions(instanceId)); - }); - - sw.Stop(); - this.logger.LogInformation( - "All {count} orchestrations were scheduled successfully in {time}ms!", - count, - sw.ElapsedMilliseconds); - return this.Ok(new - { - message = $"Scheduled {count} orchestrations prefixed with '{prefix}' in {sw.ElapsedMilliseconds}." - }); - } -} diff --git a/samples/AspNetWebApp/Utils.cs b/samples/AspNetWebApp/Utils.cs deleted file mode 100644 index 6ddabf39..00000000 --- a/samples/AspNetWebApp/Utils.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace AspNetWebApp; - -static class Utils -{ - public static async Task ParallelForEachAsync(this IEnumerable items, int maxConcurrency, Func action) - { - List tasks; - if (items is ICollection itemCollection) - { - tasks = new List(itemCollection.Count); - } - else - { - tasks = []; - } - - using SemaphoreSlim semaphore = new(maxConcurrency); - foreach (T item in items) - { - tasks.Add(InvokeThrottledAction(item, action, semaphore)); - } - - await Task.WhenAll(tasks); - } - - static async Task InvokeThrottledAction(T item, Func action, SemaphoreSlim semaphore) - { - await semaphore.WaitAsync(); - try - { - await action(item); - } - finally - { - semaphore.Release(); - } - } -} diff --git a/samples/AspNetWebApp/appsettings.Development.json b/samples/AspNetWebApp/appsettings.Development.json deleted file mode 100644 index a6e86ace..00000000 --- a/samples/AspNetWebApp/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/samples/AspNetWebApp/appsettings.Production.json b/samples/AspNetWebApp/appsettings.Production.json deleted file mode 100644 index 70b7d1d9..00000000 --- a/samples/AspNetWebApp/appsettings.Production.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "DURABLE_TASK_SCHEDULER_ENDPOINT_ADDRESS": "https://{your-durable-task-endpoint}.durabletask.io", - "DURABLE_TASK_SCHEDULER_TASK_HUB_NAME": "{your-task-hub-name}", - "CONTAINER_APP_UMI_CLIENT_ID": "{your-user-managed-identity-client-id}" -} diff --git a/samples/AspNetWebApp/appsettings.json b/samples/AspNetWebApp/appsettings.json deleted file mode 100644 index 10f68b8c..00000000 --- a/samples/AspNetWebApp/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/src/Client/AzureManaged/Client.AzureManaged.csproj b/src/Client/AzureManaged/Client.AzureManaged.csproj index 1cf0934c..29f0dfd8 100644 --- a/src/Client/AzureManaged/Client.AzureManaged.csproj +++ b/src/Client/AzureManaged/Client.AzureManaged.csproj @@ -8,7 +8,17 @@ - + + + + + + + + + + + diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index 7c3c4c34..a5432bf8 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -21,15 +21,15 @@ public static class DurableTaskSchedulerClientExtensions /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// The name of the task hub resource associated with the Durable Task Scheduler resource. /// The credential used to authenticate with the Durable Task Scheduler task hub resource. - /// Optional callback to dynamically configure DurableTaskSchedulerOptions. + /// Optional callback to dynamically configure DurableTaskSchedulerClientOptions. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string endpointAddress, string taskHubName, TokenCredential credential, - Action? configure = null) + Action? configure = null) { - builder.Services.AddOptions(builder.Name) + builder.Services.AddOptions(builder.Name) .Configure(options => { options.EndpointAddress = endpointAddress; @@ -50,15 +50,15 @@ public static void UseDurableTaskScheduler( /// /// The Durable Task client builder to configure. /// The connection string used to connect to the Durable Task Scheduler service. - /// Optional callback to dynamically configure DurableTaskSchedulerOptions. + /// Optional callback to dynamically configure DurableTaskSchedulerClientOptions. public static void UseDurableTaskScheduler( this IDurableTaskClientBuilder builder, string connectionString, - Action? configure = null) + Action? configure = null) { - var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var connectionOptions = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); - builder.Services.AddOptions(builder.Name) + builder.Services.AddOptions(builder.Name) .Configure(options => { options.EndpointAddress = connectionOptions.EndpointAddress; @@ -79,7 +79,7 @@ public static void UseDurableTaskScheduler( /// using the provided Durable Task Scheduler options. /// /// Monitor for accessing the current scheduler options configuration. - internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions { /// @@ -95,7 +95,7 @@ internal class ConfigureGrpcChannel(IOptionsMonitor /// The options instance to configure. public void Configure(string? name, GrpcDurableTaskClientOptions options) { - DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); + DurableTaskSchedulerClientOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } } diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs new file mode 100644 index 00000000..30ab86a3 --- /dev/null +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using Azure.Core; +using Azure.Identity; +using Grpc.Core; +using Grpc.Net.Client; + +namespace Microsoft.DurableTask; + +/// +/// Options for configuring the Durable Task Scheduler. +/// +public class DurableTaskSchedulerClientOptions +{ + /// + /// Gets or sets the endpoint address of the Durable Task Scheduler resource. + /// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". + /// + [Required(ErrorMessage = "Endpoint address is required")] + public string EndpointAddress { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the task hub resource associated with the Durable Task Scheduler resource. + /// + [Required(ErrorMessage = "Task hub name is required")] + public string TaskHubName { get; set; } = string.Empty; + + /// + /// Gets or sets the credential used to authenticate with the Durable Task Scheduler task hub resource. + /// + public TokenCredential? Credential { get; set; } + + /// + /// Gets or sets the resource ID of the Durable Task Scheduler resource. + /// The default value is https://durabletask.io. + /// + public string ResourceId { get; set; } = "https://durabletask.io"; + + /// + /// Gets or sets the worker ID used to identify the worker instance. + /// The default value is a string containing the machine name, process ID, and a unique identifier. + /// + public string WorkerId { get; set; } = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; + + /// + /// Gets or sets a value indicating whether to allow insecure channel credentials. + /// This should only be set to true in development/testing scenarios. + /// + public bool AllowInsecureCredentials { get; set; } + + /// + /// Creates a new instance of from a connection string. + /// + /// The connection string to parse. + /// A new instance of . + public static DurableTaskSchedulerClientOptions FromConnectionString(string connectionString) + { + return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); + } + + /// + /// Creates a new instance of from a parsed connection string. + /// + /// The connection string to parse. + /// A new instance of . + internal static DurableTaskSchedulerClientOptions FromConnectionString( + DurableTaskSchedulerConnectionString connectionString) => new() + { + EndpointAddress = connectionString.Endpoint, + TaskHubName = connectionString.TaskHubName, + Credential = GetCredentialFromConnectionString(connectionString), + }; + + /// + /// Creates a gRPC channel for communicating with the Durable Task Scheduler service. + /// + /// A configured instance that can be used to make gRPC calls. + public GrpcChannel CreateChannel() + { + Verify.NotNull(this.EndpointAddress, nameof(this.EndpointAddress)); + Verify.NotNull(this.TaskHubName, nameof(this.TaskHubName)); + string taskHubName = this.TaskHubName; + string endpoint = !this.EndpointAddress.Contains("://") + ? $"https://{this.EndpointAddress}" + : this.EndpointAddress; + AccessTokenCache? cache = + this.Credential is not null + ? new AccessTokenCache( + this.Credential, + new TokenRequestContext(new[] { $"{this.ResourceId}/.default" }), + TimeSpan.FromMinutes(5)) + : null; + CallCredentials managedBackendCreds = CallCredentials.FromInterceptor( + async (context, metadata) => + { + metadata.Add("taskhub", taskHubName); + metadata.Add("workerid", this.WorkerId); + if (cache == null) + { + return; + } + + AccessToken token = await cache.GetTokenAsync(context.CancellationToken); + metadata.Add("Authorization", $"Bearer {token.Token}"); + }); + + // Production will use HTTPS, but local testing will use HTTP + ChannelCredentials channelCreds = endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ? + ChannelCredentials.SecureSsl : + ChannelCredentials.Insecure; + return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions + { + Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds), + UnsafeUseInsecureChannelCallCredentials = this.AllowInsecureCredentials, + }); + } + + static TokenCredential? GetCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) + { + string authType = connectionString.Authentication; + + // Parse the supported auth types, in a case-insensitive way and without spaces + switch (authType.ToLower(CultureInfo.InvariantCulture).Replace(" ", string.Empty)) + { + case "defaultazure": + return new DefaultAzureCredential(); + case "managedidentity": + return new ManagedIdentityCredential(connectionString.ClientId); + case "workloadidentity": + var opts = new WorkloadIdentityCredentialOptions(); + if (!string.IsNullOrEmpty(connectionString.ClientId)) + { + opts.ClientId = connectionString.ClientId; + } + + if (!string.IsNullOrEmpty(connectionString.TenantId)) + { + opts.TenantId = connectionString.TenantId; + } + + if (connectionString.AdditionallyAllowedTenants is not null) + { + foreach (string tenant in connectionString.AdditionallyAllowedTenants) + { + opts.AdditionallyAllowedTenants.Add(tenant); + } + } + + return new WorkloadIdentityCredential(opts); + case "environment": + return new EnvironmentCredential(); + case "azurecli": + return new AzureCliCredential(); + case "azurepowershell": + return new AzurePowerShellCredential(); + case "none": + return null; + default: + throw new ArgumentException( + $"The connection string contains an unsupported authentication type '{authType}'.", + nameof(connectionString)); + } + } +} diff --git a/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs index 7d3d18b4..40dc470c 100644 --- a/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs +++ b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs @@ -7,7 +7,7 @@ namespace Microsoft.DurableTask; /// /// Represents the constituent parts of a connection string for a Durable Task Scheduler service. /// -public sealed class DurableTaskSchedulerConnectionString +sealed class DurableTaskSchedulerConnectionString { readonly DbConnectionStringBuilder builder; diff --git a/src/Shared/AzureManaged/Shared.AzureManaged.csproj b/src/Shared/AzureManaged/Shared.AzureManaged.csproj deleted file mode 100644 index d4bfb201..00000000 --- a/src/Shared/AzureManaged/Shared.AzureManaged.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net6.0 - Shared components for Azure Managed extensions for the Durable Task Framework. - true - - - - - - - - - - - - - - - - - - - diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index b8dff6b4..300e1492 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -28,9 +28,9 @@ public static void UseDurableTaskScheduler( string endpointAddress, string taskHubName, TokenCredential credential, - Action? configure = null) + Action? configure = null) { - builder.Services.AddOptions(builder.Name) + builder.Services.AddOptions(builder.Name) .Configure(options => { options.EndpointAddress = endpointAddress; @@ -55,11 +55,11 @@ public static void UseDurableTaskScheduler( public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string connectionString, - Action? configure = null) + Action? configure = null) { - var connectionOptions = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var connectionOptions = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); - builder.Services.AddOptions(builder.Name) + builder.Services.AddOptions(builder.Name) .Configure(options => { options.EndpointAddress = connectionOptions.EndpointAddress; @@ -80,7 +80,7 @@ public static void UseDurableTaskScheduler( /// using the provided Durable Task Scheduler options. /// /// Monitor for accessing the current scheduler options configuration. - internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions { /// @@ -96,7 +96,7 @@ internal class ConfigureGrpcChannel(IOptionsMonitor /// The options instance to configure. public void Configure(string? name, GrpcDurableTaskWorkerOptions options) { - DurableTaskSchedulerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); + DurableTaskSchedulerWorkerOptions source = schedulerOptions.Get(name ?? Options.DefaultName); options.Channel = source.CreateChannel(); } } diff --git a/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs similarity index 96% rename from src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs rename to src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs index 5a9ce98b..a7f79510 100644 --- a/src/Shared/AzureManaged/DurableTaskSchedulerOptions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs @@ -4,13 +4,15 @@ using System.Globalization; using Azure.Core; using Azure.Identity; +using Grpc.Core; +using Grpc.Net.Client; namespace Microsoft.DurableTask; /// /// Options for configuring the Durable Task Scheduler. /// -public class DurableTaskSchedulerOptions +public class DurableTaskSchedulerWorkerOptions { /// /// Gets or sets the endpoint address of the Durable Task Scheduler resource. @@ -53,7 +55,7 @@ public class DurableTaskSchedulerOptions /// /// The connection string to parse. /// A new instance of . - public static DurableTaskSchedulerOptions FromConnectionString(string connectionString) + public static DurableTaskSchedulerWorkerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } @@ -63,7 +65,7 @@ public static DurableTaskSchedulerOptions FromConnectionString(string connection /// /// The connection string to parse. /// A new instance of . - public static DurableTaskSchedulerOptions FromConnectionString( + internal static DurableTaskSchedulerWorkerOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) => new() { EndpointAddress = connectionString.Endpoint, diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index e8647fa6..4985279f 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -8,7 +8,17 @@ - + + + + + + + + + + + diff --git a/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj b/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj index e19f0de9..309b5a3f 100644 --- a/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj +++ b/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj @@ -5,12 +5,16 @@ - + + + + + + - diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index 5a0099dd..e05e352e 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -78,7 +78,7 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx var provider = services.BuildServiceProvider(); var ex = Assert.Throws(() => { - var options = provider.GetRequiredService>().Value; + var options = provider.GetRequiredService>().Value; }); Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); } @@ -145,7 +145,7 @@ public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() // Assert var provider = services.BuildServiceProvider(); - var optionsMonitor = provider.GetService>(); + var optionsMonitor = provider.GetService>(); optionsMonitor.Should().NotBeNull(); var options = optionsMonitor!.Get("CustomName"); options.Should().NotBeNull(); @@ -159,7 +159,7 @@ public void ConfigureGrpcChannel_ShouldConfigureClientOptions() { // Arrange var services = new ServiceCollection(); - services.AddOptions() + services.AddOptions() .Configure(options => { options.EndpointAddress = $"https://{ValidEndpoint}"; @@ -168,7 +168,7 @@ public void ConfigureGrpcChannel_ShouldConfigureClientOptions() }); var provider = services.BuildServiceProvider(); - var schedulerOptions = provider.GetRequiredService>(); + var schedulerOptions = provider.GetRequiredService>(); var configureGrpcChannel = new DurableTaskSchedulerClientExtensions.ConfigureGrpcChannel(schedulerOptions); // Act diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs new file mode 100644 index 00000000..7bc1577c --- /dev/null +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Identity; +using FluentAssertions; +using Xunit; + +namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; + +public class DurableTaskSchedulerClientOptionsTests +{ + private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + private const string ValidTaskHub = "testhub"; + + [Fact] + public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be(ValidEndpoint); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void FromConnectionString_WithManagedIdentity_ShouldCreateValidInstance() + { + // Arrange + const string clientId = "00000000-0000-0000-0000-000000000000"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be(ValidEndpoint); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Fact] + public void FromConnectionString_WithWorkloadIdentity_ShouldCreateValidInstance() + { + // Arrange + const string clientId = "00000000-0000-0000-0000-000000000000"; + const string tenantId = "11111111-1111-1111-1111-111111111111"; + string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={clientId};TenantId={tenantId};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be(ValidEndpoint); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeOfType(); + } + + [Theory] + [InlineData("Environment")] + [InlineData("AzureCLI")] + [InlineData("AzurePowerShell")] + public void FromConnectionString_WithValidAuthTypes_ShouldCreateValidInstance(string authType) + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication={authType};TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be(ValidEndpoint); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().NotBeNull(); + } + + [Fact] + public void FromConnectionString_WithInvalidAuthType_ShouldThrowArgumentException() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=InvalidAuth;TaskHub={ValidTaskHub}"; + + // Act & Assert + var action = () => DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + action.Should().Throw() + .WithMessage("*contains an unsupported authentication type*"); + } + + [Fact] + public void FromConnectionString_WithMissingRequiredProperties_ShouldThrowArgumentNullException() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure"; // Missing TaskHub + + // Act & Assert + var action = () => DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + action.Should().Throw(); + } + + [Fact] + public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=None;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be(ValidEndpoint); + options.TaskHubName.Should().Be(ValidTaskHub); + options.Credential.Should().BeNull(); + } + + [Fact] + public void DefaultProperties_ShouldHaveExpectedValues() + { + // Arrange & Act + var options = new DurableTaskSchedulerClientOptions(); + + // Assert + options.ResourceId.Should().Be("https://durabletask.io"); + options.AllowInsecureCredentials.Should().BeFalse(); + } + + [Fact] + public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() + { + // Arrange + var options = new DurableTaskSchedulerClientOptions + { + EndpointAddress = $"https://{ValidEndpoint}", + TaskHubName = ValidTaskHub, + Credential = new DefaultAzureCredential() + }; + + // Act + var channel = options.CreateChannel(); + + // Assert + channel.Should().NotBeNull(); + } + + [Fact] + public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() + { + // Arrange + var options = new DurableTaskSchedulerClientOptions + { + EndpointAddress = $"http://{ValidEndpoint}", + TaskHubName = ValidTaskHub, + AllowInsecureCredentials = true + }; + + // Act + var channel = options.CreateChannel(); + + // Assert + channel.Should().NotBeNull(); + } + + [Fact] + public void FromConnectionString_WithInvalidEndpoint_ShouldThrowArgumentException() + { + // Arrange + var connectionString = "Endpoint=not a valid endpoint;Authentication=DefaultAzure;TaskHub=testhub;"; + + // Act & Assert + var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + var action = () => options.CreateChannel(); + action.Should().Throw() + .WithMessage("Invalid URI: The hostname could not be parsed."); + } + + [Fact] + public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() + { + // Arrange + string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; + + // Act + var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + + // Assert + options.EndpointAddress.Should().Be(ValidEndpoint); + } + + [Fact] + public void CreateChannel_ShouldAddHttpsPrefix() + { + // Arrange + var options = new DurableTaskSchedulerClientOptions + { + EndpointAddress = ValidEndpoint, + TaskHubName = ValidTaskHub, + Credential = new DefaultAzureCredential() + }; + + // Act + var channel = options.CreateChannel(); + + // Assert + channel.Should().NotBeNull(); + // Note: We can't directly test the endpoint in the channel as it's not exposed + } +} diff --git a/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs b/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs index 525342b6..1be95032 100644 --- a/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs +++ b/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs @@ -3,8 +3,11 @@ using System.Threading.Tasks; using Azure.Core; using FluentAssertions; +using Microsoft.DurableTask; using Moq; using Xunit; +using System.Reflection; +using DotNext; namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; @@ -90,7 +93,7 @@ public async Task Constructor_WithNullCredential_ShouldThrowNullReferenceExcepti // Act & Assert // TODO: The constructor should validate its parameters and throw ArgumentNullException, // but currently it allows null parameters and throws NullReferenceException when used. - var action = () => cache.GetTokenAsync(cancellationToken); + Func action = () => cache.GetTokenAsync(cancellationToken); await action.Should().ThrowAsync(); } diff --git a/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj b/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj index 8174e5c5..35864cba 100644 --- a/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj +++ b/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj @@ -4,15 +4,17 @@ net6.0 + + + + + + - - - - diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs index 2dd901b9..1ce3d803 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -78,7 +78,7 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx var provider = services.BuildServiceProvider(); var ex = Assert.Throws(() => { - var options = provider.GetRequiredService>().Value; + var options = provider.GetRequiredService>().Value; }); Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); } @@ -145,7 +145,7 @@ public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() // Assert var provider = services.BuildServiceProvider(); - var optionsMonitor = provider.GetService>(); + var optionsMonitor = provider.GetService>(); optionsMonitor.Should().NotBeNull(); var options = optionsMonitor!.Get("CustomName"); options.Should().NotBeNull(); @@ -159,7 +159,7 @@ public void ConfigureGrpcChannel_ShouldConfigureWorkerOptions() { // Arrange var services = new ServiceCollection(); - services.AddOptions() + services.AddOptions() .Configure(options => { options.EndpointAddress = $"https://{ValidEndpoint}"; @@ -168,7 +168,7 @@ public void ConfigureGrpcChannel_ShouldConfigureWorkerOptions() }); var provider = services.BuildServiceProvider(); - var schedulerOptions = provider.GetRequiredService>(); + var schedulerOptions = provider.GetRequiredService>(); var configureGrpcChannel = new DurableTaskSchedulerWorkerExtensions.ConfigureGrpcChannel(schedulerOptions); // Act diff --git a/test/Shared/AzureManaged.Tests/DurableTaskSchedulerOptionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs similarity index 84% rename from test/Shared/AzureManaged.Tests/DurableTaskSchedulerOptionsTests.cs rename to test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs index 4c266cc1..1858fa3b 100644 --- a/test/Shared/AzureManaged.Tests/DurableTaskSchedulerOptionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs @@ -8,7 +8,7 @@ namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; -public class DurableTaskSchedulerOptionsTests +public class DurableTaskSchedulerWorkerOptionsTests { private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; private const string ValidTaskHub = "testhub"; @@ -20,7 +20,7 @@ public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -36,7 +36,7 @@ public void FromConnectionString_WithManagedIdentity_ShouldCreateValidInstance() string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -53,7 +53,7 @@ public void FromConnectionString_WithWorkloadIdentity_ShouldCreateValidInstance( string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={clientId};TenantId={tenantId};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -71,7 +71,7 @@ public void FromConnectionString_WithValidAuthTypes_ShouldCreateValidInstance(st string connectionString = $"Endpoint={ValidEndpoint};Authentication={authType};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -86,7 +86,7 @@ public void FromConnectionString_WithInvalidAuthType_ShouldThrowArgumentExceptio string connectionString = $"Endpoint={ValidEndpoint};Authentication=InvalidAuth;TaskHub={ValidTaskHub}"; // Act & Assert - var action = () => DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var action = () => DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); action.Should().Throw() .WithMessage("*contains an unsupported authentication type*"); } @@ -98,7 +98,7 @@ public void FromConnectionString_WithMissingRequiredProperties_ShouldThrowArgume string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure"; // Missing TaskHub // Act & Assert - var action = () => DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var action = () => DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); action.Should().Throw(); } @@ -109,7 +109,7 @@ public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential string connectionString = $"Endpoint={ValidEndpoint};Authentication=None;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -121,7 +121,7 @@ public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential public void DefaultProperties_ShouldHaveExpectedValues() { // Arrange & Act - var options = new DurableTaskSchedulerOptions(); + var options = new DurableTaskSchedulerWorkerOptions(); // Assert options.ResourceId.Should().Be("https://durabletask.io"); @@ -135,7 +135,7 @@ public void DefaultProperties_ShouldHaveExpectedValues() public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() { // Arrange - var options = new DurableTaskSchedulerOptions + var options = new DurableTaskSchedulerWorkerOptions { EndpointAddress = $"https://{ValidEndpoint}", TaskHubName = ValidTaskHub, @@ -153,7 +153,7 @@ public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() { // Arrange - var options = new DurableTaskSchedulerOptions + var options = new DurableTaskSchedulerWorkerOptions { EndpointAddress = $"http://{ValidEndpoint}", TaskHubName = ValidTaskHub, @@ -174,7 +174,7 @@ public void FromConnectionString_WithInvalidEndpoint_ShouldThrowArgumentExceptio var connectionString = "Endpoint=not a valid endpoint;Authentication=DefaultAzure;TaskHub=testhub;"; // Act & Assert - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); var action = () => options.CreateChannel(); action.Should().Throw() .WithMessage("Invalid URI: The hostname could not be parsed."); @@ -187,7 +187,7 @@ public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerOptions.FromConnectionString(connectionString); + var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -197,7 +197,7 @@ public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() public void CreateChannel_ShouldAddHttpsPrefix() { // Arrange - var options = new DurableTaskSchedulerOptions + var options = new DurableTaskSchedulerWorkerOptions { EndpointAddress = ValidEndpoint, TaskHubName = ValidTaskHub, diff --git a/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj b/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj index 6e43cac1..6fb22508 100644 --- a/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj +++ b/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj @@ -10,8 +10,10 @@ - + + + From c697fbc59aab3e3bf841c8546358f3bd11a1ebcc Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:33:51 -0800 Subject: [PATCH 50/58] refactor --- .../DurableTaskSchedulerClientExtensions.cs | 53 +++++++++++-------- .../DurableTaskSchedulerWorkerExtensions.cs | 53 +++++++++++-------- .../DurableTaskSchedulerWorkerOptions.cs | 4 +- ...rableTaskSchedulerClientExtensionsTests.cs | 2 +- 4 files changed, 67 insertions(+), 45 deletions(-) diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index a5432bf8..ecfb25ae 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -29,20 +29,12 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - builder.Services.AddOptions(builder.Name) - .Configure(options => - { - options.EndpointAddress = endpointAddress; - options.TaskHubName = taskHubName; - options.Credential = credential; - }) - .Configure(configure ?? (_ => { })) - .ValidateDataAnnotations() - .ValidateOnStart(); - - builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); - builder.UseGrpc(_ => { }); + ConfigureSchedulerOptions(builder, options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }, configure); } /// @@ -57,15 +49,34 @@ public static void UseDurableTaskScheduler( Action? configure = null) { var connectionOptions = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + ConfigureSchedulerOptions(builder, options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }, configure); + } + /// + /// Configures Durable Task client to use the Azure Durable Task Scheduler service using configuration options. + /// + /// The Durable Task client builder to configure. + /// Callback to configure DurableTaskSchedulerClientOptions. + public static void UseDurableTaskScheduler( + this IDurableTaskClientBuilder builder, + Action? configure = null) + { + ConfigureSchedulerOptions(builder, _ => { }, configure); + } + + private static void ConfigureSchedulerOptions( + IDurableTaskClientBuilder builder, + Action initialConfig, + Action? additionalConfig) + { builder.Services.AddOptions(builder.Name) - .Configure(options => - { - options.EndpointAddress = connectionOptions.EndpointAddress; - options.TaskHubName = connectionOptions.TaskHubName; - options.Credential = connectionOptions.Credential; - }) - .Configure(configure ?? (_ => { })) + .Configure(initialConfig) + .Configure(additionalConfig ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart(); diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index 300e1492..0b7b3cc0 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -30,20 +30,12 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - builder.Services.AddOptions(builder.Name) - .Configure(options => - { - options.EndpointAddress = endpointAddress; - options.TaskHubName = taskHubName; - options.Credential = credential; - }) - .Configure(configure ?? (_ => { })) - .ValidateDataAnnotations() - .ValidateOnStart(); - - builder.Services.TryAddEnumerable( - ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); - builder.UseGrpc(_ => { }); + ConfigureSchedulerOptions(builder, options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }, configure); } /// @@ -58,15 +50,34 @@ public static void UseDurableTaskScheduler( Action? configure = null) { var connectionOptions = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + ConfigureSchedulerOptions(builder, options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }, configure); + } + /// + /// Configures Durable Task worker to use the Azure Durable Task Scheduler service using configuration options. + /// + /// The Durable Task worker builder to configure. + /// Callback to configure DurableTaskSchedulerWorkerOptions. + public static void UseDurableTaskScheduler( + this IDurableTaskWorkerBuilder builder, + Action? configure = null) + { + ConfigureSchedulerOptions(builder, _ => { }, configure); + } + + private static void ConfigureSchedulerOptions( + IDurableTaskWorkerBuilder builder, + Action initialConfig, + Action? additionalConfig) + { builder.Services.AddOptions(builder.Name) - .Configure(options => - { - options.EndpointAddress = connectionOptions.EndpointAddress; - options.TaskHubName = connectionOptions.TaskHubName; - options.Credential = connectionOptions.Credential; - }) - .Configure(configure ?? (_ => { })) + .Configure(initialConfig) + .Configure(additionalConfig ?? (_ => { })) .ValidateDataAnnotations() .ValidateOnStart(); diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs index a7f79510..df704207 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs @@ -79,8 +79,8 @@ internal static DurableTaskSchedulerWorkerOptions FromConnectionString( /// A configured instance that can be used to make gRPC calls. public GrpcChannel CreateChannel() { - Check.NotNullOrEmpty(this.EndpointAddress, nameof(this.EndpointAddress)); - Check.NotNullOrEmpty(this.TaskHubName, nameof(this.TaskHubName)); + Verify.NotNull(this.EndpointAddress, nameof(this.EndpointAddress)); + Verify.NotNull(this.TaskHubName, nameof(this.TaskHubName)); string taskHubName = this.TaskHubName; string endpoint = !this.EndpointAddress.Contains("://") ? $"https://{this.EndpointAddress}" diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index e05e352e..f9131c34 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel.DataAnnotations; using Azure.Core; using Azure.Identity; using FluentAssertions; @@ -10,7 +11,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; -using System.ComponentModel.DataAnnotations; using Xunit; namespace Microsoft.DurableTask.Client.AzureManaged.Tests; From 55eea5494dd2dbfd2e36cfbcde7fcdb5dc049d48 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:59:35 -0800 Subject: [PATCH 51/58] fix --- .../AzureManaged/DurableTaskSchedulerClientOptions.cs | 8 ++++---- .../AzureManaged/DurableTaskSchedulerWorkerExtensions.cs | 4 ++-- .../AzureManaged/DurableTaskSchedulerWorkerOptions.cs | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs index 30ab86a3..12b08497 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs @@ -51,20 +51,20 @@ public class DurableTaskSchedulerClientOptions public bool AllowInsecureCredentials { get; set; } /// - /// Creates a new instance of from a connection string. + /// Creates a new instance of from a connection string. /// /// The connection string to parse. - /// A new instance of . + /// A new instance of . public static DurableTaskSchedulerClientOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } /// - /// Creates a new instance of from a parsed connection string. + /// Creates a new instance of from a parsed connection string. /// /// The connection string to parse. - /// A new instance of . + /// A new instance of . internal static DurableTaskSchedulerClientOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) => new() { diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index 0b7b3cc0..64219a8e 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -22,7 +22,7 @@ public static class DurableTaskSchedulerWorkerExtensions /// The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io". /// The name of the task hub resource associated with the Durable Task Scheduler resource. /// The credential used to authenticate with the Durable Task Scheduler task hub resource. - /// Optional callback to dynamically configure DurableTaskSchedulerOptions. + /// Optional callback to dynamically configure DurableTaskSchedulerWorkerOptions. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string endpointAddress, @@ -43,7 +43,7 @@ public static void UseDurableTaskScheduler( /// /// The Durable Task worker builder to configure. /// The connection string used to connect to the Durable Task Scheduler service. - /// Optional callback to dynamically configure DurableTaskSchedulerOptions. + /// Optional callback to dynamically configure DurableTaskSchedulerWorkerOptions. public static void UseDurableTaskScheduler( this IDurableTaskWorkerBuilder builder, string connectionString, diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs index df704207..b5df6fa6 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs @@ -51,20 +51,20 @@ public class DurableTaskSchedulerWorkerOptions public bool AllowInsecureCredentials { get; set; } /// - /// Creates a new instance of from a connection string. + /// Creates a new instance of from a connection string. /// /// The connection string to parse. - /// A new instance of . + /// A new instance of . public static DurableTaskSchedulerWorkerOptions FromConnectionString(string connectionString) { return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } /// - /// Creates a new instance of from a parsed connection string. + /// Creates a new instance of from a parsed connection string. /// /// The connection string to parse. - /// A new instance of . + /// A new instance of . internal static DurableTaskSchedulerWorkerOptions FromConnectionString( DurableTaskSchedulerConnectionString connectionString) => new() { From 1ede0d4352014d76207cef961e8fdf8a4cfc60fe Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Tue, 14 Jan 2025 23:51:11 -0800 Subject: [PATCH 52/58] fb --- .editorconfig | 8 +- .../AzureManaged/Client.AzureManaged.csproj | 2 +- .../DurableTaskSchedulerClientExtensions.cs | 36 ++++--- .../DurableTaskSchedulerClientOptions.cs | 38 +++---- .../DurableTaskSchedulerWorkerExtensions.cs | 37 ++++--- .../DurableTaskSchedulerWorkerOptions.cs | 31 +++--- .../AzureManaged/Worker.AzureManaged.csproj | 2 +- .../Client.AzureManaged.Tests.csproj | 3 +- ...rableTaskSchedulerClientExtensionsTests.cs | 101 +++++++----------- .../DurableTaskSchedulerClientOptionsTests.cs | 41 ++++--- .../AccessTokenCacheTests.cs | 81 +++++++------- ...rableTaskSchedulerConnectionStringTests.cs | 39 ++++--- .../Shared.AzureManaged.Tests.csproj | 2 +- ...rableTaskSchedulerWorkerExtensionsTests.cs | 101 +++++++----------- .../DurableTaskSchedulerWorkerOptionsTests.cs | 41 ++++--- .../Worker.AzureManaged.Tests.csproj | 2 +- 16 files changed, 254 insertions(+), 311 deletions(-) diff --git a/.editorconfig b/.editorconfig index 7c1faebc..23881078 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories +'# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true # XML project files @@ -232,3 +232,9 @@ dotnet_diagnostic.SA1400.severity = none # Access modifier must be declared dotnet_diagnostic.SA1402.severity = none # File may only contain a single type dotnet_diagnostic.SA1633.severity = warning # File must have header -- TODO: replace with IDE0073 eventually dotnet_diagnostic.SA1649.severity = none # File name must match type name + +# Enforce explicit types instead of `var` +dotnet_style_prefer_var_for_built_in_types = false:error +dotnet_style_prefer_var_for_simple_types = false:error +dotnet_style_prefer_var_when_type_is_apparent = false:error +dotnet_style_prefer_var_when_type_is_not_apparent = false:error diff --git a/src/Client/AzureManaged/Client.AzureManaged.csproj b/src/Client/AzureManaged/Client.AzureManaged.csproj index 29f0dfd8..0b80078b 100644 --- a/src/Client/AzureManaged/Client.AzureManaged.csproj +++ b/src/Client/AzureManaged/Client.AzureManaged.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0 Azure Managed extensions for the Durable Task Framework client. true diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index ecfb25ae..290b09a8 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -29,12 +29,15 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - ConfigureSchedulerOptions(builder, options => - { - options.EndpointAddress = endpointAddress; - options.TaskHubName = taskHubName; - options.Credential = credential; - }, configure); + ConfigureSchedulerOptions( + builder, + options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }, + configure); } /// @@ -49,12 +52,15 @@ public static void UseDurableTaskScheduler( Action? configure = null) { var connectionOptions = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); - ConfigureSchedulerOptions(builder, options => - { - options.EndpointAddress = connectionOptions.EndpointAddress; - options.TaskHubName = connectionOptions.TaskHubName; - options.Credential = connectionOptions.Credential; - }, configure); + ConfigureSchedulerOptions( + builder, + options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }, + configure); } /// @@ -69,7 +75,7 @@ public static void UseDurableTaskScheduler( ConfigureSchedulerOptions(builder, _ => { }, configure); } - private static void ConfigureSchedulerOptions( + static void ConfigureSchedulerOptions( IDurableTaskClientBuilder builder, Action initialConfig, Action? additionalConfig) @@ -86,11 +92,11 @@ private static void ConfigureSchedulerOptions( } /// - /// Internal configuration class that sets up gRPC channels for client options + /// Configuration class that sets up gRPC channels for client options /// using the provided Durable Task Scheduler options. /// /// Monitor for accessing the current scheduler options configuration. - internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions { /// diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs index 12b08497..8278a2d8 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.ComponentModel.DataAnnotations; -using System.Globalization; using Azure.Core; using Azure.Identity; using Grpc.Core; @@ -38,15 +37,9 @@ public class DurableTaskSchedulerClientOptions /// public string ResourceId { get; set; } = "https://durabletask.io"; - /// - /// Gets or sets the worker ID used to identify the worker instance. - /// The default value is a string containing the machine name, process ID, and a unique identifier. - /// - public string WorkerId { get; set; } = $"{Environment.MachineName},{Environment.ProcessId},{Guid.NewGuid():N}"; - /// /// Gets or sets a value indicating whether to allow insecure channel credentials. - /// This should only be set to true in development/testing scenarios. + /// This should only be set to true in local development/testing scenarios. /// public bool AllowInsecureCredentials { get; set; } @@ -60,19 +53,6 @@ public static DurableTaskSchedulerClientOptions FromConnectionString(string conn return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } - /// - /// Creates a new instance of from a parsed connection string. - /// - /// The connection string to parse. - /// A new instance of . - internal static DurableTaskSchedulerClientOptions FromConnectionString( - DurableTaskSchedulerConnectionString connectionString) => new() - { - EndpointAddress = connectionString.Endpoint, - TaskHubName = connectionString.TaskHubName, - Credential = GetCredentialFromConnectionString(connectionString), - }; - /// /// Creates a gRPC channel for communicating with the Durable Task Scheduler service. /// @@ -96,7 +76,6 @@ this.Credential is not null async (context, metadata) => { metadata.Add("taskhub", taskHubName); - metadata.Add("workerid", this.WorkerId); if (cache == null) { return; @@ -117,12 +96,25 @@ this.Credential is not null }); } + /// + /// Creates a new instance of from a parsed connection string. + /// + /// The connection string to parse. + /// A new instance of . + internal static DurableTaskSchedulerClientOptions FromConnectionString( + DurableTaskSchedulerConnectionString connectionString) => new() + { + EndpointAddress = connectionString.Endpoint, + TaskHubName = connectionString.TaskHubName, + Credential = GetCredentialFromConnectionString(connectionString), + }; + static TokenCredential? GetCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) { string authType = connectionString.Authentication; // Parse the supported auth types, in a case-insensitive way and without spaces - switch (authType.ToLower(CultureInfo.InvariantCulture).Replace(" ", string.Empty)) + switch (authType.ToLowerInvariant()) { case "defaultazure": return new DefaultAzureCredential(); diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index 64219a8e..71d7aa1e 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Azure.Core; -using Microsoft.DurableTask.Worker; using Microsoft.DurableTask.Worker.Grpc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -30,12 +29,15 @@ public static void UseDurableTaskScheduler( TokenCredential credential, Action? configure = null) { - ConfigureSchedulerOptions(builder, options => - { - options.EndpointAddress = endpointAddress; - options.TaskHubName = taskHubName; - options.Credential = credential; - }, configure); + ConfigureSchedulerOptions( + builder, + options => + { + options.EndpointAddress = endpointAddress; + options.TaskHubName = taskHubName; + options.Credential = credential; + }, + configure); } /// @@ -50,12 +52,15 @@ public static void UseDurableTaskScheduler( Action? configure = null) { var connectionOptions = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); - ConfigureSchedulerOptions(builder, options => - { - options.EndpointAddress = connectionOptions.EndpointAddress; - options.TaskHubName = connectionOptions.TaskHubName; - options.Credential = connectionOptions.Credential; - }, configure); + ConfigureSchedulerOptions( + builder, + options => + { + options.EndpointAddress = connectionOptions.EndpointAddress; + options.TaskHubName = connectionOptions.TaskHubName; + options.Credential = connectionOptions.Credential; + }, + configure); } /// @@ -70,7 +75,7 @@ public static void UseDurableTaskScheduler( ConfigureSchedulerOptions(builder, _ => { }, configure); } - private static void ConfigureSchedulerOptions( + static void ConfigureSchedulerOptions( IDurableTaskWorkerBuilder builder, Action initialConfig, Action? additionalConfig) @@ -87,11 +92,11 @@ private static void ConfigureSchedulerOptions( } /// - /// Internal configuration class that sets up gRPC channels for worker options + /// Configuration class that sets up gRPC channels for worker options /// using the provided Durable Task Scheduler options. /// /// Monitor for accessing the current scheduler options configuration. - internal class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : + class ConfigureGrpcChannel(IOptionsMonitor schedulerOptions) : IConfigureNamedOptions { /// diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs index b5df6fa6..4cc32735 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.ComponentModel.DataAnnotations; -using System.Globalization; using Azure.Core; using Azure.Identity; using Grpc.Core; @@ -46,7 +45,7 @@ public class DurableTaskSchedulerWorkerOptions /// /// Gets or sets a value indicating whether to allow insecure channel credentials. - /// This should only be set to true in development/testing scenarios. + /// This should only be set to true in local development/testing scenarios. /// public bool AllowInsecureCredentials { get; set; } @@ -60,19 +59,6 @@ public static DurableTaskSchedulerWorkerOptions FromConnectionString(string conn return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString)); } - /// - /// Creates a new instance of from a parsed connection string. - /// - /// The connection string to parse. - /// A new instance of . - internal static DurableTaskSchedulerWorkerOptions FromConnectionString( - DurableTaskSchedulerConnectionString connectionString) => new() - { - EndpointAddress = connectionString.Endpoint, - TaskHubName = connectionString.TaskHubName, - Credential = GetCredentialFromConnectionString(connectionString), - }; - /// /// Creates a gRPC channel for communicating with the Durable Task Scheduler service. /// @@ -117,12 +103,25 @@ this.Credential is not null }); } + /// + /// Creates a new instance of from a parsed connection string. + /// + /// The connection string to parse. + /// A new instance of . + internal static DurableTaskSchedulerWorkerOptions FromConnectionString( + DurableTaskSchedulerConnectionString connectionString) => new() + { + EndpointAddress = connectionString.Endpoint, + TaskHubName = connectionString.TaskHubName, + Credential = GetCredentialFromConnectionString(connectionString), + }; + static TokenCredential? GetCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString) { string authType = connectionString.Authentication; // Parse the supported auth types, in a case-insensitive way and without spaces - switch (authType.ToLower(CultureInfo.InvariantCulture).Replace(" ", string.Empty)) + switch (authType.ToLowerInvariant()) { case "defaultazure": return new DefaultAzureCredential(); diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index 4985279f..74df2840 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -1,7 +1,7 @@  - net6.0 + net6.0 Azure Managed extensions for the Durable Task Framework worker. true diff --git a/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj b/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj index 309b5a3f..d7f4726c 100644 --- a/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj +++ b/test/Client/AzureManaged.Tests/Client.AzureManaged.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0 @@ -10,7 +10,6 @@ - diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index f9131c34..76eb4ab2 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -1,12 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.ComponentModel.DataAnnotations; using Azure.Core; using Azure.Identity; using FluentAssertions; -using Grpc.Net.Client; -using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Client.Grpc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -17,24 +14,24 @@ namespace Microsoft.DurableTask.Client.AzureManaged.Tests; public class DurableTaskSchedulerClientExtensionsTests { - private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; - private const string ValidTaskHub = "testhub"; + const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + const string ValidTaskHub = "testhub"; [Fact] public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCorrectly() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); + DefaultAzureCredential credential = new DefaultAzureCredential(); // Act mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); + ServiceProvider provider = services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); options.Should().NotBeNull(); } @@ -42,8 +39,8 @@ public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCor public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectly() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; @@ -51,8 +48,8 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl mockBuilder.Object.UseDurableTaskScheduler(connectionString); // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); + ServiceProvider provider = services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); options.Should().NotBeNull(); } @@ -62,23 +59,23 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); + DefaultAzureCredential credential = new DefaultAzureCredential(); // Act - var action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); // Assert action.Should().NotThrow(); // The validation happens when building the service provider - + if (endpoint == null || taskHub == null) { - var provider = services.BuildServiceProvider(); - var ex = Assert.Throws(() => + ServiceProvider provider = services.BuildServiceProvider(); + OptionsValidationException ex = Assert.Throws(() => { - var options = provider.GetRequiredService>().Value; + DurableTaskSchedulerClientOptions options = provider.GetRequiredService>().Value; }); Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); } @@ -88,13 +85,13 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); TokenCredential? credential = null; // Act & Assert - var action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); action.Should().NotThrow(); } @@ -102,13 +99,13 @@ public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgumentException() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - var connectionString = "This is not a valid=connection string format"; + string connectionString = "This is not a valid=connection string format"; // Act - var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); // Assert action.Should().Throw() @@ -121,12 +118,12 @@ public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgum public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string connectionString) { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); // Act & Assert - var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); action.Should().Throw(); } @@ -134,49 +131,23 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); mockBuilder.Setup(b => b.Name).Returns("CustomName"); - var credential = new DefaultAzureCredential(); + DefaultAzureCredential credential = new DefaultAzureCredential(); // Act mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); // Assert - var provider = services.BuildServiceProvider(); - var optionsMonitor = provider.GetService>(); + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor? optionsMonitor = provider.GetService>(); optionsMonitor.Should().NotBeNull(); - var options = optionsMonitor!.Get("CustomName"); + DurableTaskSchedulerClientOptions options = optionsMonitor!.Get("CustomName"); options.Should().NotBeNull(); options.EndpointAddress.Should().Be(ValidEndpoint); // The https:// prefix is added by CreateChannel, not in the extension method options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeOfType(); } - - [Fact] - public void ConfigureGrpcChannel_ShouldConfigureClientOptions() - { - // Arrange - var services = new ServiceCollection(); - services.AddOptions() - .Configure(options => - { - options.EndpointAddress = $"https://{ValidEndpoint}"; - options.TaskHubName = ValidTaskHub; - options.Credential = new DefaultAzureCredential(); - }); - - var provider = services.BuildServiceProvider(); - var schedulerOptions = provider.GetRequiredService>(); - var configureGrpcChannel = new DurableTaskSchedulerClientExtensions.ConfigureGrpcChannel(schedulerOptions); - - // Act - var clientOptions = new GrpcDurableTaskClientOptions(); - configureGrpcChannel.Configure(clientOptions); - - // Assert - clientOptions.Channel.Should().NotBeNull(); - clientOptions.Channel.Should().BeOfType(); - } -} +} diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs index 7bc1577c..ff0fc43e 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Azure.Core; using Azure.Identity; using FluentAssertions; using Xunit; @@ -10,8 +9,8 @@ namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; public class DurableTaskSchedulerClientOptionsTests { - private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; - private const string ValidTaskHub = "testhub"; + const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + const string ValidTaskHub = "testhub"; [Fact] public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() @@ -20,7 +19,7 @@ public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -36,7 +35,7 @@ public void FromConnectionString_WithManagedIdentity_ShouldCreateValidInstance() string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -53,7 +52,7 @@ public void FromConnectionString_WithWorkloadIdentity_ShouldCreateValidInstance( string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={clientId};TenantId={tenantId};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -71,7 +70,7 @@ public void FromConnectionString_WithValidAuthTypes_ShouldCreateValidInstance(st string connectionString = $"Endpoint={ValidEndpoint};Authentication={authType};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -86,7 +85,7 @@ public void FromConnectionString_WithInvalidAuthType_ShouldThrowArgumentExceptio string connectionString = $"Endpoint={ValidEndpoint};Authentication=InvalidAuth;TaskHub={ValidTaskHub}"; // Act & Assert - var action = () => DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + Action action = () => DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); action.Should().Throw() .WithMessage("*contains an unsupported authentication type*"); } @@ -98,7 +97,7 @@ public void FromConnectionString_WithMissingRequiredProperties_ShouldThrowArgume string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure"; // Missing TaskHub // Act & Assert - var action = () => DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + Action action = () => DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); action.Should().Throw(); } @@ -109,7 +108,7 @@ public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential string connectionString = $"Endpoint={ValidEndpoint};Authentication=None;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -121,7 +120,7 @@ public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential public void DefaultProperties_ShouldHaveExpectedValues() { // Arrange & Act - var options = new DurableTaskSchedulerClientOptions(); + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions(); // Assert options.ResourceId.Should().Be("https://durabletask.io"); @@ -132,7 +131,7 @@ public void DefaultProperties_ShouldHaveExpectedValues() public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() { // Arrange - var options = new DurableTaskSchedulerClientOptions + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions { EndpointAddress = $"https://{ValidEndpoint}", TaskHubName = ValidTaskHub, @@ -140,7 +139,7 @@ public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() }; // Act - var channel = options.CreateChannel(); + Grpc.Core.ChannelBase channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -150,7 +149,7 @@ public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() { // Arrange - var options = new DurableTaskSchedulerClientOptions + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions { EndpointAddress = $"http://{ValidEndpoint}", TaskHubName = ValidTaskHub, @@ -158,7 +157,7 @@ public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() }; // Act - var channel = options.CreateChannel(); + Grpc.Core.ChannelBase channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -168,11 +167,11 @@ public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() public void FromConnectionString_WithInvalidEndpoint_ShouldThrowArgumentException() { // Arrange - var connectionString = "Endpoint=not a valid endpoint;Authentication=DefaultAzure;TaskHub=testhub;"; + string connectionString = "Endpoint=not a valid endpoint;Authentication=DefaultAzure;TaskHub=testhub;"; // Act & Assert - var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); - var action = () => options.CreateChannel(); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + Action action = () => options.CreateChannel(); action.Should().Throw() .WithMessage("Invalid URI: The hostname could not be parsed."); } @@ -184,7 +183,7 @@ public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); + DurableTaskSchedulerClientOptions options = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -194,7 +193,7 @@ public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() public void CreateChannel_ShouldAddHttpsPrefix() { // Arrange - var options = new DurableTaskSchedulerClientOptions + DurableTaskSchedulerClientOptions options = new DurableTaskSchedulerClientOptions { EndpointAddress = ValidEndpoint, TaskHubName = ValidTaskHub, @@ -202,7 +201,7 @@ public void CreateChannel_ShouldAddHttpsPrefix() }; // Act - var channel = options.CreateChannel(); + Grpc.Core.ChannelBase channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); diff --git a/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs b/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs index 1be95032..4ec0c825 100644 --- a/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs +++ b/test/Shared/AzureManaged.Tests/AccessTokenCacheTests.cs @@ -1,99 +1,96 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + using Azure.Core; using FluentAssertions; -using Microsoft.DurableTask; using Moq; using Xunit; -using System.Reflection; -using DotNext; namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; public class AccessTokenCacheTests { - private readonly Mock mockCredential; - private readonly TokenRequestContext tokenRequestContext; - private readonly TimeSpan margin; - private readonly CancellationToken cancellationToken; + readonly Mock mockCredential; + readonly TokenRequestContext tokenRequestContext; + readonly TimeSpan margin; + readonly CancellationToken cancellationToken; public AccessTokenCacheTests() { - mockCredential = new Mock(); - tokenRequestContext = new TokenRequestContext(new[] { "https://durabletask.azure.com/.default" }); - margin = TimeSpan.FromMinutes(5); - cancellationToken = CancellationToken.None; + this.mockCredential = new Mock(); + this.tokenRequestContext = new TokenRequestContext(new[] { "https://durabletask.azure.com/.default" }); + this.margin = TimeSpan.FromMinutes(5); + this.cancellationToken = CancellationToken.None; } [Fact] public async Task GetTokenAsync_WhenCalled_ShouldReturnToken() { // Arrange - var expectedToken = new AccessToken("test-token", DateTimeOffset.UtcNow.AddHours(1)); - mockCredential.Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + AccessToken expectedToken = new AccessToken("test-token", DateTimeOffset.UtcNow.AddHours(1)); + this.mockCredential.Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(expectedToken); - var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, margin); + AccessTokenCache cache = new AccessTokenCache(this.mockCredential.Object, this.tokenRequestContext, this.margin); // Act - var token = await cache.GetTokenAsync(cancellationToken); + AccessToken token = await cache.GetTokenAsync(this.cancellationToken); // Assert token.Should().Be(expectedToken); - mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + this.mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task GetTokenAsync_WhenTokenExpired_ShouldRequestNewToken() { // Arrange - var expiredToken = new AccessToken("expired-token", DateTimeOffset.UtcNow.AddMinutes(-5)); - var newToken = new AccessToken("new-token", DateTimeOffset.UtcNow.AddHours(1)); - var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, margin); + AccessToken expiredToken = new AccessToken("expired-token", DateTimeOffset.UtcNow.AddMinutes(-5)); + AccessToken newToken = new AccessToken("new-token", DateTimeOffset.UtcNow.AddHours(1)); + AccessTokenCache cache = new AccessTokenCache(this.mockCredential.Object, this.tokenRequestContext, this.margin); - mockCredential.SetupSequence(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + this.mockCredential.SetupSequence(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(expiredToken) .ReturnsAsync(newToken); // Act - var firstToken = await cache.GetTokenAsync(cancellationToken); - var secondToken = await cache.GetTokenAsync(cancellationToken); + AccessToken firstToken = await cache.GetTokenAsync(this.cancellationToken); + AccessToken secondToken = await cache.GetTokenAsync(this.cancellationToken); // Assert firstToken.Should().Be(expiredToken); secondToken.Should().Be(newToken); - mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + this.mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); } [Fact] public async Task GetTokenAsync_WhenTokenValid_ShouldReturnCachedToken() { // Arrange - var validToken = new AccessToken("valid-token", DateTimeOffset.UtcNow.AddHours(1)); - mockCredential.Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + AccessToken validToken = new AccessToken("valid-token", DateTimeOffset.UtcNow.AddHours(1)); + this.mockCredential.Setup(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(validToken); - var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, margin); + AccessTokenCache cache = new AccessTokenCache(this.mockCredential.Object, this.tokenRequestContext, this.margin); // Act - var firstToken = await cache.GetTokenAsync(cancellationToken); - var secondToken = await cache.GetTokenAsync(cancellationToken); + AccessToken firstToken = await cache.GetTokenAsync(this.cancellationToken); + AccessToken secondToken = await cache.GetTokenAsync(this.cancellationToken); // Assert firstToken.Should().Be(validToken); secondToken.Should().Be(validToken); - mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); + this.mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Constructor_WithNullCredential_ShouldThrowNullReferenceException() { // Arrange - var cache = new AccessTokenCache(null!, tokenRequestContext, margin); + AccessTokenCache cache = new AccessTokenCache(null!, this.tokenRequestContext, this.margin); // Act & Assert // TODO: The constructor should validate its parameters and throw ArgumentNullException, // but currently it allows null parameters and throws NullReferenceException when used. - Func action = () => cache.GetTokenAsync(cancellationToken); + Func action = () => cache.GetTokenAsync(this.cancellationToken); await action.Should().ThrowAsync(); } @@ -101,22 +98,22 @@ public async Task Constructor_WithNullCredential_ShouldThrowNullReferenceExcepti public async Task GetTokenAsync_WhenTokenNearExpiry_ShouldRequestNewToken() { // Arrange - var expiryTime = DateTimeOffset.UtcNow.AddMinutes(10); - var nearExpiryToken = new AccessToken("near-expiry-token", expiryTime); - var newToken = new AccessToken("new-token", expiryTime.AddHours(1)); - var cache = new AccessTokenCache(mockCredential.Object, tokenRequestContext, TimeSpan.FromMinutes(15)); + DateTimeOffset expiryTime = DateTimeOffset.UtcNow.AddMinutes(10); + AccessToken nearExpiryToken = new AccessToken("near-expiry-token", expiryTime); + AccessToken newToken = new AccessToken("new-token", expiryTime.AddHours(1)); + AccessTokenCache cache = new AccessTokenCache(this.mockCredential.Object, this.tokenRequestContext, TimeSpan.FromMinutes(15)); - mockCredential.SetupSequence(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) + this.mockCredential.SetupSequence(c => c.GetTokenAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(nearExpiryToken) .ReturnsAsync(newToken); // Act - var firstToken = await cache.GetTokenAsync(cancellationToken); - var secondToken = await cache.GetTokenAsync(cancellationToken); + AccessToken firstToken = await cache.GetTokenAsync(this.cancellationToken); + AccessToken secondToken = await cache.GetTokenAsync(this.cancellationToken); // Assert firstToken.Should().Be(nearExpiryToken); secondToken.Should().Be(newToken); - mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + this.mockCredential.Verify(c => c.GetTokenAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); } } diff --git a/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs index bf5631f5..e70f3e6f 100644 --- a/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs +++ b/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs @@ -2,17 +2,16 @@ // Licensed under the MIT License. using FluentAssertions; -using System.Data.Common; using Xunit; namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; public class DurableTaskSchedulerConnectionStringTests { - private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; - private const string ValidTaskHub = "testhub"; - private const string ValidClientId = "00000000-0000-0000-0000-000000000000"; - private const string ValidTenantId = "11111111-1111-1111-1111-111111111111"; + const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + const string ValidTaskHub = "testhub"; + const string ValidClientId = "00000000-0000-0000-0000-000000000000"; + const string ValidTenantId = "11111111-1111-1111-1111-111111111111"; [Fact] public void Constructor_WithValidConnectionString_ShouldParseCorrectly() @@ -21,7 +20,7 @@ public void Constructor_WithValidConnectionString_ShouldParseCorrectly() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.Endpoint.Should().Be(ValidEndpoint); @@ -36,7 +35,7 @@ public void Constructor_WithManagedIdentity_ShouldParseClientId() string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={ValidClientId};TaskHub={ValidTaskHub}"; // Act - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.ClientId.Should().Be(ValidClientId); @@ -49,7 +48,7 @@ public void Constructor_WithWorkloadIdentity_ShouldParseAllProperties() string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={ValidClientId};TenantId={ValidTenantId};TaskHub={ValidTaskHub}"; // Act - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.ClientId.Should().Be(ValidClientId); @@ -64,7 +63,7 @@ public void Constructor_WithAdditionallyAllowedTenants_ShouldParseTenantList() string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;AdditionallyAllowedTenants={tenants};TaskHub={ValidTaskHub}"; // Act - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.AdditionallyAllowedTenants.Should().NotBeNull(); @@ -79,7 +78,7 @@ public void Constructor_WithMultipleAdditionallyAllowedTenants_ShouldParseCorrec "AdditionallyAllowedTenants=tenant1,tenant2,tenant3"; // Act - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.AdditionallyAllowedTenants.Should().NotBeNull(); @@ -95,7 +94,7 @@ public void Constructor_WithCaseInsensitivePropertyNames_ShouldParseCorrectly() $"clientid={ValidClientId};tenantid={ValidTenantId}"; // Act - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.Endpoint.Should().Be(ValidEndpoint); @@ -109,10 +108,10 @@ public void Constructor_WithCaseInsensitivePropertyNames_ShouldParseCorrectly() public void Constructor_WithInvalidConnectionStringFormat_ShouldThrowFormatException() { // Arrange - var connectionString = "This is not a valid=connection string format"; + string connectionString = "This is not a valid=connection string format"; // Act & Assert - var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; + Action action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; action.Should().Throw() .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); } @@ -121,10 +120,10 @@ public void Constructor_WithInvalidConnectionStringFormat_ShouldThrowFormatExcep public void Constructor_WithEmptyConnectionString_ShouldThrowArgumentNullException() { // Arrange - var connectionString = string.Empty; + string connectionString = string.Empty; // Act & Assert - var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; + Action action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; action.Should().Throw() .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); } @@ -136,7 +135,7 @@ public void Constructor_WithDuplicateKeys_ShouldUseLastValue() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub=hub1;TaskHub=hub2"; // Act - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.TaskHubName.Should().Be("hub2"); @@ -149,7 +148,7 @@ public void Constructor_WithMissingRequiredProperties_ShouldThrowArgumentNullExc string connectionString = $"Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Missing Endpoint // Act & Assert - var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; + Action action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; action.Should().Throw() .WithMessage("*'Endpoint' property*"); } @@ -161,7 +160,7 @@ public void Constructor_WithInvalidConnectionString_ShouldThrowArgumentException string connectionString = "This is not a valid connection string"; // Act & Assert - var action = () => new DurableTaskSchedulerConnectionString(connectionString); + Action action = () => new DurableTaskSchedulerConnectionString(connectionString); action.Should().Throw() .WithMessage("*Format of the initialization string does not conform to specification*"); } @@ -172,7 +171,7 @@ public void Constructor_WithInvalidConnectionString_ShouldThrowArgumentException public void Constructor_WithNullOrEmptyConnectionString_ShouldThrowArgumentNullException(string? connectionString) { // Act & Assert - var action = () => _ = new DurableTaskSchedulerConnectionString(connectionString!).Endpoint; + Action action = () => _ = new DurableTaskSchedulerConnectionString(connectionString!).Endpoint; action.Should().Throw(); } @@ -181,7 +180,7 @@ public void GetValue_WithNonExistentProperty_ShouldReturnNull() { // Arrange string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; - var parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); + DurableTaskSchedulerConnectionString parsedConnectionString = new DurableTaskSchedulerConnectionString(connectionString); // Assert parsedConnectionString.ClientId.Should().BeNull(); diff --git a/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj b/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj index 35864cba..a30522af 100644 --- a/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj +++ b/test/Shared/AzureManaged.Tests/Shared.AzureManaged.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0 diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs index 1ce3d803..761cbcde 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -4,37 +4,34 @@ using Azure.Core; using Azure.Identity; using FluentAssertions; -using Grpc.Net.Client; -using Microsoft.DurableTask.Worker; using Microsoft.DurableTask.Worker.Grpc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Moq; -using System.ComponentModel.DataAnnotations; using Xunit; namespace Microsoft.DurableTask.Worker.AzureManaged.Tests; public class DurableTaskSchedulerWorkerExtensionsTests { - private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; - private const string ValidTaskHub = "testhub"; + const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + const string ValidTaskHub = "testhub"; [Fact] public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCorrectly() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); + DefaultAzureCredential credential = new DefaultAzureCredential(); // Act mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); + ServiceProvider provider = services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); options.Should().NotBeNull(); } @@ -42,8 +39,8 @@ public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCor public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectly() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; @@ -51,8 +48,8 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl mockBuilder.Object.UseDurableTaskScheduler(connectionString); // Assert - var provider = services.BuildServiceProvider(); - var options = provider.GetService>(); + ServiceProvider provider = services.BuildServiceProvider(); + IOptions? options = provider.GetService>(); options.Should().NotBeNull(); } @@ -62,23 +59,23 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - var credential = new DefaultAzureCredential(); + DefaultAzureCredential credential = new DefaultAzureCredential(); // Act - var action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); // Assert action.Should().NotThrow(); // The validation happens when building the service provider - + if (endpoint == null || taskHub == null) { - var provider = services.BuildServiceProvider(); - var ex = Assert.Throws(() => + ServiceProvider provider = services.BuildServiceProvider(); + OptionsValidationException ex = Assert.Throws(() => { - var options = provider.GetRequiredService>().Value; + DurableTaskSchedulerWorkerOptions options = provider.GetRequiredService>().Value; }); Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); } @@ -88,13 +85,13 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); TokenCredential? credential = null; // Act & Assert - var action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); action.Should().NotThrow(); } @@ -102,13 +99,13 @@ public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgumentException() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); - var connectionString = "This is not a valid=connection string format"; + string connectionString = "This is not a valid=connection string format"; // Act - var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); // Assert action.Should().Throw() @@ -121,12 +118,12 @@ public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgum public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string connectionString) { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); // Act & Assert - var action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); action.Should().Throw(); } @@ -134,49 +131,23 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() { // Arrange - var services = new ServiceCollection(); - var mockBuilder = new Mock(); + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder = new Mock(); mockBuilder.Setup(b => b.Services).Returns(services); mockBuilder.Setup(b => b.Name).Returns("CustomName"); - var credential = new DefaultAzureCredential(); + DefaultAzureCredential credential = new DefaultAzureCredential(); // Act mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); // Assert - var provider = services.BuildServiceProvider(); - var optionsMonitor = provider.GetService>(); + ServiceProvider provider = services.BuildServiceProvider(); + IOptionsMonitor? optionsMonitor = provider.GetService>(); optionsMonitor.Should().NotBeNull(); - var options = optionsMonitor!.Get("CustomName"); + DurableTaskSchedulerWorkerOptions options = optionsMonitor!.Get("CustomName"); options.Should().NotBeNull(); options.EndpointAddress.Should().Be(ValidEndpoint); // The https:// prefix is added by CreateChannel, not in the extension method options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeOfType(); } - - [Fact] - public void ConfigureGrpcChannel_ShouldConfigureWorkerOptions() - { - // Arrange - var services = new ServiceCollection(); - services.AddOptions() - .Configure(options => - { - options.EndpointAddress = $"https://{ValidEndpoint}"; - options.TaskHubName = ValidTaskHub; - options.Credential = new DefaultAzureCredential(); - }); - - var provider = services.BuildServiceProvider(); - var schedulerOptions = provider.GetRequiredService>(); - var configureGrpcChannel = new DurableTaskSchedulerWorkerExtensions.ConfigureGrpcChannel(schedulerOptions); - - // Act - var workerOptions = new GrpcDurableTaskWorkerOptions(); - configureGrpcChannel.Configure(workerOptions); - - // Assert - workerOptions.Channel.Should().NotBeNull(); - workerOptions.Channel.Should().BeOfType(); - } -} +} diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs index 1858fa3b..3585c8f4 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Azure.Core; using Azure.Identity; using FluentAssertions; using Xunit; @@ -10,8 +9,8 @@ namespace Microsoft.DurableTask.Shared.AzureManaged.Tests; public class DurableTaskSchedulerWorkerOptionsTests { - private const string ValidEndpoint = "myaccount.westus3.durabletask.io"; - private const string ValidTaskHub = "testhub"; + const string ValidEndpoint = "myaccount.westus3.durabletask.io"; + const string ValidTaskHub = "testhub"; [Fact] public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() @@ -20,7 +19,7 @@ public void FromConnectionString_WithDefaultAzure_ShouldCreateValidInstance() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + DurableTaskSchedulerWorkerOptions options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -36,7 +35,7 @@ public void FromConnectionString_WithManagedIdentity_ShouldCreateValidInstance() string connectionString = $"Endpoint={ValidEndpoint};Authentication=ManagedIdentity;ClientID={clientId};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + DurableTaskSchedulerWorkerOptions options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -53,7 +52,7 @@ public void FromConnectionString_WithWorkloadIdentity_ShouldCreateValidInstance( string connectionString = $"Endpoint={ValidEndpoint};Authentication=WorkloadIdentity;ClientID={clientId};TenantId={tenantId};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + DurableTaskSchedulerWorkerOptions options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -71,7 +70,7 @@ public void FromConnectionString_WithValidAuthTypes_ShouldCreateValidInstance(st string connectionString = $"Endpoint={ValidEndpoint};Authentication={authType};TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + DurableTaskSchedulerWorkerOptions options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -86,7 +85,7 @@ public void FromConnectionString_WithInvalidAuthType_ShouldThrowArgumentExceptio string connectionString = $"Endpoint={ValidEndpoint};Authentication=InvalidAuth;TaskHub={ValidTaskHub}"; // Act & Assert - var action = () => DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + Action action = () => DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); action.Should().Throw() .WithMessage("*contains an unsupported authentication type*"); } @@ -98,7 +97,7 @@ public void FromConnectionString_WithMissingRequiredProperties_ShouldThrowArgume string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure"; // Missing TaskHub // Act & Assert - var action = () => DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + Action action = () => DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); action.Should().Throw(); } @@ -109,7 +108,7 @@ public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential string connectionString = $"Endpoint={ValidEndpoint};Authentication=None;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + DurableTaskSchedulerWorkerOptions options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -121,7 +120,7 @@ public void FromConnectionString_WithNone_ShouldCreateInstanceWithNullCredential public void DefaultProperties_ShouldHaveExpectedValues() { // Arrange & Act - var options = new DurableTaskSchedulerWorkerOptions(); + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions(); // Assert options.ResourceId.Should().Be("https://durabletask.io"); @@ -135,7 +134,7 @@ public void DefaultProperties_ShouldHaveExpectedValues() public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() { // Arrange - var options = new DurableTaskSchedulerWorkerOptions + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions { EndpointAddress = $"https://{ValidEndpoint}", TaskHubName = ValidTaskHub, @@ -143,7 +142,7 @@ public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() }; // Act - var channel = options.CreateChannel(); + Grpc.Core.ChannelBase channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -153,7 +152,7 @@ public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() { // Arrange - var options = new DurableTaskSchedulerWorkerOptions + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions { EndpointAddress = $"http://{ValidEndpoint}", TaskHubName = ValidTaskHub, @@ -161,7 +160,7 @@ public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() }; // Act - var channel = options.CreateChannel(); + Grpc.Core.ChannelBase channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -171,11 +170,11 @@ public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() public void FromConnectionString_WithInvalidEndpoint_ShouldThrowArgumentException() { // Arrange - var connectionString = "Endpoint=not a valid endpoint;Authentication=DefaultAzure;TaskHub=testhub;"; + string connectionString = "Endpoint=not a valid endpoint;Authentication=DefaultAzure;TaskHub=testhub;"; // Act & Assert - var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); - var action = () => options.CreateChannel(); + DurableTaskSchedulerWorkerOptions options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + Action action = () => options.CreateChannel(); action.Should().Throw() .WithMessage("Invalid URI: The hostname could not be parsed."); } @@ -187,7 +186,7 @@ public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() string connectionString = $"Endpoint={ValidEndpoint};Authentication=DefaultAzure;TaskHub={ValidTaskHub}"; // Act - var options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); + DurableTaskSchedulerWorkerOptions options = DurableTaskSchedulerWorkerOptions.FromConnectionString(connectionString); // Assert options.EndpointAddress.Should().Be(ValidEndpoint); @@ -197,7 +196,7 @@ public void FromConnectionString_WithoutProtocol_ShouldPreserveEndpoint() public void CreateChannel_ShouldAddHttpsPrefix() { // Arrange - var options = new DurableTaskSchedulerWorkerOptions + DurableTaskSchedulerWorkerOptions options = new DurableTaskSchedulerWorkerOptions { EndpointAddress = ValidEndpoint, TaskHubName = ValidTaskHub, @@ -205,7 +204,7 @@ public void CreateChannel_ShouldAddHttpsPrefix() }; // Act - var channel = options.CreateChannel(); + Grpc.Core.ChannelBase channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); diff --git a/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj b/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj index 6fb22508..c22ad3f9 100644 --- a/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj +++ b/test/Worker/AzureManaged.Tests/Worker.AzureManaged.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0 From 02d599289363e3e1ab18bd759f464db8de396801 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 15 Jan 2025 01:38:09 -0800 Subject: [PATCH 53/58] fb --- .../AzureManaged/DurableTaskSchedulerConnectionString.cs | 2 +- .../DurableTaskSchedulerClientExtensionsTests.cs | 4 ++-- .../DurableTaskSchedulerClientOptionsTests.cs | 6 +++--- .../DurableTaskSchedulerConnectionStringTests.cs | 6 +++--- .../DurableTaskSchedulerWorkerExtensionsTests.cs | 4 ++-- .../DurableTaskSchedulerWorkerOptionsTests.cs | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs index 40dc470c..66368a55 100644 --- a/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs +++ b/src/Shared/AzureManaged/DurableTaskSchedulerConnectionString.cs @@ -69,6 +69,6 @@ public DurableTaskSchedulerConnectionString(string connectionString) string GetRequiredValue(string name) { string? value = this.GetValue(name); - return Check.NotNullOrEmpty(value, $"The connection string is missing the required '{name}' property."); + return Check.NotNullOrEmpty(value, name); } } diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index 76eb4ab2..9332bb2f 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -109,7 +109,7 @@ public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgum // Assert action.Should().Throw() - .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); + .WithMessage("Value cannot be null. (Parameter 'Endpoint')"); } [Theory] @@ -124,7 +124,7 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA // Act & Assert Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); - action.Should().Throw(); + action.Should().Throw(); } [Fact] diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs index ff0fc43e..d0790c3f 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientOptionsTests.cs @@ -139,7 +139,7 @@ public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() }; // Act - Grpc.Core.ChannelBase channel = options.CreateChannel(); + using Grpc.Net.Client.GrpcChannel channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -157,7 +157,7 @@ public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() }; // Act - Grpc.Core.ChannelBase channel = options.CreateChannel(); + using Grpc.Net.Client.GrpcChannel channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -201,7 +201,7 @@ public void CreateChannel_ShouldAddHttpsPrefix() }; // Act - Grpc.Core.ChannelBase channel = options.CreateChannel(); + using Grpc.Net.Client.GrpcChannel channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); diff --git a/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs b/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs index e70f3e6f..df2e80b5 100644 --- a/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs +++ b/test/Shared/AzureManaged.Tests/DurableTaskSchedulerConnectionStringTests.cs @@ -113,7 +113,7 @@ public void Constructor_WithInvalidConnectionStringFormat_ShouldThrowFormatExcep // Act & Assert Action action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; action.Should().Throw() - .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); + .WithMessage("Value cannot be null. (Parameter 'Endpoint')"); } [Fact] @@ -125,7 +125,7 @@ public void Constructor_WithEmptyConnectionString_ShouldThrowArgumentNullExcepti // Act & Assert Action action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; action.Should().Throw() - .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); + .WithMessage("Value cannot be null. (Parameter 'Endpoint')"); } [Fact] @@ -150,7 +150,7 @@ public void Constructor_WithMissingRequiredProperties_ShouldThrowArgumentNullExc // Act & Assert Action action = () => _ = new DurableTaskSchedulerConnectionString(connectionString).Endpoint; action.Should().Throw() - .WithMessage("*'Endpoint' property*"); + .WithMessage("Value cannot be null. (Parameter 'Endpoint')"); } [Fact] diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs index 761cbcde..c50c6fa1 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -109,7 +109,7 @@ public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgum // Assert action.Should().Throw() - .WithMessage("Value cannot be null. (Parameter 'The connection string is missing the required 'Endpoint' property.')"); + .WithMessage("Value cannot be null. (Parameter 'Endpoint')"); } [Theory] @@ -124,7 +124,7 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA // Act & Assert Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); - action.Should().Throw(); + action.Should().Throw(); } [Fact] diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs index 3585c8f4..03150b43 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerOptionsTests.cs @@ -142,7 +142,7 @@ public void CreateChannel_WithHttpsEndpoint_ShouldCreateSecureChannel() }; // Act - Grpc.Core.ChannelBase channel = options.CreateChannel(); + using Grpc.Net.Client.GrpcChannel channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -160,7 +160,7 @@ public void CreateChannel_WithHttpEndpoint_ShouldCreateInsecureChannel() }; // Act - Grpc.Core.ChannelBase channel = options.CreateChannel(); + using Grpc.Net.Client.GrpcChannel channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); @@ -204,7 +204,7 @@ public void CreateChannel_ShouldAddHttpsPrefix() }; // Act - Grpc.Core.ChannelBase channel = options.CreateChannel(); + using Grpc.Net.Client.GrpcChannel channel = options.CreateChannel(); // Assert channel.Should().NotBeNull(); From e0bd4e2b71c4085693f2395e0aaaf49fc436bc84 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 15 Jan 2025 08:52:35 -0800 Subject: [PATCH 54/58] fb --- ...rableTaskSchedulerClientExtensionsTests.cs | 21 +++++++++++++++ ...rableTaskSchedulerWorkerExtensionsTests.cs | 27 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index 9332bb2f..95d3a6f0 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -33,6 +33,12 @@ public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCor ServiceProvider provider = services.BuildServiceProvider(); IOptions? options = provider.GetService>(); options.Should().NotBeNull(); + + // Validate the configured options + var clientOptions = provider.GetRequiredService>().Value; + clientOptions.EndpointAddress.Should().Be(ValidEndpoint); + clientOptions.TaskHubName.Should().Be(ValidTaskHub); + clientOptions.Credential.Should().BeOfType(); } [Fact] @@ -51,6 +57,12 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl ServiceProvider provider = services.BuildServiceProvider(); IOptions? options = provider.GetService>(); options.Should().NotBeNull(); + + // Validate the configured options + var clientOptions = provider.GetRequiredService>().Value; + clientOptions.EndpointAddress.Should().Be(ValidEndpoint); + clientOptions.TaskHubName.Should().Be(ValidTaskHub); + clientOptions.Credential.Should().BeOfType(); } [Theory] @@ -93,6 +105,13 @@ public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() // Act & Assert Action action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); action.Should().NotThrow(); + + // Validate the configured options + ServiceProvider provider = services.BuildServiceProvider(); + var clientOptions = provider.GetRequiredService>().Value; + clientOptions.EndpointAddress.Should().Be(ValidEndpoint); + clientOptions.TaskHubName.Should().Be(ValidTaskHub); + clientOptions.Credential.Should().BeNull(); } [Fact] @@ -149,5 +168,7 @@ public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() options.EndpointAddress.Should().Be(ValidEndpoint); // The https:// prefix is added by CreateChannel, not in the extension method options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeOfType(); + options.ResourceId.Should().Be("https://durabletask.io"); + options.AllowInsecureCredentials.Should().BeFalse(); } } diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs index c50c6fa1..af6f5399 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -33,6 +33,14 @@ public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCor ServiceProvider provider = services.BuildServiceProvider(); IOptions? options = provider.GetService>(); options.Should().NotBeNull(); + + // Validate the configured options + var workerOptions = provider.GetRequiredService>().Value; + workerOptions.EndpointAddress.Should().Be(ValidEndpoint); + workerOptions.TaskHubName.Should().Be(ValidTaskHub); + workerOptions.Credential.Should().BeOfType(); + workerOptions.ResourceId.Should().Be("https://durabletask.io"); + workerOptions.AllowInsecureCredentials.Should().BeFalse(); } [Fact] @@ -51,6 +59,14 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl ServiceProvider provider = services.BuildServiceProvider(); IOptions? options = provider.GetService>(); options.Should().NotBeNull(); + + // Validate the configured options + var workerOptions = provider.GetRequiredService>().Value; + workerOptions.EndpointAddress.Should().Be(ValidEndpoint); + workerOptions.TaskHubName.Should().Be(ValidTaskHub); + workerOptions.Credential.Should().BeOfType(); + workerOptions.ResourceId.Should().Be("https://durabletask.io"); + workerOptions.AllowInsecureCredentials.Should().BeFalse(); } [Theory] @@ -93,6 +109,15 @@ public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() // Act & Assert Action action = () => mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential!); action.Should().NotThrow(); + + // Validate the configured options + ServiceProvider provider = services.BuildServiceProvider(); + var workerOptions = provider.GetRequiredService>().Value; + workerOptions.EndpointAddress.Should().Be(ValidEndpoint); + workerOptions.TaskHubName.Should().Be(ValidTaskHub); + workerOptions.Credential.Should().BeNull(); + workerOptions.ResourceId.Should().Be("https://durabletask.io"); + workerOptions.AllowInsecureCredentials.Should().BeFalse(); } [Fact] @@ -149,5 +174,7 @@ public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() options.EndpointAddress.Should().Be(ValidEndpoint); // The https:// prefix is added by CreateChannel, not in the extension method options.TaskHubName.Should().Be(ValidTaskHub); options.Credential.Should().BeOfType(); + options.ResourceId.Should().Be("https://durabletask.io"); + options.AllowInsecureCredentials.Should().BeFalse(); } } From 5b4f64808c4cbe7d3cc2969f46c5bd0224178965 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:18:20 -0800 Subject: [PATCH 55/58] fb --- .editorconfig | 8 +----- .../DurableTaskSchedulerClientOptions.cs | 2 +- .../DurableTaskSchedulerWorkerOptions.cs | 2 +- ...rableTaskSchedulerClientExtensionsTests.cs | 28 ++++++++----------- ...rableTaskSchedulerWorkerExtensionsTests.cs | 21 ++++++-------- 5 files changed, 23 insertions(+), 38 deletions(-) diff --git a/.editorconfig b/.editorconfig index 23881078..7c1faebc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -'# Remove the line below if you want to inherit .editorconfig settings from higher directories +# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true # XML project files @@ -232,9 +232,3 @@ dotnet_diagnostic.SA1400.severity = none # Access modifier must be declared dotnet_diagnostic.SA1402.severity = none # File may only contain a single type dotnet_diagnostic.SA1633.severity = warning # File must have header -- TODO: replace with IDE0073 eventually dotnet_diagnostic.SA1649.severity = none # File name must match type name - -# Enforce explicit types instead of `var` -dotnet_style_prefer_var_for_built_in_types = false:error -dotnet_style_prefer_var_for_simple_types = false:error -dotnet_style_prefer_var_when_type_is_apparent = false:error -dotnet_style_prefer_var_when_type_is_not_apparent = false:error diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs index 8278a2d8..94b33440 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs @@ -57,7 +57,7 @@ public static DurableTaskSchedulerClientOptions FromConnectionString(string conn /// Creates a gRPC channel for communicating with the Durable Task Scheduler service. /// /// A configured instance that can be used to make gRPC calls. - public GrpcChannel CreateChannel() + internal GrpcChannel CreateChannel() { Verify.NotNull(this.EndpointAddress, nameof(this.EndpointAddress)); Verify.NotNull(this.TaskHubName, nameof(this.TaskHubName)); diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs index 4cc32735..8c05b5e6 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerOptions.cs @@ -63,7 +63,7 @@ public static DurableTaskSchedulerWorkerOptions FromConnectionString(string conn /// Creates a gRPC channel for communicating with the Durable Task Scheduler service. /// /// A configured instance that can be used to make gRPC calls. - public GrpcChannel CreateChannel() + internal GrpcChannel CreateChannel() { Verify.NotNull(this.EndpointAddress, nameof(this.EndpointAddress)); Verify.NotNull(this.TaskHubName, nameof(this.TaskHubName)); diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index 95d3a6f0..599fa1c7 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -35,7 +35,7 @@ public void UseDurableTaskScheduler_WithEndpointAndCredential_ShouldConfigureCor options.Should().NotBeNull(); // Validate the configured options - var clientOptions = provider.GetRequiredService>().Value; + DurableTaskSchedulerClientOptions clientOptions = provider.GetRequiredService>().Value; clientOptions.EndpointAddress.Should().Be(ValidEndpoint); clientOptions.TaskHubName.Should().Be(ValidTaskHub); clientOptions.Credential.Should().BeOfType(); @@ -59,7 +59,7 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl options.Should().NotBeNull(); // Validate the configured options - var clientOptions = provider.GetRequiredService>().Value; + DurableTaskSchedulerClientOptions clientOptions = provider.GetRequiredService>().Value; clientOptions.EndpointAddress.Should().Be(ValidEndpoint); clientOptions.TaskHubName.Should().Be(ValidTaskHub); clientOptions.Credential.Should().BeOfType(); @@ -68,7 +68,7 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl [Theory] [InlineData(null, "testhub")] [InlineData("myaccount.westus3.durabletask.io", null)] - public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) + public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidationException(string endpoint, string taskHub) { // Arrange ServiceCollection services = new ServiceCollection(); @@ -77,22 +77,18 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx DefaultAzureCredential credential = new DefaultAzureCredential(); // Act - Action action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + ServiceProvider provider = services.BuildServiceProvider(); // Assert - action.Should().NotThrow(); // The validation happens when building the service provider - - if (endpoint == null || taskHub == null) - { - ServiceProvider provider = services.BuildServiceProvider(); - OptionsValidationException ex = Assert.Throws(() => - { - DurableTaskSchedulerClientOptions options = provider.GetRequiredService>().Value; - }); - Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); - } + var action = () => provider.GetRequiredService>().Value; + action.Should().Throw() + .WithMessage(endpoint == null + ? "DataAnnotation validation failed for 'DurableTaskSchedulerClientOptions' members: 'EndpointAddress' with the error: 'Endpoint address is required'." + : "DataAnnotation validation failed for 'DurableTaskSchedulerClientOptions' members: 'TaskHubName' with the error: 'Task hub name is required'."); } + [Fact] public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() { @@ -108,7 +104,7 @@ public void UseDurableTaskScheduler_WithNullCredential_ShouldSucceed() // Validate the configured options ServiceProvider provider = services.BuildServiceProvider(); - var clientOptions = provider.GetRequiredService>().Value; + DurableTaskSchedulerClientOptions clientOptions = provider.GetRequiredService>().Value; clientOptions.EndpointAddress.Should().Be(ValidEndpoint); clientOptions.TaskHubName.Should().Be(ValidTaskHub); clientOptions.Credential.Should().BeNull(); diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs index af6f5399..bec5a962 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -72,7 +72,7 @@ public void UseDurableTaskScheduler_WithConnectionString_ShouldConfigureCorrectl [Theory] [InlineData(null, "testhub")] [InlineData("myaccount.westus3.durabletask.io", null)] - public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullException(string endpoint, string taskHub) + public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidationException(string endpoint, string taskHub) { // Arrange ServiceCollection services = new ServiceCollection(); @@ -81,20 +81,15 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowArgumentNullEx DefaultAzureCredential credential = new DefaultAzureCredential(); // Act - Action action = () => mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + ServiceProvider provider = services.BuildServiceProvider(); // Assert - action.Should().NotThrow(); // The validation happens when building the service provider - - if (endpoint == null || taskHub == null) - { - ServiceProvider provider = services.BuildServiceProvider(); - OptionsValidationException ex = Assert.Throws(() => - { - DurableTaskSchedulerWorkerOptions options = provider.GetRequiredService>().Value; - }); - Assert.Contains(endpoint == null ? "EndpointAddress" : "TaskHubName", ex.Message); - } + var action = () => provider.GetRequiredService>().Value; + action.Should().Throw() + .WithMessage(endpoint == null + ? "DataAnnotation validation failed for 'DurableTaskSchedulerWorkerOptions' members: 'EndpointAddress' with the error: 'Endpoint address is required'." + : "DataAnnotation validation failed for 'DurableTaskSchedulerWorkerOptions' members: 'TaskHubName' with the error: 'Task hub name is required'."); } [Fact] From 6f28efc545f8a32118eea2ce0d8b76484d0af3df Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:57:13 -0800 Subject: [PATCH 56/58] remove validateonstart --- src/Client/AzureManaged/Client.AzureManaged.csproj | 2 +- .../AzureManaged/DurableTaskSchedulerClientExtensions.cs | 3 +-- .../AzureManaged/DurableTaskSchedulerWorkerExtensions.cs | 3 +-- src/Worker/AzureManaged/Worker.AzureManaged.csproj | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Client/AzureManaged/Client.AzureManaged.csproj b/src/Client/AzureManaged/Client.AzureManaged.csproj index 0b80078b..fc7bfba5 100644 --- a/src/Client/AzureManaged/Client.AzureManaged.csproj +++ b/src/Client/AzureManaged/Client.AzureManaged.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index 290b09a8..0bba1c96 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -83,8 +83,7 @@ static void ConfigureSchedulerOptions( builder.Services.AddOptions(builder.Name) .Configure(initialConfig) .Configure(additionalConfig ?? (_ => { })) - .ValidateDataAnnotations() - .ValidateOnStart(); + .ValidateDataAnnotations(); builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index 71d7aa1e..2eec714c 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -83,8 +83,7 @@ static void ConfigureSchedulerOptions( builder.Services.AddOptions(builder.Name) .Configure(initialConfig) .Configure(additionalConfig ?? (_ => { })) - .ValidateDataAnnotations() - .ValidateOnStart(); + .ValidateDataAnnotations(); builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton, ConfigureGrpcChannel>()); diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index 74df2840..cd2ca1f8 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -12,7 +12,7 @@ - + From 3334130dbb6744e8f83038ad16be32422f515ce4 Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:04:18 -0800 Subject: [PATCH 57/58] change ver --- src/Client/AzureManaged/Client.AzureManaged.csproj | 2 +- src/Worker/AzureManaged/Worker.AzureManaged.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client/AzureManaged/Client.AzureManaged.csproj b/src/Client/AzureManaged/Client.AzureManaged.csproj index fc7bfba5..22b99967 100644 --- a/src/Client/AzureManaged/Client.AzureManaged.csproj +++ b/src/Client/AzureManaged/Client.AzureManaged.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index cd2ca1f8..08f98a0f 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -12,7 +12,7 @@ - + From d87eed62fdbd570dcd908106725f572e8483576e Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Wed, 15 Jan 2025 17:27:27 -0800 Subject: [PATCH 58/58] Update CHANGELOG and mark packages as preview --- CHANGELOG.md | 1 + src/Client/AzureManaged/Client.AzureManaged.csproj | 1 + src/Worker/AzureManaged/Worker.AzureManaged.csproj | 1 + 3 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81265a38..3c9789ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Implement work item completion tokens for standalone worker scenarios ([#359](https://github.com/microsoft/durabletask-dotnet/pull/359)) - Support for worker concurrency configuration ([#359](https://github.com/microsoft/durabletask-dotnet/pull/359)) - Bump System.Text.Json to 6.0.10 +- Initial support for the Azure-managed [Durable Task Scheduler](https://techcommunity.microsoft.com/blog/appsonazureblog/announcing-limited-early-access-of-the-durable-task-scheduler-for-azure-durable-/4286526) preview. ## v1.4.0 diff --git a/src/Client/AzureManaged/Client.AzureManaged.csproj b/src/Client/AzureManaged/Client.AzureManaged.csproj index 22b99967..98202006 100644 --- a/src/Client/AzureManaged/Client.AzureManaged.csproj +++ b/src/Client/AzureManaged/Client.AzureManaged.csproj @@ -4,6 +4,7 @@ net6.0 Azure Managed extensions for the Durable Task Framework client. true + preview.1 diff --git a/src/Worker/AzureManaged/Worker.AzureManaged.csproj b/src/Worker/AzureManaged/Worker.AzureManaged.csproj index 08f98a0f..9f3b877d 100644 --- a/src/Worker/AzureManaged/Worker.AzureManaged.csproj +++ b/src/Worker/AzureManaged/Worker.AzureManaged.csproj @@ -4,6 +4,7 @@ net6.0 Azure Managed extensions for the Durable Task Framework worker. true + preview.1